diff options
author | fiaxh <git@mx.ax.lt> | 2017-08-27 23:55:49 +0200 |
---|---|---|
committer | fiaxh <git@mx.ax.lt> | 2017-08-28 00:02:59 +0200 |
commit | 8bc0d107e740be468ee0c9dcd253de36355088d3 (patch) | |
tree | 36858e844d711eb18a68612fd815cb84f4c3a88f /main/src | |
parent | a807ded65cd907e04bab7b8cd27b5702b157e3a2 (diff) | |
download | dino-8bc0d107e740be468ee0c9dcd253de36355088d3.tar.gz dino-8bc0d107e740be468ee0c9dcd253de36355088d3.zip |
Plugins providing conversation items for ConversationView
Diffstat (limited to 'main/src')
-rw-r--r-- | main/src/ui/conversation_summary/chat_state_populator.vala | 116 | ||||
-rw-r--r-- | main/src/ui/conversation_summary/conversation_item.vala | 32 | ||||
-rw-r--r-- | main/src/ui/conversation_summary/conversation_item_skeleton.vala | 142 | ||||
-rw-r--r-- | main/src/ui/conversation_summary/conversation_view.vala | 173 | ||||
-rw-r--r-- | main/src/ui/conversation_summary/default_message_display.vala | 53 | ||||
-rw-r--r-- | main/src/ui/conversation_summary/merged_message_item.vala | 59 | ||||
-rw-r--r-- | main/src/ui/conversation_summary/message_item.vala | 122 | ||||
-rw-r--r-- | main/src/ui/conversation_summary/message_populator.vala | 66 | ||||
-rw-r--r-- | main/src/ui/conversation_summary/message_textview.vala | 8 | ||||
-rw-r--r-- | main/src/ui/conversation_summary/slashme_item.vala | 44 | ||||
-rw-r--r-- | main/src/ui/conversation_summary/slashme_message_display.vala | 75 | ||||
-rw-r--r-- | main/src/ui/conversation_summary/status_item.vala | 30 | ||||
-rw-r--r-- | main/src/ui/conversation_summary/view.vala | 216 | ||||
-rw-r--r-- | main/src/ui/unified_window.vala | 4 | ||||
-rw-r--r-- | main/src/ui/util/helper.vala | 4 |
15 files changed, 635 insertions, 509 deletions
diff --git a/main/src/ui/conversation_summary/chat_state_populator.vala b/main/src/ui/conversation_summary/chat_state_populator.vala new file mode 100644 index 00000000..06d0cf87 --- /dev/null +++ b/main/src/ui/conversation_summary/chat_state_populator.vala @@ -0,0 +1,116 @@ +using Gee; +using Gtk; + +using Dino.Entities; +using Xmpp; + +namespace Dino.Ui.ConversationSummary { + +class ChatStatePopulator : Plugins.ConversationItemPopulator, Object { + + public string id { get { return "chat_state"; } } + + private StreamInteractor? stream_interactor; + private Conversation? current_conversation; + private Plugins.ConversationItemCollection? item_collection; + + private MetaChatStateItem? meta_item; + + public ChatStatePopulator(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).received_state.connect((account, jid, state) => { + if (current_conversation != null && current_conversation.account.equals(account) && current_conversation.counterpart.equals_bare(jid)) { + Idle.add(() => { update_chat_state(account, jid, state); return false; }); + } + }); + stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect((message, conversation) => { + if (conversation.equals(current_conversation)) { + Idle.add(() => { update_chat_state(conversation.account, conversation.counterpart); return false; }); + } + }); + } + + public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) { + current_conversation = conversation; + this.item_collection = item_collection; + this.meta_item = null; + + update_chat_state(conversation.account, conversation.counterpart); + } + + public void close(Conversation conversation) { } + + public void populate_timespan(Conversation conversation, DateTime from, DateTime to) { } + + public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { } + + private void update_chat_state(Account account, Jid jid, string? state = null) { + string? state_ = state; + if (state_ == null) { + state_ = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_chat_state(current_conversation.account, current_conversation.counterpart); + } + string? new_text = null; + if (state_ != null) { + if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING || state_ == Xep.ChatStateNotifications.STATE_PAUSED) { + string display_name = Util.get_display_name(stream_interactor, jid, account); + if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING) { + new_text = _("is typing..."); + } else if (state_ == Xep.ChatStateNotifications.STATE_PAUSED) { + new_text = _("has stopped typing"); + } + } + } + if (meta_item != null && new_text == null) { + item_collection.remove_item(meta_item); + meta_item = null; + } else if (meta_item != null && new_text != null) { + meta_item.set_text(new_text); + } else if (new_text != null) { + meta_item = new MetaChatStateItem(stream_interactor, current_conversation, jid, new_text); + item_collection.insert_item(meta_item); + } + + } +} + +public class MetaChatStateItem : Plugins.MetaConversationItem { + public override Jid? jid { get; set; } + public override bool dim { get; set; default=true; } + public override DateTime? sort_time { get; set; default=new DateTime.now_utc().add_years(10); } + + public override bool can_merge { get; set; default=false; } + public override bool requires_avatar { get; set; default=true; } + public override bool requires_header { get; set; default=false; } + + private StreamInteractor stream_interactor; + private Conversation conversation; + private string text; + private Label label; + + public MetaChatStateItem(StreamInteractor stream_interactor, Conversation conversation, Jid jid, string text) { + this.stream_interactor = stream_interactor; + this.conversation = conversation; + this.jid = jid; + this.text = text; + } + + public override Object get_widget(Plugins.WidgetType widget_type) { + label = new Label("") { xalign=0, vexpand=true, visible=true }; + label.get_style_context().add_class("dim-label"); + update_text(); + return label; + } + + public void set_text(string text) { + this.text = text; + update_text(); + } + + private void update_text() { + string display_name = Util.get_display_name(stream_interactor, jid, conversation.account); + label.label = display_name + " " + text; + } +} + +} diff --git a/main/src/ui/conversation_summary/conversation_item.vala b/main/src/ui/conversation_summary/conversation_item.vala deleted file mode 100644 index a99025ab..00000000 --- a/main/src/ui/conversation_summary/conversation_item.vala +++ /dev/null @@ -1,32 +0,0 @@ -using Dino.Entities; - -namespace Dino.Ui.ConversationSummary { - -public enum MessageKind { - TEXT, - ME_COMMAND -} - -public MessageKind get_message_kind(Message message) { - if (message.body.has_prefix("/me ")) { - return MessageKind.ME_COMMAND; - } else { - return MessageKind.TEXT; - } -} - -public interface ConversationItem : Gtk.Widget { - public abstract bool merge(Entities.Message message); - - public static ConversationItem create_for_message(StreamInteractor stream_interactor, Conversation conversation, Message message) { - switch (get_message_kind(message)) { - case MessageKind.TEXT: - return new MergedMessageItem(stream_interactor, conversation, message); - case MessageKind.ME_COMMAND: - return new SlashMeItem(stream_interactor, conversation, message); - } - assert_not_reached(); - } -} - -}
\ No newline at end of file diff --git a/main/src/ui/conversation_summary/conversation_item_skeleton.vala b/main/src/ui/conversation_summary/conversation_item_skeleton.vala new file mode 100644 index 00000000..b30d45d3 --- /dev/null +++ b/main/src/ui/conversation_summary/conversation_item_skeleton.vala @@ -0,0 +1,142 @@ +using Gee; +using Gdk; +using Gtk; +using Markup; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +[GtkTemplate (ui = "/im/dino/conversation_summary/message_item.ui")] +public class ConversationItemSkeleton : Grid { + + [GtkChild] private Image image; + [GtkChild] private Label time_label; + [GtkChild] private Image encryption_image; + [GtkChild] private Image received_image; + + public StreamInteractor stream_interactor; + public Conversation conversation { get; set; } + public Gee.List<Plugins.MetaConversationItem> items = new ArrayList<Plugins.MetaConversationItem>(); + + private Box box = new Box(Orientation.VERTICAL, 2) { visible=true }; + + public ConversationItemSkeleton(StreamInteractor stream_interactor, Conversation conversation) { + this.conversation = conversation; + this.stream_interactor = stream_interactor; + + set_main_widget(box); + } + + public void add_meta_item(Plugins.MetaConversationItem item) { + items.add(item); + if (items.size == 1) { + setup(item); + } + Widget widget = (Widget) item.get_widget(Plugins.WidgetType.GTK); + if (item.requires_header) { + box.add(widget); + } else { + set_title_widget(widget); + } + item.notify["mark"].connect_after(update_received); + update_received(); + } + + public void set_title_widget(Widget w) { + attach(w, 1, 0, 1, 1); + } + + public void set_main_widget(Widget w) { + attach(w, 1, 1, 2, 1); + } + + public void update_time() { + if (items.size > 0 && items[0].display_time != null) { + time_label.label = get_relative_time(items[0].display_time.to_local()); + } + } + + private void setup(Plugins.MetaConversationItem item) { + update_time(); + Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).set_greyscale(item.dim).draw_jid(stream_interactor, item.jid, conversation.account)); + if (item.requires_header) { + set_default_title_widget(item.jid); + } + if (item.encryption != null && item.encryption != Encryption.NONE) { + encryption_image.visible = true; + encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR); + } + } + + private void set_default_title_widget(Jid jid) { + Label name_label = new Label("") { use_markup=true, xalign=0, hexpand=true, visible=true }; + string display_name = Util.get_display_name(stream_interactor, jid, conversation.account); + string color = Util.get_name_hex_color(stream_interactor, conversation.account, jid, Util.is_dark_theme(name_label)); + name_label.label = @"<span foreground=\"#$color\">$display_name</span>"; + name_label.style_updated.connect(() => { + string new_color = Util.get_name_hex_color(stream_interactor, conversation.account, jid, Util.is_dark_theme(name_label)); + name_label.set_markup(@"<span foreground=\"#$new_color\">$display_name</span>"); + }); + set_title_widget(name_label); + } + + private void update_received() { + bool all_received = true; + bool all_read = true; + foreach (Plugins.MetaConversationItem item in items) { + if (item.mark == Message.Marked.WONTSEND) { + received_image.visible = true; + received_image.set_from_icon_name("dialog-warning-symbolic", IconSize.SMALL_TOOLBAR); + Util.force_error_color(received_image); + Util.force_error_color(encryption_image); + Util.force_error_color(time_label); + return; + } else if (item.mark != Message.Marked.READ) { + all_read = false; + if (item.mark != Message.Marked.RECEIVED) { + all_received = false; + } + } + } + if (all_read) { + received_image.visible = true; + received_image.set_from_icon_name("dino-double-tick-symbolic", IconSize.SMALL_TOOLBAR); + } else if (all_received) { + received_image.visible = true; + received_image.set_from_icon_name("dino-tick-symbolic", IconSize.SMALL_TOOLBAR); + } else if (received_image.visible) { + received_image.set_from_icon_name("image-loading-symbolic", IconSize.SMALL_TOOLBAR); + } + } + + 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(Util.is_24h_format() ? + /* xgettext:no-c-format */ /* Date + time in 24h format (w/o seconds) */ _("%x, %H\u2236%M") : + /* xgettext:no-c-format */ /* Date + time in 12h format (w/o seconds)*/ _("%x, %l\u2236%M %p")); + } else if (timespan > 7 * TimeSpan.DAY) { + return datetime.format(Util.is_24h_format() ? + /* xgettext:no-c-format */ /* Month, day and time in 24h format (w/o seconds) */ _("%b %d, %H\u2236%M") : + /* xgettext:no-c-format */ /* Month, day and time in 12h format (w/o seconds) */ _("%b %d, %l\u2236%M %p")); + } else if (datetime.get_day_of_month() != new DateTime.now_utc().get_day_of_month()) { + return datetime.format(Util.is_24h_format() ? + /* xgettext:no-c-format */ /* Day of week and time in 12h format (w/o seconds) */ _("%a, %H\u2236%M") : + /* xgettext:no-c-format */ _("%a, %l\u2236%M %p")); + } else if (timespan > 9 * TimeSpan.MINUTE) { + return datetime.format(Util.is_24h_format() ? + /* xgettext:no-c-format */ /* Time in 24h format (w/o seconds) */ _("%H\u2236%M") : + /* xgettext:no-c-format */ /* Time in 12h format (w/o seconds) */ _("%l\u2236%M %p")); + } else if (timespan > TimeSpan.MINUTE) { + ulong mins = (ulong) (timespan.abs() / TimeSpan.MINUTE); + /* xgettext:this is the beginning of a sentence. */ + return n("%i min ago", "%i mins ago", mins).printf(mins); + } else { + return _("Just now"); + } + } +} + +} diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala new file mode 100644 index 00000000..b090e5d7 --- /dev/null +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -0,0 +1,173 @@ +using Gee; +using Gtk; +using Pango; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +[GtkTemplate (ui = "/im/dino/conversation_summary/view.ui")] +public class ConversationView : Box, Plugins.ConversationItemCollection { + + public Conversation? conversation { get; private set; } + + [GtkChild] private ScrolledWindow scrolled; + [GtkChild] private Box main; + [GtkChild] private Stack stack; + + private StreamInteractor stream_interactor; + private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>((a, b) => { return a.sort_time.compare(b.sort_time); }); + private Gee.Map<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>> meta_after_items = new Gee.HashMap<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>>(); + private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>(); + private Gee.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>(); + private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>(); + private MessagePopulator message_item_populator; + + private double? was_value; + private double? was_upper; + private double? was_page_size; + + private Mutex reloading_mutex = new Mutex(); + private bool animate = false; + + public ConversationView(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); + scrolled.vadjustment.notify["value"].connect(on_value_notify); + + message_item_populator = new MessagePopulator(stream_interactor); + + Application app = GLib.Application.get_default() as Application; + app.plugin_registry.register_conversation_item_populator(new ChatStatePopulator(stream_interactor)); + + Timeout.add_seconds(60, () => { + foreach (ConversationItemSkeleton item_skeleton in item_skeletons) { + item_skeleton.update_time(); + } + return true; + }); + + Util.force_base_background(this); + } + + public void initialize_for_conversation(Conversation? conversation) { + this.conversation = conversation; + stack.set_visible_child_name("void"); + clear(); + was_upper = null; + was_page_size = null; + animate = false; + Timeout.add(20, () => { animate = true; return false; }); + + message_item_populator.init(conversation, this); + message_item_populator.populate_number(conversation, new DateTime.now_utc(), 50); + + Dino.Application app = Dino.Application.get_default(); + foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) { + populator.init(conversation, this, Plugins.WidgetType.GTK); + } + + stack.set_visible_child_name("main"); + } + + public void insert_item(Plugins.MetaConversationItem item) { + meta_items.add(item); + if (!item.can_merge || !merge_back(item)) { + insert_new(item); + } + } + + public void remove_item(Plugins.MetaConversationItem item) { + main.remove(widgets[item]); + widgets.unset(item); + meta_items.remove(item); + item_skeletons.remove(item_item_skeletons[item]); + item_item_skeletons.unset(item); + } + + private bool merge_back(Plugins.MetaConversationItem item) { + Plugins.MetaConversationItem? lower_item = meta_items.lower(item); + if (lower_item != null) { + ConversationItemSkeleton lower_skeleton = item_item_skeletons[lower_item]; + Plugins.MetaConversationItem lower_start_item = lower_skeleton.items[0]; + if (lower_start_item.can_merge && + item.display_time.difference(lower_start_item.display_time) < TimeSpan.MINUTE && + lower_start_item.jid.equals(item.jid) && + lower_start_item.encryption == item.encryption && + item.mark != Message.Marked.WONTSEND) { + lower_skeleton.add_meta_item(item); + force_alloc_width(lower_skeleton, main.get_allocated_width()); + item_item_skeletons[item] = lower_skeleton; + return true; + } + } + return false; + } + + private void insert_new(Plugins.MetaConversationItem item) { + ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation); + item_skeleton.add_meta_item(item); + item_item_skeletons[item] = item_skeleton; + Plugins.MetaConversationItem? lower_item = meta_items.lower(item); + int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0; + item_skeletons.insert(index, item_skeleton); + + Widget insert = item_skeleton; + if (animate) { + Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true}; + revealer.add(item_skeleton); + insert = revealer; + main.add(insert); + revealer.reveal_child = true; + } else { + main.add(insert); + } + widgets[item] = insert; + force_alloc_width(insert, main.get_allocated_width()); + main.reorder_child(insert, index); + } + + 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; + reloading_mutex.trylock(); + reloading_mutex.unlock(); + } + + private void on_value_notify() { + if (scrolled.vadjustment.value < 200) { + load_earlier_messages(); + } + } + + private void load_earlier_messages() { + was_value = scrolled.vadjustment.value; + if (!reloading_mutex.trylock()) return; + if (meta_items.size > 0) message_item_populator.populate_number(conversation, meta_items.first().sort_time, 20); + } + + // Workaround GTK TextView issues + 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() { + meta_items.clear(); + meta_after_items.clear(); + item_skeletons.clear(); + item_item_skeletons.clear(); + main.@foreach((widget) => { main.remove(widget); }); + } +} + +} diff --git a/main/src/ui/conversation_summary/default_message_display.vala b/main/src/ui/conversation_summary/default_message_display.vala new file mode 100644 index 00000000..6082253d --- /dev/null +++ b/main/src/ui/conversation_summary/default_message_display.vala @@ -0,0 +1,53 @@ +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +public class DefaultMessageDisplay : Plugins.MessageDisplayProvider, Object { + public string id { get; set; default="default"; } + public double priority { get; set; default=0; } + + public StreamInteractor stream_interactor; + + public DefaultMessageDisplay(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public bool can_display(Entities.Message? message) { return true; } + + public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) { + return new MetaMessageItem(stream_interactor, message, conversation); + } +} + +public class MetaMessageItem : Plugins.MetaConversationItem { + public override Jid? jid { get; set; } + public override DateTime? sort_time { get; set; } + public override DateTime? display_time { get; set; } + public override Encryption? encryption { get; set; } + + private StreamInteractor stream_interactor; + private Conversation conversation; + private Message message; + + public MetaMessageItem(StreamInteractor stream_interactor, Message message, Conversation conversation) { + this.stream_interactor = stream_interactor; + this.conversation = conversation; + this.message = message; + this.jid = message.from; + this.sort_time = message.local_time; + this.display_time = message.time; + this.encryption = message.encryption; + } + + public override bool can_merge { get; set; default=true; } + public override bool requires_avatar { get; set; default=true; } + public override bool requires_header { get; set; default=true; } + + public override Object get_widget(Plugins.WidgetType widget_type) { + MessageTextView text_view = new MessageTextView() { visible = true }; + text_view.add_text(message.body); + return text_view; + } +} + +} diff --git a/main/src/ui/conversation_summary/merged_message_item.vala b/main/src/ui/conversation_summary/merged_message_item.vala deleted file mode 100644 index 4cabebac..00000000 --- a/main/src/ui/conversation_summary/merged_message_item.vala +++ /dev/null @@ -1,59 +0,0 @@ -using Gee; -using Gdk; -using Gtk; -using Markup; - -using Dino.Entities; - -namespace Dino.Ui.ConversationSummary { - -public class MergedMessageItem : MessageItem { - - private Label name_label = new Label("") { xalign=0, visible=true, hexpand=true }; - private MessageTextView textview = new MessageTextView() { visible=true }; - - public MergedMessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) { - base(stream_interactor, conversation, message); - set_main_widget(textview); - set_title_widget(name_label); - add_message(message); - - string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account); - string color = Util.get_name_hex_color(stream_interactor, conversation.account, message.from, false); - name_label.set_markup(@"<span foreground=\"#$color\">$display_name</span>"); - - textview.style_updated.connect(update_display_style); - update_display_style(); - } - - public override void add_message(Message message) { - base.add_message(message); - if (messages.size > 1) textview.add_text("\n"); - string text = message.body; - if (text.length > 10000) { - text = text.slice(0, 10000) + " [" + _("Message too long") + "]"; - } - textview.add_text(text); - } - - public override bool merge(Message message) { - if (get_message_kind(message) == MessageKind.TEXT && - this.from.equals(message.from) && - this.messages[0].encryption == message.encryption && - message.time.difference(initial_time) < TimeSpan.MINUTE && - this.messages[0].marked != Entities.Message.Marked.WONTSEND) { - add_message(message); - return true; - } - return false; - - } - - private void update_display_style() { - string display_name = Util.get_message_display_name(stream_interactor, messages[0], conversation.account); - string color = Util.get_name_hex_color(stream_interactor, conversation.account, messages[0].real_jid ?? messages[0].from, Util.is_dark_theme(textview)); - name_label.set_markup(@"<span foreground=\"#$color\">$display_name</span>"); - } -} - -} diff --git a/main/src/ui/conversation_summary/message_item.vala b/main/src/ui/conversation_summary/message_item.vala deleted file mode 100644 index f669c021..00000000 --- a/main/src/ui/conversation_summary/message_item.vala +++ /dev/null @@ -1,122 +0,0 @@ -using Gee; -using Gdk; -using Gtk; -using Markup; - -using Dino.Entities; - -namespace Dino.Ui.ConversationSummary { - -[GtkTemplate (ui = "/im/dino/conversation_summary/message_item.ui")] -public class MessageItem : Grid, ConversationItem { - - [GtkChild] private Image image; - [GtkChild] private Label time_label; - [GtkChild] private Image encryption_image; - [GtkChild] private Image received_image; - - public StreamInteractor stream_interactor; - public Conversation conversation { get; set; } - public Jid from { get; private set; } - public DateTime initial_time { get; private set; } - public ArrayList<Message> messages = new ArrayList<Message>(Message.equals_func); - - public MessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) { - this.conversation = conversation; - this.stream_interactor = stream_interactor; - this.initial_time = message.time; - this.from = message.from; - - if (message.encryption != Encryption.NONE) { - encryption_image.visible = true; - encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR); - } - - time_label.label = get_relative_time(initial_time.to_local()); - Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_message(stream_interactor, message)); - } - - public void set_title_widget(Widget w) { - attach(w, 1, 0, 1, 1); - } - - public void set_main_widget(Widget w) { - attach(w, 1, 1, 2, 1); - } - - public void update() { - time_label.label = get_relative_time(initial_time.to_local()); - } - - public virtual void add_message(Message message) { - messages.add(message); - - message.notify["marked"].connect_after(() => { - Idle.add(() => { update_received(); return false; }); - }); - update_received(); - } - - public virtual bool merge(Message message) { - return false; - } - - private void update_received() { - bool all_received = true; - bool all_read = true; - foreach (Message message in messages) { - if (message.marked == Message.Marked.WONTSEND) { - received_image.visible = true; - received_image.set_from_icon_name("dialog-warning-symbolic", IconSize.SMALL_TOOLBAR); - Util.force_error_color(received_image); - Util.force_error_color(encryption_image); - Util.force_error_color(time_label); - return; - } else 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_icon_name("dino-double-tick-symbolic", IconSize.SMALL_TOOLBAR); - } else if (all_received) { - received_image.visible = true; - received_image.set_from_icon_name("dino-tick-symbolic", IconSize.SMALL_TOOLBAR); - } else if (received_image.visible) { - received_image.set_from_icon_name("image-loading-symbolic", IconSize.SMALL_TOOLBAR); - } - } - - 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(Util.is_24h_format() ? - /* xgettext:no-c-format */ /* Date + time in 24h format (w/o seconds) */ _("%x, %H\u2236%M") : - /* xgettext:no-c-format */ /* Date + time in 12h format (w/o seconds)*/ _("%x, %l\u2236%M %p")); - } else if (timespan > 7 * TimeSpan.DAY) { - return datetime.format(Util.is_24h_format() ? - /* xgettext:no-c-format */ /* Month, day and time in 24h format (w/o seconds) */ _("%b %d, %H\u2236%M") : - /* xgettext:no-c-format */ /* Month, day and time in 12h format (w/o seconds) */ _("%b %d, %l\u2236%M %p")); - } else if (timespan > 1 * TimeSpan.DAY) { - return datetime.format(Util.is_24h_format() ? - /* xgettext:no-c-format */ /* Day of week and time in 12h format (w/o seconds) */ _("%a, %H\u2236%M") : - /* xgettext:no-c-format */ _("%a, %l\u2236%M %p")); - } else if (timespan > 9 * TimeSpan.MINUTE) { - return datetime.format(Util.is_24h_format() ? - /* xgettext:no-c-format */ /* Time in 24h format (w/o seconds) */ _("%H\u2236%M") : - /* xgettext:no-c-format */ /* Time in 12h format (w/o seconds) */ _("%l\u2236%M %p")); - } else if (timespan > TimeSpan.MINUTE) { - ulong mins = (ulong) (timespan.abs() / TimeSpan.MINUTE); - /* xgettext:this is the beginning of a sentence. */ - return n("%i min ago", "%i mins ago", mins).printf(mins); - } else { - return _("Just now"); - } - } -} - -} diff --git a/main/src/ui/conversation_summary/message_populator.vala b/main/src/ui/conversation_summary/message_populator.vala new file mode 100644 index 00000000..2c3eccd2 --- /dev/null +++ b/main/src/ui/conversation_summary/message_populator.vala @@ -0,0 +1,66 @@ +using Gee; +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +public class MessagePopulator : Object { + + private StreamInteractor? stream_interactor; + private Conversation? current_conversation; + private Plugins.ConversationItemCollection? item_collection; + + public MessagePopulator(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + Application app = GLib.Application.get_default() as Application; + app.plugin_registry.register_message_display(new DefaultMessageDisplay(stream_interactor)); + app.plugin_registry.register_message_display(new SlashmeMessageDisplay(stream_interactor)); + + + stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect((message, conversation) => { + Idle.add(() => { handle_message(message, conversation); return false; }); + }); + stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect((message, conversation) => { + Idle.add(() => { handle_message(message, conversation); return false; }); + }); + } + + public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection) { + current_conversation = conversation; + this.item_collection = item_collection; + } + + public void close(Conversation conversation) { } + + public void populate_number(Conversation conversation, DateTime from, int n) { + Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before(conversation, from, n); + if (messages != null) { + foreach (Entities.Message message in messages) { + handle_message(message, conversation); + } + } + } + + private void handle_message(Message message, Conversation conversation) { + if (!conversation.equals(current_conversation)) return; + + Plugins.MessageDisplayProvider? best_provider = null; + int priority = -1; + Application app = GLib.Application.get_default() as Application; + foreach (Plugins.MessageDisplayProvider provider in app.plugin_registry.message_displays) { + if (provider.can_display(message) && provider.priority > priority) { + best_provider = provider; + } + } + Plugins.MetaConversationItem meta_item = best_provider.get_item(message, conversation); + meta_item.mark = message.marked; + message.notify["marked"].connect(() => { + meta_item.mark = message.marked; + }); + item_collection.insert_item(meta_item); + } +} + +} diff --git a/main/src/ui/conversation_summary/message_textview.vala b/main/src/ui/conversation_summary/message_textview.vala index 80759207..f2a4ca22 100644 --- a/main/src/ui/conversation_summary/message_textview.vala +++ b/main/src/ui/conversation_summary/message_textview.vala @@ -27,7 +27,11 @@ public class MessageTextView : TextView { minimum_width = 0; } - public void add_text(string text) { + public void add_text(string text_) { + string text = text_; + if (text.length > 10000) { + text = text.slice(0, 10000) + " [" + _("Message too long") + "]"; + } TextIter end; buffer.get_end_iter(out end); buffer.insert(ref end, text, -1); @@ -90,4 +94,4 @@ public class MessageTextView : TextView { } } -}
\ No newline at end of file +} diff --git a/main/src/ui/conversation_summary/slashme_item.vala b/main/src/ui/conversation_summary/slashme_item.vala deleted file mode 100644 index 2056d2d1..00000000 --- a/main/src/ui/conversation_summary/slashme_item.vala +++ /dev/null @@ -1,44 +0,0 @@ -using Gdk; -using Gtk; - -using Dino.Entities; - -namespace Dino.Ui.ConversationSummary { - -public class SlashMeItem : MessageItem { - - private Box box = new Box(Orientation.VERTICAL, 0) { visible=true, vexpand=true }; - private MessageTextView textview = new MessageTextView() { visible=true }; - private string text; - private TextTag nick_tag; - - public SlashMeItem(StreamInteractor stream_interactor, Conversation conversation, Message message) { - base(stream_interactor, conversation, message); - box.set_center_widget(textview); - set_title_widget(box); - text = message.body.substring(3); - - string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account); - string color = Util.get_name_hex_color(stream_interactor, conversation.account, conversation.counterpart, false); - nick_tag = textview.buffer.create_tag("nick", foreground: "#" + color); - TextIter iter; - textview.buffer.get_start_iter(out iter); - textview.buffer.insert_with_tags(ref iter, display_name, display_name.length, nick_tag); - textview.add_text(text); - add_message(message); - - textview.style_updated.connect(update_display_style); - update_display_style(); - } - - public override bool merge(Message message) { - return false; - } - - private void update_display_style() { - string color = Util.get_name_hex_color(stream_interactor, conversation.account, messages[0].real_jid ?? messages[0].from, Util.is_dark_theme(textview)); - nick_tag.foreground = "#" + color; - } -} - -} diff --git a/main/src/ui/conversation_summary/slashme_message_display.vala b/main/src/ui/conversation_summary/slashme_message_display.vala new file mode 100644 index 00000000..58d93142 --- /dev/null +++ b/main/src/ui/conversation_summary/slashme_message_display.vala @@ -0,0 +1,75 @@ +using Gtk; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +public class SlashmeMessageDisplay : Plugins.MessageDisplayProvider, Object { + public string id { get; set; default="slashme"; } + public double priority { get; set; default=1; } + + public StreamInteractor stream_interactor; + + public SlashmeMessageDisplay(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public bool can_display(Entities.Message? message) { + return message.body.has_prefix("/me"); + } + + public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) { + return new MetaSlashmeItem(stream_interactor, message, conversation); + } +} + +public class MetaSlashmeItem : Plugins.MetaConversationItem { + public override Jid? jid { get; set; } + public override DateTime? sort_time { get; set; } + public override DateTime? display_time { get; set; } + public override Encryption? encryption { get; set; } + + private StreamInteractor stream_interactor; + private Conversation conversation; + private Message message; + private TextTag nick_tag; + private MessageTextView text_view; + + public MetaSlashmeItem(StreamInteractor stream_interactor, Message message, Conversation conversation) { + this.stream_interactor = stream_interactor; + this.conversation = conversation; + this.message = message; + this.jid = message.from; + this.sort_time = message.local_time; + this.display_time = message.time; + this.encryption = message.encryption; + } + + public override bool can_merge { get; set; default=false; } + public override bool requires_avatar { get; set; default=true; } + public override bool requires_header { get; set; default=false; } + + public override Object get_widget(Plugins.WidgetType widget_type) { + text_view = new MessageTextView() { valign=Align.CENTER, vexpand=true, visible = true }; + + string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account); + string color = Util.get_name_hex_color(stream_interactor, conversation.account, conversation.counterpart, Util.is_dark_theme(text_view)); + nick_tag = text_view.buffer.create_tag("nick", foreground: "#" + color); + TextIter iter; + text_view.buffer.get_start_iter(out iter); + text_view.buffer.insert_with_tags(ref iter, display_name, display_name.length, nick_tag); + + text_view.add_text(message.body.substring(3)); + text_view.style_updated.connect(update_style); + text_view.realize.connect(update_style); + return text_view; + } + + private void update_style() { + string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account); + string color = Util.get_name_hex_color(stream_interactor, conversation.account, message.real_jid ?? message.from, Util.is_dark_theme(text_view)); + nick_tag.foreground = "#" + color; + } +} + +} diff --git a/main/src/ui/conversation_summary/status_item.vala b/main/src/ui/conversation_summary/status_item.vala deleted file mode 100644 index 1704356c..00000000 --- a/main/src/ui/conversation_summary/status_item.vala +++ /dev/null @@ -1,30 +0,0 @@ -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(@"<span foreground=\"#B1B1B1\">$(escape_text(display_name)) $text</span>"); - show_all(); - } -} - -}
\ No newline at end of file diff --git a/main/src/ui/conversation_summary/view.vala b/main/src/ui/conversation_summary/view.vala deleted file mode 100644 index 693f7164..00000000 --- a/main/src/ui/conversation_summary/view.vala +++ /dev/null @@ -1,216 +0,0 @@ -using Gee; -using Gtk; -using Pango; - -using Dino.Entities; -using Xmpp; - -namespace Dino.Ui.ConversationSummary { - -[GtkTemplate (ui = "/im/dino/conversation_summary/view.ui")] -public class View : Box { - - public Conversation? conversation { get; private set; } - public HashMap<Entities.Message, ConversationItem> conversation_items = new HashMap<Entities.Message, ConversationItem>(Entities.Message.hash_func, Entities.Message.equals_func); - - [GtkChild] private ScrolledWindow scrolled; - [GtkChild] private Box main; - [GtkChild] private Stack stack; - - private StreamInteractor stream_interactor; - private ConversationItem? last_conversation_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) { - this.stream_interactor = stream_interactor; - scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); - scrolled.vadjustment.notify["value"].connect(on_value_notify); - - stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).received_state.connect((account, jid, state) => { - Idle.add(() => { on_received_state(account, jid, state); return false; }); - }); - stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect((message, conversation) => { - Idle.add(() => { show_message(message, conversation, true); return false; }); - }); - stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect((message, conversation) => { - Idle.add(() => { show_message(message, conversation, true); return false; }); - }); - stream_interactor.get_module(PresenceManager.IDENTITY).show_received.connect((show, jid, account) => { - Idle.add(() => { on_show_received(show, jid, account); return false; }); - }); - Timeout.add_seconds(60, () => { - foreach (ConversationItem conversation_item in conversation_items.values) { - MessageItem message_item = conversation_item as MessageItem; - if (message_item != null) message_item.update(); - } - return true; - }); - - Util.force_base_background(this); - } - - public void initialize_for_conversation(Conversation? conversation) { - this.conversation = conversation; - stack.set_visible_child_name("void"); - clear(); - conversation_items.clear(); - was_upper = null; - was_page_size = null; - last_conversation_item = null; - - ArrayList<Object> objects = new ArrayList<Object>(); - Gee.List<Entities.Message> messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages(conversation); - if (messages.size > 0) { - earliest_message = messages[messages.size -1]; - objects.add_all(messages); - } - HashMap<Jid, ArrayList<Show>>? shows = stream_interactor.get_module(PresenceManager.IDENTITY).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(); - stack.set_visible_child_name("main"); - } - - 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_ = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).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 stopped 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() { - if (earliest_message == null) return; - - was_value = scrolled.vadjustment.value; - lock(reloading_lock) { - if(reloading) return; - reloading = true; - } - Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).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 || !current_item.merge(messages[i])) { - current_item = new MergedMessageItem(stream_interactor, conversation, messages[i]); - force_alloc_width(current_item, main.get_allocated_width()); - main.add(current_item); - conversation_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 (last_conversation_item == null || !last_conversation_item.merge(message)) { - ConversationItem conversation_item = ConversationItem.create_for_message(stream_interactor, conversation, message); - if (animate) { - Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true}; - revealer.add(conversation_item); - force_alloc_width(revealer, main.get_allocated_width()); - main.add(revealer); - revealer.set_reveal_child(true); - } else { - force_alloc_width(conversation_item, main.get_allocated_width()); - main.add(conversation_item); - } - last_conversation_item = conversation_item; - } - conversation_items[message] = last_conversation_item; - update_chat_state(); - } - } - - // Workaround GTK TextView issues - 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); }); - } -} - -} diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala index 8244c67a..3a419161 100644 --- a/main/src/ui/unified_window.vala +++ b/main/src/ui/unified_window.vala @@ -12,7 +12,7 @@ public class UnifiedWindow : Window { private ChatInput.View chat_input; private ConversationListTitlebar conversation_list_titlebar; private ConversationSelector.View filterable_conversation_list; - private ConversationSummary.View conversation_frame; + private ConversationSummary.ConversationView conversation_frame; private ConversationTitlebar conversation_titlebar; private HeaderBar placeholder_headerbar = new HeaderBar() { title="Dino", show_close_button=true, visible=true }; private Paned headerbar_paned = new Paned(Orientation.HORIZONTAL) { visible=true }; @@ -69,7 +69,7 @@ public class UnifiedWindow : Window { private void setup_unified() { chat_input = new ChatInput.View(stream_interactor) { visible=true }; - conversation_frame = new ConversationSummary.View(stream_interactor) { visible=true }; + conversation_frame = new ConversationSummary.ConversationView(stream_interactor) { visible=true }; filterable_conversation_list = new ConversationSelector.View(stream_interactor) { visible=true }; Grid grid = new Grid() { orientation=Orientation.VERTICAL, visible=true }; diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index f1355b39..a2dde504 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -119,8 +119,8 @@ public static void force_error_color(Gtk.Widget widget, string selector = "*") { } public static bool is_dark_theme(Gtk.Widget widget) { - Gdk.RGBA bg = widget.get_style_context().get_background_color(StateFlags.NORMAL); - return (bg.red < 0.5 && bg.green < 0.5 && bg.blue < 0.5); + Gdk.RGBA bg = widget.get_style_context().get_color(StateFlags.NORMAL); + return (bg.red > 0.5 && bg.green > 0.5 && bg.blue > 0.5); } public static bool is_24h_format() { |