aboutsummaryrefslogtreecommitdiff
path: root/main/src/ui/conversation_content_view/message_widget.vala
diff options
context:
space:
mode:
Diffstat (limited to 'main/src/ui/conversation_content_view/message_widget.vala')
-rw-r--r--main/src/ui/conversation_content_view/message_widget.vala267
1 files changed, 194 insertions, 73 deletions
diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala
index 25d3775b..c5a9c145 100644
--- a/main/src/ui/conversation_content_view/message_widget.vala
+++ b/main/src/ui/conversation_content_view/message_widget.vala
@@ -30,12 +30,16 @@ 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;
+ private bool text_selectable = true;
public MessageMetaItem(ContentItem content_item, StreamInteractor stream_interactor) {
base(content_item);
message_item = content_item as MessageItem;
this.stream_interactor = stream_interactor;
+ label.add_css_class("message-text");
+
stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_updated_item);
stream_interactor.get_module(MessageDeletion.IDENTITY).item_deleted.connect(on_updated_item);
@@ -73,111 +77,176 @@ 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<Xep.MessageMarkup.Span> 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<Xep.MessageMarkup.Span>();
- 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 = @"<span size=\'$size_str\'>" + markup_text + "</span>";
}
- 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 = @"<i><span size='small' color='$dim_color'>%s</span></i>".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<Xep.MessageMarkup.Span> copy_markups(Message message) {
+ var markups = new ArrayList<Xep.MessageMarkup.Span>();
+ 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<Xep.MessageMarkup.Span> markups, out int action_name_length, bool truncate = true) {
+ 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 (truncate && 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;
}
+ }
+
+ return text;
+ }
- bold_attr.end_index = display_name.length;
- italic_attr.end_index = display_name.length;
- attrs.insert(bold_attr.copy());
- attrs.insert(italic_attr.copy());
+ private Label create_text_label() {
+ var text_label = new Label("") {
+ use_markup=true,
+ xalign=0,
+ selectable=text_selectable,
+ wrap=true,
+ wrap_mode=Pango.WrapMode.WORD_CHAR,
+ hexpand=true
+ };
+ text_label.add_css_class("message-text");
+ text_label.activate_link.connect(on_label_activate_link);
+ return text_label;
+ }
+
+ public void set_text_selectable(bool selectable) {
+ text_selectable = selectable;
+ if (content_widget != null) set_text_selectable_recursive(content_widget, selectable);
+ }
+
+ private void set_text_selectable_recursive(Widget widget, bool selectable) {
+ Label? text_label = widget as Label;
+ if (text_label != null && text_label.has_css_class("message-text")) {
+ text_label.selectable = selectable;
+ }
+
+ for (Widget? child = widget.get_first_child(); child != null; child = child.get_next_sibling()) {
+ set_text_selectable_recursive(child, selectable);
}
+ }
+
+ 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<Xep.MessageMarkup.Span> 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 = @"<span size=\'$size_str\'>" + markup_text + "</span>";
- }
+ private void format_text_segment(Label text_label, CodeBlockPart part, ArrayList<Xep.MessageMarkup.Span> 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 = @"<i><span size='small' color='$dim_color'>%s</span></i>".printf(_("Message deleted"));
- theme_dependent = true;
- }
if (message.edit_to != null) {
- markup_text += @" <span size='small' color='$dim_color'>(%s)</span>".printf(_("edited"));
+ markup = @"$separator<span size='small' color='$dim_color'>(%s)</span>".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 += @" <span size='small' color='$dim_color'>%s</span>".printf(_("pending…"));
+ markup += @"$separator<span size='small' color='$dim_color'>%s</span>".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 +254,63 @@ 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 += " <span size='small' color='%s'>%s</span>".printf(error_color, _("delivery failed"));
+ string error_color = Util.rgba_to_hex(Util.get_label_pango_color(meta_label, "error_color"));
+ markup += @"$separator<span size='small' color='$error_color'>%s</span>".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<CodeBlockPart> parts, ArrayList<Xep.MessageMarkup.Span> 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<CodeBlockPart> 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;
+ }
+ set_text_selectable_recursive(content_widget, text_selectable);
+
+ 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 +318,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 +331,7 @@ public class MessageMetaItem : ContentMetaItem {
outer.set_widget(quote_widget, Plugins.WidgetType.GTK4, 1);
}
}
- return label;
+ return content_widget;
}
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) {
@@ -244,6 +352,19 @@ public class MessageMetaItem : ContentMetaItem {
actions.add(action1);
}
+ Plugins.MessageAction copy_action = new Plugins.MessageAction();
+ copy_action.name = "copy";
+ copy_action.icon_name = "dino-edit-copy-symbolic";
+ copy_action.tooltip = _("Copy message");
+ copy_action.callback = () => {
+ var markups = copy_markups(message_item.message);
+ int action_name_length;
+ string text = prepare_message(markups, out action_name_length, false);
+ Gdk.Display? display = Gdk.Display.get_default();
+ if (display != null) display.get_clipboard().set_text(text);
+ };
+ actions.add(copy_action);
+
actions.add(get_reply_action(content_item, message_item.conversation, stream_interactor));
actions.add(get_reaction_action(content_item, message_item.conversation, stream_interactor));
@@ -267,7 +388,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 +396,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);