message.rs 18 KB
Newer Older
1 2
extern crate gtk;
extern crate chrono;
3
extern crate pango;
4 5 6
extern crate glib;

use app::App;
7
use i18n::i18n;
8 9 10 11 12

use self::gtk::prelude::*;

use types::Message;
use types::Member;
13
use types::Room;
14 15 16 17 18

use self::chrono::prelude::*;

use backend::BKCommand;

19
use fractal_api as api;
20
use util::markup_text;
21

22
use std::path::Path;
23 24 25
use std::sync::mpsc::channel;
use std::sync::mpsc::{Sender, Receiver};
use std::sync::mpsc::TryRecvError;
26

27
use appop::AppOp;
28 29 30
use globals;
use widgets;
use widgets::AvatarExt;
31
use widgets::member::get_member_info;
32 33 34

// Room Message item
pub struct MessageBox<'a> {
35
    room: &'a Room,
36 37
    msg: &'a Message,
    op: &'a AppOp,
38
    username: gtk::Label,
39
    pub username_event_box: gtk::EventBox,
40 41 42
}

impl<'a> MessageBox<'a> {
43
    pub fn new(room: &'a Room, msg: &'a Message, op: &'a AppOp) -> MessageBox<'a> {
44
        let username = gtk::Label::new("");
45 46
        let eb = gtk::EventBox::new();

47 48 49
        MessageBox {
            msg: msg,
            room: room,
50 51 52
            op: op,
            username: username,
            username_event_box: eb,
53
        }
54 55
    }

56 57 58 59 60 61 62 63
    pub fn tmpwidget(&self) -> gtk::ListBoxRow {
        let w = self.widget();
        if let Some(style) = w.get_style_context() {
            style.add_class("msg-tmp");
        }
        w
    }

64
    pub fn widget(&self) -> gtk::ListBoxRow {
65 66 67 68
        // msg
        // +--------+---------+
        // | avatar | content |
        // +--------+---------+
69
        let msg_widget = gtk::Box::new(gtk::Orientation::Horizontal, 10);
70

71
        let content = self.build_room_msg_content(false);
72
        let avatar = self.build_room_msg_avatar();
73

74
        msg_widget.pack_start(&avatar, false, false, 0);
75 76
        msg_widget.pack_start(&content, true, true, 0);

77
        let row = gtk::ListBoxRow::new();
78
        self.set_msg_styles(&row);
79 80 81 82
        row.set_selectable(false);
        row.set_margin_top(12);
        row.add(&msg_widget);
        row.show_all();
83

84
        row
85 86
    }

87
    pub fn small_widget(&self) -> gtk::ListBoxRow {
88 89 90 91 92 93 94
        // msg
        // +--------+---------+
        // |        | content |
        // +--------+---------+
        let msg_widget = gtk::Box::new(gtk::Orientation::Horizontal, 5);

        let content = self.build_room_msg_content(true);
95

96
        msg_widget.pack_start(&content, true, true, 50);
97

98
        let row = gtk::ListBoxRow::new();
99
        self.set_msg_styles(&row);
100 101 102
        row.set_selectable(false);
        row.add(&msg_widget);
        row.show_all();
103

104
        row
105 106 107
    }

    fn build_room_msg_content(&self, small: bool) -> gtk::Box {
108 109 110 111 112 113 114 115 116
        // content
        // +------+
        // | info |
        // +------+
        // | body |
        // +------+
        let content = gtk::Box::new(gtk::Orientation::Vertical, 0);
        let msg = self.msg;

117 118
        if !small {
            let info = self.build_room_msg_info(self.msg);
119 120
            info.set_margin_top(2);
            info.set_margin_bottom(3);
121 122
            content.pack_start(&info, false, false, 0);
        }
123

124 125 126 127 128 129 130 131
        let body = match msg.mtype.as_ref() {
            "m.sticker" => self.build_room_msg_sticker(),
            "m.image" => self.build_room_msg_image(),
            "m.emote" => self.build_room_msg_emote(&msg),
            "m.audio" => self.build_room_audio_player(),
            "m.video" | "m.file" => self.build_room_msg_file(),
            _ => self.build_room_msg_body(&msg.body),
        };
132 133 134 135 136 137

        content.pack_start(&body, true, true, 0);

        content
    }

138
    fn build_room_msg_avatar(&self) -> widgets::Avatar {
139 140
        let sender = self.msg.sender.clone();
        let backend = self.op.backend.clone();
141
        let avatar = widgets::Avatar::avatar_new(Some(globals::MSG_ICON_SIZE));
142

143
        let fname = api::util::cache_path(&sender).unwrap_or(strn!(""));
144 145 146 147

        let pathname = fname.clone();
        let p = Path::new(&pathname);
        if p.is_file() {
148
            avatar.circle(fname, Some(globals::MSG_ICON_SIZE));
149
        } else {
150 151
            avatar.default(String::from("avatar-default-symbolic"),
                           Some(globals::MSG_ICON_SIZE));
152 153
        }

154 155 156 157
        let m = self.room.members.get(&sender);

        match m {
            Some(member) => {
158
                self.username.set_text(&member.get_alias());
159
                get_member_info(backend.clone(), avatar.clone(), self.username.clone(), sender.clone(), globals::MSG_ICON_SIZE, 10);
160 161
            }
            None => {
162
                self.username.set_text(&sender);
163
                get_member_info(backend.clone(), avatar.clone(), self.username.clone(), sender.clone(), globals::MSG_ICON_SIZE, 10);
164 165 166
            }
        };

167 168 169 170 171 172
        avatar
    }

    fn build_room_msg_username(&self, sender: &str, member: Option<&Member>) -> gtk::Label {
        let uname = match member {
            Some(m) => m.get_alias(),
173
            None => String::from(sender),
174 175
        };

176
        self.username.set_text(&uname);
177 178
        self.username.set_justify(gtk::Justification::Left);
        self.username.set_halign(gtk::Align::Start);
179 180 181
        if let Some(style) = self.username.get_style_context() {
            style.add_class("username");
        }
182

183
        self.username.clone()
184 185
    }

186 187
    /// Add classes to the widget depending on the properties:
    ///
188
    ///  * msg-mention: if the message contains the username in the body and
189
    ///                 sender is not app user
190
    ///  * msg-emote: if the message is an emote
191
    fn set_msg_styles(&self, w: &gtk::ListBoxRow) {
192
        let uname = &self.op.username.clone().unwrap_or_default();
193
        let uid = self.op.uid.clone().unwrap_or_default();
194 195
        let msg = self.msg;
        let body: &str = &msg.body;
196

197 198
        if let Some(style) = w.get_style_context() {
            // mentions
199
            if String::from(body).contains(uname) && msg.sender != uid {
200 201 202 203 204 205
                style.add_class("msg-mention");
            }
            // emotes
            if msg.mtype == "m.emote" {
                style.add_class("msg-emote");
            }
206
        }
207
    }
208

209
    fn set_label_styles(&self, w: &gtk::Label) {
210 211 212
        w.set_line_wrap(true);
        w.set_line_wrap_mode(pango::WrapMode::WordChar);
        w.set_justify(gtk::Justification::Left);
213
        w.set_xalign(0.0);
214
        w.set_valign(gtk::Align::Start);
215 216 217 218 219 220 221
        w.set_halign(gtk::Align::Start);
        w.set_selectable(true);
    }

    fn build_room_msg_body(&self, body: &str) -> gtk::Box {
        let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
        let msg = gtk::Label::new("");
222
        let uname = self.op.username.clone().unwrap_or_default();
223

224
        msg.set_markup(&markup_text(body));
225
        self.set_label_styles(&msg);
226

227 228
        if self.msg.sender != self.op.uid.clone().unwrap_or_default()
            && String::from(body).contains(&uname) {
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254

            let name = uname.clone();
            msg.connect_property_cursor_position_notify(move |w| {
                if let Some(text) = w.get_text() {
                    if let Some(attr) = highlight_username(w.clone(), &name, text) {
                        w.set_attributes(&attr);
                    }
                }
            });

            let name = uname.clone();
            msg.connect_property_selection_bound_notify(move |w| {
                if let Some(text) = w.get_text() {
                    if let Some(attr) = highlight_username(w.clone(), &name, text) {
                        w.set_attributes(&attr);
                    }
                }
            });

            if let Some(text) = msg.get_text() {
                if let Some(attr) = highlight_username(msg.clone(), &uname, text) {
                    msg.set_attributes(&attr);
                }
            }
        }

255 256 257 258 259 260 261
        bx.add(&msg);
        bx
    }

    fn build_room_msg_image(&self) -> gtk::Box {
        let msg = self.msg;
        let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
262
        let url = msg.url.clone().unwrap_or_default();
263

264
        let backend = self.op.backend.clone();
265 266 267 268
        let img_path = match msg.thumb {
            Some(ref m) => m.clone(),
            None => msg.url.clone().unwrap_or_default(),
        };
269 270
        let image = widgets::image::Image::new(&backend, &img_path)
                        .size(Some((600, 400))).build();
271

272
        let image_name = msg.body.clone();
273
        let room_id = self.room.id.clone();
274
        image.widget.connect_button_press_event(move |_, _| {
275
            let image_name = image_name.clone();
276 277
            let image_url = url.clone();
            let rid = room_id.clone();
278
            APPOP!(display_media_viewer, (image_name, image_url, rid));
279

280
            Inhibit(true)
281 282
        });

283
        if let Some(style) = image.widget.get_style_context() {
284 285 286
            style.add_class("image-widget");
        }

287 288
        bx.pack_start(&image.widget, true, true, 0);
        bx.show_all();
289 290 291
        bx
    }

292 293 294 295
    fn build_room_msg_sticker(&self) -> gtk::Box {
        let msg = self.msg;
        let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
        let backend = self.op.backend.clone();
296 297 298
        let image = widgets::image::Image::new(&backend,
                        &msg.url.clone().unwrap_or_default())
                        .size(Some((600, 400))).build();
299 300
        let w = image.widget.clone();
        w.set_tooltip_text(&self.msg.body[..]);
301

302
        bx.add(&w);
303 304 305 306

        bx
    }

307 308 309 310 311 312 313 314 315
    fn build_room_audio_player(&self) -> gtk::Box {
        let msg = self.msg;
        let bx = gtk::Box::new(gtk::Orientation::Horizontal, 6);
        let player = widgets::AudioPlayerWidget::new();

        let name = msg.body.clone();
        let url = msg.url.clone().unwrap_or_default();
        let backend = self.op.backend.clone();

316 317 318 319 320 321 322
        let (tx, rx): (Sender<String>, Receiver<String>) = channel();
        backend.send(BKCommand::GetMediaUrl(url.clone(), tx)).unwrap();

        gtk::timeout_add(50, clone!(player => move || {
            match rx.try_recv() {
                Err(TryRecvError::Empty) => gtk::Continue(true),
                Err(TryRecvError::Disconnected) => {
323
                    let msg = i18n("Could not retrieve file URI");
324 325 326 327 328 329 330 331 332 333 334
                    APPOP!(show_error, (msg));
                    gtk::Continue(true)
                },
                Ok(uri) => {
                    println!("AUDIO URI: {}", &uri);
                    player.initialize_stream(&uri);
                    gtk::Continue(false)
                }
            }
        }));

335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
        let download_btn = gtk::Button::new_from_icon_name(
            "document-save-symbolic",
            gtk::IconSize::Button.into(),
        );
        download_btn.set_tooltip_text(i18n("Save").as_str());

        download_btn.connect_clicked(clone!(name, url, backend => move |_| {
            let (tx, rx): (Sender<String>, Receiver<String>) = channel();

            backend.send(BKCommand::GetMediaAsync(url.clone(), tx)).unwrap();

            gtk::timeout_add(50, clone!(name => move || match rx.try_recv() {
                Err(TryRecvError::Empty) => gtk::Continue(true),
                Err(TryRecvError::Disconnected) => {
                    let msg = i18n("Could not download the file");
                    APPOP!(show_error, (msg));

                    gtk::Continue(true)
                },
                Ok(fname) => {
                    let name = name.clone();
                    APPOP!(save_file_as, (fname, name));

                    gtk::Continue(false)
                }
            }));
        }));

        bx.pack_start(&player.container, false, true, 0);
364
        bx.pack_start(&download_btn, false, false, 3);
365 366 367
        bx
    }

368 369 370 371 372
    fn build_room_msg_file(&self) -> gtk::Box {
        let msg = self.msg;
        let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);

        let viewbtn = gtk::Button::new();
373
        let name = msg.body.clone();
374
        let url = msg.url.clone().unwrap_or_default();
375
        let backend = self.op.backend.clone();
376 377 378 379 380 381
        viewbtn.connect_clicked(move |btn| {
            let popover = gtk::Popover::new(btn);

            let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);

            let download_btn = gtk::ModelButton::new();
382
            download_btn.set_label(&i18n("Download"));
383 384 385 386 387 388 389 390 391

            download_btn.connect_clicked(clone!(name, url, backend => move |_| {
                let (tx, rx): (Sender<String>, Receiver<String>) = channel();

                backend.send(BKCommand::GetMediaAsync(url.clone(), tx)).unwrap();

                gtk::timeout_add(50, clone!(name => move || match rx.try_recv() {
                    Err(TryRecvError::Empty) => gtk::Continue(true),
                    Err(TryRecvError::Disconnected) => {
392
                        let msg = i18n("Could not download the file");
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
                        APPOP!(show_error, (msg));

                        gtk::Continue(true)
                    },
                    Ok(fname) => {
                        let name = name.clone();
                        APPOP!(save_file_as, (fname, name));

                        gtk::Continue(false)
                    }
                }));
            }));

            vbox.pack_start(&download_btn, false, false, 6);

            let open_btn = gtk::ModelButton::new();
409
            open_btn.set_label(&i18n("Open"));
410 411 412 413 414 415 416 417 418 419

            open_btn.connect_clicked(clone!(url, backend => move |_| {
                backend.send(BKCommand::GetMedia(url.clone())).unwrap();
            }));

            vbox.pack_start(&open_btn, false, false, 6);

            vbox.show_all();
            popover.add(&vbox);
            popover.popup();
420 421 422 423 424 425 426 427
        });

