Private
Public Access
1
0

Adds link clicking support

This commit is contained in:
2025-06-18 17:36:32 -07:00
parent 4ebd310b7a
commit 0dece34012
3 changed files with 73 additions and 3 deletions

View File

@@ -30,6 +30,22 @@ public class Message : Object, Comparable<Message>, Hashable<Message>
}
}
public string markup {
owned get {
const string link_regex_pattern = "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)";
try {
var regex = new GLib.Regex(link_regex_pattern);
var escaped_text = GLib.Markup.escape_text(this.text);
return regex.replace(escaped_text, escaped_text.length, 0, "<u>\\0</u>");
} catch (GLib.RegexError e) {
GLib.warning("Error linking text: %s", e.message);
return GLib.Markup.escape_text(this.text);
}
}
}
public Message(string text, DateTime date, string? sender) {
this.text = text;
this.date = date;

View File

@@ -24,7 +24,8 @@ public class TextBubbleLayout : BubbleLayout
this.from_me = message.from_me;
this.message = message;
layout = parent.create_pango_layout(message.text);
layout = parent.create_pango_layout(null);
layout.set_markup(message.markup, -1);
var font_desc = TextBubbleLayout.body_font;
layout.set_font_description(font_desc);

View File

@@ -50,6 +50,7 @@ public class TranscriptView : Adw.Bin
private Graphene.Point _hovering_text_view_origin = Graphene.Point() { x = 0, y = 0 };
private TextView _hovering_text_view = new TextView();
private bool _queued_url_open = false;
private VisibleTextLayout? hovered_text_bubble = null;
private VisibleTextLayout? locked_text_bubble = null;
@@ -103,7 +104,8 @@ public class TranscriptView : Adw.Bin
// When the selection changes, lock the text bubble so that if the cursor moves to another bubble we don't clear it.
_hovering_text_view.buffer.mark_set.connect((location, end) => {
self.lock_text_bubble(self.hovered_text_bubble);
if (location.get_offset() == 0) { return; }
self.on_hovering_text_view_clicked(location, end);
});
// When the mouse hovers over a text bubble, configure the hovering text view to show the text of the bubble.
@@ -152,6 +154,53 @@ public class TranscriptView : Adw.Bin
});
}
private void on_hovering_text_view_clicked(TextIter location, TextMark end) {
lock_text_bubble(hovered_text_bubble);
if (!_queued_url_open) {
_queued_url_open = true;
// 100ms timeout to let the selection state settle. (this is a workaround)
GLib.Timeout.add(100, () => {
open_url_at_location(location);
_queued_url_open = false;
return false;
}, GLib.Priority.HIGH);
}
}
private void open_url_at_location(TextIter location) {
Gtk.TextTag? underline_tag = null;
foreach (unowned Gtk.TextTag tag in location.get_tags()) {
if (tag.underline != Pango.Underline.NONE) {
underline_tag = tag;
break;
}
}
if (underline_tag == null) {
return; // Click wasn't on an underlined (i.e. link) region
}
// Determine the full extent (start/end iters) of this underlined region
Gtk.TextIter start_iter = location;
Gtk.TextIter end_iter = location;
start_iter.backward_to_tag_toggle(underline_tag);
end_iter.forward_to_tag_toggle(underline_tag);
string url = _hovering_text_view.buffer.get_text(start_iter, end_iter, false);
// Try to open the URL guard against malformed data
if (url != null && url.strip().length > 0) {
try {
GLib.AppInfo.launch_default_for_uri(url.strip(), null);
} catch (GLib.Error e) {
warning("Failed to open URL %s: %s", url, e.message);
}
}
}
private void reset_for_conversation_change() {
locked_text_bubble = null;
hovered_text_bubble = null;
@@ -176,7 +225,11 @@ public class TranscriptView : Adw.Bin
if (locked_text_bubble == null) {
_hovering_text_view_origin = visible_text_layout.text_bubble.get_text_origin();
_hovering_text_view.buffer.text = visible_text_layout.text_bubble.message.text;
_hovering_text_view.buffer.text = "";
Gtk.TextIter start_iter;
_hovering_text_view.buffer.get_start_iter(out start_iter);
_hovering_text_view.buffer.insert_markup(ref start_iter, visible_text_layout.text_bubble.message.markup, -1);
overlay.queue_allocate();
}
}