From 56bc45ce4d07a7a9a415e9dc8ad2f7c3f3c9e48d Mon Sep 17 00:00:00 2001 From: fiaxh Date: Thu, 2 Mar 2017 15:37:32 +0100 Subject: Initial commit --- .../conversation_summary/merged_message_item.vala | 164 +++++++++++++++ .../conversation_summary/merged_status_item.vala | 30 +++ .../src/ui/conversation_summary/status_item.vala | 29 +++ client/src/ui/conversation_summary/view.vala | 221 +++++++++++++++++++++ 4 files changed, 444 insertions(+) create mode 100644 client/src/ui/conversation_summary/merged_message_item.vala create mode 100644 client/src/ui/conversation_summary/merged_status_item.vala create mode 100644 client/src/ui/conversation_summary/status_item.vala create mode 100644 client/src/ui/conversation_summary/view.vala (limited to 'client/src/ui/conversation_summary') diff --git a/client/src/ui/conversation_summary/merged_message_item.vala b/client/src/ui/conversation_summary/merged_message_item.vala new file mode 100644 index 00000000..b1e99d3e --- /dev/null +++ b/client/src/ui/conversation_summary/merged_message_item.vala @@ -0,0 +1,164 @@ +using Gee; +using Gdk; +using Gtk; +using Markup; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +[GtkTemplate (ui = "/org/dino-im/conversation_summary/message_item.ui")] +public class MergedMessageItem : Grid { + + public Conversation conversation { get; set; } + public Jid from { get; private set; } + public DateTime initial_time { get; private set; } + public ArrayList messages = new ArrayList(Message.equals_func); + + [GtkChild] + private Image image; + + [GtkChild] + private Label time_label; + + [GtkChild] + private Label name_label; + + [GtkChild] + private Image encryption_image; + + [GtkChild] + private Image received_image; + + [GtkChild] + private TextView message_text_view; + + public MergedMessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) { + this.conversation = conversation; + this.from = message.from; + this.initial_time = message.time; + setup_tags(); + add_message(message); + + time_label.label = get_relative_time(initial_time.to_local()); + string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account); + name_label.set_markup(@"$display_name"); + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_message(stream_interactor, message)); + if (message.encryption == Entities.Message.Encryption.PGP) { + encryption_image.visible = true; + encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR); + } + } + + public void update() { + time_label.label = get_relative_time(initial_time.to_local()); + } + + public void add_message(Message message) { + TextIter end; + message_text_view.buffer.get_end_iter(out end); + if (messages.size > 0) { + message_text_view.buffer.insert(ref end, "\n", -1); + } + message_text_view.buffer.insert(ref end, message.body, -1); + format_suffix_urls(message.body); + messages.add(message); + message.notify["marked"].connect_after(update_received); // TODO other thread? not main? css error? gtk main? + update_received(); + } + + private void update_received() { + bool all_received = true; + bool all_read = true; + foreach (Message message in messages) { + if (message.marked != Message.Marked.READ) { + all_read = false; + if (message.marked != Message.Marked.RECEIVED) { + all_received = false; + } + } + } + if (all_read) { + received_image.visible = true; + received_image.set_from_resource("/org/dino-im/img/double_tick.svg"); + } else if (all_received) { + received_image.visible = true; + received_image.set_from_resource("/org/dino-im/img/tick.svg"); + } else if (received_image.visible) { + received_image.set_from_icon_name("image-loading-symbolic", IconSize.SMALL_TOOLBAR); + } + } + + private void format_suffix_urls(string text) { + int absolute_start = message_text_view.buffer.text.length - text.length; + + Regex url_regex = new Regex("""(?i)\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))"""); + MatchInfo match_info; + url_regex.match(text, 0, out match_info); + for (; match_info.matches(); match_info.next()) { + string? url = match_info.fetch(0); + int start; + int end; + match_info.fetch_pos(0, out start, out end); + TextIter start_iter; + TextIter end_iter; + message_text_view.buffer.get_iter_at_offset(out start_iter, absolute_start + start); + message_text_view.buffer.get_iter_at_offset(out end_iter, absolute_start + end); + message_text_view.buffer.apply_tag_by_name("url", start_iter, end_iter); + } + } + + private void setup_tags() { + message_text_view.buffer.create_tag("url", underline: Pango.Underline.SINGLE, foreground: "blue"); + message_text_view.button_release_event.connect(open_url); + message_text_view.motion_notify_event.connect(change_cursor_over_url); + } + + private bool open_url(EventButton event_button) { + int buffer_x, buffer_y; + message_text_view.window_to_buffer_coords(TextWindowType.TEXT, (int) event_button.x, (int) event_button.y, out buffer_x, out buffer_y); + TextIter iter; + message_text_view.get_iter_at_location(out iter, buffer_x, buffer_y); + TextIter start_iter = iter, end_iter = iter; + if (start_iter.backward_to_tag_toggle(null) && end_iter.forward_to_tag_toggle(null)) { + string url = start_iter.get_text(end_iter); + try{ + AppInfo.launch_default_for_uri(url, null); + } catch (Error err) { + print("Tryed to open " + url); + } + } + return false; + } + + private bool change_cursor_over_url(EventMotion event_motion) { + TextIter iter; + message_text_view.get_iter_at_location(out iter, (int) event_motion.x, (int) event_motion.y); + if (iter.has_tag(message_text_view.buffer.tag_table.lookup("url"))) { + event_motion.window.set_cursor(new Cursor.for_display(get_display(), CursorType.HAND2)); + } else { + event_motion.window.set_cursor(new Cursor.for_display(get_display(), CursorType.XTERM)); + } + return false; + } + + private static string get_relative_time(DateTime datetime) { + DateTime now = new DateTime.now_local(); + TimeSpan timespan = now.difference(datetime); + if (timespan > 365 * TimeSpan.DAY) { + return datetime.format("%d.%m.%Y %H:%M"); + } else if (timespan > 7 * TimeSpan.DAY) { + return datetime.format("%d.%m %H:%M"); + } else if (timespan > 1 * TimeSpan.DAY) { + return datetime.format("%a, %H:%M"); + } else if (timespan > 9 * TimeSpan.MINUTE) { + return datetime.format("%H:%M"); + } else if (timespan > TimeSpan.MINUTE) { + return (timespan / TimeSpan.MINUTE).to_string() + " min ago"; + } else { + return "Just now"; + } + } +} + +} diff --git a/client/src/ui/conversation_summary/merged_status_item.vala b/client/src/ui/conversation_summary/merged_status_item.vala new file mode 100644 index 00000000..78b156e9 --- /dev/null +++ b/client/src/ui/conversation_summary/merged_status_item.vala @@ -0,0 +1,30 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +private class MergedStatusItem : Expander { + + private StreamInteractor stream_interactor; + private Conversation conversation; + private ArrayList statuses = new ArrayList(); + + public MergedStatusItem(StreamInteractor stream_interactor, Conversation conversation, Show show) { + set_hexpand(true); + add_status(show); + } + + public void add_status(Show show) { + statuses.add(show); + StatusItem status_item = new StatusItem(stream_interactor, conversation, @"is $(show.as)"); + if (statuses.size == 1) { + label = show.as; + } else { + label = @"changed their status $(statuses.size) times"; + add(new Label(show.as)); + } + } +} +} \ No newline at end of file diff --git a/client/src/ui/conversation_summary/status_item.vala b/client/src/ui/conversation_summary/status_item.vala new file mode 100644 index 00000000..5918d008 --- /dev/null +++ b/client/src/ui/conversation_summary/status_item.vala @@ -0,0 +1,29 @@ +using Gtk; +using Markup; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +private class StatusItem : Grid { + + private Image image = new Image(); + private Label label = new Label(""); + + private StreamInteractor stream_interactor; + private Conversation conversation; + + public StatusItem(StreamInteractor stream_interactor, Conversation conversation, string? text) { + Object(column_spacing : 7); + set_hexpand(true); + this.stream_interactor = stream_interactor; + this.conversation = conversation; + image.set_from_pixbuf((new AvatarGenerator(30, 30)).set_greyscale(true).draw_conversation(stream_interactor, conversation)); + attach(image, 0, 0, 1, 1); + attach(label, 1, 0, 1, 1); + string display_name = Util.get_display_name(stream_interactor, conversation.counterpart, conversation.account); + label.set_markup(@" $(escape_text(display_name)) $text "); + show_all(); + } +} +} \ No newline at end of file diff --git a/client/src/ui/conversation_summary/view.vala b/client/src/ui/conversation_summary/view.vala new file mode 100644 index 00000000..0ea1a32c --- /dev/null +++ b/client/src/ui/conversation_summary/view.vala @@ -0,0 +1,221 @@ +using Gee; +using Gtk; +using Pango; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui.ConversationSummary { + +[GtkTemplate (ui = "/org/dino-im/conversation_summary/view.ui")] +public class View : Box { + + public Conversation? conversation { get; private set; } + public HashMap message_items = new HashMap(Entities.Message.hash_func, Entities.Message.equals_func); + + [GtkChild] + private ScrolledWindow scrolled; + + [GtkChild] + private Box main; + + private StreamInteractor stream_interactor; + private MergedMessageItem? last_message_item; + private StatusItem typing_status; + private Entities.Message? earliest_message; + double? was_value; + double? was_upper; + double? was_page_size; + Object reloading_lock = new Object(); + bool reloading = false; + + public View(StreamInteractor stream_interactor) { + Object(homogeneous : false, spacing : 0); + this.stream_interactor = stream_interactor; + scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); + scrolled.vadjustment.notify["value"].connect(on_value_notify); + + CounterpartInteractionManager.get_instance(stream_interactor).received_state.connect((account, jid, state) => { + Idle.add(() => { on_received_state(account, jid, state); return false; }); + }); + MessageManager.get_instance(stream_interactor).message_received.connect((message, conversation) => { + Idle.add(() => { show_message(message, conversation, true); return false; }); + }); + MessageManager.get_instance(stream_interactor).message_sent.connect((message, conversation) => { + Idle.add(() => { show_message(message, conversation, true); return false; }); + }); + PresenceManager.get_instance(stream_interactor).show_received.connect((show, jid, account) => { + Idle.add(() => { on_show_received(show, jid, account); return false; }); + }); + Timeout.add_seconds(60, () => { + foreach (MergedMessageItem message_item in message_items.values) { + message_item.update(); + } + return true; + }); + } + + public void initialize_for_conversation(Conversation? conversation) { + this.conversation = conversation; + clear(); + message_items.clear(); + was_upper = null; + was_page_size = null; + last_message_item = null; + + ArrayList objects = new ArrayList(); + Gee.List? messages = MessageManager.get_instance(stream_interactor).get_messages(conversation); + if (messages != null && messages.size > 0) { + earliest_message = messages[0]; + objects.add_all(messages); + } + HashMap>? shows = PresenceManager.get_instance(stream_interactor).get_shows(conversation.counterpart, conversation.account); + if (shows != null) { + foreach (Jid jid in shows.keys) objects.add_all(shows[jid]); + } + objects.sort((a, b) => { + DateTime? dt1 = null; + DateTime? dt2 = null; + Entities.Message m1 = a as Entities.Message; + if (m1 != null) dt1 = m1.time; + Show s1 = a as Show; + if (s1 != null) dt1 = s1.datetime; + Entities.Message m2 = b as Entities.Message; + if (m2 != null) dt2 = m2.time; + Show s2 = b as Show; + if (s2 != null) dt2 = s2.datetime; + return dt1.compare(dt2); + }); + foreach (Object o in objects) { + Entities.Message message = o as Entities.Message; + Show show = o as Show; + if (message != null) { + show_message(message, conversation); + } else if (show != null) { + on_show_received(show, conversation.counterpart, conversation.account); + } + } + update_chat_state(); + } + + private void on_received_state(Account account, Jid jid, string state) { + if (conversation != null && conversation.account.equals(account) && conversation.counterpart.equals_bare(jid)) { + update_chat_state(state); + } + } + + private void update_chat_state(string? state = null) { + string? state_ = state; + if (state_ == null) { + state_ = CounterpartInteractionManager.get_instance(stream_interactor).get_chat_state(conversation.account, conversation.counterpart); + } + if (typing_status != null) { + main.remove(typing_status); + } + if (state_ != null) { + if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING || state_ == Xep.ChatStateNotifications.STATE_PAUSED) { + if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING) { + typing_status = new StatusItem(stream_interactor, conversation, "is typing..."); + } else if (state_ == Xep.ChatStateNotifications.STATE_PAUSED) { + typing_status = new StatusItem(stream_interactor, conversation, "has stoped typing"); + } + main.add(typing_status); + } + } + } + + private void on_show_received(Show show, Jid jid, Account account) { + + } + + private void on_upper_notify() { + if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1 || + scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size + scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down + } else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1){ + scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content + } + was_upper = scrolled.vadjustment.upper; + was_page_size = scrolled.vadjustment.page_size; + lock(reloading_lock) { + reloading = false; + } + } + + private void on_value_notify() { + if (scrolled.vadjustment.value < 200) { + load_earlier_messages(); + } + } + + private void load_earlier_messages() { + was_value = scrolled.vadjustment.value; + lock(reloading_lock) { + if(reloading) return; + reloading = true; + } + Gee.List? messages = MessageManager.get_instance(stream_interactor).get_messages_before(conversation, earliest_message); + if (messages != null && messages.size > 0) { + earliest_message = messages[0]; + MergedMessageItem? current_item = null; + int items_added = 0; + for (int i = 0; i < messages.size; i++) { + if (current_item != null && should_merge_message(current_item, messages[i])) { + current_item.add_message(messages[i]); + } else { + current_item = new MergedMessageItem(stream_interactor, conversation, messages[i]); + force_alloc_width(current_item, main.get_allocated_width()); + main.add(current_item); + message_items[messages[i]] = current_item; + main.reorder_child(current_item, items_added); + items_added++; + } + } + return; + } + reloading = false; + } + + private void show_message(Entities.Message message, Conversation conversation, bool animate = false) { + if (this.conversation != null && this.conversation.equals(conversation)) { + if (should_merge_message(last_message_item, message)) { + last_message_item.add_message(message); + } else { + MergedMessageItem message_item = new MergedMessageItem(stream_interactor, conversation, message); + if (animate) { + Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true}; + revealer.add(message_item); + force_alloc_width(revealer, main.get_allocated_width()); + main.add(revealer); + revealer.set_reveal_child(true); + } else { + force_alloc_width(message_item, main.get_allocated_width()); + main.add(message_item); + } + last_message_item = message_item; + } + message_items[message] = last_message_item; + update_chat_state(); + } + } + + private bool should_merge_message(MergedMessageItem? message_item, Entities.Message message) { + return message_item != null && + message_item.from.equals(message.from) && + message_item.messages.get(0).encryption == message.encryption && + message.time.difference(message_item.initial_time) < TimeSpan.MINUTE; + } + + private void force_alloc_width(Widget widget, int width) { + Allocation alloc = Allocation(); + widget.get_preferred_width(out alloc.width, null); + widget.get_preferred_height(out alloc.height, null); + alloc.width = width; + widget.size_allocate(alloc); + } + + private void clear() { + main.@foreach((widget) => { main.remove(widget); }); + } +} +} -- cgit v1.2.3-54-g00ecf