        viewbtn.set_label(&msg.body);

        bx.add(&viewbtn);
        bx
    }

428
    fn build_room_msg_date(&self, dt: &DateTime<Local>) -> gtk::Label {
429 430 431 432 433 434 435 436 437
        let now = Local::now();

        let d = if (now.year() == dt.year()) && (now.ordinal() == dt.ordinal()) {
            dt.format("%H:%M").to_string()
        } else if now.year() == dt.year() {
            dt.format("%e %b %H:%M").to_string()
        } else {
            dt.format("%e %b %Y %H:%M").to_string()
        };
438 439

        let date = gtk::Label::new("");
440
        date.set_markup(&format!("<span alpha=\"60%\">{}</span>", d.trim()));
441 442
        date.set_line_wrap(true);
        date.set_justify(gtk::Justification::Right);
443
        date.set_valign(gtk::Align::Start);
444
        date.set_halign(gtk::Align::End);
445 446 447
        if let Some(style) = date.get_style_context() {
            style.add_class("timestamp");
        }
448 449 450 451 452 453 454 455 456 457 458

        date
    }

    fn build_room_msg_info(&self, msg: &Message) -> gtk::Box {
        // info
        // +----------+------+
        // | username | date |
        // +----------+------+
        let info = gtk::Box::new(gtk::Orientation::Horizontal, 0);

459
        let member = self.room.members.get(&msg.sender);
460 461 462
        let username = self.build_room_msg_username(&msg.sender, member);
        let date = self.build_room_msg_date(&msg.date);

463 464 465
        self.username_event_box.add(&username);

        info.pack_start(&self.username_event_box, true, true, 0);
466 467 468 469
        info.pack_start(&date, false, false, 0);

        info
    }
