diff options
Diffstat (limited to 'main/src/ui')
33 files changed, 1429 insertions, 1020 deletions
diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala index 22d6d93d..86a4e288 100644 --- a/main/src/ui/application.vala +++ b/main/src/ui/application.vala @@ -32,7 +32,7 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application { window = new UnifiedWindow(this, stream_interactor); notifications = new Notifications(stream_interactor, window); notifications.start(); - notifications.conversation_selected.connect(window.on_conversation_selected); + notifications.conversation_selected.connect((conversation) => window.on_conversation_selected(conversation)); } window.present(); }); diff --git a/main/src/ui/avatar_image.vala b/main/src/ui/avatar_image.vala index 26955ae0..f690637b 100644 --- a/main/src/ui/avatar_image.vala +++ b/main/src/ui/avatar_image.vala @@ -18,6 +18,8 @@ public class AvatarImage : Misc { private bool gray; private Jid[] current_jids; private Gdk.Pixbuf[] current_avatars; + private Cairo.ImageSurface? cached_surface; + private static int8 use_image_surface = -1; public AvatarImage() { can_focus = false; @@ -78,9 +80,30 @@ public class AvatarImage : Misc { hex_color.length > 6 ? (double) hex_color.substring(6, 2).to_long(null, 16) / 255 : 1); } - public override bool draw(Cairo.Context ctx) { + public override bool draw(Cairo.Context ctx_in) { if (text_only == null && (current_jids == null || current_avatars == null || current_jids.length == 0)) return false; - double radius = 3; + + Cairo.Context ctx = ctx_in; + int width = this.width, height = this.height, base_factor = 1; + if (use_image_surface == -1) { + // TODO: detect if we have to buffer in image surface + use_image_surface = 1; + } + if (use_image_surface == 1) { + ctx_in.scale(1f/scale_factor, 1f/scale_factor); + if (cached_surface != null) { + ctx_in.set_source_surface(cached_surface, 0, 0); + ctx_in.paint(); + return true; + } + width *= scale_factor; + height *= scale_factor; + base_factor *= scale_factor; + cached_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height); + ctx = new Cairo.Context(cached_surface); + } + + double radius = 3 * base_factor; double degrees = Math.PI / 180.0; ctx.new_sub_path(); ctx.arc(width - radius, radius, radius, -90 * degrees, 0 * degrees); @@ -91,23 +114,23 @@ public class AvatarImage : Misc { ctx.clip(); if (text_only != null) { - ctx.set_source_surface(sub_surface(ctx, -1, width, height), 0, 0); + ctx.set_source_surface(sub_surface(ctx, -1, width, height, base_factor), 0, 0); ctx.paint(); } else if (current_jids.length == 4 || with_plus) { Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); Cairo.Context bufctx = new Cairo.Context(buffer); bufctx.scale(0.5, 0.5); - bufctx.set_source_surface(sub_surface(ctx, 0, width - 1, height - 1, 2), 0, 0); + bufctx.set_source_surface(sub_surface(ctx, 0, width - 1, height - 1, 2 * base_factor), 0, 0); bufctx.paint(); - bufctx.set_source_surface(sub_surface(ctx, 1, width - 1, height - 1, 2), width + 1, 0); + bufctx.set_source_surface(sub_surface(ctx, 1, width - 1, height - 1, 2 * base_factor), width + 1, 0); bufctx.paint(); - bufctx.set_source_surface(sub_surface(ctx, 2, width - 1, height - 1, 2), 0, height + 1); + bufctx.set_source_surface(sub_surface(ctx, 2, width - 1, height - 1, 2 * base_factor), 0, height + 1); bufctx.paint(); if (with_plus) { - bufctx.set_source_surface(sub_surface(ctx, -1, width - 1, height - 1, 2), width + 1, height + 1); + bufctx.set_source_surface(sub_surface(ctx, -1, width - 1, height - 1, 2 * base_factor), width + 1, height + 1); bufctx.paint(); } else { - bufctx.set_source_surface(sub_surface(ctx, 3, width - 1, height - 1, 2), width + 1, height + 1); + bufctx.set_source_surface(sub_surface(ctx, 3, width - 1, height - 1, 2 * base_factor), width + 1, height + 1); bufctx.paint(); } @@ -117,11 +140,11 @@ public class AvatarImage : Misc { Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); Cairo.Context bufctx = new Cairo.Context(buffer); bufctx.scale(0.5, 0.5); - bufctx.set_source_surface(sub_surface(ctx, 0, width - 1, height - 1, 2), 0, 0); + bufctx.set_source_surface(sub_surface(ctx, 0, width - 1, height - 1, 2 * base_factor), 0, 0); bufctx.paint(); - bufctx.set_source_surface(sub_surface(ctx, 1, width - 1, height * 2, 2), width + 1, 0); + bufctx.set_source_surface(sub_surface(ctx, 1, width - 1, height * 2, 2 * base_factor), width + 1, 0); bufctx.paint(); - bufctx.set_source_surface(sub_surface(ctx, 2, width - 1 , height - 1, 2), 0, height + 1); + bufctx.set_source_surface(sub_surface(ctx, 2, width - 1 , height - 1, 2 * base_factor), 0, height + 1); bufctx.paint(); ctx.set_source_surface(buffer, 0, 0); @@ -130,15 +153,15 @@ public class AvatarImage : Misc { Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); Cairo.Context bufctx = new Cairo.Context(buffer); bufctx.scale(0.5, 0.5); - bufctx.set_source_surface(sub_surface(ctx, 0, width - 1, height * 2, 2), 0, 0); + bufctx.set_source_surface(sub_surface(ctx, 0, width - 1, height * 2, 2 * base_factor), 0, 0); bufctx.paint(); - bufctx.set_source_surface(sub_surface(ctx, 1, width - 1, height * 2, 2), width + 1, 0); + bufctx.set_source_surface(sub_surface(ctx, 1, width - 1, height * 2, 2 * base_factor), width + 1, 0); bufctx.paint(); ctx.set_source_surface(buffer, 0, 0); ctx.paint(); } else if (current_jids.length == 1) { - ctx.set_source_surface(sub_surface(ctx, 0, width, height), 0, 0); + ctx.set_source_surface(sub_surface(ctx, 0, width, height, base_factor), 0, 0); ctx.paint(); } else { assert_not_reached(); @@ -157,6 +180,11 @@ public class AvatarImage : Misc { ctx.fill(); } + if (use_image_surface == 1) { + ctx_in.set_source_surface(ctx.get_target(), 0, 0); + ctx_in.paint(); + } + return true; } @@ -276,6 +304,7 @@ public class AvatarImage : Misc { assert(jids.length > 0); assert(jids.length < 5); assert(!with_plus || jids.length == 3); + this.cached_surface = null; this.text_only = null; this.gray = gray && allow_gray; this.with_plus = with_plus; diff --git a/main/src/ui/chat_input/view.vala b/main/src/ui/chat_input/view.vala index a1c2b83d..dd111997 100644 --- a/main/src/ui/chat_input/view.vala +++ b/main/src/ui/chat_input/view.vala @@ -32,7 +32,7 @@ public class View : Box { [GtkChild] private Separator file_separator; private EncryptionButton encryption_widget = new EncryptionButton() { margin_top=3, valign=Align.START, visible=true }; - public View(StreamInteractor stream_interactor) { + public View init(StreamInteractor stream_interactor) { this.stream_interactor = stream_interactor; occupants_tab_completor = new OccupantsTabCompletor(stream_interactor, text_input); @@ -70,6 +70,7 @@ public class View : Box { Util.force_css(frame, "* { border-radius: 3px; }"); stream_interactor.get_module(FileManager.IDENTITY).upload_available.connect(on_upload_available); + return this; } public void initialize_for_conversation(Conversation conversation) { diff --git a/main/src/ui/contact_details/muc_config_form_provider.vala b/main/src/ui/contact_details/muc_config_form_provider.vala index 072627bf..a088bd97 100644 --- a/main/src/ui/contact_details/muc_config_form_provider.vala +++ b/main/src/ui/contact_details/muc_config_form_provider.vala @@ -74,57 +74,9 @@ public class MucConfigFormProvider : Plugins.ContactDetailsProvider, Object { } } - Widget? widget = get_widget(field); + Widget? widget = Util.get_data_form_fild_widget(field); if (widget != null) contact_details.add(_("Room Configuration"), label, desc, widget); } - - private static Widget? get_widget(DataForms.DataForm.Field field) { - if (field.type_ == null) return null; - switch (field.type_) { - case DataForms.DataForm.Type.BOOLEAN: - DataForms.DataForm.BooleanField boolean_field = field as DataForms.DataForm.BooleanField; - Switch sw = new Switch() { active=boolean_field.value, valign=Align.CENTER, visible=true }; - sw.state_set.connect((state) => { - boolean_field.value = state; - return false; - }); - return sw; - case DataForms.DataForm.Type.JID_MULTI: - return null; - case DataForms.DataForm.Type.LIST_SINGLE: - DataForms.DataForm.ListSingleField list_single_field = field as DataForms.DataForm.ListSingleField; - ComboBoxText combobox = new ComboBoxText() { valign=Align.CENTER, visible=true }; - for (int i = 0; i < list_single_field.options.size; i++) { - DataForms.DataForm.Option option = list_single_field.options[i]; - combobox.append(option.value, option.label); - if (option.value == list_single_field.value) combobox.active = i; - } - combobox.changed.connect(() => { - list_single_field.value = combobox.get_active_id(); - }); - return combobox; - case DataForms.DataForm.Type.LIST_MULTI: - return null; - case DataForms.DataForm.Type.TEXT_PRIVATE: - DataForms.DataForm.TextPrivateField text_private_field = field as DataForms.DataForm.TextPrivateField; - Entry entry = new Entry() { text=text_private_field.value ?? "", valign=Align.CENTER, visible=true, visibility=false }; - entry.key_release_event.connect(() => { - text_private_field.value = entry.text; - return false; - }); - return entry; - case DataForms.DataForm.Type.TEXT_SINGLE: - DataForms.DataForm.TextSingleField text_single_field = field as DataForms.DataForm.TextSingleField; - Entry entry = new Entry() { text=text_single_field.value ?? "", valign=Align.CENTER, visible=true }; - entry.key_release_event.connect(() => { - text_single_field.value = entry.text; - return false; - }); - return entry; - default: - return null; - } - } } } diff --git a/main/src/ui/conversation_list_titlebar.vala b/main/src/ui/conversation_list_titlebar.vala index 65515019..60d9a6fb 100644 --- a/main/src/ui/conversation_list_titlebar.vala +++ b/main/src/ui/conversation_list_titlebar.vala @@ -10,7 +10,6 @@ public class ConversationListTitlebar : Gtk.HeaderBar { public signal void conversation_opened(Conversation conversation); [GtkChild] private MenuButton add_button; - [GtkChild] public ToggleButton search_button; private StreamInteractor stream_interactor; diff --git a/main/src/ui/conversation_selector/chat_row.vala b/main/src/ui/conversation_selector/chat_row.vala deleted file mode 100644 index fb427413..00000000 --- a/main/src/ui/conversation_selector/chat_row.vala +++ /dev/null @@ -1,54 +0,0 @@ -using Gdk; -using Gee; -using Gtk; - -using Dino.Entities; -using Xmpp; - -namespace Dino.Ui.ConversationSelector { - -public class ChatRow : ConversationRow { - - public ChatRow(StreamInteractor stream_interactor, Conversation conversation) { - base(stream_interactor, conversation); - has_tooltip = true; - query_tooltip.connect ((x, y, keyboard_tooltip, tooltip) => { - tooltip.set_custom(generate_tooltip()); - return true; - }); - stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect((account, jid, roster_item) => { - if (conversation.account.equals(account) && conversation.counterpart.equals(jid)) { - update_name_label(); - } - }); - } - - protected override void update_message_label() { - base.update_message_label(); - if (last_message != null && last_message.direction == Message.DIRECTION_SENT) { - nick_label.visible = true; - nick_label.label = _("Me") + ": "; - } else { - nick_label.label = ""; - } - } - - private Widget generate_tooltip() { - Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_selector/chat_row_tooltip.ui"); - Box main_box = builder.get_object("main_box") as Box; - Box inner_box = builder.get_object("inner_box") as Box; - Label jid_label = builder.get_object("jid_label") as Label; - - jid_label.label = conversation.counterpart.to_string(); - - Gee.List<Jid>? full_jids = stream_interactor.get_module(PresenceManager.IDENTITY).get_full_jids(conversation.counterpart, conversation.account); - if (full_jids != null) { - for (int i = 0; i < full_jids.size; i++) { - inner_box.add(get_fulljid_box(full_jids[i])); - } - } - return main_box; - } -} - -} diff --git a/main/src/ui/conversation_selector/conversation_row.vala b/main/src/ui/conversation_selector/conversation_row.vala index d79b840b..8157fde8 100644 --- a/main/src/ui/conversation_selector/conversation_row.vala +++ b/main/src/ui/conversation_selector/conversation_row.vala @@ -3,13 +3,14 @@ using Gdk; using Gtk; using Pango; +using Dino; using Dino.Entities; using Xmpp; namespace Dino.Ui.ConversationSelector { [GtkTemplate (ui = "/im/dino/Dino/conversation_selector/conversation_row.ui")] -public abstract class ConversationRow : ListBoxRow { +public class ConversationRow : ListBoxRow { public signal void closed(); @@ -27,7 +28,7 @@ public abstract class ConversationRow : ListBoxRow { protected const int AVATAR_SIZE = 40; - protected Message? last_message; + protected ContentItem? last_content_item; protected bool read = true; @@ -41,45 +42,122 @@ public abstract class ConversationRow : ListBoxRow { this.conversation = conversation; this.stream_interactor = stream_interactor; + switch (conversation.type_) { + case Conversation.Type.CHAT: + stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect((account, jid, roster_item) => { + if (conversation.account.equals(account) && conversation.counterpart.equals(jid)) { + update_name_label(); + } + }); + break; + case Conversation.Type.GROUPCHAT: + closed.connect(() => { + stream_interactor.get_module(MucManager.IDENTITY).part(conversation.account, conversation.counterpart); + }); + stream_interactor.get_module(MucManager.IDENTITY).room_name_set.connect((account, jid, room_name) => { + if (conversation != null && conversation.counterpart.equals_bare(jid) && conversation.account.equals(account)) { + update_name_label(); + } + }); + break; + case Conversation.Type.GROUPCHAT_PM: + break; + } + + // Set tooltip + switch (conversation.type_) { + case Conversation.Type.CHAT: + has_tooltip = true; + query_tooltip.connect ((x, y, keyboard_tooltip, tooltip) => { + tooltip.set_custom(generate_tooltip()); + return true; + }); + break; + case Conversation.Type.GROUPCHAT: + has_tooltip = true; + set_tooltip_text(conversation.counterpart.bare_jid.to_string()); + break; + case Conversation.Type.GROUPCHAT_PM: + break; + } + + stream_interactor.get_module(ContentItemStore.IDENTITY).new_item.connect((item, c) => { + if (conversation.equals(c)) { + content_item_received(item); + } + }); + last_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation); + x_button.clicked.connect(close_conversation); image.set_jid(stream_interactor, conversation.counterpart, conversation.account); conversation.notify["read-up-to"].connect(update_read); update_name_label(); - message_received(); - + content_item_received(); } public void update() { update_time_label(); } - public void message_received(Entities.Message? m = null) { - last_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_last_message(conversation) ?? m; + public void content_item_received(ContentItem? ci = null) { + last_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_latest(conversation) ?? ci; update_message_label(); update_time_label(); update_read(); } - protected void update_name_label(string? new_name = null) { + protected void update_name_label() { name_label.label = Util.get_conversation_display_name(stream_interactor, conversation); } protected void update_time_label(DateTime? new_time = null) { - if (last_message != null) { + if (last_content_item != null) { time_label.visible = true; - time_label.label = get_relative_time(last_message.time.to_local()); + time_label.label = get_relative_time(last_content_item.display_time.to_local()); } } - protected virtual void update_message_label() { - if (last_message != null) { + protected void update_message_label() { + if (last_content_item != null) { + switch (last_content_item.type_) { + case MessageItem.TYPE: + MessageItem message_item = last_content_item as MessageItem; + Message last_message = message_item.message; + + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + nick_label.label = Util.get_message_display_name(stream_interactor, last_message, conversation.account) + ": "; + } else { + nick_label.label = last_message.direction == Message.DIRECTION_SENT ? _("Me") + ": " : ""; + } + + message_label.label = Markup.escape_text((new Regex("\\s+")).replace_literal(last_message.body, -1, 0, " ")); + break; + case FileItem.TYPE: + FileItem file_item = last_content_item as FileItem; + FileTransfer transfer = file_item.file_transfer; + + if (conversation.type_ != Conversation.Type.GROUPCHAT) { + nick_label.label = transfer.direction == Message.DIRECTION_SENT ? _("Me") + ": " : ""; + } + + if (transfer.direction == Message.DIRECTION_SENT) { + message_label.label = "<i>" + (transfer.mime_type.has_prefix("image") ? _("Image sent") : _("File sent") ) + "</i>"; + } else { + message_label.label = "<i>" +(transfer.mime_type.has_prefix("image") ? _("Image received") : _("File received") ) + "</i>"; + } + break; + } + nick_label.visible = true; message_label.visible = true; - message_label.label = (new Regex("\\s+")).replace_literal(last_message.body, -1, 0, " "); } } protected void update_read() { + MessageItem? message_item = last_content_item as MessageItem; + if (message_item == null) return; + Message last_message = message_item.message; + bool read_was = read; read = last_message == null || (conversation.read_up_to != null && last_message.equals(conversation.read_up_to)); if (read == read_was) return; @@ -143,6 +221,23 @@ public abstract class ConversationRow : ListBoxRow { } } + private Widget generate_tooltip() { + Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_selector/chat_row_tooltip.ui"); + Box main_box = builder.get_object("main_box") as Box; + Box inner_box = builder.get_object("inner_box") as Box; + Label jid_label = builder.get_object("jid_label") as Label; + + jid_label.label = conversation.counterpart.to_string(); + + Gee.List<Jid>? full_jids = stream_interactor.get_module(PresenceManager.IDENTITY).get_full_jids(conversation.counterpart, conversation.account); + if (full_jids != null) { + for (int i = 0; i < full_jids.size; i++) { + inner_box.add(get_fulljid_box(full_jids[i])); + } + } + return main_box; + } + private static string get_relative_time(DateTime datetime) { DateTime now = new DateTime.now_utc(); TimeSpan timespan = now.difference(datetime); diff --git a/main/src/ui/conversation_selector/groupchat_pm_row.vala b/main/src/ui/conversation_selector/groupchat_pm_row.vala deleted file mode 100644 index 795bdcb6..00000000 --- a/main/src/ui/conversation_selector/groupchat_pm_row.vala +++ /dev/null @@ -1,43 +0,0 @@ -using Gdk; -using Gee; -using Gtk; - -using Dino.Entities; - -namespace Dino.Ui.ConversationSelector { - -public class GroupchatPmRow : ConversationRow { - - public GroupchatPmRow(StreamInteractor stream_interactor, Conversation conversation) { - base(stream_interactor, conversation); - has_tooltip = true; - query_tooltip.connect ((x, y, keyboard_tooltip, tooltip) => { - tooltip.set_custom(generate_tooltip()); - return true; - }); - } - - protected override void update_message_label() { - base.update_message_label(); - if (last_message != null && last_message.direction == Message.DIRECTION_SENT) { - nick_label.visible = true; - nick_label.label = _("Me") + ": "; - } else { - nick_label.label = ""; - } - } - - private Widget generate_tooltip() { - Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_selector/chat_row_tooltip.ui"); - Box main_box = builder.get_object("main_box") as Box; - Box inner_box = builder.get_object("inner_box") as Box; - Label jid_label = builder.get_object("jid_label") as Label; - jid_label.label = conversation.counterpart.to_string(); - if (stream_interactor.get_module(MucManager.IDENTITY).is_joined(conversation.counterpart, conversation.account)) { - inner_box.add(get_fulljid_box(conversation.counterpart)); - } - return main_box; - } -} - -} diff --git a/main/src/ui/conversation_selector/groupchat_row.vala b/main/src/ui/conversation_selector/groupchat_row.vala deleted file mode 100644 index fdbfa40b..00000000 --- a/main/src/ui/conversation_selector/groupchat_row.vala +++ /dev/null @@ -1,26 +0,0 @@ -using Dino.Entities; - -namespace Dino.Ui.ConversationSelector { - -public class GroupchatRow : ConversationRow { - - public GroupchatRow(StreamInteractor stream_interactor, Conversation conversation) { - base(stream_interactor, conversation); - has_tooltip = true; - set_tooltip_text(conversation.counterpart.bare_jid.to_string()); - - closed.connect(() => { - stream_interactor.get_module(MucManager.IDENTITY).part(conversation.account, conversation.counterpart); - }); - } - - protected override void update_message_label() { - base.update_message_label(); - if (last_message != null) { - nick_label.visible = true; - nick_label.label = Util.get_message_display_name(stream_interactor, last_message, conversation.account) + ": "; - } - } -} - -} diff --git a/main/src/ui/conversation_selector/list.vala b/main/src/ui/conversation_selector/list.vala index e250c4cd..8d71419b 100644 --- a/main/src/ui/conversation_selector/list.vala +++ b/main/src/ui/conversation_selector/list.vala @@ -67,7 +67,6 @@ public class List : ListBox { private void on_message_received(Entities.Message message, Conversation conversation) { if (rows.has_key(conversation)) { - rows[conversation].message_received(message); invalidate_sort(); } } @@ -75,19 +74,13 @@ public class List : ListBox { private void add_conversation(Conversation conversation) { ConversationRow row; if (!rows.has_key(conversation)) { - if (conversation.type_ == Conversation.Type.GROUPCHAT) { - row = new GroupchatRow(stream_interactor, conversation); - } else if (conversation.type_ == Conversation.Type.GROUPCHAT_PM){ - row = new GroupchatPmRow(stream_interactor, conversation); - } else { - row = new ChatRow(stream_interactor, conversation); - } + row = new ConversationRow(stream_interactor, conversation); rows[conversation] = row; add(row); row.closed.connect(() => { select_next_conversation(conversation); }); row.main_revealer.set_reveal_child(true); } - //invalidate_sort(); + invalidate_sort(); } private void select_next_conversation(Conversation conversation) { diff --git a/main/src/ui/conversation_selector/view.vala b/main/src/ui/conversation_selector/view.vala index b6b02848..d06ad133 100644 --- a/main/src/ui/conversation_selector/view.vala +++ b/main/src/ui/conversation_selector/view.vala @@ -10,43 +10,14 @@ namespace Dino.Ui.ConversationSelector { public class View : Box { public List conversation_list; - [GtkChild] public SearchEntry search_entry; - [GtkChild] public Revealer search_revealer; [GtkChild] private ScrolledWindow scrolled; - public View(StreamInteractor stream_interactor) { + public View init(StreamInteractor stream_interactor) { conversation_list = new List(stream_interactor) { visible=true }; scrolled.add(conversation_list); - search_entry.key_release_event.connect(search_key_release_event); - search_entry.search_changed.connect(search_changed); + return this; } - public void conversation_selected(Conversation? conversation) { - search_entry.set_text(""); - } - - private void refilter() { - string[]? values = null; - string str = search_entry.get_text (); - if (str != "") values = str.split(" "); - conversation_list.set_filter_values(values); - } - - private void search_changed(Editable editable) { - refilter(); - } - - private bool search_key_release_event(EventKey event) { - conversation_list.select_row(conversation_list.get_row_at_y(0)); - if (event.keyval == Key.Down) { - ConversationRow? row = (ConversationRow) conversation_list.get_row_at_index(0); - if (row != null) { - conversation_list.select_row(row); - row.grab_focus(); - } - } - return false; - } } } diff --git a/main/src/ui/conversation_summary/chat_state_populator.vala b/main/src/ui/conversation_summary/chat_state_populator.vala index 1ea52a6d..d07ab743 100644 --- a/main/src/ui/conversation_summary/chat_state_populator.vala +++ b/main/src/ui/conversation_summary/chat_state_populator.vala @@ -6,7 +6,7 @@ using Xmpp; namespace Dino.Ui.ConversationSummary { -class ChatStatePopulator : Plugins.ConversationItemPopulator, Object { +class ChatStatePopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object { public string id { get { return "chat_state"; } } @@ -43,8 +43,6 @@ class ChatStatePopulator : Plugins.ConversationItemPopulator, Object { 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) { HashMap<Jid, string>? states = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_chat_states(current_conversation); diff --git a/main/src/ui/conversation_summary/content_item_widget_factory.vala b/main/src/ui/conversation_summary/content_item_widget_factory.vala new file mode 100644 index 00000000..26b66664 --- /dev/null +++ b/main/src/ui/conversation_summary/content_item_widget_factory.vala @@ -0,0 +1,227 @@ +using Gee; +using Gdk; +using Gtk; +using Xmpp; + +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +public class ContentItemWidgetFactory : Object { + + private StreamInteractor stream_interactor; + private HashMap<string, WidgetGenerator> generators = new HashMap<string, WidgetGenerator>(); + + public ContentItemWidgetFactory(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + generators[MessageItem.TYPE] = new MessageItemWidgetGenerator(stream_interactor); + generators[FileItem.TYPE] = new FileItemWidgetGenerator(stream_interactor); + } + + public Widget? get_widget(ContentItem item) { + WidgetGenerator? generator = generators[item.type_]; + if (generator != null) { + return (Widget?) generator.get_widget(item); + } + return null; + } + + public void register_widget_generator(WidgetGenerator generator) { + generators[generator.handles_type] = generator; + } +} + +public interface WidgetGenerator : Object { + public abstract string handles_type { get; set; } + public abstract Object get_widget(ContentItem item); +} + +public class MessageItemWidgetGenerator : WidgetGenerator, Object { + + public string handles_type { get; set; default=FileItem.TYPE; } + + private StreamInteractor stream_interactor; + + public MessageItemWidgetGenerator(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public Object get_widget(ContentItem item) { + MessageItem message_item = item as MessageItem; + Conversation conversation = message_item.conversation; + Message message = message_item.message; + + Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, vexpand=true, visible=true }; + string markup_text = message.body; + if (markup_text.length > 10000) { + markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]"; + } + if (message_item.message.body.has_prefix("/me")) { + markup_text = markup_text.substring(3); + } + + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + markup_text = Util.parse_add_markup(markup_text, conversation.nickname, true, true); + } else { + markup_text = Util.parse_add_markup(markup_text, null, true, true); + } + + if (message_item.message.body.has_prefix("/me")) { + string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account); + update_me_style(stream_interactor, message.real_jid ?? message.from, display_name, conversation.account, label, markup_text); + label.realize.connect(() => update_me_style(stream_interactor, message.real_jid ?? message.from, display_name, conversation.account, label, markup_text)); + label.style_updated.connect(() => update_me_style(stream_interactor, message.real_jid ?? message.from, display_name, conversation.account, label, markup_text)); + } + + label.label = markup_text; + return label; + } + + public static void update_me_style(StreamInteractor stream_interactor, Jid jid, string display_name, Account account, Label label, string action_text) { + string color = Util.get_name_hex_color(stream_interactor, account, jid, Util.is_dark_theme(label)); + label.label = @"<span color=\"#$(color)\">$(Markup.escape_text(display_name))</span>" + action_text; + } +} + +public class FileItemWidgetGenerator : WidgetGenerator, Object { + + public StreamInteractor stream_interactor; + public string handles_type { get; set; default=FileItem.TYPE; } + + private const int MAX_HEIGHT = 300; + private const int MAX_WIDTH = 600; + + public FileItemWidgetGenerator(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + } + + public Object get_widget(ContentItem item) { + FileItem file_item = item as FileItem; + FileTransfer transfer = file_item.file_transfer; + if (transfer.mime_type != null && transfer.mime_type.has_prefix("image")) { + return getImageWidget(transfer); + } else { + return getDefaultWidget(transfer); + } + } + + private Widget getImageWidget(FileTransfer file_transfer) { + Image image = new Image() { halign=Align.START, visible = true }; + Gdk.Pixbuf pixbuf; + try { + pixbuf = new Gdk.Pixbuf.from_file(file_transfer.get_file().get_path()); + } catch (Error error) { + return null; + } + + int max_scaled_height = MAX_HEIGHT * image.scale_factor; + if (pixbuf.height > max_scaled_height) { + pixbuf = pixbuf.scale_simple((int) ((double) max_scaled_height / pixbuf.height * pixbuf.width), max_scaled_height, Gdk.InterpType.BILINEAR); + } + int max_scaled_width = MAX_WIDTH * image.scale_factor; + if (pixbuf.width > max_scaled_width) { + pixbuf = pixbuf.scale_simple(max_scaled_width, (int) ((double) max_scaled_width / pixbuf.width * pixbuf.height), Gdk.InterpType.BILINEAR); + } + pixbuf = crop_corners(pixbuf, 3 * image.get_scale_factor()); + Util.image_set_from_scaled_pixbuf(image, pixbuf); + Util.force_css(image, "* { box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.1); margin: 2px; border-radius: 3px; }"); + + Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_summary/image_toolbar.ui"); + Widget toolbar = builder.get_object("main") as Widget; + Util.force_background(toolbar, "rgba(0, 0, 0, 0.5)"); + Util.force_css(toolbar, "* { padding: 3px; border-radius: 3px; }"); + + Label url_label = builder.get_object("url_label") as Label; + Util.force_color(url_label, "#eee"); + + if (file_transfer.file_name != null && file_transfer.file_name != "") { + string caption = file_transfer.file_name; + url_label.label = caption; + } else { + url_label.visible = false; + } + + Image open_image = builder.get_object("open_image") as Image; + Util.force_css(open_image, "*:not(:hover) { color: #eee; }"); + Button open_button = builder.get_object("open_button") as Button; + Util.force_css(open_button, "*:hover { background-color: rgba(255,255,255,0.3); border-color: transparent; }"); + open_button.clicked.connect(() => { + try{ + AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); + } catch (Error err) { + print("Tried to open file://" + file_transfer.get_file().get_path() + " " + err.message + "\n"); + } + }); + + Revealer toolbar_revealer = new Revealer() { transition_type=RevealerTransitionType.CROSSFADE, transition_duration=400, visible=true }; + toolbar_revealer.add(toolbar); + + Grid grid = new Grid() { visible=true }; + grid.attach(toolbar_revealer, 0, 0, 1, 1); + grid.attach(image, 0, 0, 1, 1); + + EventBox event_box = new EventBox() { halign=Align.START, visible=true }; + event_box.add(grid); + event_box.enter_notify_event.connect(() => { toolbar_revealer.reveal_child = true; return false; }); + event_box.leave_notify_event.connect(() => { toolbar_revealer.reveal_child = false; return false; }); + + return event_box; + } + + private static Gdk.Pixbuf crop_corners(Gdk.Pixbuf pixbuf, double radius = 3) { + Cairo.Context ctx = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.ARGB32, pixbuf.width, pixbuf.height)); + Gdk.cairo_set_source_pixbuf(ctx, pixbuf, 0, 0); + double degrees = Math.PI / 180.0; + ctx.new_sub_path(); + ctx.arc(pixbuf.width - radius, radius, radius, -90 * degrees, 0 * degrees); + ctx.arc(pixbuf.width - radius, pixbuf.height - radius, radius, 0 * degrees, 90 * degrees); + ctx.arc(radius, pixbuf.height - radius, radius, 90 * degrees, 180 * degrees); + ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees); + ctx.close_path(); + ctx.clip(); + ctx.paint(); + return Gdk.pixbuf_get_from_surface(ctx.get_target(), 0, 0, pixbuf.width, pixbuf.height); + } + + private Widget getDefaultWidget(FileTransfer file_transfer) { + Box main_box = new Box(Orientation.HORIZONTAL, 4) { halign=Align.START, visible=true }; + string? icon_name = ContentType.get_generic_icon_name(file_transfer.mime_type); + Image content_type_image = new Image.from_icon_name(icon_name, IconSize.DND) { visible=true }; + main_box.add(content_type_image); + + Box right_box = new Box(Orientation.VERTICAL, 0) { visible=true }; + Label name_label = new Label(file_transfer.file_name) { xalign=0, yalign=0, visible=true}; + right_box.add(name_label); + Label mime_label = new Label("<span size='small'>" + _("File") + ": " + file_transfer.mime_type + "</span>") { use_markup=true, xalign=0, yalign=1, visible=true}; + mime_label.get_style_context().add_class("dim-label"); + right_box.add(mime_label); + main_box.add(right_box); + + EventBox event_box = new EventBox() { halign=Align.START, visible=true }; + event_box.add(main_box); + + event_box.enter_notify_event.connect((event) => { + event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.HAND2)); + return false; + }); + event_box.leave_notify_event.connect((event) => { + event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.XTERM)); + return false; + }); + event_box.button_release_event.connect((event_button) => { + if (event_button.button == 1) { + try{ + AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); + } catch (Error err) { + print("Tried to open " + file_transfer.get_file().get_path()); + } + } + return false; + }); + + return event_box; + } +} + +} diff --git a/main/src/ui/conversation_summary/content_populator.vala b/main/src/ui/conversation_summary/content_populator.vala new file mode 100644 index 00000000..754446d3 --- /dev/null +++ b/main/src/ui/conversation_summary/content_populator.vala @@ -0,0 +1,110 @@ +using Gee; +using Gtk; + +using Xmpp; +using Dino.Entities; + +namespace Dino.Ui.ConversationSummary { + +public class ContentProvider : ContentItemCollection, Object { + + private StreamInteractor stream_interactor; + private ContentItemWidgetFactory widget_factory; + private Conversation? current_conversation; + private Plugins.ConversationItemCollection? item_collection; + + public ContentProvider(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + this.widget_factory = new ContentItemWidgetFactory(stream_interactor); + } + + public void init(Plugins.ConversationItemCollection item_collection, Conversation conversation, Plugins.WidgetType type) { + if (current_conversation != null) { + stream_interactor.get_module(ContentItemStore.IDENTITY).uninit(current_conversation, this); + } + current_conversation = conversation; + this.item_collection = item_collection; + stream_interactor.get_module(ContentItemStore.IDENTITY).init(conversation, this); + } + + public void insert_item(ContentItem item) { + item_collection.insert_item(new ContentMetaItem(item, widget_factory)); + } + + public void remove_item(ContentItem item) { } + + + public Gee.List<ContentMetaItem> populate_latest(Conversation conversation, int n) { + Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_n_latest(conversation, n); + Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>(); + foreach (ContentItem item in items) { + ret.add(new ContentMetaItem(item, widget_factory)); + } + return ret; + } + + public Gee.List<ContentMetaItem> populate_before(Conversation conversation, ContentItem before_item, int n) { + Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>(); + Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_before(conversation, before_item, n); + foreach (ContentItem item in items) { + ret.add(new ContentMetaItem(item, widget_factory)); + } + return ret; + } + + public Gee.List<ContentMetaItem> populate_after(Conversation conversation, ContentItem after_item, int n) { + Gee.List<ContentMetaItem> ret = new ArrayList<ContentMetaItem>(); + Gee.List<ContentItem> items = stream_interactor.get_module(ContentItemStore.IDENTITY).get_after(conversation, after_item, n); + foreach (ContentItem item in items) { + ret.add(new ContentMetaItem(item, widget_factory)); + } + return ret; + } + + public ContentMetaItem get_content_meta_item(ContentItem content_item) { + return new ContentMetaItem(content_item, widget_factory); + } +} + +public class ContentMetaItem : 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; } + + public ContentItem content_item; + private ContentItemWidgetFactory widget_factory; + + public ContentMetaItem(ContentItem content_item, ContentItemWidgetFactory widget_factory) { + this.jid = content_item.jid; + this.sort_time = content_item.sort_time; + this.seccondary_sort_indicator = content_item.id; + this.display_time = content_item.display_time; + this.encryption = content_item.encryption; + this.mark = content_item.mark; + + WeakRef weak_item = WeakRef(content_item); + content_item.notify["mark"].connect(() => { + ContentItem? ci = weak_item.get() as ContentItem; + if (ci == null) return; + this.mark = ci.mark; + }); + + this.can_merge = true; + this.requires_avatar = true; + this.requires_header = true; + + this.content_item = content_item; + this.widget_factory = widget_factory; + } + + 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 type) { + return widget_factory.get_widget(content_item); + } +} + +} diff --git a/main/src/ui/conversation_summary/conversation_item_skeleton.vala b/main/src/ui/conversation_summary/conversation_item_skeleton.vala index a8da93ef..a4e45f7a 100644 --- a/main/src/ui/conversation_summary/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_summary/conversation_item_skeleton.vala @@ -176,7 +176,7 @@ public class DefaultSkeletonHeader : Box { return datetime.format(format); } - public virtual string get_relative_time(DateTime datetime) { + public static string get_relative_time(DateTime datetime) { DateTime now = new DateTime.now_local(); TimeSpan timespan = now.difference(datetime); if (timespan > 365 * TimeSpan.DAY) { diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index b4a34f3b..83da81aa 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -11,19 +11,19 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins public Conversation? conversation { get; private set; } - [GtkChild] private ScrolledWindow scrolled; + [GtkChild] public ScrolledWindow scrolled; [GtkChild] private Revealer notification_revealer; [GtkChild] private Box notifications; [GtkChild] private Box main; [GtkChild] private Stack stack; private StreamInteractor stream_interactor; - private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(sort_meta_items); - private Gee.Map<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>> meta_after_items = new Gee.HashMap<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>>(); + private Gee.TreeSet<Plugins.MetaConversationItem> content_items = new Gee.TreeSet<Plugins.MetaConversationItem>(compare_meta_items); + private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(compare_meta_items); 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 ContentProvider content_populator; private SubscriptionNotitication subscription_notification; private double? was_value; @@ -33,24 +33,25 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins private Mutex reloading_mutex = Mutex(); private bool animate = false; private bool firstLoad = true; + private bool at_current_content = true; + private bool reload_messages = true; - public ConversationView(StreamInteractor stream_interactor) { + public ConversationView init(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); + content_populator = new ContentProvider(stream_interactor); subscription_notification = new SubscriptionNotitication(stream_interactor); - insert_item.connect(on_insert_item); - remove_item.connect(on_remove_item); + insert_item.connect(filter_insert_item); + remove_item.connect(do_remove_item); add_meta_notification.connect(on_add_meta_notification); remove_meta_notification.connect(on_remove_meta_notification); Application app = GLib.Application.get_default() as Application; - app.plugin_registry.register_conversation_item_populator(new ChatStatePopulator(stream_interactor)); - app.plugin_registry.register_conversation_item_populator(new FilePopulator(stream_interactor)); - app.plugin_registry.register_conversation_item_populator(new DateSeparatorPopulator(stream_interactor)); + app.plugin_registry.register_conversation_addition_populator(new ChatStatePopulator(stream_interactor)); + app.plugin_registry.register_conversation_addition_populator(new DateSeparatorPopulator(stream_interactor)); Timeout.add_seconds(60, () => { foreach (ConversationItemSkeleton item_skeleton in item_skeletons) { @@ -59,68 +60,144 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins return true; }); - Util.force_base_background(this); + return this; } - // Workaround GTK TextView issues: Delay first load of contents public void initialize_for_conversation(Conversation? conversation) { + // Workaround for rendering issues if (firstLoad) { - int timeout = firstLoad ? 1000 : 0; - Timeout.add(timeout, () => { - initialize_for_conversation_(conversation); + main.visible = false; + Idle.add(() => { + main.visible=true; return false; }); firstLoad = false; - } else { - initialize_for_conversation_(conversation); } + stack.set_visible_child_name("void"); + initialize_for_conversation_(conversation); + display_latest(); + stack.set_visible_child_name("main"); + } + public void initialize_around_message(Conversation conversation, ContentItem content_item) { + stack.set_visible_child_name("void"); + clear(); + initialize_for_conversation_(conversation); + Gee.List<ContentMetaItem> before_items = content_populator.populate_before(conversation, content_item, 40); + foreach (ContentMetaItem item in before_items) { + do_insert_item(item); + } + ContentMetaItem meta_item = content_populator.get_content_meta_item(content_item); + meta_item.can_merge = false; + Widget w = insert_new(meta_item); + content_items.add(meta_item); + meta_items.add(meta_item); + + Gee.List<ContentMetaItem> after_items = content_populator.populate_after(conversation, content_item, 40); + foreach (ContentMetaItem item in after_items) { + do_insert_item(item); + } + if (after_items.size == 40) { + at_current_content = false; + } + { + int h = 0, i = 0; + main.@foreach((widget) => { + if (i >= before_items.size) return; + ConversationItemSkeleton? sk = widget as ConversationItemSkeleton; + i += sk != null ? sk.items.size : 1; + int minimum_height, natural_height; + widget.get_preferred_height_for_width(main.get_allocated_width() - 2 * main.margin, out minimum_height, out natural_height); + h += minimum_height + 15; + }); + } + + reload_messages = false; + Timeout.add(700, () => { + int h = 0, i = 0; + main.@foreach((widget) => { + if (i >= before_items.size) return; + ConversationItemSkeleton? sk = widget as ConversationItemSkeleton; + i += sk != null ? sk.items.size : 1; + h += widget.get_allocated_height() + 15; + }); + scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3; + w.get_style_context().add_class("highlight-once"); + reload_messages = true; + stack.set_visible_child_name("main"); + return false; + }); } private void initialize_for_conversation_(Conversation? conversation) { + // Deinitialize old conversation Dino.Application app = Dino.Application.get_default(); if (this.conversation != null) { - foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) { + foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) { populator.close(conversation); } foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) { populator.close(conversation); } } + + // Clear data structures + clear_notifications(); this.conversation = conversation; - stack.set_visible_child_name("void"); - clear(); - was_upper = null; - was_page_size = null; + + + // Init for new conversation + foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) { + populator.init(conversation, this, Plugins.WidgetType.GTK); + } + content_populator.init(this, conversation, Plugins.WidgetType.GTK); + subscription_notification.init(conversation, this); + animate = false; Timeout.add(20, () => { animate = true; return false; }); + } - foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) { - populator.init(conversation, this, Plugins.WidgetType.GTK); + private void display_latest() { + clear(); + + Gee.List<ContentMetaItem> items = content_populator.populate_latest(conversation, 40); + foreach (ContentMetaItem item in items) { + do_insert_item(item); } + Application app = GLib.Application.get_default() as Application; foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) { populator.init(conversation, this, Plugins.WidgetType.GTK); } - message_item_populator.init(conversation, this); - message_item_populator.populate_latest(conversation, 40); Idle.add(() => { on_value_notify(); return false; }); + } - subscription_notification.init(conversation, this); - - stack.set_visible_child_name("main"); + public void filter_insert_item(Plugins.MetaConversationItem item) { + if (meta_items.size > 0) { + bool after_last = meta_items.last().sort_time.compare(item.sort_time) < 0; + bool within_range = meta_items.last().sort_time.compare(item.sort_time) > 0 && meta_items.first().sort_time.compare(item.sort_time) < 0; + bool accept = within_range || (at_current_content && after_last); + if (!accept) { + return; + } + } + do_insert_item(item); } - public void on_insert_item(Plugins.MetaConversationItem item) { + public void do_insert_item(Plugins.MetaConversationItem item) { lock (meta_items) { if (!item.can_merge || !merge_back(item)) { insert_new(item); } } + if (item as ContentMetaItem != null) { + content_items.add(item); + } + meta_items.add(item); } - public void on_remove_item(Plugins.MetaConversationItem item) { - lock (meta_items) { - ConversationItemSkeleton? skeleton = item_item_skeletons[item]; + private void do_remove_item(Plugins.MetaConversationItem item) { + ConversationItemSkeleton? skeleton = item_item_skeletons[item]; + if (skeleton != null) { if (skeleton.items.size > 1) { skeleton.remove_meta_item(item); } else { @@ -130,6 +207,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins item_skeletons.remove(skeleton); item_item_skeletons.unset(item); } + content_items.remove(item); meta_items.remove(item); } } @@ -173,10 +251,9 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins lower_start_item.encryption == item.encryption && (item.mark == Message.Marked.WONTSEND) == (lower_start_item.mark == Message.Marked.WONTSEND)) { lower_skeleton.add_meta_item(item); - force_alloc_width(lower_skeleton, main.get_allocated_width()); + widgets[item] = widgets[lower_start_item]; item_item_skeletons[item] = lower_skeleton; - meta_items.add(item); return true; } @@ -184,7 +261,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins return false; } - private void insert_new(Plugins.MetaConversationItem item) { + private Widget insert_new(Plugins.MetaConversationItem item) { Plugins.MetaConversationItem? lower_item = meta_items.lower(item); // Does another skeleton need to be split? @@ -203,7 +280,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins item_item_skeletons[item] = item_skeleton; int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0; item_skeletons.insert(index, item_skeleton); - meta_items.add(item); // Insert widget Widget insert = item_skeleton; @@ -217,22 +293,22 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins main.add(insert); } widgets[item] = insert; - force_alloc_width(insert, main.get_allocated_width()); main.reorder_child(insert, index); // If an item from the past was added, add everything between that item and the (post-)first present item if (index == 0) { Dino.Application app = Dino.Application.get_default(); if (item_skeletons.size == 1) { - foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) { + foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) { populator.populate_timespan(conversation, item.sort_time, new DateTime.now_utc()); } } else { - foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) { + foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) { populator.populate_timespan(conversation, item.sort_time, meta_items.higher(item).sort_time); } } } + return insert; } private void split_at_time(ConversationItemSkeleton split_skeleton, DateTime time) { @@ -241,12 +317,12 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins while(i < split_skeleton.items.size) { Plugins.MetaConversationItem meta_item = split_skeleton.items[i]; if (time.compare(meta_item.display_time) < 0) { - remove_item(meta_item); + do_remove_item(meta_item); if (!already_divided) { insert_new(meta_item); already_divided = true; } else { - insert_item(meta_item); + do_insert_item(meta_item); } } i++; @@ -254,55 +330,80 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins } 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 + if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size + if (at_current_content) { + 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; + was_value = scrolled.vadjustment.value; reloading_mutex.trylock(); reloading_mutex.unlock(); } private void on_value_notify() { - if (scrolled.vadjustment.value < 200) { + if (scrolled.vadjustment.value < 400) { load_earlier_messages(); + } else if (scrolled.vadjustment.upper - (scrolled.vadjustment.value + scrolled.vadjustment.page_size) < 400) { + load_later_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_before(conversation, meta_items.first(), 20); + if (meta_items.size > 0) { + Gee.List<ContentMetaItem> items = content_populator.populate_before(conversation, (content_items.first() as ContentMetaItem).content_item, 20); + foreach (ContentMetaItem item in items) { + do_insert_item(item); + } + } else { + reloading_mutex.unlock(); + } + } + + private void load_later_messages() { + if (!reloading_mutex.trylock()) return; + if (meta_items.size > 0 && !at_current_content) { + Gee.List<ContentMetaItem> items = content_populator.populate_after(conversation, (content_items.last() as ContentMetaItem).content_item, 20); + if (items.size == 0) { + at_current_content = true; + } + foreach (ContentMetaItem item in items) { + do_insert_item(item); + } + } else { + reloading_mutex.unlock(); + } } - private static int sort_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) { + private static int compare_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) { int res = a.sort_time.compare(b.sort_time); if (res == 0) { - if (a.seccondary_sort_indicator < b.seccondary_sort_indicator) res = -1; - else if (a.seccondary_sort_indicator > b.seccondary_sort_indicator) res = 1; + if (a.seccondary_sort_indicator < b.seccondary_sort_indicator) { + res = -1; + } else if (a.seccondary_sort_indicator > b.seccondary_sort_indicator) { + res = 1; + } } return res; } - // 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() { + was_upper = null; + was_page_size = null; + content_items.clear(); meta_items.clear(); - meta_after_items.clear(); item_skeletons.clear(); item_item_skeletons.clear(); widgets.clear(); main.@foreach((widget) => { widget.destroy(); }); + } + + private void clear_notifications() { notifications.@foreach((widget) => { widget.destroy(); }); notification_revealer.transition_duration = 0; notification_revealer.set_reveal_child(false); diff --git a/main/src/ui/conversation_summary/date_separator_populator.vala b/main/src/ui/conversation_summary/date_separator_populator.vala index 34005ab6..6a1ba782 100644 --- a/main/src/ui/conversation_summary/date_separator_populator.vala +++ b/main/src/ui/conversation_summary/date_separator_populator.vala @@ -6,7 +6,7 @@ using Xmpp; namespace Dino.Ui.ConversationSummary { -class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Object { +class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Plugins.ConversationAdditionPopulator, Object { public string id { get { return "date_separator"; } } @@ -35,8 +35,6 @@ class DateSeparatorPopulator : Plugins.ConversationItemPopulator, Object { public void populate_timespan(Conversation conversation, DateTime after, DateTime before) { } - public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { } - private void on_insert_item(Plugins.MetaConversationItem item) { if (item.display_time == null) return; diff --git a/main/src/ui/conversation_summary/default_file_display.vala b/main/src/ui/conversation_summary/default_file_display.vala deleted file mode 100644 index 1547440b..00000000 --- a/main/src/ui/conversation_summary/default_file_display.vala +++ /dev/null @@ -1,95 +0,0 @@ -using Gdk; -using Gtk; - -using Dino.Entities; -using Xmpp; - -namespace Dino.Ui.ConversationSummary { - -public class DefaultFileDisplay : 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; } - public override Entities.Message.Marked? mark { get; set; } - - 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; } - - private const int MAX_HEIGHT = 300; - private const int MAX_WIDTH = 600; - - private StreamInteractor stream_interactor; - private FileTransfer file_transfer; - - public DefaultFileDisplay(StreamInteractor stream_interactor, FileTransfer file_transfer) { - this.stream_interactor = stream_interactor; - this.file_transfer = file_transfer; - - this.jid = file_transfer.direction == FileTransfer.DIRECTION_SENT ? file_transfer.account.bare_jid.with_resource(file_transfer.account.resourcepart) : file_transfer.counterpart; - this.sort_time = file_transfer.time; - this.seccondary_sort_indicator = file_transfer.id + 0.2903; - this.display_time = file_transfer.time; - this.encryption = file_transfer.encryption; - this.mark = file_to_message_state(file_transfer.state); - file_transfer.notify["state"].connect_after(() => { - this.mark = file_to_message_state(file_transfer.state); - }); - } - - public override Object? get_widget(Plugins.WidgetType widget_type) { - Box main_box = new Box(Orientation.HORIZONTAL, 4) { halign=Align.START, visible=true }; - string? icon_name = ContentType.get_generic_icon_name(file_transfer.mime_type); - Image content_type_image = new Image.from_icon_name(icon_name, IconSize.DND) { visible=true }; - main_box.add(content_type_image); - - Box right_box = new Box(Orientation.VERTICAL, 0) { visible=true }; - Label name_label = new Label(file_transfer.file_name) { xalign=0, yalign=0, visible=true}; - right_box.add(name_label); - Label mime_label = new Label("<span size='small'>" + _("File") + ": " + file_transfer.mime_type + "</span>") { use_markup=true, xalign=0, yalign=1, visible=true}; - mime_label.get_style_context().add_class("dim-label"); - right_box.add(mime_label); - main_box.add(right_box); - - EventBox event_box = new EventBox() { halign=Align.START, visible=true }; - event_box.add(main_box); - - event_box.enter_notify_event.connect((event) => { - event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.HAND2)); - return false; - }); - event_box.leave_notify_event.connect((event) => { - event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.XTERM)); - return false; - }); - event_box.button_release_event.connect((event_button) => { - if (event_button.button == 1) { - try{ - AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); - } catch (Error err) { - print("Tried to open " + file_transfer.get_file().get_path()); - } - } - return false; - }); - - return event_box; - } - - private Entities.Message.Marked file_to_message_state(FileTransfer.State state) { - switch (state) { - case FileTransfer.State.IN_PROCESS: - return Entities.Message.Marked.UNSENT; - case FileTransfer.State.COMPLETE: - return Entities.Message.Marked.NONE; - case FileTransfer.State.NOT_STARTED: - return Entities.Message.Marked.UNSENT; - case FileTransfer.State.FAILED: - return Entities.Message.Marked.WONTSEND; - } - assert_not_reached(); - } -} - -} diff --git a/main/src/ui/conversation_summary/default_message_display.vala b/main/src/ui/conversation_summary/default_message_display.vala deleted file mode 100644 index 519e5107..00000000 --- a/main/src/ui/conversation_summary/default_message_display.vala +++ /dev/null @@ -1,58 +0,0 @@ -using Dino.Entities; -using Xmpp; - -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.seccondary_sort_indicator = message.id + 0.2085; - 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); - if (conversation.type_ == Conversation.Type.GROUPCHAT) { - text_view.highlight_word(conversation.nickname); - } - return text_view; - } -} - -} diff --git a/main/src/ui/conversation_summary/file_populator.vala b/main/src/ui/conversation_summary/file_populator.vala deleted file mode 100644 index af7bc992..00000000 --- a/main/src/ui/conversation_summary/file_populator.vala +++ /dev/null @@ -1,54 +0,0 @@ -using Gee; -using Gtk; - -using Dino.Entities; -using Xmpp; - -namespace Dino.Ui.ConversationSummary { - -class FilePopulator : Plugins.ConversationItemPopulator, Object { - - public string id { get { return "file"; } } - - private StreamInteractor? stream_interactor; - private Conversation? current_conversation; - private Plugins.ConversationItemCollection? item_collection; - - public FilePopulator(StreamInteractor stream_interactor) { - this.stream_interactor = stream_interactor; - - stream_interactor.get_module(FileManager.IDENTITY).received_file.connect((file_transfer) => { - if (current_conversation != null && current_conversation.account.equals(file_transfer.account) && current_conversation.counterpart.equals_bare(file_transfer.counterpart)) { - insert_file(file_transfer); - } - }); - } - - public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) { - current_conversation = conversation; - this.item_collection = item_collection; - } - - public void close(Conversation conversation) { } - - public void populate_timespan(Conversation conversation, DateTime from, DateTime to) { - Gee.List<FileTransfer> transfers = stream_interactor.get_module(FileManager.IDENTITY).get_file_transfers(conversation.account, conversation.counterpart, from, to); - foreach (FileTransfer transfer in transfers) { - insert_file(transfer); - } - } - - public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { } - - private void insert_file(FileTransfer transfer) { - Plugins.MetaConversationItem item = null; - if (transfer.mime_type != null && transfer.mime_type.has_prefix("image")) { - item = new ImageDisplay(stream_interactor, transfer); - } else { - item = new DefaultFileDisplay(stream_interactor, transfer); - } - item_collection.insert_item(item); - } -} - -} diff --git a/main/src/ui/conversation_summary/image_display.vala b/main/src/ui/conversation_summary/image_display.vala deleted file mode 100644 index 15880836..00000000 --- a/main/src/ui/conversation_summary/image_display.vala +++ /dev/null @@ -1,137 +0,0 @@ -using Gtk; - -using Dino.Entities; -using Xmpp; - -namespace Dino.Ui.ConversationSummary { - -public class ImageDisplay : 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; } - public override Entities.Message.Marked? mark { get; set; } - - 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; } - - private const int MAX_HEIGHT = 300; - private const int MAX_WIDTH = 600; - - private StreamInteractor stream_interactor; - private FileTransfer file_transfer; - - public ImageDisplay(StreamInteractor stream_interactor, FileTransfer file_transfer) { - this.stream_interactor = stream_interactor; - this.file_transfer = file_transfer; - - this.jid = file_transfer.direction == FileTransfer.DIRECTION_SENT ? file_transfer.account.bare_jid.with_resource(file_transfer.account.resourcepart) : file_transfer.counterpart; - this.sort_time = file_transfer.time; - this.seccondary_sort_indicator = file_transfer.id + 0.2903; - this.display_time = file_transfer.time; - this.encryption = file_transfer.encryption; - this.mark = file_to_message_state(file_transfer.state); - file_transfer.notify["state"].connect_after(() => { - this.mark = file_to_message_state(file_transfer.state); - }); - } - - public override Object? get_widget(Plugins.WidgetType widget_type) { - Image image = new Image() { halign=Align.START, visible = true }; - Gdk.Pixbuf pixbuf; - try { - pixbuf = new Gdk.Pixbuf.from_file(file_transfer.get_file().get_path()); - } catch (Error error) { - return null; - } - - int max_scaled_height = MAX_HEIGHT * image.scale_factor; - if (pixbuf.height > max_scaled_height) { - pixbuf = pixbuf.scale_simple((int) ((double) max_scaled_height / pixbuf.height * pixbuf.width), max_scaled_height, Gdk.InterpType.BILINEAR); - } - int max_scaled_width = MAX_WIDTH * image.scale_factor; - if (pixbuf.width > max_scaled_width) { - pixbuf = pixbuf.scale_simple(max_scaled_width, (int) ((double) max_scaled_width / pixbuf.width * pixbuf.height), Gdk.InterpType.BILINEAR); - } - pixbuf = crop_corners(pixbuf, 3 * image.get_scale_factor()); - Util.image_set_from_scaled_pixbuf(image, pixbuf); - Util.force_css(image, "* { box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.1); margin: 2px; border-radius: 3px; }"); - - Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_summary/image_toolbar.ui"); - Widget toolbar = builder.get_object("main") as Widget; - Util.force_background(toolbar, "rgba(0, 0, 0, 0.5)"); - Util.force_css(toolbar, "* { padding: 3px; border-radius: 3px; }"); - - Label url_label = builder.get_object("url_label") as Label; - Util.force_color(url_label, "#eee"); - update_info(url_label, file_transfer.file_name); - - Image open_image = builder.get_object("open_image") as Image; - Util.force_css(open_image, "*:not(:hover) { color: #eee; }"); - Button open_button = builder.get_object("open_button") as Button; - Util.force_css(open_button, "*:hover { background-color: rgba(255,255,255,0.3); border-color: transparent; }"); - open_button.clicked.connect(() => { - try{ - AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); - } catch (Error err) { - print("Tried to open file://" + file_transfer.get_file().get_path() + " " + err.message + "\n"); - } - }); - - Revealer toolbar_revealer = new Revealer() { transition_type=RevealerTransitionType.CROSSFADE, transition_duration=400, visible=true }; - toolbar_revealer.add(toolbar); - - Grid grid = new Grid() { visible=true }; - grid.attach(toolbar_revealer, 0, 0, 1, 1); - grid.attach(image, 0, 0, 1, 1); - - EventBox event_box = new EventBox() { halign=Align.START, visible=true }; - event_box.add(grid); - event_box.enter_notify_event.connect(() => { toolbar_revealer.reveal_child = true; return false; }); - event_box.leave_notify_event.connect(() => { toolbar_revealer.reveal_child = false; return false; }); - - return event_box; - } - - private static Gdk.Pixbuf crop_corners(Gdk.Pixbuf pixbuf, double radius = 3) { - Cairo.Context ctx = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.ARGB32, pixbuf.width, pixbuf.height)); - Gdk.cairo_set_source_pixbuf(ctx, pixbuf, 0, 0); - double degrees = Math.PI / 180.0; - ctx.new_sub_path(); - ctx.arc(pixbuf.width - radius, radius, radius, -90 * degrees, 0 * degrees); - ctx.arc(pixbuf.width - radius, pixbuf.height - radius, radius, 0 * degrees, 90 * degrees); - ctx.arc(radius, pixbuf.height - radius, radius, 90 * degrees, 180 * degrees); - ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees); - ctx.close_path(); - ctx.clip(); - ctx.paint(); - return Gdk.pixbuf_get_from_surface(ctx.get_target(), 0, 0, pixbuf.width, pixbuf.height); - } - - private void update_info(Label url_label, string? info) { - string url = info ?? ""; - if (url.has_prefix("https://")) url = url.substring(8); - if (url.has_prefix("http://")) url = url.substring(7); - if (url.has_prefix("www.")) url = url.substring(4); - string[] slash_split = url.split("/"); - if (slash_split.length > 2) url = slash_split[0] + "/…/" + slash_split[slash_split.length - 1]; - url_label.label = url; - } - - private Entities.Message.Marked file_to_message_state(FileTransfer.State state) { - switch (state) { - case FileTransfer.State.IN_PROCESS: - return Entities.Message.Marked.UNSENT; - case FileTransfer.State.COMPLETE: - return Entities.Message.Marked.NONE; - case FileTransfer.State.NOT_STARTED: - return Entities.Message.Marked.UNSENT; - case FileTransfer.State.FAILED: - return Entities.Message.Marked.WONTSEND; - } - assert_not_reached(); - } -} - -} diff --git a/main/src/ui/conversation_summary/message_populator.vala b/main/src/ui/conversation_summary/message_populator.vala deleted file mode 100644 index b342306b..00000000 --- a/main/src/ui/conversation_summary/message_populator.vala +++ /dev/null @@ -1,81 +0,0 @@ -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; - private HashMap<Plugins.MetaConversationItem, Message> meta_message = new HashMap<Plugins.MetaConversationItem, Message>(); - - 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(handle_message); - stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(handle_message); - } - - 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_latest(Conversation conversation, int n) { - Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages(conversation, n); - if (messages != null) { - foreach (Entities.Message message in messages) { - handle_message(message, conversation); - } - } - } - - public void populate_before(Conversation conversation, Plugins.MetaConversationItem item, int n) { - Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(conversation, meta_message[item], 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; - double 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; - priority = provider.priority; - } - } - Plugins.MetaConversationItem? meta_item = best_provider.get_item(message, conversation); - if (meta_item == null) return; - meta_message[meta_item] = message; - - meta_item.mark = message.marked; - WeakRef weak_meta_item = WeakRef(meta_item); - WeakRef weak_message = WeakRef(message); - message.notify["marked"].connect(() => { - Plugins.MetaConversationItem? mi = weak_meta_item.get() as Plugins.MetaConversationItem; - Message? m = weak_message.get() as Message; - if (mi == null || m == null) return; - mi.mark = m.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 deleted file mode 100644 index 0b5ed6e4..00000000 --- a/main/src/ui/conversation_summary/message_textview.vala +++ /dev/null @@ -1,158 +0,0 @@ -using Gdk; -using Gtk; - -using Dino.Entities; - -namespace Dino.Ui.ConversationSummary { - -public class MessageTextView : TextView { - - private TextTag link_tag; - private TextTag bold_tag; - - public MessageTextView() { - Object(editable:false, hexpand:true, wrap_mode:WrapMode.WORD_CHAR); - - link_tag = buffer.create_tag("url", underline: Pango.Underline.SINGLE, foreground: "blue"); - bold_tag = buffer.create_tag("semibold", weight: Pango.Weight.SEMIBOLD); - button_release_event.connect((event_button) => { - if (event_button.button == 1) { - open_url(event_button); - } - return false; - }); - motion_notify_event.connect(change_cursor_over_url); - - update_display_style(); - Util.force_base_background(this, "textview, text:not(:selected)"); - style_updated.connect(update_display_style); - populate_popup.connect(populate_context_menu); - } - - // Workaround GTK TextView issues - public override void get_preferred_width (out int minimum_width, out int natural_width) { - base.get_preferred_width(out minimum_width, out natural_width); - minimum_width = 0; - } - - 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); - format_suffix_urls(text); - } - - public void highlight_word(string word) { - Regex word_regex = new Regex("""\b""" + Regex.escape_string(word) + """\b"""); - MatchInfo match_info; - word_regex.match(buffer.text, 0, out match_info); - for (; match_info.matches(); match_info.next()) { - int start; - int end; - match_info.fetch_pos(0, out start, out end); - start = buffer.text[0:start].char_count(); - end = buffer.text[0:end].char_count(); - TextIter start_iter; - TextIter end_iter; - buffer.get_iter_at_offset(out start_iter, start); - buffer.get_iter_at_offset(out end_iter, end); - buffer.apply_tag_by_name("semibold", start_iter, end_iter); - } - } - - private void update_display_style() { - LinkButton lnk = new LinkButton("http://example.com"); - RGBA link_color = lnk.get_style_context().get_color(StateFlags.LINK); - link_tag.foreground_rgba = link_color; - } - - private string? find_url_at_location(int x, int y) { - TextIter iter; - get_iter_at_location(out iter, x, y); - TextIter start_iter = iter, end_iter = iter; - if (start_iter.backward_to_tag_toggle(link_tag) && end_iter.forward_to_tag_toggle(link_tag)) { - return start_iter.get_text(end_iter); - } - - return null; - } - - private void populate_context_menu(Gtk.Menu popup) { - popup.@foreach((widget) => { widget.destroy(); }); - - Gdk.Window window = get_window(TextWindowType.TEXT); - List<weak Seat> seats = window.get_display().list_seats(); - if (seats.length() > 0) { - int device_x, device_y; - window.get_device_position(seats.nth_data(0).get_pointer(), out device_x, out device_y, null); - string url = find_url_at_location(device_x, device_y); - if (url != null) { - Gtk.MenuItem copy_url_item = new Gtk.MenuItem.with_label(_("Copy Link Address")) { visible=true }; - copy_url_item.activate.connect(() => { - Clipboard.get_default(window.get_display()).set_text(url, url.length); - }); - popup.append(copy_url_item); - } - } - - Gtk.MenuItem copy_item = new Gtk.MenuItem.with_label(_("Copy")) { visible=true }; - copy_item.sensitive = buffer.get_has_selection(); - copy_item.activate.connect(() => this.copy_clipboard() ); - popup.append(copy_item); - - Gtk.MenuItem select_all_item = new Gtk.MenuItem.with_label(_("Select All")) { visible=true }; - select_all_item.activate.connect(() => this.select_all(true) ); - popup.append(select_all_item); - } - - private void format_suffix_urls(string text) { - int absolute_start = buffer.text.char_count() - text.char_count(); - - 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()) { - int start; - int end; - match_info.fetch_pos(0, out start, out end); - start = text[0:start].char_count(); - end = text[0:end].char_count(); - TextIter start_iter; - TextIter end_iter; - buffer.get_iter_at_offset(out start_iter, absolute_start + start); - buffer.get_iter_at_offset(out end_iter, absolute_start + end); - buffer.apply_tag_by_name("url", start_iter, end_iter); - } - } - - private bool open_url(EventButton event_button) { - int buffer_x, buffer_y; - window_to_buffer_coords(TextWindowType.TEXT, (int) event_button.x, (int) event_button.y, out buffer_x, out buffer_y); - string url = find_url_at_location(buffer_x, buffer_y); - if (url != null) { - try{ - AppInfo.launch_default_for_uri(url, null); - } catch (Error err) { - print("Tried to open " + url); - } - } - return false; - } - - private bool change_cursor_over_url(EventMotion event_motion) { - TextIter iter; - get_iter_at_location(out iter, (int) event_motion.x, (int) event_motion.y); - if (iter.has_tag(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; - } -} - -} diff --git a/main/src/ui/conversation_summary/slashme_message_display.vala b/main/src/ui/conversation_summary/slashme_message_display.vala deleted file mode 100644 index 1ee20748..00000000 --- a/main/src/ui/conversation_summary/slashme_message_display.vala +++ /dev/null @@ -1,79 +0,0 @@ -using Gtk; - -using Dino.Entities; -using Xmpp; - -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.seccondary_sort_indicator = message.id + 0.0845; - 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 }; - if (conversation.type_ == Conversation.Type.GROUPCHAT) { - text_view.highlight_word(conversation.nickname); - } - - 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 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_titlebar/search_entry.vala b/main/src/ui/conversation_titlebar/search_entry.vala new file mode 100644 index 00000000..b452bdce --- /dev/null +++ b/main/src/ui/conversation_titlebar/search_entry.vala @@ -0,0 +1,30 @@ +using Gtk; +using Gee; + +using Dino.Entities; + +namespace Dino.Ui { + +public class SearchMenuEntry : Plugins.ConversationTitlebarEntry, Object { + public string id { get { return "search"; } } + + Plugins.ConversationTitlebarWidget search_button; + + public SearchMenuEntry(Plugins.ConversationTitlebarWidget search_button) { + this.search_button = search_button; + } + + public double order { get { return 1; } } + public Plugins.ConversationTitlebarWidget? get_widget(Plugins.WidgetType type) { + if (type == Plugins.WidgetType.GTK) { + return search_button; + } + return null; + } +} + +public class GlobalSearchButton : Plugins.ConversationTitlebarWidget, Gtk.ToggleButton { + public new void set_conversation(Conversation conversation) { } +} + +} diff --git a/main/src/ui/conversation_titlebar/view.vala b/main/src/ui/conversation_titlebar/view.vala index d01cd9bb..7ee47311 100644 --- a/main/src/ui/conversation_titlebar/view.vala +++ b/main/src/ui/conversation_titlebar/view.vala @@ -11,6 +11,7 @@ public class ConversationTitlebar : Gtk.HeaderBar { private Window window; private Conversation? conversation; private Gee.List<Plugins.ConversationTitlebarWidget> widgets = new ArrayList<Plugins.ConversationTitlebarWidget>(); + public GlobalSearchButton search_button = new GlobalSearchButton() { visible = true }; public ConversationTitlebar(StreamInteractor stream_interactor, Window window) { this.stream_interactor = stream_interactor; @@ -19,9 +20,11 @@ public class ConversationTitlebar : Gtk.HeaderBar { this.get_style_context().add_class("dino-right"); show_close_button = true; hexpand = true; + search_button.set_image(new Gtk.Image.from_icon_name("system-search-symbolic", Gtk.IconSize.MENU) { visible = true }); Application app = GLib.Application.get_default() as Application; app.plugin_registry.register_contact_titlebar_entry(new MenuEntry(stream_interactor)); + app.plugin_registry.register_contact_titlebar_entry(new SearchMenuEntry(search_button)); app.plugin_registry.register_contact_titlebar_entry(new OccupantsEntry(stream_interactor, window)); foreach(var e in app.plugin_registry.conversation_titlebar_entries) { @@ -33,6 +36,12 @@ public class ConversationTitlebar : Gtk.HeaderBar { } + stream_interactor.get_module(MucManager.IDENTITY).room_name_set.connect((account, jid, room_name) => { + if (conversation != null && conversation.counterpart.equals_bare(jid) && conversation.account.equals(account)) { + update_title(); + } + }); + stream_interactor.get_module(MucManager.IDENTITY).subject_set.connect((account, jid, subject) => { if (conversation != null && conversation.counterpart.equals_bare(jid) && conversation.account.equals(account)) { update_subtitle(subject); diff --git a/main/src/ui/global_search.vala b/main/src/ui/global_search.vala new file mode 100644 index 00000000..99a69e1b --- /dev/null +++ b/main/src/ui/global_search.vala @@ -0,0 +1,267 @@ +using Gee; +using Gtk; +using Pango; + +using Dino.Entities; + +namespace Dino.Ui { + +[GtkTemplate (ui = "/im/dino/Dino/global_search.ui")] +class GlobalSearch : Overlay { + public signal void selected_item(MessageItem item); + private StreamInteractor stream_interactor; + private string search = ""; + private int loaded_results = -1; + private Mutex reloading_mutex = Mutex(); + + [GtkChild] public SearchEntry search_entry; + [GtkChild] public Label entry_number_label; + [GtkChild] public ScrolledWindow results_scrolled; + [GtkChild] public Box results_box; + [GtkChild] public Stack results_empty_stack; + [GtkChild] public Frame auto_complete_overlay; + [GtkChild] public ListBox auto_complete_list; + + public GlobalSearch init(StreamInteractor stream_interactor) { + this.stream_interactor = stream_interactor; + + search_entry.search_changed.connect(() => { + set_search(search_entry.text); + }); + search_entry.notify["text"].connect_after(() => { update_auto_complete(); }); + search_entry.notify["cursor-position"].connect_after(() => { update_auto_complete(); }); + + results_scrolled.vadjustment.notify["value"].connect(() => { + if (results_scrolled.vadjustment.upper - (results_scrolled.vadjustment.value + results_scrolled.vadjustment.page_size) < 100) { + if (!reloading_mutex.trylock()) return; + Gee.List<MessageItem> new_messages = stream_interactor.get_module(SearchProcessor.IDENTITY).match_messages(search, loaded_results); + if (new_messages.size == 0) { + reloading_mutex.unlock(); + return; + } + loaded_results += new_messages.size; + append_messages(new_messages); + } + }); + results_scrolled.vadjustment.notify["upper"].connect_after(() => { + reloading_mutex.trylock(); + reloading_mutex.unlock(); + }); + + event.connect((event) => { + if (auto_complete_overlay.visible) { + if (event.type == Gdk.EventType.KEY_PRESS && event.key.keyval == Gdk.Key.Up) { + var row = auto_complete_list.get_selected_row(); + var index = row == null ? -1 : row.get_index() - 1; + if (index == -1) index = (int)auto_complete_list.get_children().length() - 1; + auto_complete_list.select_row(auto_complete_list.get_row_at_index(index)); + return true; + } + if (event.type == Gdk.EventType.KEY_PRESS && event.key.keyval == Gdk.Key.Down) { + var row = auto_complete_list.get_selected_row(); + var index = row == null ? 0 : row.get_index() + 1; + if (index == auto_complete_list.get_children().length()) index = 0; + auto_complete_list.select_row(auto_complete_list.get_row_at_index(index)); + return true; + } + if (event.type == Gdk.EventType.KEY_PRESS && event.key.keyval == Gdk.Key.Tab || + event.type == Gdk.EventType.KEY_RELEASE && event.key.keyval == Gdk.Key.Return) { + auto_complete_list.get_selected_row().activate(); + return true; + } + } + // TODO: Handle cursor movement in results + // TODO: Direct all keystrokes to text input + return false; + }); + + return this; + } + + private void update_auto_complete() { + Gee.List<SearchSuggestion> suggestions = stream_interactor.get_module(SearchProcessor.IDENTITY).suggest_auto_complete(search_entry.text, search_entry.cursor_position); + auto_complete_overlay.visible = suggestions.size > 0; + if (suggestions.size > 0) { + auto_complete_list.@foreach((widget) => auto_complete_list.remove(widget)); + foreach(SearchSuggestion suggestion in suggestions) { + Builder builder = new Builder.from_resource("/im/dino/Dino/search_autocomplete.ui"); + AvatarImage avatar = (AvatarImage)builder.get_object("image"); + avatar.set_jid(stream_interactor, suggestion.jid, suggestion.account); + Label label = (Label)builder.get_object("label"); + string display_name = Util.get_display_name(stream_interactor, suggestion.jid, suggestion.account); + if (display_name != suggestion.jid.to_string()) { + label.set_markup(@"$display_name <span font_weight='light' fgalpha='80%'>$(suggestion.jid)</span>"); + } else { + label.label = display_name; + } + ListBoxRow row = new ListBoxRow() { visible = true, can_focus = false }; + row.add((Widget)builder.get_object("root")); + row.activate.connect(() => { + handle_suggestion(suggestion); + }); + auto_complete_list.add(row); + } + auto_complete_list.select_row(auto_complete_list.get_row_at_index(0)); + } + } + + private void handle_suggestion(SearchSuggestion suggestion) { + search_entry.move_cursor(MovementStep.LOGICAL_POSITIONS, suggestion.start_index - search_entry.cursor_position, false); + search_entry.delete_from_cursor(DeleteType.CHARS, suggestion.end_index - suggestion.start_index); + search_entry.insert_at_cursor(suggestion.completion + " "); + } + + private void clear_search() { + results_box.@foreach((widget) => { widget.destroy(); }); + } + + private void set_search(string search) { + clear_search(); + this.search = search; + + if (get_keywords(search).is_empty) { + results_empty_stack.set_visible_child_name("empty"); + return; + } + + Gee.List<MessageItem> messages = stream_interactor.get_module(SearchProcessor.IDENTITY).match_messages(search); + if (messages.size == 0) { + results_empty_stack.set_visible_child_name("no-result"); + } else { + results_empty_stack.set_visible_child_name("results"); + + int match_count = messages.size < 10 ? messages.size : stream_interactor.get_module(SearchProcessor.IDENTITY).count_match_messages(search); + entry_number_label.label = "<i>" + _("%i search results").printf(match_count) + "</i>"; + loaded_results += messages.size; + append_messages(messages); + } + } + + private void append_messages(Gee.List<MessageItem> messages) { + foreach (MessageItem item in messages) { + Gee.List<MessageItem> before_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before_message(item.conversation, item.message.local_time, item.message.id, 1); + Gee.List<MessageItem> after_message = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_after_message(item.conversation, item.message.local_time, item.message.id, 1); + + Box context_box = new Box(Orientation.VERTICAL, 5) { visible=true }; + if (before_message != null && before_message.size > 0) { + context_box.add(get_context_message_widget(before_message.first())); + } + + Widget match_widget = get_match_message_widget(item); + context_box.add(match_widget); + + if (after_message != null && after_message.size > 0) { + context_box.add(get_context_message_widget(after_message.first())); + } + + Label date_label = new Label(ConversationSummary.DefaultSkeletonHeader.get_relative_time(item.display_time)) { xalign=0, visible=true }; + date_label.get_style_context().add_class("dim-label"); + + string display_name = Util.get_conversation_display_name(stream_interactor, item.conversation); + string title = item.message.type_ == Message.Type.GROUPCHAT ? _("In %s").printf(display_name) : _("With %s").printf(display_name); + Box header_box = new Box(Orientation.HORIZONTAL, 10) { margin_start=7, visible=true }; + header_box.add(new Label(@"<b>$(Markup.escape_text(title))</b>") { ellipsize=EllipsizeMode.END, xalign=0, use_markup=true, visible=true }); + header_box.add(date_label); + + Box result_box = new Box(Orientation.VERTICAL, 7) { visible=true }; + result_box.add(header_box); + result_box.add(context_box); + + results_box.add(result_box); + } + } + + private Widget get_match_message_widget(MessageItem item) { + Grid grid = get_skeleton(item); + grid.margin_top = 3; + grid.margin_bottom = 3; + + string text = item.message.body.replace("\n", "").replace("\r", ""); + if (text.length > 200) { + int index = text.index_of(search); + if (index + search.length <= 100) { + text = text.substring(0, 150) + " … " + text.substring(text.length - 50, 50); + } else if (index >= text.length - 100) { + text = text.substring(0, 50) + " … " + text.substring(text.length - 150, 150); + } else { + text = text.substring(0, 25) + " … " + text.substring(index - 50, 50) + text.substring(index, 100) + " … " + text.substring(text.length - 25, 25); + } + } + Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, vexpand=true, visible=true }; + string markup_text = Markup.escape_text(text); + + // Build regex containing all keywords + string regex_str = "("; + Gee.List<string> keywords = get_keywords(Regex.escape_string(search.down())); + bool first = true; + foreach (string keyword in keywords) { + if (first) { + first = false; + } else { + regex_str += "|"; + } + regex_str += "\\b" + keyword; + } + regex_str += ")"; + + // Color the keywords + int elongated_by = 0; + Regex highlight_regex = new Regex(regex_str); + MatchInfo match_info; + string markup_text_bak = markup_text.down(); + highlight_regex.match(markup_text_bak, 0, out match_info); + for (; match_info.matches(); match_info.next()) { + int start, end; + match_info.fetch_pos(0, out start, out end); + markup_text = markup_text[0:start+elongated_by] + "<span bgcolor=\"yellow\">" + markup_text[start+elongated_by:end+elongated_by] + "</span>" + markup_text[end+elongated_by:markup_text.length]; + elongated_by += "<span bgcolor=\"yellow\">".length + "</span>".length; + } + markup_text_bak += ""; // We need markup_text_bak to live until here because url_regex.match does not copy the string + + label.label = markup_text; + grid.attach(label, 1, 1, 1, 1); + + Button button = new Button() { relief=ReliefStyle.NONE, visible=true }; + button.clicked.connect(() => { + selected_item(item); + }); + button.add(grid); + return button; + } + + private Grid get_context_message_widget(MessageItem item) { + Grid grid = get_skeleton(item); + grid.margin_start = 7; + Label label = new Label(item.message.body.replace("\n", "").replace("\r", "")) { ellipsize=EllipsizeMode.MIDDLE, xalign=0, visible=true }; + grid.attach(label, 1, 1, 1, 1); + grid.opacity = 0.55; + return grid; + } + + private Grid get_skeleton(MessageItem item) { + AvatarImage image = new AvatarImage() { height=32, width=32, margin_end=7, valign=Align.START, visible=true, allow_gray = false }; + image.set_jid(stream_interactor, item.jid, item.message.account); + Grid grid = new Grid() { row_homogeneous=false, visible=true }; + grid.attach(image, 0, 0, 1, 2); + + string display_name = Util.get_display_name(stream_interactor, item.jid, item.message.account); + string color = Util.get_name_hex_color(stream_interactor, item.message.account, item.jid, false); // TODO Util.is_dark_theme(name_label) + Label name_label = new Label("") { use_markup=true, xalign=0, visible=true }; + name_label.label = @"<span size='small' foreground=\"#$color\">$display_name</span>"; + grid.attach(name_label, 1, 0, 1, 1); + return grid; + } + + private static Gee.List<string> get_keywords(string search_string) { + Gee.List<string> ret = new ArrayList<string>(); + foreach (string search in search_string.split(" ")) { + bool is_filter = search.has_prefix("from:") || search.has_prefix("in:") || search.has_prefix("with:"); + if (!is_filter && search != "") { + ret.add(search); + } + } + return ret; + } +} + +} diff --git a/main/src/ui/manage_accounts/add_account_dialog.vala b/main/src/ui/manage_accounts/add_account_dialog.vala index 5715db51..f9ab794e 100644 --- a/main/src/ui/manage_accounts/add_account_dialog.vala +++ b/main/src/ui/manage_accounts/add_account_dialog.vala @@ -11,29 +11,120 @@ public class AddAccountDialog : Gtk.Dialog { public signal void added(Account account); - [GtkChild] private Button cancel_button; - [GtkChild] private Button ok_button; - [GtkChild] private Entry alias_entry; + [GtkChild] private Stack stack; + + [GtkChild] private Revealer notification_revealer; + [GtkChild] private Label notification_label; + + // Sign in + [GtkChild] private Box sign_in_box; [GtkChild] private Entry jid_entry; + [GtkChild] private Entry alias_entry; [GtkChild] private Entry password_entry; + [GtkChild] private Button sign_in_continue; + [GtkChild] private Button serverlist_button; + + // Select Server + [GtkChild] private Box create_account_box; + [GtkChild] private Button login_button; + [GtkChild] private Stack select_server_continue_stack; + [GtkChild] private Button select_server_continue; + [GtkChild] private Label register_form_continue_label; + [GtkChild] private ListBox server_list_box; + [GtkChild] private Entry server_entry; + + // Register Form + [GtkChild] private Box register_box; + [GtkChild] private Label register_title; + [GtkChild] private Box form_box; + [GtkChild] private Button register_form_back; + [GtkChild] private Stack register_form_continue_stack; + [GtkChild] private Button register_form_continue; + + private static string[] server_list = new string[]{ + "5222.de", + "jabber.fr", + "movim.eu", + "yax.im" + }; + private HashMap<ListBoxRow, string> list_box_jids = new HashMap<ListBoxRow, string>(); + private Jid? server_jid = null; + private Xep.InBandRegistration.Form? form = null; public AddAccountDialog(StreamInteractor stream_interactor) { - Object(use_header_bar : 1); this.title = _("Add Account"); - cancel_button.clicked.connect(() => { close(); }); - ok_button.clicked.connect(on_ok_button_clicked); + // Sign in jid_entry.changed.connect(on_jid_entry_changed); jid_entry.focus_out_event.connect(on_jid_entry_focus_out_event); + sign_in_continue.clicked.connect(on_sign_in_continue_clicked); + serverlist_button.clicked.connect(show_select_server); + + // Select Server + server_entry.changed.connect(() => { + Jid? jid = Jid.parse(server_entry.text); + select_server_continue.sensitive = jid != null && jid.localpart == null && jid.resourcepart == null; + }); + select_server_continue.clicked.connect(on_select_server_continue); + login_button.clicked.connect(show_sign_in); + + foreach (string server in server_list) { + ListBoxRow list_box_row = new ListBoxRow() { visible=true }; + list_box_row.add(new Label(server) { xalign=0, margin=3, margin_start=7, margin_end=7, visible=true }); + list_box_jids[list_box_row] = server; + server_list_box.add(list_box_row); + } + + // Register Form + register_form_continue.clicked.connect(on_register_form_continue_clicked); + register_form_back.clicked.connect(show_select_server); + + show_sign_in(); + } + + private void show_sign_in() { + sign_in_box.visible = true; + stack.visible_child_name = "login"; + create_account_box.visible = false; + register_box.visible = false; + set_default(sign_in_continue); + animate_window_resize(sign_in_box); + } + + private void show_select_server() { + server_entry.text = ""; + server_entry.grab_focus(); + set_default(select_server_continue); + + server_list_box.row_selected.disconnect(on_server_list_row_selected); + server_list_box.unselect_all(); + server_list_box.row_selected.connect(on_server_list_row_selected); + + create_account_box.visible = true; + stack.visible_child_name = "server"; + sign_in_box.visible = false; + register_box.visible = false; + + animate_window_resize(create_account_box); + } + + private void show_register_form() { + register_box.visible = true; + stack.visible_child_name = "form"; + sign_in_box.visible = false; + create_account_box.visible = false; + + set_default(register_form_continue); + animate_window_resize(register_box); } private void on_jid_entry_changed() { Jid? jid = Jid.parse(jid_entry.text); if (jid != null && jid.localpart != null && jid.resourcepart == null) { - ok_button.set_sensitive(true); + sign_in_continue.set_sensitive(true); jid_entry.secondary_icon_name = null; } else { - ok_button.set_sensitive(false); + sign_in_continue.set_sensitive(false); } } @@ -41,7 +132,6 @@ public class AddAccountDialog : Gtk.Dialog { Jid? jid = Jid.parse(jid_entry.text); if (jid == null || jid.localpart == null || jid.resourcepart != null) { jid_entry.secondary_icon_name = "dialog-warning-symbolic"; - // TODO why doesn't the tooltip work jid_entry.set_icon_tooltip_text(EntryIconPosition.SECONDARY, _("JID should be of the form “user@example.com”")); } else { jid_entry.secondary_icon_name = null; @@ -49,13 +139,131 @@ public class AddAccountDialog : Gtk.Dialog { return false; } - private void on_ok_button_clicked() { + private void on_sign_in_continue_clicked() { Jid jid = new Jid(jid_entry.get_text()); string password = password_entry.get_text(); string alias = alias_entry.get_text(); + store_account(jid, password, alias); + close(); + } + + private void on_select_server_continue() { + server_jid = new Jid(server_entry.text); + request_show_register_form.begin(); + } + + private void on_server_list_row_selected(ListBox box, ListBoxRow? row) { + server_jid = new Jid(list_box_jids[row]); + request_show_register_form.begin(); + } + + private async void request_show_register_form() { + select_server_continue_stack.visible_child_name = "spinner"; + form = yield Register.get_registration_form(server_jid); + if (select_server_continue_stack == null) { + return; + } + select_server_continue_stack.visible_child_name = "label"; + if (form != null) { + set_register_form(server_jid, form); + show_register_form(); + } else { + display_notification(_("No response from server")); + } + } + + private void set_register_form(Jid server, Xep.InBandRegistration.Form form) { + form_box.foreach((widget) => { widget.destroy(); }); + register_title.label = _("Register on %s").printf(server.to_string()); + + if (form.oob != null) { + form_box.add(new Label(_("The server requires to sign up through a website")){ use_markup=true, visible=true } ); + form_box.add(new Label(@"<a href=\"$(form.oob)\">$(form.oob)</a>") { use_markup=true, visible=true }); + register_form_continue_label.label = _("Open Registration"); + register_form_continue.visible = true; + register_form_continue.grab_focus(); + } else if (form.fields.size > 0) { + int i = 0; + foreach (Xep.DataForms.DataForm.Field field in form.fields) { + if (field.label != null && field.label != "") { + form_box.add(new Label(field.label) { xalign=0, margin_top=7, visible=true }); + } + Widget field_widget = Util.get_data_form_fild_widget(field); + if (field_widget != null) { + form_box.add(field_widget); + } + i++; + } + register_form_continue.visible = true; + register_form_continue_label.label = _("Register"); + } else { + form_box.add(new Label(_("Check %s for information on how to sign up").printf(@"<a href=\"http://$(server)\">$(server)</a>")) { use_markup=true, visible=true }); + register_form_continue.visible = false; + } + } + + private async void on_register_form_continue_clicked() { + notification_revealer.set_reveal_child(false); + // Button is opening a registration website + if (form.oob != null) { + try { + AppInfo.launch_default_for_uri(form.oob, null); + } catch (Error e) { } + show_sign_in(); + return; + } + + register_form_continue_stack.visible_child_name = "spinner"; + string? error = yield Register.submit_form(server_jid, form); + if (register_form_continue_stack == null) { + return; + } + register_form_continue_stack.visible_child_name = "label"; + if (error == null) { + string? username = null, password = null; + foreach (Xep.DataForms.DataForm.Field field in form.fields) { + switch (field.var) { + case "username": username = field.get_value_string(); break; + case "password": password = field.get_value_string(); break; + } + } + store_account(new Jid(username + "@" + server_jid.domainpart), password, ""); + close(); + } else { + display_notification(error); + } + } + + private void store_account(Jid jid, string password, string? alias) { Account account = new Account(jid, null, password, alias); added(account); - close(); + } + + private void display_notification(string text) { + notification_label.label = text; + notification_revealer.set_reveal_child(true); + Timeout.add_seconds(5, () => { + notification_revealer.set_reveal_child(false); + return false; + }); + } + + private void animate_window_resize(Widget widget) { // TODO code duplication + int def_height, curr_width, curr_height; + get_size(out curr_width, out curr_height); + widget.get_preferred_height(null, out def_height); + def_height += 5; + int difference = def_height - curr_height; + Timer timer = new Timer(); + Timeout.add((int) (stack.transition_duration / 30), + () => { + ulong microsec; + timer.elapsed(out microsec); + ulong millisec = microsec / 1000; + double partial = double.min(1, (double) millisec / stack.transition_duration); + resize(curr_width, (int) (curr_height + difference * partial)); + return millisec < stack.transition_duration; + }); } } diff --git a/main/src/ui/manage_accounts/dialog.vala b/main/src/ui/manage_accounts/dialog.vala index 5706fc8c..1a370349 100644 --- a/main/src/ui/manage_accounts/dialog.vala +++ b/main/src/ui/manage_accounts/dialog.vala @@ -215,15 +215,6 @@ public class Dialog : Gtk.Dialog { if (error != null) { state_label.label = get_connection_error_description(error); state_label.get_style_context().add_class("is_error"); - - if (error.source == ConnectionManager.ConnectionError.Source.SASL || - error.source == ConnectionManager.ConnectionError.Source.TLS || - error.reconnect_recomendation == ConnectionManager.ConnectionError.Reconnect.NEVER) { - active_switch.state_set.disconnect(change_account_state); - active_switch.active = false; - active_switch.state_set.connect(change_account_state); - } - } else { ConnectionManager.ConnectionState state = stream_interactor.connection_manager.get_state(account); switch (state) { diff --git a/main/src/ui/notifications.vala b/main/src/ui/notifications.vala index f7540a4d..77a290a0 100644 --- a/main/src/ui/notifications.vala +++ b/main/src/ui/notifications.vala @@ -41,19 +41,36 @@ public class Notifications : Object { } public void start() { - stream_interactor.get_module(NotificationEvents.IDENTITY).notify_message.connect(notify_message); + stream_interactor.get_module(NotificationEvents.IDENTITY).notify_content_item.connect(notify_content_item); stream_interactor.get_module(NotificationEvents.IDENTITY).notify_subscription_request.connect(notify_subscription_request); + stream_interactor.get_module(NotificationEvents.IDENTITY).notify_connection_error.connect(notify_connection_error); } - private void notify_message(Entities.Message message, Conversation conversation) { + private void notify_content_item(ContentItem content_item, Conversation conversation) { if (!notifications.has_key(conversation)) { notifications[conversation] = new Notification(""); notifications[conversation].set_default_action_and_target_value("app.open-conversation", new Variant.int32(conversation.id)); } string display_name = Util.get_conversation_display_name(stream_interactor, conversation); - string text = message.body; + string text = ""; + switch (content_item.type_) { + case MessageItem.TYPE: + Message message = (content_item as MessageItem).message; + text = message.body; + break; + case FileItem.TYPE: + FileItem file_item = content_item as FileItem; + FileTransfer transfer = file_item.file_transfer; + + if (transfer.direction == Message.DIRECTION_SENT) { + text = transfer.mime_type.has_prefix("image") ? _("Image sent") : _("File sent"); + } else { + text = transfer.mime_type.has_prefix("image") ? _("Image received") : _("File received"); + } + break; + } if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(conversation.counterpart, conversation.account)) { - string muc_occupant = Util.get_display_name(stream_interactor, message.from, conversation.account); + string muc_occupant = Util.get_display_name(stream_interactor, content_item.jid, conversation.account); text = @"$muc_occupant: $text"; } notifications[conversation].set_title(display_name); @@ -79,6 +96,19 @@ public class Notifications : Object { active_ids.add(conversation.id.to_string() + "-subscription"); } + private void notify_connection_error(Account account, ConnectionManager.ConnectionError error) { + Notification notification = new Notification(_("Failed connecting to %s").printf(account.bare_jid.domainpart)); + switch (error.source) { + case ConnectionManager.ConnectionError.Source.SASL: + notification.set_body("Wrong password"); + break; + case ConnectionManager.ConnectionError.Source.TLS: + notification.set_body("Invalid TLS certificate"); + break; + } + window.get_application().send_notification(account.id.to_string() + "-connection-error", notification); + } + private Icon get_pixbuf_icon(Cairo.ImageSurface surface) throws Error { Gdk.Pixbuf avatar = Gdk.pixbuf_get_from_surface(surface, 0, 0, surface.get_width(), surface.get_height()); uint8[] buffer; diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala index e2798def..61a22085 100644 --- a/main/src/ui/unified_window.vala +++ b/main/src/ui/unified_window.vala @@ -1,11 +1,12 @@ using Gee; +using Gdk; using Gtk; using Dino.Entities; namespace Dino.Ui { -public class UnifiedWindow : Window { +public class UnifiedWindow : Gtk.Window { private NoAccountsPlaceholder accounts_placeholder = new NoAccountsPlaceholder() { visible=true }; private NoConversationsPlaceholder conversations_placeholder = new NoConversationsPlaceholder() { visible=true }; @@ -16,7 +17,12 @@ public class UnifiedWindow : Window { 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 }; - private Paned paned = new Paned(Orientation.HORIZONTAL) { visible=true }; + private Paned paned; + private Revealer goto_end_revealer; + private Button goto_end_button; + private Revealer search_revealer; + private SearchEntry search_entry; + private GlobalSearch search_box; private Stack stack = new Stack() { visible=true }; private StreamInteractor stream_interactor; @@ -36,8 +42,47 @@ public class UnifiedWindow : Window { setup_unified(); setup_stack(); - conversation_list_titlebar.search_button.bind_property("active", filterable_conversation_list.search_revealer, "reveal-child", - BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + var vadjustment = conversation_frame.scrolled.vadjustment; + vadjustment.notify["value"].connect(() => { + goto_end_revealer.reveal_child = vadjustment.value < vadjustment.upper - vadjustment.page_size; + }); + goto_end_button.clicked.connect(() => { + conversation_frame.initialize_for_conversation(conversation); + }); + + conversation_titlebar.search_button.clicked.connect(() => { + search_revealer.reveal_child = conversation_titlebar.search_button.active; + }); + search_revealer.notify["child-revealed"].connect(() => { + if (search_revealer.child_revealed) { + if (conversation_frame.conversation != null && search_box.search_entry.text == "") { + reset_search_entry(); + } + search_box.search_entry.grab_focus(); + } + }); + search_box.selected_item.connect((item) => { + on_conversation_selected(item.conversation, false, false); + conversation_frame.initialize_around_message(item.conversation, item); + close_search(); + }); + event.connect((event) => { + if (event.type == EventType.BUTTON_PRESS) { + int dest_x, dest_y; + bool ret = search_box.translate_coordinates(this, 0, 0, out dest_x, out dest_y); + int geometry_x, geometry_y, geometry_width, geometry_height; + this.get_window().get_geometry(out geometry_x, out geometry_y, out geometry_width, out geometry_height); + if (ret && event.button.x_root - geometry_x < dest_x || event.button.y_root - geometry_y < dest_y) { + close_search(); + } + } else if (event.type == EventType.KEY_RELEASE) { + if (event.key.keyval == Gdk.Key.Escape) { + close_search(); + } + } + return false; + }); + paned.bind_property("position", headerbar_paned, "position", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); focus_in_event.connect(on_focus_in_event); @@ -50,38 +95,60 @@ public class UnifiedWindow : Window { accounts_placeholder.primary_button.clicked.connect(() => { get_application().activate_action("accounts", null); }); conversations_placeholder.primary_button.clicked.connect(() => { get_application().activate_action("add_chat", null); }); conversations_placeholder.secondary_button.clicked.connect(() => { get_application().activate_action("add_conference", null); }); - filterable_conversation_list.conversation_list.conversation_selected.connect(on_conversation_selected); - conversation_list_titlebar.conversation_opened.connect(on_conversation_selected); + filterable_conversation_list.conversation_list.conversation_selected.connect((conversation) => on_conversation_selected(conversation)); + conversation_list_titlebar.conversation_opened.connect((conversation) => on_conversation_selected(conversation)); check_stack(); } - public void on_conversation_selected(Conversation conversation) { + private void reset_search_entry() { + if (conversation_frame.conversation != null) { + switch (conversation.type_) { + case Conversation.Type.CHAT: + case Conversation.Type.GROUPCHAT_PM: + search_box.search_entry.text = @"with:$(conversation.counterpart) "; + break; + case Conversation.Type.GROUPCHAT: + search_box.search_entry.text = @"in:$(conversation.counterpart) "; + break; + } + } + } + + public void on_conversation_selected(Conversation conversation, bool do_reset_search = true, bool default_initialize_conversation = true) { if (this.conversation == null || !this.conversation.equals(conversation)) { this.conversation = conversation; stream_interactor.get_module(ChatInteraction.IDENTITY).on_conversation_selected(conversation); conversation.active = true; // only for conversation_selected filterable_conversation_list.conversation_list.on_conversation_selected(conversation); // only for conversation_opened + if (do_reset_search) { + reset_search_entry(); + } chat_input.initialize_for_conversation(conversation); - conversation_frame.initialize_for_conversation(conversation); + if (default_initialize_conversation) { + conversation_frame.initialize_for_conversation(conversation); + } conversation_titlebar.initialize_for_conversation(conversation); } } + private void close_search() { + conversation_titlebar.search_button.active = false; + search_revealer.reveal_child = false; + } + private void setup_unified() { - chat_input = new ChatInput.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 }; - grid.get_style_context().add_class("dino-conversation"); - grid.add(conversation_frame); - grid.add(chat_input); - - paned.set_position(300); - paned.pack1(filterable_conversation_list, false, false); - paned.pack2(grid, true, false); + Builder builder = new Builder.from_resource("/im/dino/Dino/unified_main_content.ui"); + paned = (Paned) builder.get_object("paned"); + chat_input = ((ChatInput.View) builder.get_object("chat_input")).init(stream_interactor); + conversation_frame = ((ConversationSummary.ConversationView) builder.get_object("conversation_frame")).init(stream_interactor); + filterable_conversation_list = ((ConversationSelector.View) builder.get_object("conversation_list")).init(stream_interactor); + goto_end_revealer = (Revealer) builder.get_object("goto_end_revealer"); + goto_end_button = (Button) builder.get_object("goto_end_button"); + search_box = ((GlobalSearch) builder.get_object("search_box")).init(stream_interactor); + search_revealer = (Revealer) builder.get_object("search_revealer"); + search_entry = (SearchEntry) builder.get_object("search_entry"); } private void setup_headerbar() { diff --git a/main/src/ui/util/data_forms.vala b/main/src/ui/util/data_forms.vala new file mode 100644 index 00000000..11308462 --- /dev/null +++ b/main/src/ui/util/data_forms.vala @@ -0,0 +1,57 @@ +using Gee; +using Gtk; + +using Dino.Entities; +using Xmpp.Xep; + +namespace Dino.Ui.Util { + +public static Widget? get_data_form_fild_widget(DataForms.DataForm.Field field) { + if (field.type_ == null) return null; + switch (field.type_) { + case DataForms.DataForm.Type.BOOLEAN: + DataForms.DataForm.BooleanField boolean_field = field as DataForms.DataForm.BooleanField; + Switch sw = new Switch() { active=boolean_field.value, valign=Align.CENTER, visible=true }; + sw.state_set.connect((state) => { + boolean_field.value = state; + return false; + }); + return sw; + case DataForms.DataForm.Type.JID_MULTI: + return null; + case DataForms.DataForm.Type.LIST_SINGLE: + DataForms.DataForm.ListSingleField list_single_field = field as DataForms.DataForm.ListSingleField; + ComboBoxText combobox = new ComboBoxText() { valign=Align.CENTER, visible=true }; + for (int i = 0; i < list_single_field.options.size; i++) { + DataForms.DataForm.Option option = list_single_field.options[i]; + combobox.append(option.value, option.label); + if (option.value == list_single_field.value) combobox.active = i; + } + combobox.changed.connect(() => { + list_single_field.value = combobox.get_active_id(); + }); + return combobox; + case DataForms.DataForm.Type.LIST_MULTI: + return null; + case DataForms.DataForm.Type.TEXT_PRIVATE: + DataForms.DataForm.TextPrivateField text_private_field = field as DataForms.DataForm.TextPrivateField; + Entry entry = new Entry() { text=text_private_field.value ?? "", valign=Align.CENTER, visible=true, visibility=false }; + entry.key_release_event.connect(() => { + text_private_field.value = entry.text; + return false; + }); + return entry; + case DataForms.DataForm.Type.TEXT_SINGLE: + DataForms.DataForm.TextSingleField text_single_field = field as DataForms.DataForm.TextSingleField; + Entry entry = new Entry() { text=text_single_field.value ?? "", valign=Align.CENTER, visible=true }; + entry.key_release_event.connect(() => { + text_single_field.value = entry.text; + return false; + }); + return entry; + default: + return null; + } +} + +} diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index 3cadfffb..e51b8344 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -59,7 +59,13 @@ public static string get_conversation_display_name(StreamInteractor stream_inter } public static string get_display_name(StreamInteractor stream_interactor, Jid jid, Account account) { - if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid, account)) { + if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(jid, account)) { + string room_name = stream_interactor.get_module(MucManager.IDENTITY).get_room_name(account, jid); + if (room_name != null && room_name != jid.localpart) { + return room_name; + } + return jid.bare_jid.to_string(); + } else if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid, account)) { return jid.resourcepart; } else { if (jid.equals_bare(account.bare_jid)) { @@ -118,10 +124,6 @@ public static void force_background(Gtk.Widget widget, string color, string sele force_css(widget, force_background_css.printf(selector, color)); } -public static void force_base_background(Gtk.Widget widget, string selector = "*") { - force_background(widget, "@theme_base_color", selector); -} - public static void force_color(Gtk.Widget widget, string color, string selector = "*") { force_css(widget, force_color_css.printf(selector, color)); } @@ -142,4 +144,63 @@ public static bool is_24h_format() { return settings_format == "24h" || p_format == " "; } +public static string parse_add_markup(string s_, string? highlight_word, bool parse_links, bool parse_text_markup, bool already_escaped_ = false) { + string s = s_; + bool already_escaped = already_escaped_; + + if (parse_links) { + 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(s.down(), 0, out match_info); + if (match_info.matches()) { + int start, end; + match_info.fetch_pos(0, out start, out end); + string link = s[start:end]; + return parse_add_markup(s[0:start], highlight_word, parse_links, parse_text_markup, already_escaped) + + "<a href=\"" + Markup.escape_text(link) + "\">" + parse_add_markup(link, highlight_word, false, false, already_escaped) + "</a>" + + parse_add_markup(s[end:s.length], highlight_word, parse_links, parse_text_markup, already_escaped); + } + } + + if (!already_escaped) { + s = Markup.escape_text(s); + already_escaped = true; + } + + if (highlight_word != null) { + Regex highlight_regex = new Regex("\\b" + Regex.escape_string(highlight_word.down()) + "\\b"); + MatchInfo match_info; + highlight_regex.match(s.down(), 0, out match_info); + if (match_info.matches()) { + int start, end; + match_info.fetch_pos(0, out start, out end); + return parse_add_markup(s[0:start], highlight_word, parse_links, parse_text_markup, already_escaped) + + "<b>" + s[start:end] + "</b>" + + parse_add_markup(s[end:s.length], highlight_word, parse_links, parse_text_markup, already_escaped); + } + } + + if (parse_text_markup) { + string[] markup_string = new string[]{"`", "_", "*"}; + string[] convenience_tag = new string[]{"tt", "i", "b"}; + + for (int i = 0; i < markup_string.length; i++) { + Regex regex = new Regex(Regex.escape_string(markup_string[i]) + ".+" + Regex.escape_string(markup_string[i])); + MatchInfo match_info; + regex.match(s.down(), 0, out match_info); + if (match_info.matches()) { + int start, end; + match_info.fetch_pos(0, out start, out end); + start += markup_string[i].length; + end -= markup_string[i].length; + return parse_add_markup(s[0:start], highlight_word, parse_links, parse_text_markup, already_escaped) + + @"<$(convenience_tag[i])>" + s[start:end] + @"</$(convenience_tag[i])>" + + parse_add_markup(s[end:s.length], highlight_word, parse_links, parse_text_markup, already_escaped); + } + } + } + + return s; +} + } |