From f9e3b8ba7da220746151179ed10a90c2ac215ffc Mon Sep 17 00:00:00 2001 From: frosty Date: Mon, 17 Aug 2026 16:32:52 -0400 Subject: Begin implementing codeblocks feature --- .../conversation_content_view/message_widget.vala | 235 ++++++++++++++------- 1 file changed, 161 insertions(+), 74 deletions(-) (limited to 'main/src/ui/conversation_content_view/message_widget.vala') diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala index 25d3775b..f8db1a45 100644 --- a/main/src/ui/conversation_content_view/message_widget.vala +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -30,6 +30,7 @@ public class MessageMetaItem : ContentMetaItem { uint pending_timeout_id = -1; public Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true, vexpand=true }; + private Widget? content_widget; public MessageMetaItem(ContentItem content_item, StreamInteractor stream_interactor) { base(content_item); @@ -73,111 +74,159 @@ public class MessageMetaItem : ContentMetaItem { update_label(); } - private void generate_markup_text(ContentItem item, Label label) { - MessageItem message_item = item as MessageItem; - Conversation conversation = message_item.conversation; + private void generate_markup_text(Label label, string text, ArrayList markups, int action_name_length) { Message message = message_item.message; + label.set_attributes(create_text_attributes(text, 0, markups, action_name_length)); - // Get a copy of the markup spans, such that we can modify them - var markups = new ArrayList(); - foreach (var markup in message.get_markups()) { - markups.add(new Xep.MessageMarkup.Span() { types=markup.types, start_char=markup.start_char, end_char=markup.end_char }); + bool theme_dependent; + string markup_text = parse_text_markup(label, text, out theme_dependent); + + int only_emoji_count = Util.get_only_emoji_count(markup_text); + if (only_emoji_count != -1) { + string size_str = only_emoji_count < 5 ? "xx-large" : "large"; + markup_text = @"" + markup_text + ""; } - string markup_text = message.body; + string dim_color = Util.is_dark_theme(label) ? "#BDBDBD" : "#707070"; - var attrs = new AttrList(); - label.set_attributes(attrs); + if (message.body == "") { + markup_text = @"%s".printf(_("Message deleted")); + theme_dependent = true; + } + markup_text += get_meta_markup(label, true, ref theme_dependent); - if (markup_text == null) return; // TODO remove + if (theme_dependent && realize_id == -1 && !label.get_realized()) { + realize_id = label.realize.connect(update_label); + } else if (!theme_dependent && realize_id != -1) { + label.disconnect(realize_id); + } + label.label = markup_text; + } - // Only process messages up to a certain size - if (markup_text.length > 10000) { - markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]"; + private ArrayList copy_markups(Message message) { + var markups = new ArrayList(); + foreach (var markup in message.get_markups()) { + markups.add(new Xep.MessageMarkup.Span() { types=markup.types, start_char=markup.start_char, end_char=markup.end_char }); } + return markups; + } - bool theme_dependent = false; + private string prepare_message(ArrayList markups, out int action_name_length) { + Message message = message_item.message; + string text = message.body ?? ""; + action_name_length = 0; - markup_text = Util.remove_fallbacks_adjust_markups(markup_text, message.quoted_item_id > 0, message.get_fallbacks(), markups); + if (text.char_count() > 10000) { + text = text.substring(0, text.index_of_nth_char(10000)) + " [" + _("Message too long") + "]"; + } - var bold_attr = Pango.attr_weight_new(Pango.Weight.BOLD); - var italic_attr = Pango.attr_style_new(Pango.Style.ITALIC); - var strikethrough_attr = Pango.attr_strikethrough_new(true); + text = Util.remove_fallbacks_adjust_markups(text, message.quoted_item_id > 0, message.get_fallbacks(), markups); - // Prefix message with name instead of /me - if (markup_text.has_prefix("/me ")) { - string display_name = Util.get_participant_display_name(stream_interactor, conversation, message.from); - markup_text = display_name + " " + markup_text.substring(4); + if (text.has_prefix("/me ")) { + string display_name = Util.get_participant_display_name(stream_interactor, message_item.conversation, message.from); + text = display_name + " " + text.substring(4); + action_name_length = display_name.char_count(); foreach (Xep.MessageMarkup.Span span in markups) { - int length = display_name.char_count() - 4 + 1; - span.start_char += length; - span.end_char += length; + int difference = action_name_length - 3; + span.start_char += difference; + span.end_char += difference; } - - bold_attr.end_index = display_name.length; - italic_attr.end_index = display_name.length; - attrs.insert(bold_attr.copy()); - attrs.insert(italic_attr.copy()); } + return text; + } + + private Label create_text_label() { + var text_label = new Label("") { + use_markup=true, + xalign=0, + selectable=true, + wrap=true, + wrap_mode=Pango.WrapMode.WORD_CHAR, + hexpand=true + }; + text_label.activate_link.connect(on_label_activate_link); + return text_label; + } + + private void add_text_attribute(AttrList attributes, Attribute attribute, string text, int source_start, int span_start, int span_end) { + int source_end = source_start + text.char_count(); + int start = int.max(source_start, span_start); + int end = int.min(source_end, span_end); + if (start >= end) return; + + attribute.start_index = text.index_of_nth_char(start - source_start); + attribute.end_index = text.index_of_nth_char(end - source_start); + attributes.insert(attribute.copy()); + } + + private AttrList create_text_attributes(string text, int source_start, ArrayList markups, int action_name_length) { + var attributes = new AttrList(); foreach (var markup in markups) { - foreach (var ty in markup.types) { - Attribute attr = null; - switch (ty) { + foreach (var type in markup.types) { + Attribute attribute; + switch (type) { case Xep.MessageMarkup.SpanType.EMPHASIS: - attr = Pango.attr_style_new(Pango.Style.ITALIC); + attribute = Pango.attr_style_new(Pango.Style.ITALIC); break; case Xep.MessageMarkup.SpanType.STRONG_EMPHASIS: - attr = Pango.attr_weight_new(Pango.Weight.BOLD); + attribute = Pango.attr_weight_new(Pango.Weight.BOLD); break; case Xep.MessageMarkup.SpanType.DELETED: - attr = Pango.attr_strikethrough_new(true); + attribute = Pango.attr_strikethrough_new(true); break; + default: + continue; } - attr.start_index = markup_text.index_of_nth_char(markup.start_char); - attr.end_index = markup_text.index_of_nth_char(markup.end_char); - attrs.insert(attr.copy()); + add_text_attribute(attributes, (owned) attribute, text, source_start, markup.start_char, markup.end_char); } } - // Work around pango bug - markup_text = Util.unbreak_space_around_non_spacing_mark((owned) markup_text); + if (action_name_length > 0) { + add_text_attribute(attributes, Pango.attr_weight_new(Pango.Weight.BOLD), text, source_start, 0, action_name_length); + add_text_attribute(attributes, Pango.attr_style_new(Pango.Style.ITALIC), text, source_start, 0, action_name_length); + } + + return attributes; + } + private string parse_text_markup(Label text_label, string text, out bool theme_dependent) { + string markup_text = Util.unbreak_space_around_non_spacing_mark(text); + theme_dependent = false; + Conversation conversation = message_item.conversation; if (conversation.type_ == Conversation.Type.GROUPCHAT) { - markup_text = Util.parse_add_markup_theme(markup_text, conversation.nickname, true, true, true, Util.is_dark_theme(this.label), ref theme_dependent); - } else { - markup_text = Util.parse_add_markup_theme(markup_text, null, true, true, true, Util.is_dark_theme(this.label), ref theme_dependent); + return Util.parse_add_markup_theme(markup_text, conversation.nickname, true, true, true, Util.is_dark_theme(text_label), ref theme_dependent); } + return Util.parse_add_markup_theme(markup_text, null, true, true, true, Util.is_dark_theme(text_label), ref theme_dependent); + } - int only_emoji_count = Util.get_only_emoji_count(markup_text); - if (only_emoji_count != -1) { - string size_str = only_emoji_count < 5 ? "xx-large" : "large"; - markup_text = @"" + markup_text + ""; - } + private void format_text_segment(Label text_label, CodeBlockPart part, ArrayList markups, int action_name_length) { + text_label.set_attributes(create_text_attributes(part.text, part.source_start_char, markups, action_name_length)); + bool theme_dependent; + text_label.label = parse_text_markup(text_label, part.text, out theme_dependent); + } - string dim_color = Util.is_dark_theme(this.label) ? "#BDBDBD" : "#707070"; + private string get_meta_markup(Label meta_label, bool inline, ref bool theme_dependent) { + Message message = message_item.message; + string dim_color = Util.is_dark_theme(meta_label) ? "#BDBDBD" : "#707070"; + string separator = inline ? " " : ""; + string markup = ""; - if (message.body == "") { - markup_text = @"%s".printf(_("Message deleted")); - theme_dependent = true; - } if (message.edit_to != null) { - markup_text += @" (%s)".printf(_("edited")); + markup = @"$separator(%s)".printf(_("edited")); + separator = " "; theme_dependent = true; } - // Append message status info additional_info = AdditionalInfo.NONE; if (message.direction == Message.DIRECTION_SENT && (message.marked == Message.Marked.SENDING || message.marked == Message.Marked.UNSENT)) { - // Append "pending..." iff message has not been sent yet if (message.time.compare(new DateTime.now_utc().add_seconds(-10)) < 0) { - markup_text += @" %s".printf(_("pending…")); + markup += @"$separator%s".printf(_("pending…")); theme_dependent = true; additional_info = AdditionalInfo.PENDING; } else { int time_diff = (- (int) message.time.difference(new DateTime.now_utc()) / 1000); - if (pending_timeout_id != -1) Source.remove(pending_timeout_id); pending_timeout_id = Timeout.add(10000 - time_diff, () => { update_label(); pending_timeout_id = -1; @@ -185,24 +234,62 @@ public class MessageMetaItem : ContentMetaItem { }); } } else if (message.direction == Message.DIRECTION_SENT && message.marked == Message.Marked.ERROR) { - // Append "delivery failed" if there was a server error - // TODO: Use a dedicated label, not appended text - string error_color = Util.rgba_to_hex(Util.get_label_pango_color(label, "error_color")); - markup_text += " %s".printf(error_color, _("delivery failed")); + string error_color = Util.rgba_to_hex(Util.get_label_pango_color(meta_label, "error_color")); + markup += @"$separator%s".printf(_("delivery failed")); theme_dependent = true; additional_info = AdditionalInfo.DELIVERY_FAILED; } - if (theme_dependent && realize_id == -1) { - realize_id = label.realize.connect(update_label); - } else if (!theme_dependent && realize_id != -1) { - label.disconnect(realize_id); + return markup; + } + + private Widget generate_code_widget(ArrayList parts, ArrayList markups, int action_name_length) { + var content = new Box(Orientation.VERTICAL, 6) { hexpand=true }; + foreach (CodeBlockPart part in parts) { + if (part.is_code) { + content.append(new CodeBlockWidget(part.text, part.language)); + } else { + Label text_label = create_text_label(); + format_text_segment(text_label, part, markups, action_name_length); + content.append(text_label); + } } - label.label = markup_text; + + Label meta_label = create_text_label(); + bool theme_dependent = false; + string meta_markup = get_meta_markup(meta_label, false, ref theme_dependent); + if (meta_markup != "") { + meta_label.label = meta_markup; + content.append(meta_label); + } + return content; } public void update_label() { - generate_markup_text(content_item, label); + if (pending_timeout_id != -1) { + Source.remove(pending_timeout_id); + pending_timeout_id = -1; + } + if (realize_id != -1) { + label.disconnect(realize_id); + realize_id = -1; + } + + var markups = copy_markups(message_item.message); + int action_name_length; + string text = prepare_message(markups, out action_name_length); + bool has_blocks; + ArrayList parts = CodeBlockParser.parse(text, out has_blocks); + if (has_blocks) { + content_widget = generate_code_widget(parts, markups, action_name_length); + } else { + generate_markup_text(label, text, markups, action_name_length); + content_widget = label; + } + + if (outer != null && !in_edit_mode) { + outer.set_widget(content_widget, Plugins.WidgetType.GTK4, 2); + } } public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { @@ -210,7 +297,7 @@ public class MessageMetaItem : ContentMetaItem { this.notify["in-edit-mode"].connect(on_in_edit_mode_changed); - outer.set_widget(label, Plugins.WidgetType.GTK4, 2); + outer.set_widget(content_widget, Plugins.WidgetType.GTK4, 2); if (message_item.message.quoted_item_id > 0) { var quoted_content_item = stream_interactor.get_module(ContentItemStore.IDENTITY).get_item_by_id(message_item.conversation, message_item.message.quoted_item_id); @@ -223,7 +310,7 @@ public class MessageMetaItem : ContentMetaItem { outer.set_widget(quote_widget, Plugins.WidgetType.GTK4, 1); } } - return label; + return content_widget; } public override Gee.List? get_item_actions(Plugins.WidgetType type) { @@ -267,7 +354,7 @@ public class MessageMetaItem : ContentMetaItem { edit_mode.cancelled.connect(() => { in_edit_mode = false; - outer.set_widget(label, Plugins.WidgetType.GTK4, 2); + outer.set_widget(content_widget, Plugins.WidgetType.GTK4, 2); }); edit_mode.send.connect(() => { string text = edit_mode.chat_text_view.text_view.buffer.text; @@ -275,7 +362,7 @@ public class MessageMetaItem : ContentMetaItem { Dino.send_message(message_item.conversation, text, message_item.message.quoted_item_id, message_item.message, markups); in_edit_mode = false; - outer.set_widget(label, Plugins.WidgetType.GTK4, 2); + outer.set_widget(content_widget, Plugins.WidgetType.GTK4, 2); }); edit_mode.chat_text_view.set_text(message); -- cgit v1.3