470 471 472 473 474 475 476 477

    fn build_room_msg_emote(&self, msg: &Message) -> gtk::Box {
        let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
        let member = self.room.members.get(&msg.sender);
        let sender: &str = &msg.sender;

        let sname = match member {
            Some(m) => m.get_alias(),
478
            None => String::from(sender),
479 480 481 482 483
        };

        let msg_label = gtk::Label::new("");
        let body: &str = &msg.body;

484
        msg_label.set_markup(&format!("<b>{}</b> {}", sname, markup_text(body)));
485

486
        self.set_label_styles(&msg_label);
487 488 489 490

        bx.add(&msg_label);
        bx
    }
491
}
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559

fn highlight_username(label: gtk::Label, alias: &String, input: String) -> Option<pango::AttrList> {
    fn contains((start, end): (i32, i32), item: i32) -> bool {
        match start <= end {
            true => start <= item && end > item,
            false => start <= item || end > item,
        }
    }

    let input = input.to_lowercase();
    let bounds = label.get_selection_bounds();
    let context = gtk::Widget::get_style_context (&label.clone().upcast::<gtk::Widget>())?;
    let fg  = gtk::StyleContext::lookup_color (&context, "theme_selected_bg_color")?;
    let red = fg.red * 65535. + 0.5;
    let green = fg.green * 65535. + 0.5;
    let blue = fg.blue * 65535. + 0.5;
    let color = pango::Attribute::new_foreground(red as u16, green as u16, blue as u16)?;

    let attr = pango::AttrList::new();
    let mut input = input.clone();
    let alias = &alias.to_lowercase();
    let mut removed_char = 0;
    while input.contains(alias) {
        let pos = {
            let start = input.find(alias)? as i32;
            (start, start + alias.len() as i32)
        };
        let mut color = color.clone();
        let mark_start = removed_char as i32 + pos.0;
        let mark_end = removed_char as i32 + pos.1;
        let mut final_pos = Some((mark_start, mark_end));
        /* exclude selected text */
        if let Some((bounds_start, bounds_end)) = bounds {
            /* If the selection is within the alias */
            if contains((mark_start, mark_end), bounds_start) &&
                contains((mark_start, mark_end), bounds_end) {
                    final_pos = Some((mark_start, bounds_start));
                    /* Add blue color after a selection */
                    let mut color = color.clone();
                    color.set_start_index(bounds_end as u32);
                    color.set_end_index(mark_end as u32);
                    attr.insert(color);
                } else {
                    /* The alias starts inside a selection */
                    if contains(bounds?, mark_start) {
                        final_pos = Some((bounds_end, final_pos?.1));
                    }
                    /* The alias ends inside a selection */
                    if contains(bounds?, mark_end - 1) {
                        final_pos = Some((final_pos?.0, bounds_start));
                    }
                }
        }

        if let Some((start, end)) = final_pos {
            color.set_start_index(start as u32);
            color.set_end_index(end as u32);
            attr.insert(color);
        }
        {
            let end = pos.1 as usize;
            input.drain(0..end);
        }
        removed_char = removed_char + pos.1 as u32;
    }

    Some(attr)
}