diff --git a/libdino/src/service/notification_events.vala b/libdino/src/service/notification_events.vala index 2d6a2545..a8164970 100644 --- a/libdino/src/service/notification_events.vala +++ b/libdino/src/service/notification_events.vala @@ -52,7 +52,6 @@ public class NotificationEvents : StreamInteractionModule, Object { return; } if (!should_notify_message(message, conversation)) return; - if (!should_notify_message(message, conversation)) return; if (stream_interactor.get_module(ChatInteraction.IDENTITY).is_active_focus()) return; notify_message(message, conversation); } @@ -62,7 +61,7 @@ public class NotificationEvents : StreamInteractionModule, Object { if (notify == Conversation.NotifySetting.OFF) return false; Jid? nick = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account); if (notify == Conversation.NotifySetting.HIGHLIGHT && nick != null) { - return Regex.match_simple("""\b""" + Regex.escape_string(nick.resourcepart) + """\b""", message.body, RegexCompileFlags.CASELESS); + return Regex.match_simple("\\b" + Regex.escape_string(nick.resourcepart) + "\\b", message.body, RegexCompileFlags.CASELESS); } return true; } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 65d84bdd..e6cecb94 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -109,7 +109,6 @@ SOURCES src/ui/conversation_summary/conversation_item_skeleton.vala src/ui/conversation_summary/conversation_view.vala src/ui/conversation_summary/date_separator_populator.vala - src/ui/conversation_summary/message_textview.vala src/ui/conversation_summary/subscription_notification.vala src/ui/conversation_titlebar/menu_entry.vala src/ui/conversation_titlebar/occupants_entry.vala diff --git a/main/data/conversation_summary/view.ui b/main/data/conversation_summary/view.ui index 2bc13752..90d3d7c1 100644 --- a/main/data/conversation_summary/view.ui +++ b/main/data/conversation_summary/view.ui @@ -15,6 +15,7 @@ never + True True @@ -29,12 +30,6 @@ True - - - True - True - - diff --git a/main/data/global_search.ui b/main/data/global_search.ui index 44abf6de..4814f236 100644 --- a/main/data/global_search.ui +++ b/main/data/global_search.ui @@ -133,6 +133,7 @@ vertical 25 10 + start True diff --git a/main/data/theme.css b/main/data/theme.css index 226689b3..174ce5b2 100644 --- a/main/data/theme.css +++ b/main/data/theme.css @@ -29,10 +29,6 @@ window.dino-main .dino-conversation .highlight-once { animation-name: highlight; } -window.dino-main .dino-conversation textview, window.dino-main .dino-conversation textview text { - background: transparent; -} - window.dino-main .dino-sidebar > frame { background: @insensitive_bg_color; border-left: 1px solid @borders; @@ -51,11 +47,6 @@ window.dino-main .dino-sidebar frame.auto-complete list > row { transition: none; } -window.dino-main .dino-sidebar textview, -window.dino-main .dino-sidebar textview text { - background-color: transparent; -} - window.dino-main .dino-chatinput frame box { background: transparent; } diff --git a/main/src/ui/conversation_summary/content_item_widget_factory.vala b/main/src/ui/conversation_summary/content_item_widget_factory.vala index 8a2bf136..9e1edcb0 100644 --- a/main/src/ui/conversation_summary/content_item_widget_factory.vala +++ b/main/src/ui/conversation_summary/content_item_widget_factory.vala @@ -1,6 +1,7 @@ using Gee; using Gdk; using Gtk; +using Xmpp; using Dino.Entities; @@ -51,33 +52,36 @@ public class MessageItemWidgetGenerator : WidgetGenerator, Object { Conversation conversation = message_item.conversation; Message message = message_item.message; - MessageTextView text_view = new MessageTextView() { vexpand=true, visible = true }; - + 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")) { - text_view.add_text(message.body.substring(3)); - } else { - text_view.add_text(message.body); + markup_text = markup_text.substring(3); } - if (conversation.type_ == Conversation.Type.GROUPCHAT) { - text_view.highlight_word(conversation.nickname); + markup_text = Markup.escape_text(markup_text); + + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + markup_text = Util.make_word_bold_markup(markup_text, conversation.nickname); } + if (message_item.message.body.has_prefix("/me")) { 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)); - TextTag nick_tag = text_view.buffer.create_tag(null, 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.style_updated.connect(() => update_style(stream_interactor, message, conversation, nick_tag, text_view)); - text_view.realize.connect(() => update_style(stream_interactor, message, conversation, nick_tag, text_view)); + 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)); } - return text_view; + + markup_text = Util.make_link_markup(markup_text); + + label.label = markup_text; + return label; } - public static void update_style(StreamInteractor stream_interactor, Message message, Conversation conversation, TextTag nick_tag, TextView text_view) { - 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; + 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 = @"$(Markup.escape_text(display_name))" + action_text; } } diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index 9a3e5902..af6e96c1 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -61,24 +61,20 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { 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, () => { - stack.set_visible_child_name("void"); - initialize_for_conversation_(conversation); - display_latest(); - stack.set_visible_child_name("main"); + main.visible = false; + Idle.add(() => { + main.visible=true; return false; }); firstLoad = false; - } else { - stack.set_visible_child_name("void"); - initialize_for_conversation_(conversation); - display_latest(); - stack.set_visible_child_name("main"); } + 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) { @@ -232,7 +228,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { lower_start_item.encryption == item.encryption && (item.mark == Message.Marked.WONTSEND) == (lower_start_item.mark == Message.Marked.WONTSEND)) { lower_skeleton.add_meta_item(item); - Util.force_alloc_width(lower_skeleton, main.get_allocated_width()); widgets[item] = widgets[lower_start_item]; item_item_skeletons[item] = lower_skeleton; @@ -275,7 +270,6 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { main.add(insert); } widgets[item] = insert; - Util.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 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 71ca35f8..00000000 --- a/main/src/ui/conversation_summary/message_textview.vala +++ /dev/null @@ -1,157 +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(); - 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(bold_tag, 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 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(link_tag, 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/global_search.vala b/main/src/ui/global_search.vala index 82aac76a..99a69e1b 100644 --- a/main/src/ui/global_search.vala +++ b/main/src/ui/global_search.vala @@ -148,7 +148,6 @@ class GlobalSearch : Overlay { } Widget match_widget = get_match_message_widget(item); - Util.force_alloc_width(match_widget, results_empty_stack.get_allocated_width() - results_box.margin * 2); context_box.add(match_widget); if (after_message != null && after_message.size > 0) { @@ -188,30 +187,39 @@ class GlobalSearch : Overlay { text = text.substring(0, 25) + " … " + text.substring(index - 50, 50) + text.substring(index, 100) + " … " + text.substring(text.length - 25, 25); } } - TextView tv = new TextView() { wrap_mode=Gtk.WrapMode.WORD_CHAR, hexpand=true, visible=true }; - tv.buffer.text = text; - TextTag link_tag = tv.buffer.create_tag("hit", background: "yellow"); + 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 keywords = get_keywords(Regex.escape_string(search.down())); + bool first = true; foreach (string keyword in keywords) { - Regex url_regex = new Regex(keyword.down()); - MatchInfo match_info; - url_regex.match(text.down(), 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; - tv.buffer.get_iter_at_offset(out start_iter, start); - tv.buffer.get_iter_at_offset(out end_iter, end); - tv.buffer.apply_tag(link_tag, start_iter, end_iter); + if (first) { + first = false; + } else { + regex_str += "|"; } + regex_str += "\\b" + keyword; } + regex_str += ")"; - grid.attach(tv, 1, 1, 1, 1); + // 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] + "" + markup_text[start+elongated_by:end+elongated_by] + "" + markup_text[end+elongated_by:markup_text.length]; + elongated_by += "".length + "".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(() => { diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index 4e9e942d..06715ad6 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -138,13 +138,39 @@ public static bool is_24h_format() { return settings_format == "24h" || p_format == " "; } -// Workaround GTK TextView issues -public static 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); +public static string make_word_bold_markup(string s, string word) { + string ret = s; + int elongated_by = 0; + Regex highlight_regex = new Regex("\\b" + Regex.escape_string(word.down()) + "\\b"); + MatchInfo match_info; + string markup_text_bak = s.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); + ret = ret[0:start+elongated_by] + "" + ret[start+elongated_by:end+elongated_by] + "" + ret[end+elongated_by:ret.length]; + elongated_by += 7; + } + markup_text_bak += ""; // We need markup_text_bak to live until here because url_regex.match does not copy the string + return ret; +} + +public static string make_link_markup(string s) { + string ret = s; + 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`!()\[\]{};:'".,<>?«»“”‘’]))"""); + int elongated_by = 0; + MatchInfo match_info; + string markup_text_bak = ret.down(); + url_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); + string link = ret[start+elongated_by:end+elongated_by]; + ret = ret[0:start+elongated_by] + "" + link + "" + ret[end+elongated_by:ret.length]; + elongated_by += 15 + link.length; + } + markup_text_bak += ""; // We need markup_text_bak to live until here because url_regex.match does not copy the string + return ret; } }