From 4eff88a51b4a6fece6422d6060260164659a6cc6 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Mon, 28 Apr 2025 17:29:32 -0700 Subject: [PATCH 01/63] initial commit: barebones --- .gitignore | 1 + meson.build | 7 +++++++ src/conversation-list-view.vala | 32 ++++++++++++++++++++++++++++++++ src/kordophone-application.vala | 24 ++++++++++++++++++++++++ src/main-window.vala | 16 ++++++++++++++++ src/meson.build | 16 ++++++++++++++++ 6 files changed, 96 insertions(+) create mode 100644 .gitignore create mode 100644 meson.build create mode 100644 src/conversation-list-view.vala create mode 100644 src/kordophone-application.vala create mode 100644 src/main-window.vala create mode 100644 src/meson.build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..b6281f2 --- /dev/null +++ b/meson.build @@ -0,0 +1,7 @@ +project('kordophone', 'vala', + version : '0.1.0', + meson_version : '>=0.56.0', + default_options : ['warning_level=2'] +) + +subdir('src') \ No newline at end of file diff --git a/src/conversation-list-view.vala b/src/conversation-list-view.vala new file mode 100644 index 0000000..99f9c3f --- /dev/null +++ b/src/conversation-list-view.vala @@ -0,0 +1,32 @@ +using Adw; +using Gtk; + +public class ConversationListView : Adw.Bin +{ + private Adw.ToolbarView container; + private ListBox list_box; + private Adw.HeaderBar header_bar; + + public ConversationListView () { + container = new Adw.ToolbarView (); + set_child (container); + + list_box = new ListBox (); + list_box.add_css_class ("boxed-list"); + list_box.set_selection_mode (SelectionMode.SINGLE); + container.set_content (list_box); + + header_bar = new Adw.HeaderBar (); + header_bar.set_title_widget (new Label ("Kordophone")); + container.add_top_bar (header_bar); + + // Populate with test data + for (int i = 0; i < 10; i++) { + var row = new ActionRow (); + row.title = "Conversation %d".printf(i); + list_box.append (row); + } + } + + +} \ No newline at end of file diff --git a/src/kordophone-application.vala b/src/kordophone-application.vala new file mode 100644 index 0000000..fef0eaa --- /dev/null +++ b/src/kordophone-application.vala @@ -0,0 +1,24 @@ +using Adw; +using Gtk; + +public class KordophoneApp : Adw.Application +{ + private MainWindow window; + + public KordophoneApp () { + Object (application_id: "net.buzzert.kordophone2", flags: ApplicationFlags.FLAGS_NONE); + } + + protected override void activate () { + window = new MainWindow (); + window.set_default_size (1200, 1000); + window.application = this; + + window.present (); + } + + public static int main (string[] args) { + var app = new KordophoneApp (); + return app.run (args); + } +} \ No newline at end of file diff --git a/src/main-window.vala b/src/main-window.vala new file mode 100644 index 0000000..6b45613 --- /dev/null +++ b/src/main-window.vala @@ -0,0 +1,16 @@ +using Adw; +using Gtk; + +public class MainWindow : Adw.ApplicationWindow +{ + public MainWindow () { + Object (title: "Kordophone"); + + var split_view = new NavigationSplitView (); + split_view.set_min_sidebar_width (400); + set_content (split_view); + + var conversation_list_page = new NavigationPage (new ConversationListView (), "Conversations"); + split_view.sidebar = conversation_list_page; + } +} \ No newline at end of file diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..1c0192b --- /dev/null +++ b/src/meson.build @@ -0,0 +1,16 @@ +dependencies = [ + dependency('gtk4', required : true), + dependency('libadwaita-1', required : true) +] + +sources = [ + 'kordophone-application.vala', + 'main-window.vala', + 'conversation-list-view.vala', +] + +executable('kordophone', + sources, + dependencies : dependencies, + install : true +) \ No newline at end of file From a1250c8ebee45c62f79f64bd013a2cc78b8bd0b5 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Mon, 28 Apr 2025 18:21:02 -0700 Subject: [PATCH 02/63] adds dbus messaging for getting conversations. needs org --- src/conversation-list-model.vala | 118 +++++++++++++++++++++++++++++++ src/conversation-list-view.vala | 31 +++++--- src/conversation-row.vala | 45 ++++++++++++ src/conversation.vala | 77 ++++++++++++++++++++ src/meson.build | 9 ++- src/repository-service.vala | 18 +++++ 6 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 src/conversation-list-model.vala create mode 100644 src/conversation-row.vala create mode 100644 src/conversation.vala create mode 100644 src/repository-service.vala diff --git a/src/conversation-list-model.vala b/src/conversation-list-model.vala new file mode 100644 index 0000000..f16cbec --- /dev/null +++ b/src/conversation-list-model.vala @@ -0,0 +1,118 @@ +using GLib; +using Gee; + +public class ConversationListModel : Object, ListModel +{ + public SortedSet conversations { + owned get { return _conversations.read_only_view; } + } + + private SortedSet _conversations; + private RepositoryService repository; + private uint dbus_watch_id; + + public ConversationListModel() { + _conversations = new TreeSet((a, b) => { + // Sort by date in descending order (newest first) + return (int)(b.date - a.date); + }); + + connect_to_dbus.begin(); + } + + ~ConversationListModel() { + if (dbus_watch_id > 0) { + Bus.unwatch_name(dbus_watch_id); + } + } + + private async void connect_to_dbus() { + bool connected = false; + const string path = "/net/buzzert/kordophonecd/daemon"; + + try { + debug("Trying to connect to DBus service at path: %s", path); + repository = yield Bus.get_proxy(BusType.SESSION, + "net.buzzert.kordophonecd", + path); + + // Test the connection + repository.get_version(); + + // If we get here, connection succeeded + debug("Connected to DBus service at path: %s", path); + connected = true; + + // Listen for updates + repository.conversations_updated.connect(load_conversations); + + // Initial load + load_conversations(); + } catch (Error e) { + debug("Failed to connect to kordophonecd at %s: %s", path, e.message); + } + + if (!connected) { + warning("Failed to connect to kordophonecd on any known path"); + + // Watch for the service to appear + dbus_watch_id = Bus.watch_name(BusType.SESSION, + "net.buzzert.kordophonecd", + BusNameWatcherFlags.AUTO_START, + () => { + connect_to_dbus.begin(); + }, + null); + } + } + + public void load_conversations() { + if (repository == null) { + return; + } + + try { + Variant conversations_variant = repository.get_conversations(); + + // Clear existing set + uint old_count = _conversations.size; + _conversations.clear(); + + // Notify of removal + if (old_count > 0) { + items_changed(0, old_count, 0); + } + + // Process each conversation + size_t n_children = conversations_variant.n_children(); + uint position = 0; + + for (size_t i = 0; i < n_children; i++) { + Variant child = conversations_variant.get_child_value(i); + var conversation = new Conversation.from_variant(child); + _conversations.add(conversation); + position++; + } + + // Notify of additions + if (position > 0) { + items_changed(0, 0, position); + } + } catch (Error e) { + warning("Failed to load conversations: %s", e.message); + } + } + + // ListModel implementation + public Type get_item_type() { + return typeof(Conversation); + } + + public uint get_n_items() { + return _conversations.size; + } + + public Object? get_item(uint position) { + return _conversations.to_array()[position]; + } +} \ No newline at end of file diff --git a/src/conversation-list-view.vala b/src/conversation-list-view.vala index 99f9c3f..9342272 100644 --- a/src/conversation-list-view.vala +++ b/src/conversation-list-view.vala @@ -5,28 +5,43 @@ public class ConversationListView : Adw.Bin { private Adw.ToolbarView container; private ListBox list_box; + private ScrolledWindow scrolled_window; private Adw.HeaderBar header_bar; + private ConversationListModel conversation_model; public ConversationListView () { container = new Adw.ToolbarView (); set_child (container); + scrolled_window = new ScrolledWindow (); + container.set_content (scrolled_window); + list_box = new ListBox (); list_box.add_css_class ("boxed-list"); list_box.set_selection_mode (SelectionMode.SINGLE); - container.set_content (list_box); + scrolled_window.set_child (list_box); header_bar = new Adw.HeaderBar (); header_bar.set_title_widget (new Label ("Kordophone")); container.add_top_bar (header_bar); - // Populate with test data - for (int i = 0; i < 10; i++) { - var row = new ActionRow (); - row.title = "Conversation %d".printf(i); - list_box.append (row); - } + // Set up refresh button + var refresh_button = new Button.from_icon_name ("view-refresh-symbolic"); + refresh_button.tooltip_text = "Refresh Conversations"; + refresh_button.clicked.connect (() => { + if (conversation_model != null) { + conversation_model.load_conversations (); + } + }); + header_bar.pack_end (refresh_button); + + // Set up model and bind to list + conversation_model = new ConversationListModel (); + list_box.bind_model (conversation_model, create_conversation_row); } - + private Widget create_conversation_row (Object item) { + Conversation conversation = (Conversation) item; + return new ConversationRow (conversation); + } } \ No newline at end of file diff --git a/src/conversation-row.vala b/src/conversation-row.vala new file mode 100644 index 0000000..260e9ad --- /dev/null +++ b/src/conversation-row.vala @@ -0,0 +1,45 @@ +using Adw; +using Gtk; + +public class ConversationRow : Adw.ActionRow { + private Label? unread_badge; + + public ConversationRow(Conversation conversation) { + Object(); + + title = conversation.display_name; + subtitle = conversation.last_message_preview; + subtitle_lines = 1; + + // Add unread badge if needed + if (conversation.is_unread && conversation.unread_count > 0) { + unread_badge = new Label(conversation.unread_count.to_string()); + unread_badge.add_css_class("badge"); + unread_badge.add_css_class("accent"); + add_suffix(unread_badge); + } + + // Add timestamp if available + if (conversation.date > 0) { + var datetime = new DateTime.from_unix_local(conversation.date); + if (datetime != null) { + var now = new DateTime.now_local(); + + string time_str; + if (datetime.get_year() == now.get_year() && + datetime.get_day_of_year() == now.get_day_of_year()) { + // Today - show time + time_str = datetime.format("%H:%M"); + } else { + // Not today - show date + time_str = datetime.format("%b %d"); + } + + var time_label = new Label(time_str); + time_label.add_css_class("dim-label"); + time_label.margin_start = 8; + add_suffix(time_label); + } + } + } +} \ No newline at end of file diff --git a/src/conversation.vala b/src/conversation.vala new file mode 100644 index 0000000..b58f038 --- /dev/null +++ b/src/conversation.vala @@ -0,0 +1,77 @@ +using GLib; + +public class Conversation : Object { + public string id { get; set; default = ""; } + public string last_message_preview { get; set; default = ""; } + public bool is_unread { get; set; default = false; } + public int64 date { get; set; default = 0; } + public string[] participants { get; set; default = new string[0]; } + public int unread_count { get; set; default = 0; } + + public string display_name { + owned get { + if (_display_name != null && _display_name.length > 0) { + return _display_name; + } + + if (participants.length == 1) { + return participants[0]; + } + + if (participants.length > 1) { + return string.join(", ", participants); + } + + return "Untitled"; + } + } + + private string? _display_name = null; + + public Conversation.from_variant (Variant dict) { + id = ""; + last_message_preview = ""; + participants = new string[0]; + + if (dict.get_type_string() != "a{sv}") { + warning("Expected dictionary variant, got %s", dict.get_type_string()); + return; + } + + // Safe extraction with type checking + Variant? id_variant = dict.lookup_value("id", VariantType.STRING); + if (id_variant != null) { + id = id_variant.get_string(); + } + + Variant? display_name_variant = dict.lookup_value("display_name", VariantType.STRING); + if (display_name_variant != null) { + _display_name = display_name_variant.get_string(); + } + + Variant? last_message_variant = dict.lookup_value("last_message_preview", VariantType.STRING); + if (last_message_variant != null) { + last_message_preview = last_message_variant.get_string(); + } + + Variant? is_unread_variant = dict.lookup_value("is_unread", VariantType.BOOLEAN); + if (is_unread_variant != null) { + is_unread = is_unread_variant.get_boolean(); + } + + Variant? date_variant = dict.lookup_value("date", VariantType.INT64); + if (date_variant != null) { + date = date_variant.get_int64(); + } + + Variant? participants_variant = dict.lookup_value("participants", new VariantType("as")); + if (participants_variant != null) { + participants = participants_variant.dup_strv(); + } + + Variant? unread_count_variant = dict.lookup_value("unread_count", VariantType.INT32); + if (unread_count_variant != null) { + unread_count = unread_count_variant.get_int32(); + } + } +} \ No newline at end of file diff --git a/src/meson.build b/src/meson.build index 1c0192b..9476a2b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,16 +1,23 @@ dependencies = [ dependency('gtk4', required : true), - dependency('libadwaita-1', required : true) + dependency('libadwaita-1', required : true), + dependency('gio-2.0', required : true), + dependency('gee-0.8', required : true) ] sources = [ 'kordophone-application.vala', 'main-window.vala', 'conversation-list-view.vala', + 'conversation.vala', + 'conversation-row.vala', + 'conversation-list-model.vala', + 'repository-service.vala' ] executable('kordophone', sources, dependencies : dependencies, + vala_args: ['--pkg', 'posix'], install : true ) \ No newline at end of file diff --git a/src/repository-service.vala b/src/repository-service.vala new file mode 100644 index 0000000..44d90b1 --- /dev/null +++ b/src/repository-service.vala @@ -0,0 +1,18 @@ +using GLib; + +[DBus (name = "net.buzzert.kordophone.Repository")] +public interface RepositoryService : Object { + public abstract string get_version() throws DBusError, IOError; + + [DBus (signature = "aa{sv}")] + public abstract Variant get_conversations() throws DBusError, IOError; + + public abstract void sync_all_conversations() throws DBusError, IOError; + public abstract void sync_conversation(string conversation_id) throws DBusError, IOError; + + [DBus (signature = "aa{sv}")] + public abstract Variant get_messages(string conversation_id, string last_message_id) throws DBusError, IOError; + + public signal void conversations_updated(); + public signal void messages_updated(string conversation_id); +} \ No newline at end of file From 101694ddbcbb061128b74ed29483a2031623029f Mon Sep 17 00:00:00 2001 From: James Magahern Date: Mon, 28 Apr 2025 18:40:16 -0700 Subject: [PATCH 03/63] Some fixups for the badge --- src/conversation-row.vala | 19 +++++++++++-------- src/conversation.vala | 8 ++------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/conversation-row.vala b/src/conversation-row.vala index 260e9ad..c94e080 100644 --- a/src/conversation-row.vala +++ b/src/conversation-row.vala @@ -2,7 +2,7 @@ using Adw; using Gtk; public class ConversationRow : Adw.ActionRow { - private Label? unread_badge; + private Label unread_badge; public ConversationRow(Conversation conversation) { Object(); @@ -10,13 +10,16 @@ public class ConversationRow : Adw.ActionRow { title = conversation.display_name; subtitle = conversation.last_message_preview; subtitle_lines = 1; - - // Add unread badge if needed - if (conversation.is_unread && conversation.unread_count > 0) { - unread_badge = new Label(conversation.unread_count.to_string()); - unread_badge.add_css_class("badge"); - unread_badge.add_css_class("accent"); - add_suffix(unread_badge); + + unread_badge = new Label(conversation.unread_count.to_string()); + unread_badge.add_css_class("badge"); + unread_badge.add_css_class("accent"); + add_prefix(unread_badge); + + if (conversation.is_unread) { + unread_badge.opacity = 1.0; + } else { + unread_badge.opacity = 0.0; } // Add timestamp if available diff --git a/src/conversation.vala b/src/conversation.vala index b58f038..a88ddbd 100644 --- a/src/conversation.vala +++ b/src/conversation.vala @@ -3,11 +3,12 @@ using GLib; public class Conversation : Object { public string id { get; set; default = ""; } public string last_message_preview { get; set; default = ""; } - public bool is_unread { get; set; default = false; } public int64 date { get; set; default = 0; } public string[] participants { get; set; default = new string[0]; } public int unread_count { get; set; default = 0; } + public bool is_unread { get { return unread_count > 0; } } + public string display_name { owned get { if (_display_name != null && _display_name.length > 0) { @@ -54,11 +55,6 @@ public class Conversation : Object { last_message_preview = last_message_variant.get_string(); } - Variant? is_unread_variant = dict.lookup_value("is_unread", VariantType.BOOLEAN); - if (is_unread_variant != null) { - is_unread = is_unread_variant.get_boolean(); - } - Variant? date_variant = dict.lookup_value("date", VariantType.INT64); if (date_variant != null) { date = date_variant.get_int64(); From 907a69385d2a0fcceaec8dc957405906470a8503 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 30 Apr 2025 14:24:33 -0700 Subject: [PATCH 04/63] reorg --- .../kordophone-application.vala | 0 src/{ => application}/main-window.vala | 0 .../conversation-list-model.vala | 0 .../conversation-list-view.vala | 0 .../conversation-row.vala | 0 src/meson.build | 17 ++++++++++------- src/{ => models}/conversation.vala | 0 src/{ => service}/repository-service.vala | 0 8 files changed, 10 insertions(+), 7 deletions(-) rename src/{ => application}/kordophone-application.vala (100%) rename src/{ => application}/main-window.vala (100%) rename src/{ => conversation-list}/conversation-list-model.vala (100%) rename src/{ => conversation-list}/conversation-list-view.vala (100%) rename src/{ => conversation-list}/conversation-row.vala (100%) rename src/{ => models}/conversation.vala (100%) rename src/{ => service}/repository-service.vala (100%) diff --git a/src/kordophone-application.vala b/src/application/kordophone-application.vala similarity index 100% rename from src/kordophone-application.vala rename to src/application/kordophone-application.vala diff --git a/src/main-window.vala b/src/application/main-window.vala similarity index 100% rename from src/main-window.vala rename to src/application/main-window.vala diff --git a/src/conversation-list-model.vala b/src/conversation-list/conversation-list-model.vala similarity index 100% rename from src/conversation-list-model.vala rename to src/conversation-list/conversation-list-model.vala diff --git a/src/conversation-list-view.vala b/src/conversation-list/conversation-list-view.vala similarity index 100% rename from src/conversation-list-view.vala rename to src/conversation-list/conversation-list-view.vala diff --git a/src/conversation-row.vala b/src/conversation-list/conversation-row.vala similarity index 100% rename from src/conversation-row.vala rename to src/conversation-list/conversation-row.vala diff --git a/src/meson.build b/src/meson.build index 9476a2b..3795d9c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -6,13 +6,16 @@ dependencies = [ ] sources = [ - 'kordophone-application.vala', - 'main-window.vala', - 'conversation-list-view.vala', - 'conversation.vala', - 'conversation-row.vala', - 'conversation-list-model.vala', - 'repository-service.vala' + 'application/kordophone-application.vala', + 'application/main-window.vala', + + 'service/repository-service.vala', + + 'conversation-list/conversation-list-view.vala', + 'conversation-list/conversation-list-model.vala', + 'conversation-list/conversation-row.vala', + + 'models/conversation.vala', ] executable('kordophone', diff --git a/src/conversation.vala b/src/models/conversation.vala similarity index 100% rename from src/conversation.vala rename to src/models/conversation.vala diff --git a/src/repository-service.vala b/src/service/repository-service.vala similarity index 100% rename from src/repository-service.vala rename to src/service/repository-service.vala From 56fba9b72cc857a45b9bb1e3a3b787d82aa3b925 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 30 Apr 2025 14:53:17 -0700 Subject: [PATCH 05/63] Use generated dbus interface rather than editing it every time --- .../conversation-list-model.vala | 2 +- src/meson.build | 2 +- src/service/interface/dbusservice.vala | 50 ++++++++++++ src/service/interface/generate.sh | 10 +++ .../xml/net.buzzert.kordophonecd.Server.xml | 79 +++++++++++++++++++ src/service/repository-service.vala | 18 ----- 6 files changed, 141 insertions(+), 20 deletions(-) create mode 100644 src/service/interface/dbusservice.vala create mode 100755 src/service/interface/generate.sh create mode 100644 src/service/interface/xml/net.buzzert.kordophonecd.Server.xml delete mode 100644 src/service/repository-service.vala diff --git a/src/conversation-list/conversation-list-model.vala b/src/conversation-list/conversation-list-model.vala index f16cbec..b2a513d 100644 --- a/src/conversation-list/conversation-list-model.vala +++ b/src/conversation-list/conversation-list-model.vala @@ -8,7 +8,7 @@ public class ConversationListModel : Object, ListModel } private SortedSet _conversations; - private RepositoryService repository; + private DBusService.Repository repository; private uint dbus_watch_id; public ConversationListModel() { diff --git a/src/meson.build b/src/meson.build index 3795d9c..70740ac 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,7 +9,7 @@ sources = [ 'application/kordophone-application.vala', 'application/main-window.vala', - 'service/repository-service.vala', + 'service/interface/dbusservice.vala', 'conversation-list/conversation-list-view.vala', 'conversation-list/conversation-list-model.vala', diff --git a/src/service/interface/dbusservice.vala b/src/service/interface/dbusservice.vala new file mode 100644 index 0000000..0dbd5af --- /dev/null +++ b/src/service/interface/dbusservice.vala @@ -0,0 +1,50 @@ +/* Generated by vala-dbus-binding-tool 1.0-aa2fb. Do not modify! */ +/* Generated with: vala-dbus-binding-tool --no-synced --strip-namespace=net --strip-namespace=buzzert --rename-namespace=kordophone:DBusService --api-path=xml/ */ +using GLib; + +namespace DBusService { + + [DBus (name = "net.buzzert.kordophone.Settings", timeout = 120000)] + public interface Settings : GLib.Object { + + [DBus (name = "ServerURL")] + public abstract string server_u_r_l { owned get; set; } + + [DBus (name = "Username")] + public abstract string username { owned get; set; } + + [DBus (name = "CredentialItem")] + public abstract GLib.ObjectPath credential_item { owned get; set; } + + [DBus (name = "SetServer")] + public abstract void set_server(string url, string user) throws DBusError, IOError; + + [DBus (name = "ConfigChanged")] + public signal void config_changed(); + } + + [DBus (name = "net.buzzert.kordophone.Repository", timeout = 120000)] + public interface Repository : GLib.Object { + + [DBus (name = "GetVersion")] + public abstract string get_version() throws DBusError, IOError; + + [DBus (name = "GetConversations")] + public abstract GLib.HashTable[] get_conversations() throws DBusError, IOError; + + [DBus (name = "SyncAllConversations")] + public abstract void sync_all_conversations() throws DBusError, IOError; + + [DBus (name = "SyncConversation")] + public abstract void sync_conversation(string conversation_id) throws DBusError, IOError; + + [DBus (name = "ConversationsUpdated")] + public signal void conversations_updated(); + + [DBus (name = "GetMessages")] + public abstract GLib.HashTable[] get_messages(string conversation_id, string last_message_id) throws DBusError, IOError; + + [DBus (name = "MessagesUpdated")] + public signal void messages_updated(string conversation_id); + } +} diff --git a/src/service/interface/generate.sh b/src/service/interface/generate.sh new file mode 100755 index 0000000..72bed52 --- /dev/null +++ b/src/service/interface/generate.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +if ! command -v vala-dbus-binding-tool >/dev/null 2>&1; then + echo "Error: vala-dbus-binding-tool not found. Please install it first." + echo "https://github.com/freesmartphone/vala-dbus-binding-tool" + exit 1 +fi + +vala-dbus-binding-tool --no-synced --strip-namespace=net --strip-namespace=buzzert --rename-namespace=kordophone:DBusService --api-path=xml/ + diff --git a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml new file mode 100644 index 0000000..bbe5263 --- /dev/null +++ b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/service/repository-service.vala b/src/service/repository-service.vala deleted file mode 100644 index 44d90b1..0000000 --- a/src/service/repository-service.vala +++ /dev/null @@ -1,18 +0,0 @@ -using GLib; - -[DBus (name = "net.buzzert.kordophone.Repository")] -public interface RepositoryService : Object { - public abstract string get_version() throws DBusError, IOError; - - [DBus (signature = "aa{sv}")] - public abstract Variant get_conversations() throws DBusError, IOError; - - public abstract void sync_all_conversations() throws DBusError, IOError; - public abstract void sync_conversation(string conversation_id) throws DBusError, IOError; - - [DBus (signature = "aa{sv}")] - public abstract Variant get_messages(string conversation_id, string last_message_id) throws DBusError, IOError; - - public signal void conversations_updated(); - public signal void messages_updated(string conversation_id); -} \ No newline at end of file From 3e1fa63fdff431826ffa1fa5c041f557da9e385f Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 30 Apr 2025 15:19:44 -0700 Subject: [PATCH 06/63] reorg: separate dbus code out of conversation list model and into repository --- .../conversation-list-model.vala | 62 +------------ src/meson.build | 4 +- src/models/conversation.vala | 46 +++------- src/service/repository.vala | 88 +++++++++++++++++++ 4 files changed, 109 insertions(+), 91 deletions(-) create mode 100644 src/service/repository.vala diff --git a/src/conversation-list/conversation-list-model.vala b/src/conversation-list/conversation-list-model.vala index b2a513d..18fc2fa 100644 --- a/src/conversation-list/conversation-list-model.vala +++ b/src/conversation-list/conversation-list-model.vala @@ -8,8 +8,6 @@ public class ConversationListModel : Object, ListModel } private SortedSet _conversations; - private DBusService.Repository repository; - private uint dbus_watch_id; public ConversationListModel() { _conversations = new TreeSet((a, b) => { @@ -17,62 +15,12 @@ public class ConversationListModel : Object, ListModel return (int)(b.date - a.date); }); - connect_to_dbus.begin(); - } - - ~ConversationListModel() { - if (dbus_watch_id > 0) { - Bus.unwatch_name(dbus_watch_id); - } - } - - private async void connect_to_dbus() { - bool connected = false; - const string path = "/net/buzzert/kordophonecd/daemon"; - - try { - debug("Trying to connect to DBus service at path: %s", path); - repository = yield Bus.get_proxy(BusType.SESSION, - "net.buzzert.kordophonecd", - path); - - // Test the connection - repository.get_version(); - - // If we get here, connection succeeded - debug("Connected to DBus service at path: %s", path); - connected = true; - - // Listen for updates - repository.conversations_updated.connect(load_conversations); - - // Initial load - load_conversations(); - } catch (Error e) { - debug("Failed to connect to kordophonecd at %s: %s", path, e.message); - } - - if (!connected) { - warning("Failed to connect to kordophonecd on any known path"); - - // Watch for the service to appear - dbus_watch_id = Bus.watch_name(BusType.SESSION, - "net.buzzert.kordophonecd", - BusNameWatcherFlags.AUTO_START, - () => { - connect_to_dbus.begin(); - }, - null); - } + Repository.get_instance().conversations_updated.connect(load_conversations); } public void load_conversations() { - if (repository == null) { - return; - } - try { - Variant conversations_variant = repository.get_conversations(); + Conversation[] conversations = Repository.get_instance().get_conversations(); // Clear existing set uint old_count = _conversations.size; @@ -84,12 +32,10 @@ public class ConversationListModel : Object, ListModel } // Process each conversation - size_t n_children = conversations_variant.n_children(); uint position = 0; - for (size_t i = 0; i < n_children; i++) { - Variant child = conversations_variant.get_child_value(i); - var conversation = new Conversation.from_variant(child); + for (int i = 0; i < conversations.length; i++) { + var conversation = conversations[i]; _conversations.add(conversation); position++; } diff --git a/src/meson.build b/src/meson.build index 70740ac..fe69dab 100644 --- a/src/meson.build +++ b/src/meson.build @@ -2,7 +2,8 @@ dependencies = [ dependency('gtk4', required : true), dependency('libadwaita-1', required : true), dependency('gio-2.0', required : true), - dependency('gee-0.8', required : true) + dependency('gee-0.8', required : true), + dependency('gio-unix-2.0', required : true) ] sources = [ @@ -10,6 +11,7 @@ sources = [ 'application/main-window.vala', 'service/interface/dbusservice.vala', + 'service/repository.vala', 'conversation-list/conversation-list-view.vala', 'conversation-list/conversation-list-model.vala', diff --git a/src/models/conversation.vala b/src/models/conversation.vala index a88ddbd..3e489ea 100644 --- a/src/models/conversation.vala +++ b/src/models/conversation.vala @@ -1,7 +1,7 @@ using GLib; public class Conversation : Object { - public string id { get; set; default = ""; } + public string guid { get; set; default = ""; } public string last_message_preview { get; set; default = ""; } public int64 date { get; set; default = 0; } public string[] participants { get; set; default = new string[0]; } @@ -29,45 +29,27 @@ public class Conversation : Object { private string? _display_name = null; - public Conversation.from_variant (Variant dict) { - id = ""; - last_message_preview = ""; - participants = new string[0]; - - if (dict.get_type_string() != "a{sv}") { - warning("Expected dictionary variant, got %s", dict.get_type_string()); - return; - } - - // Safe extraction with type checking - Variant? id_variant = dict.lookup_value("id", VariantType.STRING); - if (id_variant != null) { - id = id_variant.get_string(); + public Conversation.from_hash_table(HashTable conversation_data) { + guid = conversation_data["guid"].get_string(); + + if (conversation_data.contains("last_message_preview")) { + last_message_preview = conversation_data["last_message_preview"].get_string(); } - Variant? display_name_variant = dict.lookup_value("display_name", VariantType.STRING); - if (display_name_variant != null) { - _display_name = display_name_variant.get_string(); + if (conversation_data.contains("participants")) { + participants = conversation_data["participants"].dup_strv(); } - Variant? last_message_variant = dict.lookup_value("last_message_preview", VariantType.STRING); - if (last_message_variant != null) { - last_message_preview = last_message_variant.get_string(); + if (conversation_data.contains("unread_count")) { + unread_count = conversation_data["unread_count"].get_int32(); } - Variant? date_variant = dict.lookup_value("date", VariantType.INT64); - if (date_variant != null) { - date = date_variant.get_int64(); + if (conversation_data.contains("date")) { + date = conversation_data["date"].get_int64(); } - Variant? participants_variant = dict.lookup_value("participants", new VariantType("as")); - if (participants_variant != null) { - participants = participants_variant.dup_strv(); - } - - Variant? unread_count_variant = dict.lookup_value("unread_count", VariantType.INT32); - if (unread_count_variant != null) { - unread_count = unread_count_variant.get_int32(); + if (conversation_data.contains("display_name")) { + _display_name = conversation_data["display_name"].get_string(); } } } \ No newline at end of file diff --git a/src/service/repository.vala b/src/service/repository.vala new file mode 100644 index 0000000..82a1a7a --- /dev/null +++ b/src/service/repository.vala @@ -0,0 +1,88 @@ +using GLib; +using Gee; + +public class Repository : Object +{ + public signal void conversations_updated(); + + public static Repository get_instance() { + if (instance == null) { + instance = new Repository(); + } + + return instance; + } + + private static Repository instance = null; + private DBusService.Repository dbus_repository; + private uint dbus_watch_id; + + private Repository() { + connect_to_dbus.begin((obj, res) => { + connect_to_dbus.end(res); + }); + } + + ~Repository() { + if (dbus_watch_id > 0) { + Bus.unwatch_name(dbus_watch_id); + } + } + + public Conversation[] get_conversations() throws Error { + if (dbus_repository == null) { + throw new Error(1337, 1, "Repository not connected"); + } + + var conversations = dbus_repository.get_conversations(); + Conversation[] returned_conversations = new Conversation[conversations.length]; + + for (int i = 0; i < conversations.length; i++) { + returned_conversations[i] = new Conversation.from_hash_table(conversations[i]); + } + + return returned_conversations; + } + + private async void connect_to_dbus() { + bool connected = false; + const string path = "/net/buzzert/kordophonecd/daemon"; + + try { + debug("Trying to connect to DBus service at path: %s", path); + dbus_repository = yield Bus.get_proxy(BusType.SESSION, + "net.buzzert.kordophonecd", + path); + + // Test the connection + dbus_repository.get_version(); + + // If we get here, connection succeeded + debug("Connected to DBus service at path: %s", path); + connected = true; + + // Listen for updates + dbus_repository.conversations_updated.connect(() => { + conversations_updated(); + }); + + // Initial load + conversations_updated(); + } catch (Error e) { + debug("Failed to connect to kordophonecd at %s: %s", path, e.message); + } + + if (!connected) { + warning("Failed to connect to kordophonecd on any known path"); + + // Watch for the service to appear + dbus_watch_id = Bus.watch_name(BusType.SESSION, + "net.buzzert.kordophonecd", + BusNameWatcherFlags.AUTO_START, + () => { + connect_to_dbus.begin(); + }, + null); + } + } +} From e976b3db4c4245ad17e6c00643e07d58a3ef5f6b Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 30 Apr 2025 15:58:47 -0700 Subject: [PATCH 07/63] initial scaffolding for inverted, custom message list --- src/application/kordophone-application.vala | 13 ++++ src/application/main-window.vala | 3 + src/conversation-list/conversation-row.vala | 2 + src/meson.build | 12 ++++ src/message-list/message-list-model.vala | 72 ++++++++++++++++++++ src/message-list/message-list-view.vala | 73 +++++++++++++++++++++ src/models/message.vala | 16 +++++ src/resources/kordophone.gresource.xml | 6 ++ src/resources/style.css | 16 +++++ src/service/repository.vala | 20 ++++++ 10 files changed, 233 insertions(+) create mode 100644 src/message-list/message-list-model.vala create mode 100644 src/message-list/message-list-view.vala create mode 100644 src/models/message.vala create mode 100644 src/resources/kordophone.gresource.xml create mode 100644 src/resources/style.css diff --git a/src/application/kordophone-application.vala b/src/application/kordophone-application.vala index fef0eaa..0d5f465 100644 --- a/src/application/kordophone-application.vala +++ b/src/application/kordophone-application.vala @@ -9,6 +9,19 @@ public class KordophoneApp : Adw.Application Object (application_id: "net.buzzert.kordophone2", flags: ApplicationFlags.FLAGS_NONE); } + protected override void startup () { + base.startup (); + + // Load CSS from resources + var provider = new Gtk.CssProvider (); + provider.load_from_resource ("/net/buzzert/kordophone2/style.css"); + Gtk.StyleContext.add_provider_for_display ( + Gdk.Display.get_default (), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ); + } + protected override void activate () { window = new MainWindow (); window.set_default_size (1200, 1000); diff --git a/src/application/main-window.vala b/src/application/main-window.vala index 6b45613..a03e3a1 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -12,5 +12,8 @@ public class MainWindow : Adw.ApplicationWindow var conversation_list_page = new NavigationPage (new ConversationListView (), "Conversations"); split_view.sidebar = conversation_list_page; + + var message_list_page = new NavigationPage (new MessageListView (new MessageListModel ("123")), "Messages"); + split_view.content = message_list_page; } } \ No newline at end of file diff --git a/src/conversation-list/conversation-row.vala b/src/conversation-list/conversation-row.vala index c94e080..7238235 100644 --- a/src/conversation-list/conversation-row.vala +++ b/src/conversation-list/conversation-row.vala @@ -11,6 +11,8 @@ public class ConversationRow : Adw.ActionRow { subtitle = conversation.last_message_preview; subtitle_lines = 1; + add_css_class("conversation-row"); + unread_badge = new Label(conversation.unread_count.to_string()); unread_badge.add_css_class("badge"); unread_badge.add_css_class("accent"); diff --git a/src/meson.build b/src/meson.build index fe69dab..a6d9545 100644 --- a/src/meson.build +++ b/src/meson.build @@ -6,6 +6,13 @@ dependencies = [ dependency('gio-unix-2.0', required : true) ] +gnome = import('gnome') +resources = gnome.compile_resources( + 'kordophone-resources', + 'resources/kordophone.gresource.xml', + source_dir: 'resources' +) + sources = [ 'application/kordophone-application.vala', 'application/main-window.vala', @@ -17,11 +24,16 @@ sources = [ 'conversation-list/conversation-list-model.vala', 'conversation-list/conversation-row.vala', + 'message-list/message-list-view.vala', + 'message-list/message-list-model.vala', + 'models/conversation.vala', + 'models/message.vala', ] executable('kordophone', sources, + resources, dependencies : dependencies, vala_args: ['--pkg', 'posix'], install : true diff --git a/src/message-list/message-list-model.vala b/src/message-list/message-list-model.vala new file mode 100644 index 0000000..029ef39 --- /dev/null +++ b/src/message-list/message-list-model.vala @@ -0,0 +1,72 @@ +using GLib; +using Gee; + +public class MessageListModel : Object, ListModel +{ + public SortedSet messages { + owned get { return _messages.read_only_view; } + } + + private string _conversation_guid; + private SortedSet _messages; + + public MessageListModel(string conversation_guid) { + _messages = new TreeSet((a, b) => { + // Sort by date in descending order (newest first) + return (int)(b.date - a.date); + }); + + Repository.get_instance().messages_updated.connect(got_messages_updated); + _conversation_guid = conversation_guid; + } + + public void load_messages() { + try { + Message[] messages = Repository.get_instance().get_messages(_conversation_guid); + + // Clear existing set + uint old_count = _messages.size; + _messages.clear(); + + // Notify of removal + if (old_count > 0) { + items_changed(0, old_count, 0); + } + + // Process each conversation + uint position = 0; + + for (int i = 0; i < messages.length; i++) { + var message = messages[i]; + _messages.add(message); + position++; + } + + // Notify of additions + if (position > 0) { + items_changed(0, 0, position); + } + } catch (Error e) { + warning("Failed to load messages: %s", e.message); + } + } + + private void got_messages_updated(string conversation_guid) { + if (conversation_guid == _conversation_guid) { + load_messages(); + } + } + + // ListModel implementation + public Type get_item_type() { + return typeof(Message); + } + + public uint get_n_items() { + return _messages.size; + } + + public Object? get_item(uint position) { + return _messages.to_array()[position]; + } +} \ No newline at end of file diff --git a/src/message-list/message-list-view.vala b/src/message-list/message-list-view.vala new file mode 100644 index 0000000..508e94a --- /dev/null +++ b/src/message-list/message-list-view.vala @@ -0,0 +1,73 @@ +using Adw; +using Gtk; + +public class MessageListView : Adw.Bin +{ + private Adw.ToolbarView container; + + private MessageDrawingArea message_drawing_area = new MessageDrawingArea(); + private ScrolledWindow scrolled_window = new ScrolledWindow(); + + public MessageListView(MessageListModel model) { + container = new Adw.ToolbarView(); + set_child(container); + + scrolled_window.set_child(message_drawing_area); + scrolled_window.add_css_class("message-list-scroller"); + container.set_content(scrolled_window); + + var header_bar = new Adw.HeaderBar(); + header_bar.set_title_widget(new Label("Messages")); + container.add_top_bar(header_bar); + } +} + + +private class MessageDrawingArea : Widget { + public MessageDrawingArea() { + } + + public override SizeRequestMode get_request_mode() { + return SizeRequestMode.HEIGHT_FOR_WIDTH; + } + + public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { + GLib.message("Measure orientation: %s, for_size: %d", orientation.to_string(), for_size); + + if (orientation == Orientation.HORIZONTAL) { + // Horizontal, so we take up the full width provided + minimum = 0; + natural = for_size; + } else { + GLib.message("Vertical measure for width: %d", for_size); + minimum = 1500; + natural = 1500; + } + + minimum_baseline = -1; + natural_baseline = -1; + } + + public override void snapshot(Snapshot snapshot) { + var width = get_width(); + var height = get_height(); + + GLib.message("Snapshot width: %d, height: %d", width, height); + + var rect = Graphene.Rect().init(0, 0, width, height); + snapshot.append_color({1.0f, 0.0f, 0.0f, 1.0f}, rect); + + // Create a text layout + var layout = create_pango_layout("Hello World!"); + layout.set_width(width * Pango.SCALE); + + // Set text attributes + var font_desc = Pango.FontDescription.from_string("Sans 14"); + layout.set_font_description(font_desc); + + // Draw the text in white + snapshot.append_layout(layout, Gdk.RGBA() { red = 1.0f, green = 1.0f, blue = 1.0f, alpha = 1.0f }); + } +} + + diff --git a/src/models/message.vala b/src/models/message.vala new file mode 100644 index 0000000..66e5276 --- /dev/null +++ b/src/models/message.vala @@ -0,0 +1,16 @@ +using GLib; + +public class Message : Object +{ + public string guid { get; set; default = ""; } + public string content { get; set; default = ""; } + public int64 date { get; set; default = 0; } + public string?sender { get; set; default = null; } + + public Message.from_hash_table(HashTable message_data) { + guid = message_data["guid"].get_string(); + content = message_data["content"].get_string(); + date = message_data["date"].get_int64(); + sender = message_data["sender"].get_string(); + } +} \ No newline at end of file diff --git a/src/resources/kordophone.gresource.xml b/src/resources/kordophone.gresource.xml new file mode 100644 index 0000000..43040ca --- /dev/null +++ b/src/resources/kordophone.gresource.xml @@ -0,0 +1,6 @@ + + + + style.css + + \ No newline at end of file diff --git a/src/resources/style.css b/src/resources/style.css new file mode 100644 index 0000000..cbe637e --- /dev/null +++ b/src/resources/style.css @@ -0,0 +1,16 @@ +/* Kordophone application styles */ + +.conversation-row { + padding: 8px 12px; + border-bottom: 1px solid alpha(#000, 0.1); +} + +.conversation-row:selected { + background-color: alpha(@accent_bg_color, 0.50); +} + +.message-list-scroller { + /* Invert the y-axis, so the messages are drawn bottom-to-top */ + /* Individual messages are drawn upside down in the custom renderer */ + transform: scale(1, -1); +} \ No newline at end of file diff --git a/src/service/repository.vala b/src/service/repository.vala index 82a1a7a..6258574 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -4,6 +4,7 @@ using Gee; public class Repository : Object { public signal void conversations_updated(); + public signal void messages_updated(string conversation_guid); public static Repository get_instance() { if (instance == null) { @@ -43,6 +44,21 @@ public class Repository : Object return returned_conversations; } + + public Message[] get_messages(string conversation_guid, string last_message_id = "") throws Error { + if (dbus_repository == null) { + throw new Error(1337, 1, "Repository not connected"); + } + + var messages = dbus_repository.get_messages(conversation_guid, last_message_id); + Message[] returned_messages = new Message[messages.length]; + + for (int i = 0; i < messages.length; i++) { + returned_messages[i] = new Message.from_hash_table(messages[i]); + } + + return returned_messages; + } private async void connect_to_dbus() { bool connected = false; @@ -65,6 +81,10 @@ public class Repository : Object dbus_repository.conversations_updated.connect(() => { conversations_updated(); }); + + dbus_repository.messages_updated.connect((conversation_guid) => { + messages_updated(conversation_guid); + }); // Initial load conversations_updated(); From 4c7c31ab8d087e2f22f6a0abc64f15b904c5c5a3 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 30 Apr 2025 19:12:00 -0700 Subject: [PATCH 08/63] implement bubble view --- src/meson.build | 2 + src/message-list/message-drawing-area.vala | 96 ++++++++ src/message-list/message-layout.vala | 249 +++++++++++++++++++++ src/message-list/message-list-view.vala | 60 +---- src/models/message.vala | 8 +- src/resources/style.css | 4 + 6 files changed, 368 insertions(+), 51 deletions(-) create mode 100644 src/message-list/message-drawing-area.vala create mode 100644 src/message-list/message-layout.vala diff --git a/src/meson.build b/src/meson.build index a6d9545..bfede65 100644 --- a/src/meson.build +++ b/src/meson.build @@ -26,6 +26,8 @@ sources = [ 'message-list/message-list-view.vala', 'message-list/message-list-model.vala', + 'message-list/message-drawing-area.vala', + 'message-list/message-layout.vala', 'models/conversation.vala', 'models/message.vala', diff --git a/src/message-list/message-drawing-area.vala b/src/message-list/message-drawing-area.vala new file mode 100644 index 0000000..0fa74f2 --- /dev/null +++ b/src/message-list/message-drawing-area.vala @@ -0,0 +1,96 @@ +using Gtk; +using Gee; + +private class MessageDrawingArea : Widget +{ + private SortedSet _messages = new TreeSet(); + private ArrayList _message_layouts = new ArrayList(); + + private const float bubble_padding = 10.0f; + private const float bubble_margin = 18.0f; + + public MessageDrawingArea() { + add_css_class("message-drawing-area"); + } + + public void set_messages(SortedSet messages) { + _messages = messages; + recompute_message_layouts(); + } + + public override SizeRequestMode get_request_mode() { + return SizeRequestMode.HEIGHT_FOR_WIDTH; + } + + public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) + { + if (orientation == Orientation.HORIZONTAL) { + // Horizontal, so we take up the full width provided + minimum = 0; + natural = for_size; + } else { + // compute total message layout height + float total_height = 0.0f; + _message_layouts.foreach((message_layout) => { + total_height += message_layout.get_height() + bubble_padding; + return true; + }); + + minimum = (int)total_height; + natural = (int)total_height; + } + + minimum_baseline = -1; + natural_baseline = -1; + } + + public override void size_allocate(int width, int height, int baseline) { + base.size_allocate(width, height, baseline); + recompute_message_layouts(); + } + + public override void snapshot(Snapshot snapshot) { + var container_width = get_width(); + float y_offset = 0; + _message_layouts.foreach((message_layout) => { + var message_width = message_layout.get_width(); + var message_height = message_layout.get_height(); + + snapshot.save(); + + // Flip the y-axis, since our parent is upside down (so newest messages are at the bottom) + snapshot.scale(1.0f, -1.0f); + + // Translate to the correct position + snapshot.translate(Graphene.Point() { + x = (message_layout.from_me ? (container_width - message_width - bubble_margin) : bubble_margin), + y = y_offset + }); + + // Undo the y-axis flip, origin is top left + snapshot.translate(Graphene.Point() { x = 0, y = -message_height }); + + message_layout.draw(snapshot); + snapshot.restore(); + + y_offset -= message_height + bubble_padding; + + return true; + }); + } + + private void recompute_message_layouts() { + var container_width = get_width(); + float max_width = container_width * 0.90f; + + _message_layouts.clear(); + _messages + .order_by((a, b) => (int)b.date - (int)a.date) // reverse order + .foreach((message) => { + _message_layouts.add(new MessageLayout(message, this, max_width)); + return true; + }); + + queue_draw(); + } +} \ No newline at end of file diff --git a/src/message-list/message-layout.vala b/src/message-list/message-layout.vala new file mode 100644 index 0000000..8135a96 --- /dev/null +++ b/src/message-list/message-layout.vala @@ -0,0 +1,249 @@ +using Gtk; +using Gee; + +private class MessageLayout : Object +{ + public Message message; + + private float max_width; + private Pango.Layout layout; + private Widget parent; + + const float tail_width = 15.0f; + const float tail_curve_offset = 2.5f; + const float tail_side_offset = 0.0f; + const float tail_bottom_padding = 4.0f; + const float corner_radius = 32.0f; + const float text_padding = 18.0f; + + const string font_description = "Sans 13"; + + public MessageLayout(Message message, Widget parent, float max_width) { + this.message = message; + this.max_width = max_width; + this.parent = parent; + + layout = parent.create_pango_layout(message.content); + + // Set text attributes + var font_desc = Pango.FontDescription.from_string(font_description); + layout.set_font_description(font_desc); + + // Set max width + layout.set_width((int)text_available_width * Pango.SCALE); + } + + public bool from_me { + get { + return message.sender == null; + } + } + + private float text_available_width { + get { + return max_width - text_x_offset - text_padding; + } + } + + private float text_x_offset { + get { + return from_me ? text_padding : tail_width + text_padding; + } + } + + private float text_x_padding { + get { + // Opposite of text_x_offset + return from_me ? tail_width + text_padding : text_padding; + } + } + + private Gdk.RGBA background_color { + get { + return from_me ? parent.get_color() : Gdk.RGBA() { + red = 1.0f, + green = 1.0f, + blue = 1.0f, + alpha = 0.08f + }; + } + } + + public float get_height() { + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + + return logical_rect.height + corner_radius + tail_bottom_padding; + } + + public float get_width() { + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + + return logical_rect.width + text_x_offset + text_x_padding; + } + + public void draw(Snapshot snapshot) { + with_bubble_clip(snapshot, (snapshot) => { + draw_background(snapshot); + draw_text(snapshot); + }); + } + + private void draw_text(Snapshot snapshot) + { + snapshot.save(); + + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + snapshot.translate(Graphene.Point() { + x = text_x_offset, + y = ((get_height() - tail_bottom_padding) - logical_rect.height) / 2 + }); + + snapshot.append_layout(layout, Gdk.RGBA() { + red = 1.0f, + green = 1.0f, + blue = 1.0f, + alpha = 1.0f + }); + + snapshot.restore(); + } + + private void draw_background(Snapshot snapshot) + { + snapshot.save(); + + var width = get_width(); + var height = get_height(); + var color = background_color; + var bounds = Graphene.Rect().init(0, 0, width, height); + + const float gradient_darkening = 0.67f; + var start_point = Graphene.Point() { x = 0, y = 0 }; + var end_point = Graphene.Point() { x = 0, y = height }; + var stops = new Gsk.ColorStop[] { + Gsk.ColorStop() { offset = 0.0f, color = color }, + Gsk.ColorStop() { offset = 1.0f, color = Gdk.RGBA () { + red = color.red * gradient_darkening, + green = color.green * gradient_darkening, + blue = color.blue * gradient_darkening, + alpha = color.alpha + } }, + }; + + snapshot.append_linear_gradient(bounds, start_point, end_point, stops); + + snapshot.restore(); + } + + private void with_bubble_clip(Snapshot snapshot, Func func) { + var width = get_width(); + var height = get_height(); + var path = create_bubble_path(width, height, from_me); + snapshot.push_fill(path, Gsk.FillRule.WINDING); + func(snapshot); + snapshot.pop(); + } + + private Gsk.Path create_bubble_path(float width, float height, bool tail_on_right = false) + { + var builder = new Gsk.PathBuilder(); + + float bubble_width = width - tail_width; + float bubble_height = height - tail_bottom_padding; + + // Base position adjustments based on tail position + float x = tail_on_right ? 0.0f : tail_width; + float y = 0.0f; + + // Calculate tail direction multiplier (-1 for left, 1 for right) + float dir = tail_on_right ? 1.0f : -1.0f; + + // Calculate tail side positions based on direction + float tail_side_x = tail_on_right ? (x + bubble_width) : x; + + // Start at top corner opposite to the tail + builder.move_to(tail_on_right ? (x + corner_radius) : (x + bubble_width - corner_radius), y); + + // Top edge + builder.line_to(tail_on_right ? (x + bubble_width - corner_radius) : (x + corner_radius), y); + + // Top corner on tail side + if (tail_on_right) { + builder.html_arc_to(x + bubble_width, y, + x + bubble_width, y + corner_radius, + corner_radius); + } else { + builder.html_arc_to(x, y, + x, y + corner_radius, + corner_radius); + } + + // Side edge on tail side + builder.line_to(tail_side_x, y + bubble_height - corner_radius); + + // Corner with tail + float tail_tip_x = tail_side_x + (dir * (tail_width - tail_curve_offset)); + float tail_tip_y = y + bubble_height; + + // Control points for the bezier curve + float ctrl_point1_x = tail_side_x + (dir * tail_side_offset); + float ctrl_point1_y = y + bubble_height - corner_radius/3; + + float ctrl_point2_x = tail_side_x + (dir * (tail_width / 2.0f)); + float ctrl_point2_y = y + bubble_height - 2; + + // Point where the tail meets the bottom edge + float tail_base_x = tail_side_x - (dir * corner_radius/2); + float tail_base_y = y + bubble_height; + + // Draw the corner with tail using bezier curves + builder.cubic_to(ctrl_point1_x, ctrl_point1_y, + ctrl_point2_x, ctrl_point2_y, + tail_tip_x, tail_tip_y); + + builder.cubic_to(tail_tip_x - (dir * tail_curve_offset), tail_tip_y + tail_curve_offset, + tail_base_x + (dir * tail_width), tail_base_y, + tail_base_x, tail_base_y); + + // Bottom edge + builder.line_to(tail_on_right ? (x + corner_radius) : (x + bubble_width - corner_radius), y + bubble_height); + + // Bottom corner opposite to tail + if (tail_on_right) { + builder.html_arc_to(x, y + bubble_height, + x, y + bubble_height - corner_radius, + corner_radius); + } else { + builder.html_arc_to(x + bubble_width, y + bubble_height, + x + bubble_width, y + bubble_height - corner_radius, + corner_radius); + } + + // Side edge opposite to tail + if (tail_on_right) { + builder.line_to(x, y + corner_radius); + + // Top corner to close path + builder.html_arc_to(x, y, + x + corner_radius, y, + corner_radius); + } else { + builder.line_to(x + bubble_width, y + corner_radius); + + // Top corner to close path + builder.html_arc_to(x + bubble_width, y, + x + bubble_width - corner_radius, y, + corner_radius); + } + + // Close the path + builder.close(); + + return builder.to_path(); + } +} + + diff --git a/src/message-list/message-list-view.vala b/src/message-list/message-list-view.vala index 508e94a..4ea9b46 100644 --- a/src/message-list/message-list-view.vala +++ b/src/message-list/message-list-view.vala @@ -1,5 +1,6 @@ using Adw; using Gtk; +using Gee; public class MessageListView : Adw.Bin { @@ -19,55 +20,14 @@ public class MessageListView : Adw.Bin var header_bar = new Adw.HeaderBar(); header_bar.set_title_widget(new Label("Messages")); container.add_top_bar(header_bar); + + // Create test message set + var messages = new TreeSet(); + messages.add(new Message("Hello, world!", 1, "user")); + messages.add(new Message("How, are you?", 2, null)); + messages.add(new Message("I'm fine, thank you!", 3, "user")); + messages.add(new Message("GTK also supports color expressions, which allow colors to be transformed to new ones and can be nested, providing a rich language to define colors. Color expressions resemble functions, taking 1 or more colors and in some cases a number as arguments.", 4, "user")); + + message_drawing_area.set_messages(messages); } } - - -private class MessageDrawingArea : Widget { - public MessageDrawingArea() { - } - - public override SizeRequestMode get_request_mode() { - return SizeRequestMode.HEIGHT_FOR_WIDTH; - } - - public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { - GLib.message("Measure orientation: %s, for_size: %d", orientation.to_string(), for_size); - - if (orientation == Orientation.HORIZONTAL) { - // Horizontal, so we take up the full width provided - minimum = 0; - natural = for_size; - } else { - GLib.message("Vertical measure for width: %d", for_size); - minimum = 1500; - natural = 1500; - } - - minimum_baseline = -1; - natural_baseline = -1; - } - - public override void snapshot(Snapshot snapshot) { - var width = get_width(); - var height = get_height(); - - GLib.message("Snapshot width: %d, height: %d", width, height); - - var rect = Graphene.Rect().init(0, 0, width, height); - snapshot.append_color({1.0f, 0.0f, 0.0f, 1.0f}, rect); - - // Create a text layout - var layout = create_pango_layout("Hello World!"); - layout.set_width(width * Pango.SCALE); - - // Set text attributes - var font_desc = Pango.FontDescription.from_string("Sans 14"); - layout.set_font_description(font_desc); - - // Draw the text in white - snapshot.append_layout(layout, Gdk.RGBA() { red = 1.0f, green = 1.0f, blue = 1.0f, alpha = 1.0f }); - } -} - - diff --git a/src/models/message.vala b/src/models/message.vala index 66e5276..b7fc7c9 100644 --- a/src/models/message.vala +++ b/src/models/message.vala @@ -5,7 +5,13 @@ public class Message : Object public string guid { get; set; default = ""; } public string content { get; set; default = ""; } public int64 date { get; set; default = 0; } - public string?sender { get; set; default = null; } + public string? sender { get; set; default = null; } + + public Message(string content, int64 date, string? sender) { + this.content = content; + this.date = date; + this.sender = sender; + } public Message.from_hash_table(HashTable message_data) { guid = message_data["guid"].get_string(); diff --git a/src/resources/style.css b/src/resources/style.css index cbe637e..bc9a39f 100644 --- a/src/resources/style.css +++ b/src/resources/style.css @@ -13,4 +13,8 @@ /* Invert the y-axis, so the messages are drawn bottom-to-top */ /* Individual messages are drawn upside down in the custom renderer */ transform: scale(1, -1); +} + +.message-drawing-area { + color: darker(@accent_bg_color); } \ No newline at end of file From a7e88bd3c33dbe0a3a5ba68dcad1fa55c7144df5 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 30 Apr 2025 19:50:36 -0700 Subject: [PATCH 09/63] wire up message loading --- src/application/main-window.vala | 19 +++++++++-- .../conversation-list-view.vala | 7 ++++ src/conversation-list/conversation-row.vala | 4 +-- src/message-list/message-drawing-area.vala | 1 + src/message-list/message-layout.vala | 4 +-- src/message-list/message-list-model.vala | 4 +++ src/message-list/message-list-view.vala | 32 +++++++++++++------ src/models/message.vala | 19 +++++++---- 8 files changed, 69 insertions(+), 21 deletions(-) diff --git a/src/application/main-window.vala b/src/application/main-window.vala index a03e3a1..3420fd5 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -3,6 +3,9 @@ using Gtk; public class MainWindow : Adw.ApplicationWindow { + private ConversationListView conversation_list_view; + private MessageListView message_list_view; + public MainWindow () { Object (title: "Kordophone"); @@ -10,10 +13,22 @@ public class MainWindow : Adw.ApplicationWindow split_view.set_min_sidebar_width (400); set_content (split_view); - var conversation_list_page = new NavigationPage (new ConversationListView (), "Conversations"); + conversation_list_view = new ConversationListView (); + conversation_list_view.conversation_selected.connect (conversation_selected); + + var conversation_list_page = new NavigationPage (conversation_list_view, "Conversations"); split_view.sidebar = conversation_list_page; - var message_list_page = new NavigationPage (new MessageListView (new MessageListModel ("123")), "Messages"); + message_list_view = new MessageListView (new MessageListModel ("123")); + var message_list_page = new NavigationPage (message_list_view, "Messages"); split_view.content = message_list_page; } + + private void conversation_selected(string? conversation_guid) { + if (conversation_guid == null) { + message_list_view.model = null; + } else { + message_list_view.model = new MessageListModel (conversation_guid); + } + } } \ No newline at end of file diff --git a/src/conversation-list/conversation-list-view.vala b/src/conversation-list/conversation-list-view.vala index 9342272..d667870 100644 --- a/src/conversation-list/conversation-list-view.vala +++ b/src/conversation-list/conversation-list-view.vala @@ -3,6 +3,8 @@ using Gtk; public class ConversationListView : Adw.Bin { + public signal void conversation_selected(string? conversation_guid); + private Adw.ToolbarView container; private ListBox list_box; private ScrolledWindow scrolled_window; @@ -21,6 +23,11 @@ public class ConversationListView : Adw.Bin list_box.set_selection_mode (SelectionMode.SINGLE); scrolled_window.set_child (list_box); + list_box.row_selected.connect ((row) => { + var conversation_row = (ConversationRow?) row; + conversation_selected(conversation_row != null ? conversation_row.conversation.guid : null); + }); + header_bar = new Adw.HeaderBar (); header_bar.set_title_widget (new Label ("Kordophone")); container.add_top_bar (header_bar); diff --git a/src/conversation-list/conversation-row.vala b/src/conversation-list/conversation-row.vala index 7238235..5ddb121 100644 --- a/src/conversation-list/conversation-row.vala +++ b/src/conversation-list/conversation-row.vala @@ -2,11 +2,11 @@ using Adw; using Gtk; public class ConversationRow : Adw.ActionRow { + public Conversation conversation; private Label unread_badge; public ConversationRow(Conversation conversation) { - Object(); - + this.conversation = conversation; title = conversation.display_name; subtitle = conversation.last_message_preview; subtitle_lines = 1; diff --git a/src/message-list/message-drawing-area.vala b/src/message-list/message-drawing-area.vala index 0fa74f2..8791059 100644 --- a/src/message-list/message-drawing-area.vala +++ b/src/message-list/message-drawing-area.vala @@ -92,5 +92,6 @@ private class MessageDrawingArea : Widget }); queue_draw(); + queue_resize(); } } \ No newline at end of file diff --git a/src/message-list/message-layout.vala b/src/message-list/message-layout.vala index 8135a96..86c66ec 100644 --- a/src/message-list/message-layout.vala +++ b/src/message-list/message-layout.vala @@ -23,7 +23,7 @@ private class MessageLayout : Object this.max_width = max_width; this.parent = parent; - layout = parent.create_pango_layout(message.content); + layout = parent.create_pango_layout(message.text); // Set text attributes var font_desc = Pango.FontDescription.from_string(font_description); @@ -35,7 +35,7 @@ private class MessageLayout : Object public bool from_me { get { - return message.sender == null; + return message.from_me; } } diff --git a/src/message-list/message-list-model.vala b/src/message-list/message-list-model.vala index 029ef39..3bfd75e 100644 --- a/src/message-list/message-list-model.vala +++ b/src/message-list/message-list-model.vala @@ -3,6 +3,8 @@ using Gee; public class MessageListModel : Object, ListModel { + public signal void messages_changed(); + public SortedSet messages { owned get { return _messages.read_only_view; } } @@ -49,6 +51,8 @@ public class MessageListModel : Object, ListModel } catch (Error e) { warning("Failed to load messages: %s", e.message); } + + messages_changed(); } private void got_messages_updated(string conversation_guid) { diff --git a/src/message-list/message-list-view.vala b/src/message-list/message-list-view.vala index 4ea9b46..84f6e40 100644 --- a/src/message-list/message-list-view.vala +++ b/src/message-list/message-list-view.vala @@ -4,12 +4,31 @@ using Gee; public class MessageListView : Adw.Bin { + public MessageListModel? model { + get { + return _model; + } + set { + _model = value; + + if (model != null) { + model.messages_changed.connect(reload_messages); + model.load_messages(); + } else { + message_drawing_area.set_messages(new TreeSet()); + } + } + } + + private MessageListModel? _model = null; private Adw.ToolbarView container; private MessageDrawingArea message_drawing_area = new MessageDrawingArea(); private ScrolledWindow scrolled_window = new ScrolledWindow(); - public MessageListView(MessageListModel model) { + public MessageListView(MessageListModel? model = null) { + this.model = model; + container = new Adw.ToolbarView(); set_child(container); @@ -20,14 +39,9 @@ public class MessageListView : Adw.Bin var header_bar = new Adw.HeaderBar(); header_bar.set_title_widget(new Label("Messages")); container.add_top_bar(header_bar); + } - // Create test message set - var messages = new TreeSet(); - messages.add(new Message("Hello, world!", 1, "user")); - messages.add(new Message("How, are you?", 2, null)); - messages.add(new Message("I'm fine, thank you!", 3, "user")); - messages.add(new Message("GTK also supports color expressions, which allow colors to be transformed to new ones and can be nested, providing a rich language to define colors. Color expressions resemble functions, taking 1 or more colors and in some cases a number as arguments.", 4, "user")); - - message_drawing_area.set_messages(messages); + private void reload_messages() { + message_drawing_area.set_messages(_model.messages); } } diff --git a/src/models/message.vala b/src/models/message.vala index b7fc7c9..8a1ff4b 100644 --- a/src/models/message.vala +++ b/src/models/message.vala @@ -3,19 +3,26 @@ using GLib; public class Message : Object { public string guid { get; set; default = ""; } - public string content { get; set; default = ""; } + public string text { get; set; default = ""; } public int64 date { get; set; default = 0; } - public string? sender { get; set; default = null; } + public string sender { get; set; default = null; } - public Message(string content, int64 date, string? sender) { - this.content = content; + public bool from_me { + get { + // Hm, this may have been accidental. + return sender == "(Me)"; + } + } + + public Message(string text, int64 date, string? sender) { + this.text = text; this.date = date; this.sender = sender; } public Message.from_hash_table(HashTable message_data) { - guid = message_data["guid"].get_string(); - content = message_data["content"].get_string(); + guid = message_data["id"].get_string(); + text = message_data["text"].get_string(); date = message_data["date"].get_int64(); sender = message_data["sender"].get_string(); } From f80d1a609b3b50eb3f0ed58f66cd672486452b90 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 30 Apr 2025 21:19:24 -0700 Subject: [PATCH 10/63] attempt to resolve scaling issues on 2x displays --- src/message-list/message-layout.vala | 106 ++++++++++++++++----------- src/resources/style.css | 2 +- 2 files changed, 63 insertions(+), 45 deletions(-) diff --git a/src/message-list/message-layout.vala b/src/message-list/message-layout.vala index 86c66ec..e70a3fe 100644 --- a/src/message-list/message-layout.vala +++ b/src/message-list/message-layout.vala @@ -1,6 +1,24 @@ using Gtk; using Gee; +private struct MessageLayoutConstants { + public float tail_width; + public float tail_curve_offset; + public float tail_side_offset; + public float tail_bottom_padding; + public float corner_radius; + public float text_padding; + + public MessageLayoutConstants(float scale_factor) { + tail_width = 15.0f / scale_factor; + tail_curve_offset = 2.5f / scale_factor; + tail_side_offset = 0.0f / scale_factor; + tail_bottom_padding = 4.0f / scale_factor; + corner_radius = 32.0f / scale_factor; + text_padding = 18.0f / scale_factor; + } +} + private class MessageLayout : Object { public Message message; @@ -8,25 +26,25 @@ private class MessageLayout : Object private float max_width; private Pango.Layout layout; private Widget parent; - - const float tail_width = 15.0f; - const float tail_curve_offset = 2.5f; - const float tail_side_offset = 0.0f; - const float tail_bottom_padding = 4.0f; - const float corner_radius = 32.0f; - const float text_padding = 18.0f; - - const string font_description = "Sans 13"; + private MessageLayoutConstants constants; public MessageLayout(Message message, Widget parent, float max_width) { this.message = message; this.max_width = max_width; this.parent = parent; + this.constants = MessageLayoutConstants(parent.get_scale_factor()); layout = parent.create_pango_layout(message.text); - // Set text attributes - var font_desc = Pango.FontDescription.from_string(font_description); + // Get the system font settings + var settings = Gtk.Settings.get_default(); + var font_name = settings.gtk_font_name; + + // Create font description from system font + var font_desc = Pango.FontDescription.from_string(font_name); + + var size = font_desc.get_size(); + font_desc.set_size((int)(size)); layout.set_font_description(font_desc); // Set max width @@ -41,20 +59,20 @@ private class MessageLayout : Object private float text_available_width { get { - return max_width - text_x_offset - text_padding; + return max_width - text_x_offset - constants.text_padding; } } private float text_x_offset { get { - return from_me ? text_padding : tail_width + text_padding; + return from_me ? constants.text_padding : constants.tail_width + constants.text_padding; } } private float text_x_padding { get { // Opposite of text_x_offset - return from_me ? tail_width + text_padding : text_padding; + return from_me ? constants.tail_width + constants.text_padding : constants.text_padding; } } @@ -73,7 +91,7 @@ private class MessageLayout : Object Pango.Rectangle ink_rect, logical_rect; layout.get_pixel_extents(out ink_rect, out logical_rect); - return logical_rect.height + corner_radius + tail_bottom_padding; + return logical_rect.height + constants.corner_radius + constants.tail_bottom_padding; } public float get_width() { @@ -98,7 +116,7 @@ private class MessageLayout : Object layout.get_pixel_extents(out ink_rect, out logical_rect); snapshot.translate(Graphene.Point() { x = text_x_offset, - y = ((get_height() - tail_bottom_padding) - logical_rect.height) / 2 + y = ((get_height() - constants.tail_bottom_padding) - logical_rect.height) / 2 }); snapshot.append_layout(layout, Gdk.RGBA() { @@ -151,11 +169,11 @@ private class MessageLayout : Object { var builder = new Gsk.PathBuilder(); - float bubble_width = width - tail_width; - float bubble_height = height - tail_bottom_padding; + float bubble_width = width - constants.tail_width; + float bubble_height = height - constants.tail_bottom_padding; // Base position adjustments based on tail position - float x = tail_on_right ? 0.0f : tail_width; + float x = tail_on_right ? 0.0f : constants.tail_width; float y = 0.0f; // Calculate tail direction multiplier (-1 for left, 1 for right) @@ -165,38 +183,38 @@ private class MessageLayout : Object float tail_side_x = tail_on_right ? (x + bubble_width) : x; // Start at top corner opposite to the tail - builder.move_to(tail_on_right ? (x + corner_radius) : (x + bubble_width - corner_radius), y); + builder.move_to(tail_on_right ? (x + constants.corner_radius) : (x + bubble_width - constants.corner_radius), y); // Top edge - builder.line_to(tail_on_right ? (x + bubble_width - corner_radius) : (x + corner_radius), y); + builder.line_to(tail_on_right ? (x + bubble_width - constants.corner_radius) : (x + constants.corner_radius), y); // Top corner on tail side if (tail_on_right) { builder.html_arc_to(x + bubble_width, y, - x + bubble_width, y + corner_radius, - corner_radius); + x + bubble_width, y + constants.corner_radius, + constants.corner_radius); } else { builder.html_arc_to(x, y, - x, y + corner_radius, - corner_radius); + x, y + constants.corner_radius, + constants.corner_radius); } // Side edge on tail side - builder.line_to(tail_side_x, y + bubble_height - corner_radius); + builder.line_to(tail_side_x, y + bubble_height - constants.corner_radius); // Corner with tail - float tail_tip_x = tail_side_x + (dir * (tail_width - tail_curve_offset)); + float tail_tip_x = tail_side_x + (dir * (constants.tail_width - constants.tail_curve_offset)); float tail_tip_y = y + bubble_height; // Control points for the bezier curve - float ctrl_point1_x = tail_side_x + (dir * tail_side_offset); - float ctrl_point1_y = y + bubble_height - corner_radius/3; + float ctrl_point1_x = tail_side_x + (dir * constants.tail_side_offset); + float ctrl_point1_y = y + bubble_height - constants.corner_radius/3; - float ctrl_point2_x = tail_side_x + (dir * (tail_width / 2.0f)); + float ctrl_point2_x = tail_side_x + (dir * (constants.tail_width / 2.0f)); float ctrl_point2_y = y + bubble_height - 2; // Point where the tail meets the bottom edge - float tail_base_x = tail_side_x - (dir * corner_radius/2); + float tail_base_x = tail_side_x - (dir * constants.corner_radius/2); float tail_base_y = y + bubble_height; // Draw the corner with tail using bezier curves @@ -204,39 +222,39 @@ private class MessageLayout : Object ctrl_point2_x, ctrl_point2_y, tail_tip_x, tail_tip_y); - builder.cubic_to(tail_tip_x - (dir * tail_curve_offset), tail_tip_y + tail_curve_offset, - tail_base_x + (dir * tail_width), tail_base_y, + builder.cubic_to(tail_tip_x - (dir * constants.tail_curve_offset), tail_tip_y + constants.tail_curve_offset, + tail_base_x + (dir * constants.tail_width), tail_base_y, tail_base_x, tail_base_y); // Bottom edge - builder.line_to(tail_on_right ? (x + corner_radius) : (x + bubble_width - corner_radius), y + bubble_height); + builder.line_to(tail_on_right ? (x + constants.corner_radius) : (x + bubble_width - constants.corner_radius), y + bubble_height); // Bottom corner opposite to tail if (tail_on_right) { builder.html_arc_to(x, y + bubble_height, - x, y + bubble_height - corner_radius, - corner_radius); + x, y + bubble_height - constants.corner_radius, + constants.corner_radius); } else { builder.html_arc_to(x + bubble_width, y + bubble_height, - x + bubble_width, y + bubble_height - corner_radius, - corner_radius); + x + bubble_width, y + bubble_height - constants.corner_radius, + constants.corner_radius); } // Side edge opposite to tail if (tail_on_right) { - builder.line_to(x, y + corner_radius); + builder.line_to(x, y + constants.corner_radius); // Top corner to close path builder.html_arc_to(x, y, - x + corner_radius, y, - corner_radius); + x + constants.corner_radius, y, + constants.corner_radius); } else { - builder.line_to(x + bubble_width, y + corner_radius); + builder.line_to(x + bubble_width, y + constants.corner_radius); // Top corner to close path builder.html_arc_to(x + bubble_width, y, - x + bubble_width - corner_radius, y, - corner_radius); + x + bubble_width - constants.corner_radius, y, + constants.corner_radius); } // Close the path diff --git a/src/resources/style.css b/src/resources/style.css index bc9a39f..20b65e0 100644 --- a/src/resources/style.css +++ b/src/resources/style.css @@ -1,7 +1,7 @@ /* Kordophone application styles */ .conversation-row { - padding: 8px 12px; + padding: 6px 10px; border-bottom: 1px solid alpha(#000, 0.1); } From 410182eab8316624619ace78a5290c88f6f11efc Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 2 May 2025 15:09:12 -0700 Subject: [PATCH 11/63] implements sending --- src/application/main-window.vala | 29 ++++++++-- .../conversation-list-model.vala | 1 + src/meson.build | 2 + src/message-list/message-layout.vala | 2 +- src/message-list/message-list-model.vala | 8 +-- src/resources/style.css | 11 ++++ src/service/interface/dbusservice.vala | 9 +++ .../xml/net.buzzert.kordophonecd.Server.xml | 19 +++++++ src/service/repository.vala | 8 +++ src/transcript-view/transcript-view.vala | 57 +++++++++++++++++++ 10 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 src/transcript-view/transcript-view.vala diff --git a/src/application/main-window.vala b/src/application/main-window.vala index 3420fd5..685c775 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -4,7 +4,7 @@ using Gtk; public class MainWindow : Adw.ApplicationWindow { private ConversationListView conversation_list_view; - private MessageListView message_list_view; + private TranscriptView transcript_view; public MainWindow () { Object (title: "Kordophone"); @@ -19,16 +19,33 @@ public class MainWindow : Adw.ApplicationWindow var conversation_list_page = new NavigationPage (conversation_list_view, "Conversations"); split_view.sidebar = conversation_list_page; - message_list_view = new MessageListView (new MessageListModel ("123")); - var message_list_page = new NavigationPage (message_list_view, "Messages"); - split_view.content = message_list_page; + transcript_view = new TranscriptView (); + transcript_view.on_send.connect (on_transcript_send); + + var transcript_page = new NavigationPage (transcript_view, "Transcript"); + split_view.content = transcript_page; } private void conversation_selected(string? conversation_guid) { if (conversation_guid == null) { - message_list_view.model = null; + transcript_view.message_list.model = null; } else { - message_list_view.model = new MessageListModel (conversation_guid); + transcript_view.message_list.model = new MessageListModel (conversation_guid); } } + + private void on_transcript_send(string message) { + if (transcript_view.message_list.model == null) { + GLib.warning("No conversation selected"); + return; + } + + var selected_conversation = transcript_view.message_list.model.conversation_guid; + if (selected_conversation == null) { + GLib.warning("No conversation selected"); + return; + } + + Repository.get_instance().send_message(selected_conversation, message); + } } \ No newline at end of file diff --git a/src/conversation-list/conversation-list-model.vala b/src/conversation-list/conversation-list-model.vala index 18fc2fa..643f971 100644 --- a/src/conversation-list/conversation-list-model.vala +++ b/src/conversation-list/conversation-list-model.vala @@ -16,6 +16,7 @@ public class ConversationListModel : Object, ListModel }); Repository.get_instance().conversations_updated.connect(load_conversations); + Repository.get_instance().messages_updated.connect(load_conversations); } public void load_conversations() { diff --git a/src/meson.build b/src/meson.build index bfede65..f143b9d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -31,6 +31,8 @@ sources = [ 'models/conversation.vala', 'models/message.vala', + + 'transcript-view/transcript-view.vala', ] executable('kordophone', diff --git a/src/message-list/message-layout.vala b/src/message-list/message-layout.vala index e70a3fe..85e57fd 100644 --- a/src/message-list/message-layout.vala +++ b/src/message-list/message-layout.vala @@ -14,7 +14,7 @@ private struct MessageLayoutConstants { tail_curve_offset = 2.5f / scale_factor; tail_side_offset = 0.0f / scale_factor; tail_bottom_padding = 4.0f / scale_factor; - corner_radius = 32.0f / scale_factor; + corner_radius = 24.0f / scale_factor; text_padding = 18.0f / scale_factor; } } diff --git a/src/message-list/message-list-model.vala b/src/message-list/message-list-model.vala index 3bfd75e..0f8b05a 100644 --- a/src/message-list/message-list-model.vala +++ b/src/message-list/message-list-model.vala @@ -9,7 +9,7 @@ public class MessageListModel : Object, ListModel owned get { return _messages.read_only_view; } } - private string _conversation_guid; + public string conversation_guid { get; private set; } private SortedSet _messages; public MessageListModel(string conversation_guid) { @@ -19,12 +19,12 @@ public class MessageListModel : Object, ListModel }); Repository.get_instance().messages_updated.connect(got_messages_updated); - _conversation_guid = conversation_guid; + this.conversation_guid = conversation_guid; } public void load_messages() { try { - Message[] messages = Repository.get_instance().get_messages(_conversation_guid); + Message[] messages = Repository.get_instance().get_messages(conversation_guid); // Clear existing set uint old_count = _messages.size; @@ -56,7 +56,7 @@ public class MessageListModel : Object, ListModel } private void got_messages_updated(string conversation_guid) { - if (conversation_guid == _conversation_guid) { + if (conversation_guid == this.conversation_guid) { load_messages(); } } diff --git a/src/resources/style.css b/src/resources/style.css index 20b65e0..f3cadf4 100644 --- a/src/resources/style.css +++ b/src/resources/style.css @@ -17,4 +17,15 @@ .message-drawing-area { color: darker(@accent_bg_color); +} + +.message-input-box { + margin-bottom: 14px; + margin-top: 14px; + margin-left: 14px; + margin-right: 14px; +} + +.message-input-entry { + font-size: 1.1rem; } \ No newline at end of file diff --git a/src/service/interface/dbusservice.vala b/src/service/interface/dbusservice.vala index 0dbd5af..ded23fd 100644 --- a/src/service/interface/dbusservice.vala +++ b/src/service/interface/dbusservice.vala @@ -32,6 +32,9 @@ namespace DBusService { [DBus (name = "GetConversations")] public abstract GLib.HashTable[] get_conversations() throws DBusError, IOError; + [DBus (name = "SyncConversationList")] + public abstract void sync_conversation_list() throws DBusError, IOError; + [DBus (name = "SyncAllConversations")] public abstract void sync_all_conversations() throws DBusError, IOError; @@ -41,9 +44,15 @@ namespace DBusService { [DBus (name = "ConversationsUpdated")] public signal void conversations_updated(); + [DBus (name = "DeleteAllConversations")] + public abstract void delete_all_conversations() throws DBusError, IOError; + [DBus (name = "GetMessages")] public abstract GLib.HashTable[] get_messages(string conversation_id, string last_message_id) throws DBusError, IOError; + [DBus (name = "SendMessage")] + public abstract string send_message(string conversation_id, string text) throws DBusError, IOError; + [DBus (name = "MessagesUpdated")] public signal void messages_updated(string conversation_id); } diff --git a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml index bbe5263..01fb1c3 100644 --- a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml +++ b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml @@ -24,6 +24,11 @@ + + + + @@ -40,6 +45,11 @@ value="Emitted when the list of conversations is updated."/> + + + + @@ -48,6 +58,15 @@ + + + + + + + + 0); + } + + private void on_request_send() { + if (message_entry.text.length > 0) { + on_send(message_entry.text); + message_entry.text = ""; + } + } +} + From ef0312ccbd16b6ced9acc214064d208c13809c58 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 2 May 2025 15:51:43 -0700 Subject: [PATCH 12/63] ~buzzert/Kordophone#9: gtk v2: Conversation selected state lost when reloading --- .../conversation-list-model.vala | 97 +++++++++++++++---- .../conversation-list-view.vala | 42 +++++++- src/models/conversation.vala | 17 ++++ 3 files changed, 137 insertions(+), 19 deletions(-) diff --git a/src/conversation-list/conversation-list-model.vala b/src/conversation-list/conversation-list-model.vala index 643f971..3fe3dd8 100644 --- a/src/conversation-list/conversation-list-model.vala +++ b/src/conversation-list/conversation-list-model.vala @@ -21,29 +21,90 @@ public class ConversationListModel : Object, ListModel public void load_conversations() { try { - Conversation[] conversations = Repository.get_instance().get_conversations(); + Conversation[] new_conversations = Repository.get_instance().get_conversations(); - // Clear existing set - uint old_count = _conversations.size; - _conversations.clear(); - - // Notify of removal - if (old_count > 0) { - items_changed(0, old_count, 0); + // Create a map of old conversations for quick lookup + var old_conversations_map = new HashMap(); + foreach (var conv in _conversations) { + old_conversations_map[conv.guid] = conv; } - // Process each conversation - uint position = 0; - - for (int i = 0; i < conversations.length; i++) { - var conversation = conversations[i]; - _conversations.add(conversation); - position++; + // Create a map of new conversations for quick lookup + var new_conversations_map = new HashMap(); + foreach (var conv in new_conversations) { + new_conversations_map[conv.guid] = conv; } - // Notify of additions - if (position > 0) { - items_changed(0, 0, position); + // Find removed conversations + var removed_positions = new ArrayList(); + var current_position = 0; + foreach (var old_conv in _conversations) { + if (!new_conversations_map.has_key(old_conv.guid)) { + removed_positions.add(current_position); + } + current_position++; + } + + // Remove conversations in reverse order to maintain correct positions + for (int i = removed_positions.size - 1; i >= 0; i--) { + var pos = removed_positions[i]; + _conversations.remove(_conversations.to_array()[pos]); + items_changed(pos, 1, 0); + } + + // Find added conversations and changed conversations + var added_conversations = new ArrayList(); + var changed_conversations = new ArrayList(); + foreach (var new_conv in new_conversations) { + if (!old_conversations_map.has_key(new_conv.guid)) { + added_conversations.add(new_conv); + } else { + var old_conv = old_conversations_map[new_conv.guid]; + if (!old_conv.equals(new_conv)) { + changed_conversations.add(new_conv); + } + } + } + + // Add new conversations + foreach (var conv in added_conversations) { + _conversations.add(conv); + // Find the position by counting how many items are before this one + uint pos = 0; + foreach (var existing_conv in _conversations) { + if (existing_conv.guid == conv.guid) break; + pos++; + } + items_changed(pos, 0, 1); + } + + // Update changed conversations + GLib.message("Changed conversations: %d", changed_conversations.size); + foreach (var conv in changed_conversations) { + // Find position of old conversation + uint old_pos = 0; + var old_conv = old_conversations_map[conv.guid]; + foreach (var existing_conv in _conversations) { + if (existing_conv.guid == old_conv.guid) break; + old_pos++; + } + + // Remove the old one + _conversations.remove(old_conv); + + // Add the new one + _conversations.add(conv); + + // Find the new (sorted) position + uint new_pos = 0; + foreach (var existing_conv in _conversations) { + if (existing_conv.guid == conv.guid) break; + new_pos++; + } + + // Notify of the change + items_changed(old_pos, 1, 0); + items_changed(new_pos, 0, 1); } } catch (Error e) { warning("Failed to load conversations: %s", e.message); diff --git a/src/conversation-list/conversation-list-view.vala b/src/conversation-list/conversation-list-view.vala index d667870..90a0450 100644 --- a/src/conversation-list/conversation-list-view.vala +++ b/src/conversation-list/conversation-list-view.vala @@ -11,6 +11,9 @@ public class ConversationListView : Adw.Bin private Adw.HeaderBar header_bar; private ConversationListModel conversation_model; + private string? selected_conversation_guid = null; + private bool selection_update_queued = false; + public ConversationListView () { container = new Adw.ToolbarView (); set_child (container); @@ -25,7 +28,10 @@ public class ConversationListView : Adw.Bin list_box.row_selected.connect ((row) => { var conversation_row = (ConversationRow?) row; - conversation_selected(conversation_row != null ? conversation_row.conversation.guid : null); + if (conversation_row != null) { + selected_conversation_guid = conversation_row.conversation.guid; + conversation_selected(selected_conversation_guid); + } }); header_bar = new Adw.HeaderBar (); @@ -44,9 +50,43 @@ public class ConversationListView : Adw.Bin // Set up model and bind to list conversation_model = new ConversationListModel (); + conversation_model.items_changed.connect (on_items_changed); list_box.bind_model (conversation_model, create_conversation_row); } + private void on_items_changed (uint position, uint removed, uint added) { + enqueue_selection_update(); + } + + private void enqueue_selection_update() { + if (selection_update_queued) { + return; + } + + selection_update_queued = true; + GLib.Idle.add(() => { + update_selection(); + selection_update_queued = false; + return false; + }, GLib.Priority.HIGH); + } + + private void update_selection() { + // Re-select selected_conversation_guid, if it has changed. + if (selected_conversation_guid != null) { + for (uint i = 0; i < conversation_model.get_n_items(); i++) { + var conversation = (Conversation) conversation_model.get_item(i); + if (conversation.guid == selected_conversation_guid) { + var row = list_box.get_row_at_index((int)i); + if (row != null) { + list_box.select_row(row); + } + } + } + } + } + + private Widget create_conversation_row (Object item) { Conversation conversation = (Conversation) item; return new ConversationRow (conversation); diff --git a/src/models/conversation.vala b/src/models/conversation.vala index 3e489ea..c7c68a0 100644 --- a/src/models/conversation.vala +++ b/src/models/conversation.vala @@ -52,4 +52,21 @@ public class Conversation : Object { _display_name = conversation_data["display_name"].get_string(); } } + + public bool equals(Conversation other) { + if (other == null) return false; + if (guid != other.guid) return false; + if (date != other.date) return false; + if (unread_count != other.unread_count) return false; + if (last_message_preview != other.last_message_preview) return false; + if (_display_name != other._display_name) return false; + + // Compare participants arrays + if (participants.length != other.participants.length) return false; + for (int i = 0; i < participants.length; i++) { + if (participants[i] != other.participants[i]) return false; + } + + return true; + } } \ No newline at end of file From 0f565756df99dda186613dc6c516230fed96607f Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 3 May 2025 01:11:26 -0700 Subject: [PATCH 13/63] adds setting screen --- src/application/kordophone-application.vala | 4 + src/application/main-window.vala | 9 ++ src/application/preferences-window.vala | 73 +++++++++++ .../conversation-list-view.vala | 10 ++ src/conversation-list/conversation-row.vala | 7 +- src/meson.build | 6 +- src/models/conversation.vala | 2 +- src/resources/style.css | 5 +- src/service/dbus-service-base.vala | 60 +++++++++ src/service/interface/dbusservice.vala | 3 - .../xml/net.buzzert.kordophonecd.Server.xml | 6 - src/service/repository.vala | 93 +++++--------- src/service/settings.vala | 115 ++++++++++++++++++ 13 files changed, 316 insertions(+), 77 deletions(-) create mode 100644 src/application/preferences-window.vala create mode 100644 src/service/dbus-service-base.vala create mode 100644 src/service/settings.vala diff --git a/src/application/kordophone-application.vala b/src/application/kordophone-application.vala index 0d5f465..758e4dd 100644 --- a/src/application/kordophone-application.vala +++ b/src/application/kordophone-application.vala @@ -20,6 +20,10 @@ public class KordophoneApp : Adw.Application provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ); + + // Warm up dbus connections + Settings.get_instance(); + Repository.get_instance(); } protected override void activate () { diff --git a/src/application/main-window.vala b/src/application/main-window.vala index 685c775..ccb44fe 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -24,6 +24,15 @@ public class MainWindow : Adw.ApplicationWindow var transcript_page = new NavigationPage (transcript_view, "Transcript"); split_view.content = transcript_page; + + var show_settings_action = new SimpleAction ("settings", null); + show_settings_action.activate.connect(show_settings); + add_action(show_settings_action); + } + + private void show_settings () { + var dialog = new PreferencesWindow (this); + dialog.present (this); } private void conversation_selected(string? conversation_guid) { diff --git a/src/application/preferences-window.vala b/src/application/preferences-window.vala new file mode 100644 index 0000000..d5f2681 --- /dev/null +++ b/src/application/preferences-window.vala @@ -0,0 +1,73 @@ +using Adw; +using Gtk; + +public class PreferencesWindow : Adw.PreferencesDialog { + private Adw.EntryRow server_url_row; + private Adw.EntryRow username_row; + private Adw.PasswordEntryRow password_row; + private Settings settings; + + public PreferencesWindow (Gtk.Window parent) { + Object ( + title: "Settings" + ); + + add_css_class ("settings-dialog"); + + var page = new PreferencesPage (); + page.margin_top = 14; + page.margin_bottom = 14; + page.margin_start = 50; + page.margin_end = 50; + add (page); + + var connection_group = new PreferencesGroup (); + connection_group.title = "Connection Settings"; + page.add (connection_group); + + server_url_row = new Adw.EntryRow (); + server_url_row.title = "Server URL"; + connection_group.add (server_url_row); + + username_row = new Adw.EntryRow (); + username_row.title = "Username"; + connection_group.add (username_row); + + password_row = new Adw.PasswordEntryRow (); + password_row.title = "Password"; + connection_group.add (password_row); + + settings = Settings.get_instance(); + settings.settings_ready.connect(load_settings); + if (settings.is_connected) { + message("settings is connected"); + load_settings(); + } + } + + private void load_settings() { + try { + username_row.text = settings.get_username(); + server_url_row.text = settings.get_server_url(); + password_row.text = settings.get_password(); + } catch (Error e) { + warning("Failed to load settings: %s", e.message); + } + + setup_change_callbacks(); + } + + private void setup_change_callbacks() { + server_url_row.changed.connect(() => { + settings.set_server_url(server_url_row.text); + }); + + username_row.changed.connect(() => { + settings.set_username(username_row.text); + }); + + password_row.changed.connect(() => { + settings.set_password(password_row.text); + }); + } +} \ No newline at end of file diff --git a/src/conversation-list/conversation-list-view.vala b/src/conversation-list/conversation-list-view.vala index 90a0450..cf2fd52 100644 --- a/src/conversation-list/conversation-list-view.vala +++ b/src/conversation-list/conversation-list-view.vala @@ -48,6 +48,16 @@ public class ConversationListView : Adw.Bin }); header_bar.pack_end (refresh_button); + // Setup application menu + var app_menu = new Menu (); + app_menu.append ("Refresh", "refresh"); + app_menu.append ("Settings...", "win.settings"); + app_menu.append ("Quit", "app.quit"); + + var menu_button = new Gtk.MenuButton (); + menu_button.menu_model = app_menu; + header_bar.pack_end (menu_button); + // Set up model and bind to list conversation_model = new ConversationListModel (); conversation_model.items_changed.connect (on_items_changed); diff --git a/src/conversation-list/conversation-row.vala b/src/conversation-list/conversation-row.vala index 5ddb121..5f75b80 100644 --- a/src/conversation-list/conversation-row.vala +++ b/src/conversation-list/conversation-row.vala @@ -7,8 +7,11 @@ public class ConversationRow : Adw.ActionRow { public ConversationRow(Conversation conversation) { this.conversation = conversation; - title = conversation.display_name; - subtitle = conversation.last_message_preview; + + title = conversation.display_name.strip(); + title_lines = 1; + + subtitle = conversation.last_message_preview.strip(); subtitle_lines = 1; add_css_class("conversation-row"); diff --git a/src/meson.build b/src/meson.build index f143b9d..73c3bdb 100644 --- a/src/meson.build +++ b/src/meson.build @@ -3,7 +3,8 @@ dependencies = [ dependency('libadwaita-1', required : true), dependency('gio-2.0', required : true), dependency('gee-0.8', required : true), - dependency('gio-unix-2.0', required : true) + dependency('gio-unix-2.0', required : true), + dependency('libsecret-1', required : true), ] gnome = import('gnome') @@ -16,9 +17,12 @@ resources = gnome.compile_resources( sources = [ 'application/kordophone-application.vala', 'application/main-window.vala', + 'application/preferences-window.vala', 'service/interface/dbusservice.vala', + 'service/dbus-service-base.vala', 'service/repository.vala', + 'service/settings.vala', 'conversation-list/conversation-list-view.vala', 'conversation-list/conversation-list-model.vala', diff --git a/src/models/conversation.vala b/src/models/conversation.vala index c7c68a0..563d84f 100644 --- a/src/models/conversation.vala +++ b/src/models/conversation.vala @@ -20,7 +20,7 @@ public class Conversation : Object { } if (participants.length > 1) { - return string.join(", ", participants); + return string.joinv(", ", participants); } return "Untitled"; diff --git a/src/resources/style.css b/src/resources/style.css index f3cadf4..bcad32c 100644 --- a/src/resources/style.css +++ b/src/resources/style.css @@ -28,4 +28,7 @@ .message-input-entry { font-size: 1.1rem; -} \ No newline at end of file +} + + + diff --git a/src/service/dbus-service-base.vala b/src/service/dbus-service-base.vala new file mode 100644 index 0000000..f0a2ffa --- /dev/null +++ b/src/service/dbus-service-base.vala @@ -0,0 +1,60 @@ +public abstract class DBusServiceBase : Object { + protected uint dbus_watch_id; + public bool is_connected { get; private set; default = false; } + + protected const string DBUS_PATH = "/net/buzzert/kordophonecd/daemon"; + protected const string DBUS_NAME = "net.buzzert.kordophonecd"; + + protected DBusServiceBase() { + connect_to_dbus.begin((obj, res) => { + connect_to_dbus.end(res); + }); + } + + ~DBusServiceBase() { + if (dbus_watch_id > 0) { + Bus.unwatch_name(dbus_watch_id); + } + } + + protected abstract void setup_signals(); + protected abstract async Object? get_proxy() throws Error; + protected abstract string get_service_name(); + + private async void connect_to_dbus() { + try { + debug("Trying to connect to %s service at path: %s", get_service_name(), DBUS_PATH); + + var proxy = yield get_proxy(); + if (proxy == null) { + throw new Error(1337, 1, "Failed to get proxy"); + } + + // If we get here, connection succeeded + debug("Connected to %s service at path: %s", get_service_name(), DBUS_PATH); + is_connected = true; + + setup_signals(); + + } catch (Error e) { + debug("Failed to connect to %s at %s: %s", get_service_name(), DBUS_PATH, e.message); + } + + if (!is_connected) { + warning("Failed to connect to %s on any known path", get_service_name()); + + // Watch for the service to appear + dbus_watch_id = Bus.watch_name(BusType.SESSION, + DBUS_NAME, + BusNameWatcherFlags.AUTO_START, + () => { + connect_to_dbus.begin(); + }, + null); + } + } + + protected Error create_not_connected_error() { + return new Error(1337, 1, @"$(get_service_name()) not connected"); + } +} \ No newline at end of file diff --git a/src/service/interface/dbusservice.vala b/src/service/interface/dbusservice.vala index ded23fd..df38113 100644 --- a/src/service/interface/dbusservice.vala +++ b/src/service/interface/dbusservice.vala @@ -13,9 +13,6 @@ namespace DBusService { [DBus (name = "Username")] public abstract string username { owned get; set; } - [DBus (name = "CredentialItem")] - public abstract GLib.ObjectPath credential_item { owned get; set; } - [DBus (name = "SetServer")] public abstract void set_server(string url, string user) throws DBusError, IOError; diff --git a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml index 01fb1c3..7623714 100644 --- a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml +++ b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml @@ -80,12 +80,6 @@ - - - - - diff --git a/src/service/repository.vala b/src/service/repository.vala index f828cf2..f0e5d69 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -1,8 +1,7 @@ using GLib; using Gee; -public class Repository : Object -{ +public class Repository : DBusServiceBase { public signal void conversations_updated(); public signal void messages_updated(string conversation_guid); @@ -15,24 +14,38 @@ public class Repository : Object } private static Repository instance = null; - private DBusService.Repository dbus_repository; - private uint dbus_watch_id; + private DBusService.Repository? dbus_repository; private Repository() { - connect_to_dbus.begin((obj, res) => { - connect_to_dbus.end(res); - }); + base(); } - ~Repository() { - if (dbus_watch_id > 0) { - Bus.unwatch_name(dbus_watch_id); - } + protected override string get_service_name() { + return "Repository"; + } + + protected override async Object? get_proxy() throws Error { + dbus_repository = yield Bus.get_proxy(BusType.SESSION, DBUS_NAME, DBUS_PATH); + dbus_repository.get_version(); // Test the connection + return dbus_repository; + } + + protected override void setup_signals() { + dbus_repository.conversations_updated.connect(() => { + conversations_updated(); + }); + + dbus_repository.messages_updated.connect((conversation_guid) => { + messages_updated(conversation_guid); + }); + + // Initial load + conversations_updated(); } public Conversation[] get_conversations() throws Error { - if (dbus_repository == null) { - throw new Error(1337, 1, "Repository not connected"); + if (!is_connected || dbus_repository == null) { + throw create_not_connected_error(); } var conversations = dbus_repository.get_conversations(); @@ -46,8 +59,8 @@ public class Repository : Object } public Message[] get_messages(string conversation_guid, string last_message_id = "") throws Error { - if (dbus_repository == null) { - throw new Error(1337, 1, "Repository not connected"); + if (!is_connected || dbus_repository == null) { + throw create_not_connected_error(); } var messages = dbus_repository.get_messages(conversation_guid, last_message_id); @@ -61,56 +74,10 @@ public class Repository : Object } public string send_message(string conversation_guid, string message) throws Error { - if (dbus_repository == null) { - throw new Error(1337, 1, "Repository not connected"); + if (!is_connected || dbus_repository == null) { + throw create_not_connected_error(); } return dbus_repository.send_message(conversation_guid, message); } - - private async void connect_to_dbus() { - bool connected = false; - const string path = "/net/buzzert/kordophonecd/daemon"; - - try { - debug("Trying to connect to DBus service at path: %s", path); - dbus_repository = yield Bus.get_proxy(BusType.SESSION, - "net.buzzert.kordophonecd", - path); - - // Test the connection - dbus_repository.get_version(); - - // If we get here, connection succeeded - debug("Connected to DBus service at path: %s", path); - connected = true; - - // Listen for updates - dbus_repository.conversations_updated.connect(() => { - conversations_updated(); - }); - - dbus_repository.messages_updated.connect((conversation_guid) => { - messages_updated(conversation_guid); - }); - - // Initial load - conversations_updated(); - } catch (Error e) { - debug("Failed to connect to kordophonecd at %s: %s", path, e.message); - } - - if (!connected) { - warning("Failed to connect to kordophonecd on any known path"); - - // Watch for the service to appear - dbus_watch_id = Bus.watch_name(BusType.SESSION, - "net.buzzert.kordophonecd", - BusNameWatcherFlags.AUTO_START, - () => { - connect_to_dbus.begin(); - }, - null); - } - } } diff --git a/src/service/settings.vala b/src/service/settings.vala new file mode 100644 index 0000000..07adfd0 --- /dev/null +++ b/src/service/settings.vala @@ -0,0 +1,115 @@ +using GLib; + +public class Settings : DBusServiceBase { + public signal void config_changed(); + public signal void settings_ready(); + + public static Settings get_instance() { + if (instance == null) { + instance = new Settings(); + } + return instance; + } + + private static Settings instance = null; + private DBusService.Settings? dbus_settings; + private Secret.Service secret_service; + + private Settings() { + base(); + + try { + secret_service = Secret.Service.get_sync(Secret.ServiceFlags.OPEN_SESSION); + } catch (Error e) { + warning("Failed to get secret service: %s", e.message); + } + } + + protected override string get_service_name() { + return "Settings"; + } + + protected override async Object? get_proxy() throws Error { + dbus_settings = yield Bus.get_proxy(BusType.SESSION, DBUS_NAME, DBUS_PATH); + return dbus_settings; + } + + protected override void setup_signals() { + dbus_settings.config_changed.connect(() => { + config_changed(); + }); + + settings_ready(); + } + + public string get_server_url() throws Error { + if (!is_connected || dbus_settings == null) { + throw create_not_connected_error(); + } + return dbus_settings.server_u_r_l; + } + + public void set_server_url(string url) throws Error { + if (!is_connected || dbus_settings == null) { + throw create_not_connected_error(); + } + dbus_settings.server_u_r_l = url; + } + + public string get_username() throws Error { + if (!is_connected || dbus_settings == null) { + throw create_not_connected_error(); + } + return dbus_settings.username; + } + + public void set_username(string username) throws Error { + if (!is_connected || dbus_settings == null) { + throw create_not_connected_error(); + } + dbus_settings.username = username; + } + + public void set_server(string url, string username) throws Error { + if (!is_connected || dbus_settings == null) { + throw create_not_connected_error(); + } + dbus_settings.set_server(url, username); + } + + private HashTable password_attributes() { + var attributes = new HashTable(str_hash, str_equal); + attributes["service"] = "net.buzzert.kordophonecd"; + attributes["target"] = "default"; + attributes["username"] = get_username(); + return attributes; + } + + public string get_password() throws Error { + var attributes = password_attributes(); + var password = secret_service.lookup_sync(null, attributes, null); + if (password == null) { + warning("No password found for user %s", get_username()); + return ""; + } + + return password.get_text(); + } + + public void set_password(string password) throws Error { + var attributes = password_attributes(); + bool result = secret_service.store_sync( + null, + attributes, + Secret.COLLECTION_DEFAULT, + "Kordophone Keystore", + new Secret.Value(password, password.length, "text/plain"), + null + ); + + if (!result) { + warning("Failed to store password for user %s", get_username()); + throw new Error(1337, 1, "Failed to store password"); + } + } +} \ No newline at end of file From e44120712fb6ec364404350a34223e1c77ce0832 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 3 May 2025 18:19:17 -0700 Subject: [PATCH 14/63] fixes for very large conversation lists --- src/application/kordophone-application.vala | 7 ++ src/application/main-window.vala | 1 + src/application/preferences-window.vala | 5 +- .../conversation-list-view.vala | 21 +++--- src/conversation-list/conversation-row.vala | 9 ++- src/service/dbus-service-base.vala | 66 +++-------------- src/service/interface/dbusservice.vala | 2 +- .../xml/net.buzzert.kordophonecd.Server.xml | 3 + src/service/repository.vala | 73 ++++++++++--------- src/service/settings.vala | 59 ++++++--------- 10 files changed, 103 insertions(+), 143 deletions(-) diff --git a/src/application/kordophone-application.vala b/src/application/kordophone-application.vala index 758e4dd..3260af4 100644 --- a/src/application/kordophone-application.vala +++ b/src/application/kordophone-application.vala @@ -21,6 +21,13 @@ public class KordophoneApp : Adw.Application Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ); + // Setup application actions + var quit_action = new SimpleAction("quit", null); + quit_action.activate.connect(() => { + this.quit(); + }); + add_action(quit_action); + // Warm up dbus connections Settings.get_instance(); Repository.get_instance(); diff --git a/src/application/main-window.vala b/src/application/main-window.vala index ccb44fe..6ce22cc 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -40,6 +40,7 @@ public class MainWindow : Adw.ApplicationWindow transcript_view.message_list.model = null; } else { transcript_view.message_list.model = new MessageListModel (conversation_guid); + Repository.get_instance().sync_conversation(conversation_guid); } } diff --git a/src/application/preferences-window.vala b/src/application/preferences-window.vala index d5f2681..76969fa 100644 --- a/src/application/preferences-window.vala +++ b/src/application/preferences-window.vala @@ -39,10 +39,7 @@ public class PreferencesWindow : Adw.PreferencesDialog { settings = Settings.get_instance(); settings.settings_ready.connect(load_settings); - if (settings.is_connected) { - message("settings is connected"); - load_settings(); - } + load_settings(); } private void load_settings() { diff --git a/src/conversation-list/conversation-list-view.vala b/src/conversation-list/conversation-list-view.vala index cf2fd52..11bffe7 100644 --- a/src/conversation-list/conversation-list-view.vala +++ b/src/conversation-list/conversation-list-view.vala @@ -38,21 +38,22 @@ public class ConversationListView : Adw.Bin header_bar.set_title_widget (new Label ("Kordophone")); container.add_top_bar (header_bar); - // Set up refresh button - var refresh_button = new Button.from_icon_name ("view-refresh-symbolic"); - refresh_button.tooltip_text = "Refresh Conversations"; - refresh_button.clicked.connect (() => { + // Setup application menu + var app_menu = new Menu (); + app_menu.append ("Refresh", "list.refresh"); + app_menu.append ("Settings...", "win.settings"); + app_menu.append ("Quit", "app.quit"); + + var refresh_action = new SimpleAction("refresh", null); + refresh_action.activate.connect (() => { if (conversation_model != null) { conversation_model.load_conversations (); } }); - header_bar.pack_end (refresh_button); - // Setup application menu - var app_menu = new Menu (); - app_menu.append ("Refresh", "refresh"); - app_menu.append ("Settings...", "win.settings"); - app_menu.append ("Quit", "app.quit"); + var action_group = new SimpleActionGroup (); + action_group.add_action(refresh_action); + insert_action_group ("list", action_group); var menu_button = new Gtk.MenuButton (); menu_button.menu_model = app_menu; diff --git a/src/conversation-list/conversation-row.vala b/src/conversation-list/conversation-row.vala index 5f75b80..d809040 100644 --- a/src/conversation-list/conversation-row.vala +++ b/src/conversation-list/conversation-row.vala @@ -11,7 +11,14 @@ public class ConversationRow : Adw.ActionRow { title = conversation.display_name.strip(); title_lines = 1; - subtitle = conversation.last_message_preview.strip(); + var preview = conversation.last_message_preview + .strip() + .replace("\n", " ") + .replace("<", "\\<") + .replace(">", "\\>") + .replace("&", "&"); + + subtitle = preview.length > 100 ? preview.substring(0, 100) : preview; subtitle_lines = 1; add_css_class("conversation-row"); diff --git a/src/service/dbus-service-base.vala b/src/service/dbus-service-base.vala index f0a2ffa..cf8cd8a 100644 --- a/src/service/dbus-service-base.vala +++ b/src/service/dbus-service-base.vala @@ -1,60 +1,12 @@ -public abstract class DBusServiceBase : Object { - protected uint dbus_watch_id; - public bool is_connected { get; private set; default = false; } - +public abstract class DBusServiceProxy : Object { protected const string DBUS_PATH = "/net/buzzert/kordophonecd/daemon"; protected const string DBUS_NAME = "net.buzzert.kordophonecd"; - - protected DBusServiceBase() { - connect_to_dbus.begin((obj, res) => { - connect_to_dbus.end(res); - }); + + protected DBusServiceProxy() { } - - ~DBusServiceBase() { - if (dbus_watch_id > 0) { - Bus.unwatch_name(dbus_watch_id); - } - } - - protected abstract void setup_signals(); - protected abstract async Object? get_proxy() throws Error; - protected abstract string get_service_name(); - - private async void connect_to_dbus() { - try { - debug("Trying to connect to %s service at path: %s", get_service_name(), DBUS_PATH); - - var proxy = yield get_proxy(); - if (proxy == null) { - throw new Error(1337, 1, "Failed to get proxy"); - } - - // If we get here, connection succeeded - debug("Connected to %s service at path: %s", get_service_name(), DBUS_PATH); - is_connected = true; - - setup_signals(); - - } catch (Error e) { - debug("Failed to connect to %s at %s: %s", get_service_name(), DBUS_PATH, e.message); - } - - if (!is_connected) { - warning("Failed to connect to %s on any known path", get_service_name()); - - // Watch for the service to appear - dbus_watch_id = Bus.watch_name(BusType.SESSION, - DBUS_NAME, - BusNameWatcherFlags.AUTO_START, - () => { - connect_to_dbus.begin(); - }, - null); - } - } - - protected Error create_not_connected_error() { - return new Error(1337, 1, @"$(get_service_name()) not connected"); - } -} \ No newline at end of file +} + +protected errordomain DBusServiceProxyError { + NOT_CONNECTED, + PASSWORD_STORAGE; +} \ No newline at end of file diff --git a/src/service/interface/dbusservice.vala b/src/service/interface/dbusservice.vala index df38113..16aceb1 100644 --- a/src/service/interface/dbusservice.vala +++ b/src/service/interface/dbusservice.vala @@ -27,7 +27,7 @@ namespace DBusService { public abstract string get_version() throws DBusError, IOError; [DBus (name = "GetConversations")] - public abstract GLib.HashTable[] get_conversations() throws DBusError, IOError; + public abstract GLib.HashTable[] get_conversations(int limit, int offset) throws DBusError, IOError; [DBus (name = "SyncConversationList")] public abstract void sync_conversation_list() throws DBusError, IOError; diff --git a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml index 7623714..8198155 100644 --- a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml +++ b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml @@ -11,6 +11,9 @@ + + + { + + private void connect_to_repository() { + try { + this.dbus_repository = Bus.get_proxy_sync(BusType.SESSION, DBUS_NAME, DBUS_PATH); + this.dbus_repository.conversations_updated.connect(() => { + conversations_updated(); + }); + + this.dbus_repository.messages_updated.connect((conversation_guid) => { + messages_updated(conversation_guid); + }); + conversations_updated(); - }); - - dbus_repository.messages_updated.connect((conversation_guid) => { - messages_updated(conversation_guid); - }); - - // Initial load - conversations_updated(); + } catch (GLib.Error e) { + warning("Failed to connect to repository: %s", e.message); + } } - public Conversation[] get_conversations() throws Error { - if (!is_connected || dbus_repository == null) { - throw create_not_connected_error(); + public Conversation[] get_conversations(int limit = 200) throws DBusServiceProxyError, GLib.Error { + if (dbus_repository == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); } - - var conversations = dbus_repository.get_conversations(); + + var conversations = dbus_repository.get_conversations(limit, 0); Conversation[] returned_conversations = new Conversation[conversations.length]; for (int i = 0; i < conversations.length; i++) { @@ -58,9 +55,9 @@ public class Repository : DBusServiceBase { return returned_conversations; } - public Message[] get_messages(string conversation_guid, string last_message_id = "") throws Error { - if (!is_connected || dbus_repository == null) { - throw create_not_connected_error(); + public Message[] get_messages(string conversation_guid, string last_message_id = "") throws DBusServiceProxyError, GLib.Error { + if (dbus_repository == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); } var messages = dbus_repository.get_messages(conversation_guid, last_message_id); @@ -73,11 +70,19 @@ public class Repository : DBusServiceBase { return returned_messages; } - public string send_message(string conversation_guid, string message) throws Error { - if (!is_connected || dbus_repository == null) { - throw create_not_connected_error(); + public string send_message(string conversation_guid, string message) throws DBusServiceProxyError, GLib.Error { + if (dbus_repository == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); } return dbus_repository.send_message(conversation_guid, message); } + + public void sync_conversation(string conversation_guid) throws DBusServiceProxyError, GLib.Error { + if (dbus_repository == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); + } + + dbus_repository.sync_conversation(conversation_guid); + } } diff --git a/src/service/settings.vala b/src/service/settings.vala index 07adfd0..a8d5674 100644 --- a/src/service/settings.vala +++ b/src/service/settings.vala @@ -1,6 +1,7 @@ using GLib; -public class Settings : DBusServiceBase { +public class Settings : DBusServiceProxy +{ public signal void config_changed(); public signal void settings_ready(); @@ -20,59 +21,45 @@ public class Settings : DBusServiceBase { try { secret_service = Secret.Service.get_sync(Secret.ServiceFlags.OPEN_SESSION); - } catch (Error e) { + + this.dbus_settings = Bus.get_proxy_sync(BusType.SESSION, DBUS_NAME, DBUS_PATH); + settings_ready(); + } catch (GLib.Error e) { warning("Failed to get secret service: %s", e.message); } } - protected override string get_service_name() { - return "Settings"; - } - - protected override async Object? get_proxy() throws Error { - dbus_settings = yield Bus.get_proxy(BusType.SESSION, DBUS_NAME, DBUS_PATH); - return dbus_settings; - } - - protected override void setup_signals() { - dbus_settings.config_changed.connect(() => { - config_changed(); - }); - - settings_ready(); - } - - public string get_server_url() throws Error { - if (!is_connected || dbus_settings == null) { - throw create_not_connected_error(); + public string get_server_url() throws DBusServiceProxyError, GLib.Error { + if (dbus_settings == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Settings not connected"); } return dbus_settings.server_u_r_l; } - public void set_server_url(string url) throws Error { - if (!is_connected || dbus_settings == null) { - throw create_not_connected_error(); + public void set_server_url(string url) throws Error, GLib.Error { + if (dbus_settings == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Settings not connected"); } dbus_settings.server_u_r_l = url; } - public string get_username() throws Error { - if (!is_connected || dbus_settings == null) { - throw create_not_connected_error(); + public string get_username() throws Error, GLib.Error { + if (dbus_settings == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Settings not connected"); } return dbus_settings.username; } - public void set_username(string username) throws Error { - if (!is_connected || dbus_settings == null) { - throw create_not_connected_error(); + public void set_username(string username) throws Error, GLib.Error { + if (dbus_settings == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Settings not connected"); } dbus_settings.username = username; } - public void set_server(string url, string username) throws Error { - if (!is_connected || dbus_settings == null) { - throw create_not_connected_error(); + public void set_server(string url, string username) throws Error, GLib.Error { + if (dbus_settings == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Settings not connected"); } dbus_settings.set_server(url, username); } @@ -96,7 +83,7 @@ public class Settings : DBusServiceBase { return password.get_text(); } - public void set_password(string password) throws Error { + public void set_password(string password) throws Error, GLib.Error { var attributes = password_attributes(); bool result = secret_service.store_sync( null, @@ -109,7 +96,7 @@ public class Settings : DBusServiceBase { if (!result) { warning("Failed to store password for user %s", get_username()); - throw new Error(1337, 1, "Failed to store password"); + throw new DBusServiceProxyError.PASSWORD_STORAGE("Failed to store password"); } } } \ No newline at end of file From 518608a04e9304af5df55e92891ccc1897b7b40a Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 3 May 2025 21:45:17 -0700 Subject: [PATCH 15/63] attempt to resolve chatter problems --- src/application/main-window.vala | 6 ++++-- src/message-list/message-layout.vala | 17 +++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/application/main-window.vala b/src/application/main-window.vala index 6ce22cc..9d8d7a7 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -39,8 +39,10 @@ public class MainWindow : Adw.ApplicationWindow if (conversation_guid == null) { transcript_view.message_list.model = null; } else { - transcript_view.message_list.model = new MessageListModel (conversation_guid); - Repository.get_instance().sync_conversation(conversation_guid); + if (transcript_view.message_list.model == null || transcript_view.message_list.model.conversation_guid != conversation_guid) { + transcript_view.message_list.model = new MessageListModel (conversation_guid); + Repository.get_instance().sync_conversation(conversation_guid); + } } } diff --git a/src/message-list/message-layout.vala b/src/message-list/message-layout.vala index 85e57fd..266534b 100644 --- a/src/message-list/message-layout.vala +++ b/src/message-list/message-layout.vala @@ -131,8 +131,6 @@ private class MessageLayout : Object private void draw_background(Snapshot snapshot) { - snapshot.save(); - var width = get_width(); var height = get_height(); var color = background_color; @@ -152,17 +150,20 @@ private class MessageLayout : Object }; snapshot.append_linear_gradient(bounds, start_point, end_point, stops); - - snapshot.restore(); } private void with_bubble_clip(Snapshot snapshot, Func func) { var width = get_width(); var height = get_height(); - var path = create_bubble_path(width, height, from_me); - snapshot.push_fill(path, Gsk.FillRule.WINDING); - func(snapshot); - snapshot.pop(); + + if (width > 10 && height > 10) { + var path = create_bubble_path(width, height, from_me); + snapshot.push_fill(path, Gsk.FillRule.WINDING); + func(snapshot); + snapshot.pop(); + } else { + func(snapshot); + } } private Gsk.Path create_bubble_path(float width, float height, bool tail_on_right = false) From 21c926456d3832c510a454d975551b4f30bcfcac Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 3 May 2025 22:12:26 -0700 Subject: [PATCH 16/63] reorg: message layout becomes interface for other types of chat items (like date) --- src/meson.build | 5 +- .../bubble-layout.vala} | 101 +++--------------- .../layouts/chat-item-layout.vala | 10 ++ .../layouts/text-bubble-layout.vala | 96 +++++++++++++++++ src/message-list/message-drawing-area.vala | 33 +++--- 5 files changed, 142 insertions(+), 103 deletions(-) rename src/message-list/{message-layout.vala => layouts/bubble-layout.vala} (69%) create mode 100644 src/message-list/layouts/chat-item-layout.vala create mode 100644 src/message-list/layouts/text-bubble-layout.vala diff --git a/src/meson.build b/src/meson.build index 73c3bdb..882c1e5 100644 --- a/src/meson.build +++ b/src/meson.build @@ -31,7 +31,10 @@ sources = [ 'message-list/message-list-view.vala', 'message-list/message-list-model.vala', 'message-list/message-drawing-area.vala', - 'message-list/message-layout.vala', + + 'message-list/layouts/bubble-layout.vala', + 'message-list/layouts/chat-item-layout.vala', + 'message-list/layouts/text-bubble-layout.vala', 'models/conversation.vala', 'models/message.vala', diff --git a/src/message-list/message-layout.vala b/src/message-list/layouts/bubble-layout.vala similarity index 69% rename from src/message-list/message-layout.vala rename to src/message-list/layouts/bubble-layout.vala index 266534b..e4d123d 100644 --- a/src/message-list/message-layout.vala +++ b/src/message-list/layouts/bubble-layout.vala @@ -1,7 +1,6 @@ using Gtk; -using Gee; -private struct MessageLayoutConstants { +private struct BubbleLayoutConstants { public float tail_width; public float tail_curve_offset; public float tail_side_offset; @@ -9,7 +8,7 @@ private struct MessageLayoutConstants { public float corner_radius; public float text_padding; - public MessageLayoutConstants(float scale_factor) { + public BubbleLayoutConstants(float scale_factor) { tail_width = 15.0f / scale_factor; tail_curve_offset = 2.5f / scale_factor; tail_side_offset = 0.0f / scale_factor; @@ -19,61 +18,18 @@ private struct MessageLayoutConstants { } } -private class MessageLayout : Object +private abstract class BubbleLayout : Object, ChatItemLayout { - public Message message; + public bool from_me { get; set; } - private float max_width; - private Pango.Layout layout; - private Widget parent; - private MessageLayoutConstants constants; + protected float max_width; + protected Widget parent; + protected BubbleLayoutConstants constants; - public MessageLayout(Message message, Widget parent, float max_width) { - this.message = message; + protected BubbleLayout(Widget parent, float max_width) { this.max_width = max_width; this.parent = parent; - this.constants = MessageLayoutConstants(parent.get_scale_factor()); - - layout = parent.create_pango_layout(message.text); - - // Get the system font settings - var settings = Gtk.Settings.get_default(); - var font_name = settings.gtk_font_name; - - // Create font description from system font - var font_desc = Pango.FontDescription.from_string(font_name); - - var size = font_desc.get_size(); - font_desc.set_size((int)(size)); - layout.set_font_description(font_desc); - - // Set max width - layout.set_width((int)text_available_width * Pango.SCALE); - } - - public bool from_me { - get { - return message.from_me; - } - } - - private float text_available_width { - get { - return max_width - text_x_offset - constants.text_padding; - } - } - - private float text_x_offset { - get { - return from_me ? constants.text_padding : constants.tail_width + constants.text_padding; - } - } - - private float text_x_padding { - get { - // Opposite of text_x_offset - return from_me ? constants.tail_width + constants.text_padding : constants.text_padding; - } + this.constants = BubbleLayoutConstants(parent.get_scale_factor()); } private Gdk.RGBA background_color { @@ -87,47 +43,18 @@ private class MessageLayout : Object } } - public float get_height() { - Pango.Rectangle ink_rect, logical_rect; - layout.get_pixel_extents(out ink_rect, out logical_rect); + public abstract float get_height(); - return logical_rect.height + constants.corner_radius + constants.tail_bottom_padding; - } - - public float get_width() { - Pango.Rectangle ink_rect, logical_rect; - layout.get_pixel_extents(out ink_rect, out logical_rect); - - return logical_rect.width + text_x_offset + text_x_padding; - } + public abstract float get_width(); public void draw(Snapshot snapshot) { with_bubble_clip(snapshot, (snapshot) => { draw_background(snapshot); - draw_text(snapshot); + draw_content(snapshot); }); } - private void draw_text(Snapshot snapshot) - { - snapshot.save(); - - Pango.Rectangle ink_rect, logical_rect; - layout.get_pixel_extents(out ink_rect, out logical_rect); - snapshot.translate(Graphene.Point() { - x = text_x_offset, - y = ((get_height() - constants.tail_bottom_padding) - logical_rect.height) / 2 - }); - - snapshot.append_layout(layout, Gdk.RGBA() { - red = 1.0f, - green = 1.0f, - blue = 1.0f, - alpha = 1.0f - }); - - snapshot.restore(); - } + public abstract void draw_content(Snapshot snapshot); private void draw_background(Snapshot snapshot) { @@ -152,7 +79,7 @@ private class MessageLayout : Object snapshot.append_linear_gradient(bounds, start_point, end_point, stops); } - private void with_bubble_clip(Snapshot snapshot, Func func) { + protected void with_bubble_clip(Snapshot snapshot, Func func) { var width = get_width(); var height = get_height(); diff --git a/src/message-list/layouts/chat-item-layout.vala b/src/message-list/layouts/chat-item-layout.vala new file mode 100644 index 0000000..4a3c18e --- /dev/null +++ b/src/message-list/layouts/chat-item-layout.vala @@ -0,0 +1,10 @@ +using Gtk; + +interface ChatItemLayout : Object +{ + public abstract bool from_me { get; set; } + + public abstract float get_height(); + public abstract float get_width(); + public abstract void draw(Snapshot snapshot); +} diff --git a/src/message-list/layouts/text-bubble-layout.vala b/src/message-list/layouts/text-bubble-layout.vala new file mode 100644 index 0000000..30de42c --- /dev/null +++ b/src/message-list/layouts/text-bubble-layout.vala @@ -0,0 +1,96 @@ +using Gtk; + +private class TextBubbleLayout : BubbleLayout +{ + public Message message; + private Pango.Layout layout; + + public TextBubbleLayout(Message message, Widget parent, float max_width) { + base(parent, max_width); + + this.from_me = message.from_me; + this.message = message; + + layout = parent.create_pango_layout(message.text); + + // Get the system font settings + var settings = Gtk.Settings.get_default(); + var font_name = settings.gtk_font_name; + + // Create font description from system font + var font_desc = Pango.FontDescription.from_string(font_name); + + var size = font_desc.get_size(); + font_desc.set_size((int)(size)); + layout.set_font_description(font_desc); + + // Set max width + layout.set_width((int)text_available_width * Pango.SCALE); + } + + private float text_available_width { + get { + return max_width - text_x_offset - constants.text_padding; + } + } + + private float text_x_offset { + get { + return from_me ? constants.text_padding : constants.tail_width + constants.text_padding; + } + } + + private float text_x_padding { + get { + // Opposite of text_x_offset + return from_me ? constants.tail_width + constants.text_padding : constants.text_padding; + } + } + + private Gdk.RGBA background_color { + get { + return from_me ? parent.get_color() : Gdk.RGBA() { + red = 1.0f, + green = 1.0f, + blue = 1.0f, + alpha = 0.08f + }; + } + } + + public override float get_height() { + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + + return logical_rect.height + constants.corner_radius + constants.tail_bottom_padding; + } + + public override float get_width() { + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + + return logical_rect.width + text_x_offset + text_x_padding; + } + + public override void draw_content(Snapshot snapshot) { + snapshot.save(); + + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + snapshot.translate(Graphene.Point() { + x = text_x_offset, + y = ((get_height() - constants.tail_bottom_padding) - logical_rect.height) / 2 + }); + + snapshot.append_layout(layout, Gdk.RGBA() { + red = 1.0f, + green = 1.0f, + blue = 1.0f, + alpha = 1.0f + }); + + snapshot.restore(); + } +} + + diff --git a/src/message-list/message-drawing-area.vala b/src/message-list/message-drawing-area.vala index 8791059..7d0fce4 100644 --- a/src/message-list/message-drawing-area.vala +++ b/src/message-list/message-drawing-area.vala @@ -4,7 +4,7 @@ using Gee; private class MessageDrawingArea : Widget { private SortedSet _messages = new TreeSet(); - private ArrayList _message_layouts = new ArrayList(); + private ArrayList _chat_items = new ArrayList(); private const float bubble_padding = 10.0f; private const float bubble_margin = 18.0f; @@ -31,8 +31,8 @@ private class MessageDrawingArea : Widget } else { // compute total message layout height float total_height = 0.0f; - _message_layouts.foreach((message_layout) => { - total_height += message_layout.get_height() + bubble_padding; + _chat_items.foreach((chat_item) => { + total_height += chat_item.get_height() + bubble_padding; return true; }); @@ -52,9 +52,9 @@ private class MessageDrawingArea : Widget public override void snapshot(Snapshot snapshot) { var container_width = get_width(); float y_offset = 0; - _message_layouts.foreach((message_layout) => { - var message_width = message_layout.get_width(); - var message_height = message_layout.get_height(); + _chat_items.foreach((chat_item) => { + var message_width = chat_item.get_width(); + var message_height = chat_item.get_height(); snapshot.save(); @@ -63,14 +63,14 @@ private class MessageDrawingArea : Widget // Translate to the correct position snapshot.translate(Graphene.Point() { - x = (message_layout.from_me ? (container_width - message_width - bubble_margin) : bubble_margin), + x = (chat_item.from_me ? (container_width - message_width - bubble_margin) : bubble_margin), y = y_offset }); // Undo the y-axis flip, origin is top left snapshot.translate(Graphene.Point() { x = 0, y = -message_height }); - message_layout.draw(snapshot); + chat_item.draw(snapshot); snapshot.restore(); y_offset -= message_height + bubble_padding; @@ -83,14 +83,17 @@ private class MessageDrawingArea : Widget var container_width = get_width(); float max_width = container_width * 0.90f; - _message_layouts.clear(); - _messages - .order_by((a, b) => (int)b.date - (int)a.date) // reverse order - .foreach((message) => { - _message_layouts.add(new MessageLayout(message, this, max_width)); - return true; - }); + _chat_items.clear(); + var sorted_messages = _messages + .order_by((a, b) => (int)b.date - (int)a.date); // reverse order + + + sorted_messages.foreach((message) => { + _chat_items.add(new TextBubbleLayout(message, this, max_width)); + return true; + }); + queue_draw(); queue_resize(); } From d3dfffd65256941ad5aeb1e9d065ec6600a93944 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 3 May 2025 22:41:51 -0700 Subject: [PATCH 17/63] show dates in transcript --- src/meson.build | 1 + .../layouts/date-item-layout.vala | 48 +++++++++++++++++++ src/message-list/message-drawing-area.vala | 30 ++++++++---- src/message-list/message-list-model.vala | 2 +- src/models/message.vala | 6 +-- 5 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 src/message-list/layouts/date-item-layout.vala diff --git a/src/meson.build b/src/meson.build index 882c1e5..8a7a252 100644 --- a/src/meson.build +++ b/src/meson.build @@ -34,6 +34,7 @@ sources = [ 'message-list/layouts/bubble-layout.vala', 'message-list/layouts/chat-item-layout.vala', + 'message-list/layouts/date-item-layout.vala', 'message-list/layouts/text-bubble-layout.vala', 'models/conversation.vala', diff --git a/src/message-list/layouts/date-item-layout.vala b/src/message-list/layouts/date-item-layout.vala new file mode 100644 index 0000000..a87a714 --- /dev/null +++ b/src/message-list/layouts/date-item-layout.vala @@ -0,0 +1,48 @@ +using Gtk; + +class DateItemLayout : Object, ChatItemLayout { + public bool from_me { get; set; } + + private Pango.Layout layout; + private float max_width; + + public DateItemLayout(string date_str, Widget parent, float max_width) { + this.max_width = max_width; + + layout = parent.create_pango_layout(date_str); + layout.set_font_description(Pango.FontDescription.from_string("Sans 9")); + layout.set_alignment(Pango.Alignment.CENTER); + } + + public float get_height() { + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + + return logical_rect.height + 50.0f; + } + + public float get_width() { + return max_width; + } + + public void draw(Snapshot snapshot) { + snapshot.save(); + + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + + snapshot.translate(Graphene.Point() { + x = (max_width - logical_rect.width) / 2, + y = (get_height() - logical_rect.height) / 2 + }); + + snapshot.append_layout(layout, Gdk.RGBA() { + red = 1.0f, + green = 1.0f, + blue = 1.0f, + alpha = 0.5f + }); + + snapshot.restore(); + } +} \ No newline at end of file diff --git a/src/message-list/message-drawing-area.vala b/src/message-list/message-drawing-area.vala index 7d0fce4..43490dd 100644 --- a/src/message-list/message-drawing-area.vala +++ b/src/message-list/message-drawing-area.vala @@ -53,8 +53,8 @@ private class MessageDrawingArea : Widget var container_width = get_width(); float y_offset = 0; _chat_items.foreach((chat_item) => { - var message_width = chat_item.get_width(); - var message_height = chat_item.get_height(); + var item_width = chat_item.get_width(); + var item_height = chat_item.get_height(); snapshot.save(); @@ -63,17 +63,17 @@ private class MessageDrawingArea : Widget // Translate to the correct position snapshot.translate(Graphene.Point() { - x = (chat_item.from_me ? (container_width - message_width - bubble_margin) : bubble_margin), + x = (chat_item.from_me ? (container_width - item_width - bubble_margin) : bubble_margin), y = y_offset }); // Undo the y-axis flip, origin is top left - snapshot.translate(Graphene.Point() { x = 0, y = -message_height }); + snapshot.translate(Graphene.Point() { x = 0, y = -item_height }); chat_item.draw(snapshot); snapshot.restore(); - y_offset -= message_height + bubble_padding; + y_offset -= item_height + bubble_padding; return true; }); @@ -85,12 +85,24 @@ private class MessageDrawingArea : Widget _chat_items.clear(); - var sorted_messages = _messages - .order_by((a, b) => (int)b.date - (int)a.date); // reverse order + var reversed_messages = _messages + .order_by((a, b) => b.date.compare(a.date)); // reverse order + + DateTime? last_date = null; + reversed_messages.foreach((message) => { + // Remember everything in here is backwards. + if (last_date == null) { + last_date = message.date; + } else if (last_date.difference(message.date) > (TimeSpan.MINUTE * 25)) { + var date_item = new DateItemLayout(last_date.to_local().format("%b %d, %Y at %H:%M"), this, max_width); + _chat_items.add(date_item); + last_date = message.date; + } + + var text_bubble = new TextBubbleLayout(message, this, max_width); + _chat_items.add(text_bubble); - sorted_messages.foreach((message) => { - _chat_items.add(new TextBubbleLayout(message, this, max_width)); return true; }); diff --git a/src/message-list/message-list-model.vala b/src/message-list/message-list-model.vala index 0f8b05a..b1d362f 100644 --- a/src/message-list/message-list-model.vala +++ b/src/message-list/message-list-model.vala @@ -15,7 +15,7 @@ public class MessageListModel : Object, ListModel public MessageListModel(string conversation_guid) { _messages = new TreeSet((a, b) => { // Sort by date in descending order (newest first) - return (int)(b.date - a.date); + return a.date.compare(b.date); }); Repository.get_instance().messages_updated.connect(got_messages_updated); diff --git a/src/models/message.vala b/src/models/message.vala index 8a1ff4b..e8ab948 100644 --- a/src/models/message.vala +++ b/src/models/message.vala @@ -4,7 +4,7 @@ public class Message : Object { public string guid { get; set; default = ""; } public string text { get; set; default = ""; } - public int64 date { get; set; default = 0; } + public DateTime date { get; set; default = new DateTime.now_local(); } public string sender { get; set; default = null; } public bool from_me { @@ -14,7 +14,7 @@ public class Message : Object } } - public Message(string text, int64 date, string? sender) { + public Message(string text, DateTime date, string? sender) { this.text = text; this.date = date; this.sender = sender; @@ -23,7 +23,7 @@ public class Message : Object public Message.from_hash_table(HashTable message_data) { guid = message_data["id"].get_string(); text = message_data["text"].get_string(); - date = message_data["date"].get_int64(); sender = message_data["sender"].get_string(); + date = new DateTime.from_unix_utc(message_data["date"].get_int64()); } } \ No newline at end of file From dd917463109bd1abace7f0e46de853ec5c155b31 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 3 May 2025 22:47:56 -0700 Subject: [PATCH 18/63] reorg: message-list -> transcript --- src/application/main-window.vala | 31 ++++++++++++------- src/meson.build | 18 ++++++----- src/resources/style.css | 2 +- .../layouts/bubble-layout.vala | 0 .../layouts/chat-item-layout.vala | 0 .../layouts/date-item-layout.vala | 0 .../layouts/text-bubble-layout.vala | 0 .../message-list-model.vala | 0 .../message-list-view.vala | 12 +++---- .../transcript-container-view.vala} | 12 +++---- .../transcript-drawing-area.vala} | 6 ++-- 11 files changed, 46 insertions(+), 35 deletions(-) rename src/{message-list => transcript}/layouts/bubble-layout.vala (100%) rename src/{message-list => transcript}/layouts/chat-item-layout.vala (100%) rename src/{message-list => transcript}/layouts/date-item-layout.vala (100%) rename src/{message-list => transcript}/layouts/text-bubble-layout.vala (100%) rename src/{message-list => transcript}/message-list-model.vala (100%) rename src/{message-list => transcript}/message-list-view.vala (70%) rename src/{transcript-view/transcript-view.vala => transcript/transcript-container-view.vala} (85%) rename src/{message-list/message-drawing-area.vala => transcript/transcript-drawing-area.vala} (96%) diff --git a/src/application/main-window.vala b/src/application/main-window.vala index 9d8d7a7..6a3d850 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -4,7 +4,7 @@ using Gtk; public class MainWindow : Adw.ApplicationWindow { private ConversationListView conversation_list_view; - private TranscriptView transcript_view; + private TranscriptContainerView transcript_container_view; public MainWindow () { Object (title: "Kordophone"); @@ -19,10 +19,10 @@ public class MainWindow : Adw.ApplicationWindow var conversation_list_page = new NavigationPage (conversation_list_view, "Conversations"); split_view.sidebar = conversation_list_page; - transcript_view = new TranscriptView (); - transcript_view.on_send.connect (on_transcript_send); + transcript_container_view = new TranscriptContainerView (); + transcript_container_view.on_send.connect (on_transcript_send); - var transcript_page = new NavigationPage (transcript_view, "Transcript"); + var transcript_page = new NavigationPage (transcript_container_view, "Transcript"); split_view.content = transcript_page; var show_settings_action = new SimpleAction ("settings", null); @@ -37,27 +37,36 @@ public class MainWindow : Adw.ApplicationWindow private void conversation_selected(string? conversation_guid) { if (conversation_guid == null) { - transcript_view.message_list.model = null; + transcript_container_view.transcript_view.model = null; } else { - if (transcript_view.message_list.model == null || transcript_view.message_list.model.conversation_guid != conversation_guid) { - transcript_view.message_list.model = new MessageListModel (conversation_guid); - Repository.get_instance().sync_conversation(conversation_guid); + if (transcript_container_view.transcript_view.model == null || transcript_container_view.transcript_view.model.conversation_guid != conversation_guid) { + transcript_container_view.transcript_view.model = new MessageListModel (conversation_guid); + + try { + Repository.get_instance().sync_conversation(conversation_guid); + } catch (Error e) { + GLib.warning("Failed to sync conversation: %s", e.message); + } } } } private void on_transcript_send(string message) { - if (transcript_view.message_list.model == null) { + if (transcript_container_view.transcript_view.model == null) { GLib.warning("No conversation selected"); return; } - var selected_conversation = transcript_view.message_list.model.conversation_guid; + var selected_conversation = transcript_container_view.transcript_view.model.conversation_guid; if (selected_conversation == null) { GLib.warning("No conversation selected"); return; } - Repository.get_instance().send_message(selected_conversation, message); + try { + Repository.get_instance().send_message(selected_conversation, message); + } catch (Error e) { + GLib.warning("Failed to send message: %s", e.message); + } } } \ No newline at end of file diff --git a/src/meson.build b/src/meson.build index 8a7a252..7ccb410 100644 --- a/src/meson.build +++ b/src/meson.build @@ -28,19 +28,21 @@ sources = [ 'conversation-list/conversation-list-model.vala', 'conversation-list/conversation-row.vala', - 'message-list/message-list-view.vala', - 'message-list/message-list-model.vala', - 'message-list/message-drawing-area.vala', + 'transcript/transcript-container-view.vala', + 'transcript/transcript-drawing-area.vala', - 'message-list/layouts/bubble-layout.vala', - 'message-list/layouts/chat-item-layout.vala', - 'message-list/layouts/date-item-layout.vala', - 'message-list/layouts/text-bubble-layout.vala', + 'transcript/message-list-view.vala', + 'transcript/message-list-model.vala', + + 'transcript/layouts/bubble-layout.vala', + 'transcript/layouts/chat-item-layout.vala', + 'transcript/layouts/date-item-layout.vala', + 'transcript/layouts/text-bubble-layout.vala', 'models/conversation.vala', 'models/message.vala', - 'transcript-view/transcript-view.vala', + ] executable('kordophone', diff --git a/src/resources/style.css b/src/resources/style.css index bcad32c..58b7ebb 100644 --- a/src/resources/style.css +++ b/src/resources/style.css @@ -15,7 +15,7 @@ transform: scale(1, -1); } -.message-drawing-area { +.transcript-drawing-area { color: darker(@accent_bg_color); } diff --git a/src/message-list/layouts/bubble-layout.vala b/src/transcript/layouts/bubble-layout.vala similarity index 100% rename from src/message-list/layouts/bubble-layout.vala rename to src/transcript/layouts/bubble-layout.vala diff --git a/src/message-list/layouts/chat-item-layout.vala b/src/transcript/layouts/chat-item-layout.vala similarity index 100% rename from src/message-list/layouts/chat-item-layout.vala rename to src/transcript/layouts/chat-item-layout.vala diff --git a/src/message-list/layouts/date-item-layout.vala b/src/transcript/layouts/date-item-layout.vala similarity index 100% rename from src/message-list/layouts/date-item-layout.vala rename to src/transcript/layouts/date-item-layout.vala diff --git a/src/message-list/layouts/text-bubble-layout.vala b/src/transcript/layouts/text-bubble-layout.vala similarity index 100% rename from src/message-list/layouts/text-bubble-layout.vala rename to src/transcript/layouts/text-bubble-layout.vala diff --git a/src/message-list/message-list-model.vala b/src/transcript/message-list-model.vala similarity index 100% rename from src/message-list/message-list-model.vala rename to src/transcript/message-list-model.vala diff --git a/src/message-list/message-list-view.vala b/src/transcript/message-list-view.vala similarity index 70% rename from src/message-list/message-list-view.vala rename to src/transcript/message-list-view.vala index 84f6e40..e91f430 100644 --- a/src/message-list/message-list-view.vala +++ b/src/transcript/message-list-view.vala @@ -2,7 +2,7 @@ using Adw; using Gtk; using Gee; -public class MessageListView : Adw.Bin +public class TranscriptView : Adw.Bin { public MessageListModel? model { get { @@ -15,7 +15,7 @@ public class MessageListView : Adw.Bin model.messages_changed.connect(reload_messages); model.load_messages(); } else { - message_drawing_area.set_messages(new TreeSet()); + transcript_drawing_area.set_messages(new TreeSet()); } } } @@ -23,16 +23,16 @@ public class MessageListView : Adw.Bin private MessageListModel? _model = null; private Adw.ToolbarView container; - private MessageDrawingArea message_drawing_area = new MessageDrawingArea(); + private TranscriptDrawingArea transcript_drawing_area = new TranscriptDrawingArea(); private ScrolledWindow scrolled_window = new ScrolledWindow(); - public MessageListView(MessageListModel? model = null) { + public TranscriptView(MessageListModel? model = null) { this.model = model; container = new Adw.ToolbarView(); set_child(container); - scrolled_window.set_child(message_drawing_area); + scrolled_window.set_child(transcript_drawing_area); scrolled_window.add_css_class("message-list-scroller"); container.set_content(scrolled_window); @@ -42,6 +42,6 @@ public class MessageListView : Adw.Bin } private void reload_messages() { - message_drawing_area.set_messages(_model.messages); + transcript_drawing_area.set_messages(_model.messages); } } diff --git a/src/transcript-view/transcript-view.vala b/src/transcript/transcript-container-view.vala similarity index 85% rename from src/transcript-view/transcript-view.vala rename to src/transcript/transcript-container-view.vala index 59c4266..9a17edb 100644 --- a/src/transcript-view/transcript-view.vala +++ b/src/transcript/transcript-container-view.vala @@ -1,22 +1,22 @@ using Gtk; using Adw; -class TranscriptView : Adw.Bin { - public MessageListView message_list; +class TranscriptContainerView : Adw.Bin { + public TranscriptView transcript_view; public Entry message_entry; public signal void on_send(string message); private Box container; private Button send_button; - public TranscriptView () { + public TranscriptContainerView () { container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); set_child (container); // Create message list view - message_list = new MessageListView(); - message_list.set_vexpand(true); - container.append(message_list); + transcript_view = new TranscriptView(); + transcript_view.set_vexpand(true); + container.append(transcript_view); // Create bottom box for input var input_box = new Box(Orientation.HORIZONTAL, 6); diff --git a/src/message-list/message-drawing-area.vala b/src/transcript/transcript-drawing-area.vala similarity index 96% rename from src/message-list/message-drawing-area.vala rename to src/transcript/transcript-drawing-area.vala index 43490dd..0a88969 100644 --- a/src/message-list/message-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -1,7 +1,7 @@ using Gtk; using Gee; -private class MessageDrawingArea : Widget +private class TranscriptDrawingArea : Widget { private SortedSet _messages = new TreeSet(); private ArrayList _chat_items = new ArrayList(); @@ -9,8 +9,8 @@ private class MessageDrawingArea : Widget private const float bubble_padding = 10.0f; private const float bubble_margin = 18.0f; - public MessageDrawingArea() { - add_css_class("message-drawing-area"); + public TranscriptDrawingArea() { + add_css_class("transcript-drawing-area"); } public void set_messages(SortedSet messages) { From 786d982ce0349a6882130a571d527af1f82a1baf Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 3 May 2025 23:19:15 -0700 Subject: [PATCH 19/63] Add sender annotations --- src/meson.build | 1 + src/transcript/layouts/bubble-layout.vala | 3 +- src/transcript/layouts/chat-item-layout.vala | 2 + src/transcript/layouts/date-item-layout.vala | 1 + .../layouts/sender-annotation-layout.vala | 56 ++++++++++++++++++ .../layouts/text-bubble-layout.vala | 1 + src/transcript/message-list-model.vala | 11 ++++ src/transcript/message-list-view.vala | 1 + src/transcript/transcript-drawing-area.vala | 57 +++++++++++-------- 9 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 src/transcript/layouts/sender-annotation-layout.vala diff --git a/src/meson.build b/src/meson.build index 7ccb410..20b668f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -37,6 +37,7 @@ sources = [ 'transcript/layouts/bubble-layout.vala', 'transcript/layouts/chat-item-layout.vala', 'transcript/layouts/date-item-layout.vala', + 'transcript/layouts/sender-annotation-layout.vala', 'transcript/layouts/text-bubble-layout.vala', 'models/conversation.vala', diff --git a/src/transcript/layouts/bubble-layout.vala b/src/transcript/layouts/bubble-layout.vala index e4d123d..a256c1a 100644 --- a/src/transcript/layouts/bubble-layout.vala +++ b/src/transcript/layouts/bubble-layout.vala @@ -21,7 +21,8 @@ private struct BubbleLayoutConstants { private abstract class BubbleLayout : Object, ChatItemLayout { public bool from_me { get; set; } - + public float vertical_padding { get; set; } + protected float max_width; protected Widget parent; protected BubbleLayoutConstants constants; diff --git a/src/transcript/layouts/chat-item-layout.vala b/src/transcript/layouts/chat-item-layout.vala index 4a3c18e..e1f5a06 100644 --- a/src/transcript/layouts/chat-item-layout.vala +++ b/src/transcript/layouts/chat-item-layout.vala @@ -3,8 +3,10 @@ using Gtk; interface ChatItemLayout : Object { public abstract bool from_me { get; set; } + public abstract float vertical_padding { get; set; } public abstract float get_height(); public abstract float get_width(); + public abstract void draw(Snapshot snapshot); } diff --git a/src/transcript/layouts/date-item-layout.vala b/src/transcript/layouts/date-item-layout.vala index a87a714..4e18ee2 100644 --- a/src/transcript/layouts/date-item-layout.vala +++ b/src/transcript/layouts/date-item-layout.vala @@ -2,6 +2,7 @@ using Gtk; class DateItemLayout : Object, ChatItemLayout { public bool from_me { get; set; } + public float vertical_padding { get; set; } private Pango.Layout layout; private float max_width; diff --git a/src/transcript/layouts/sender-annotation-layout.vala b/src/transcript/layouts/sender-annotation-layout.vala new file mode 100644 index 0000000..b078d48 --- /dev/null +++ b/src/transcript/layouts/sender-annotation-layout.vala @@ -0,0 +1,56 @@ +using Gtk; + +private class SenderAnnotationLayout : Object, ChatItemLayout +{ + public string sender; + public float max_width; + public bool from_me { get; set; default = false; } + public float vertical_padding { get; set; default = 0.0f; } + + private Pango.Layout layout; + private BubbleLayoutConstants constants; + private const float padding = 10.0f; + + public SenderAnnotationLayout(string sender, float max_width, Widget parent) { + this.sender = sender; + this.max_width = max_width; + this.constants = BubbleLayoutConstants(parent.get_scale_factor()); + + layout = parent.create_pango_layout(sender); + var font_desc = Pango.FontDescription.from_string("Sans 8"); + layout.set_font_description(font_desc); + } + + public float get_height() { + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + + return logical_rect.height + padding; + } + + public float get_width() { + return max_width; + } + + public void draw(Snapshot snapshot) { + snapshot.save(); + + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + + + snapshot.translate(Graphene.Point() { + x = constants.text_padding + constants.tail_width, + y = get_height() - logical_rect.height, + }); + + snapshot.append_layout(layout, Gdk.RGBA() { + red = 1.0f, + green = 1.0f, + blue = 1.0f, + alpha = 0.8f + }); + + snapshot.restore(); + } +} \ No newline at end of file diff --git a/src/transcript/layouts/text-bubble-layout.vala b/src/transcript/layouts/text-bubble-layout.vala index 30de42c..6d685f2 100644 --- a/src/transcript/layouts/text-bubble-layout.vala +++ b/src/transcript/layouts/text-bubble-layout.vala @@ -23,6 +23,7 @@ private class TextBubbleLayout : BubbleLayout var size = font_desc.get_size(); font_desc.set_size((int)(size)); layout.set_font_description(font_desc); + layout.set_wrap(Pango.WrapMode.WORD_CHAR); // Set max width layout.set_width((int)text_available_width * Pango.SCALE); diff --git a/src/transcript/message-list-model.vala b/src/transcript/message-list-model.vala index b1d362f..037e1f2 100644 --- a/src/transcript/message-list-model.vala +++ b/src/transcript/message-list-model.vala @@ -9,8 +9,16 @@ public class MessageListModel : Object, ListModel owned get { return _messages.read_only_view; } } + public bool is_group_chat { + get { + return participants.size > 2; + } + } + public string conversation_guid { get; private set; } + private SortedSet _messages; + private HashSet participants = new HashSet(); public MessageListModel(string conversation_guid) { _messages = new TreeSet((a, b) => { @@ -29,6 +37,7 @@ public class MessageListModel : Object, ListModel // Clear existing set uint old_count = _messages.size; _messages.clear(); + participants.clear(); // Notify of removal if (old_count > 0) { @@ -41,6 +50,8 @@ public class MessageListModel : Object, ListModel for (int i = 0; i < messages.length; i++) { var message = messages[i]; _messages.add(message); + participants.add(message.sender); + position++; } diff --git a/src/transcript/message-list-view.vala b/src/transcript/message-list-view.vala index e91f430..4b37cd2 100644 --- a/src/transcript/message-list-view.vala +++ b/src/transcript/message-list-view.vala @@ -42,6 +42,7 @@ public class TranscriptView : Adw.Bin } private void reload_messages() { + transcript_drawing_area.show_sender = _model.is_group_chat; transcript_drawing_area.set_messages(_model.messages); } } diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 0a88969..77add9b 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -3,10 +3,11 @@ using Gee; private class TranscriptDrawingArea : Widget { + public bool show_sender = true; + private SortedSet _messages = new TreeSet(); private ArrayList _chat_items = new ArrayList(); - private const float bubble_padding = 10.0f; private const float bubble_margin = 18.0f; public TranscriptDrawingArea() { @@ -32,7 +33,7 @@ private class TranscriptDrawingArea : Widget // compute total message layout height float total_height = 0.0f; _chat_items.foreach((chat_item) => { - total_height += chat_item.get_height() + bubble_padding; + total_height += chat_item.get_height() + chat_item.vertical_padding; return true; }); @@ -50,9 +51,12 @@ private class TranscriptDrawingArea : Widget } public override void snapshot(Snapshot snapshot) { - var container_width = get_width(); float y_offset = 0; - _chat_items.foreach((chat_item) => { + var container_width = get_width(); + + // Draw each item in reverse order, since the messages are in reverse order + for (int i = _chat_items.size - 1; i >= 0; i--) { + var chat_item = _chat_items[i]; var item_width = chat_item.get_width(); var item_height = chat_item.get_height(); @@ -73,38 +77,45 @@ private class TranscriptDrawingArea : Widget chat_item.draw(snapshot); snapshot.restore(); - y_offset -= item_height + bubble_padding; - - return true; - }); + y_offset -= item_height + chat_item.vertical_padding; + } } private void recompute_message_layouts() { var container_width = get_width(); float max_width = container_width * 0.90f; - _chat_items.clear(); - - var reversed_messages = _messages - .order_by((a, b) => b.date.compare(a.date)); // reverse order - DateTime? last_date = null; - reversed_messages.foreach((message) => { - // Remember everything in here is backwards. - - if (last_date == null) { - last_date = message.date; - } else if (last_date.difference(message.date) > (TimeSpan.MINUTE * 25)) { - var date_item = new DateItemLayout(last_date.to_local().format("%b %d, %Y at %H:%M"), this, max_width); - _chat_items.add(date_item); - last_date = message.date; + string? last_sender = null; + ArrayList items = new ArrayList(); + _messages.foreach((message) => { + // Date Annotation + DateTime date = message.date; + if (last_date != null && date.difference(last_date) > (TimeSpan.MINUTE * 25)) { + var date_item = new DateItemLayout(date.to_local().format("%b %d, %Y at %H:%M"), this, max_width); + items.add(date_item); + last_date = date; } + // Sender Annotation + if (show_sender && !message.from_me && last_sender != message.sender) { + var sender_bubble = new SenderAnnotationLayout(message.sender, max_width, this); + items.add(sender_bubble); + } + + // Text Bubble var text_bubble = new TextBubbleLayout(message, this, max_width); - _chat_items.add(text_bubble); + text_bubble.vertical_padding = (last_sender == message.sender) ? 0.0f : 10.0f; + items.add(text_bubble); + + last_sender = message.sender; + last_date = date; return true; }); + + _chat_items.clear(); + _chat_items.add_all(items); queue_draw(); queue_resize(); From 7ccdbced30ad5ceb6c7cb97b130f8c8f503622a3 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 3 May 2025 23:26:53 -0700 Subject: [PATCH 20/63] Show conversation display name in title --- src/application/main-window.vala | 14 ++++++++------ src/conversation-list/conversation-list-model.vala | 10 ++++++++++ src/conversation-list/conversation-list-view.vala | 6 ++++-- src/transcript/message-list-view.vala | 8 +++++++- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/application/main-window.vala b/src/application/main-window.vala index 6a3d850..e53a6c9 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -35,15 +35,17 @@ public class MainWindow : Adw.ApplicationWindow dialog.present (this); } - private void conversation_selected(string? conversation_guid) { - if (conversation_guid == null) { - transcript_container_view.transcript_view.model = null; + private void conversation_selected(Conversation conversation) { + TranscriptView transcript_view = transcript_container_view.transcript_view; + if (conversation == null) { + transcript_view.model = null; } else { - if (transcript_container_view.transcript_view.model == null || transcript_container_view.transcript_view.model.conversation_guid != conversation_guid) { - transcript_container_view.transcript_view.model = new MessageListModel (conversation_guid); + if (transcript_view.model == null || transcript_view.model.conversation_guid != conversation.guid) { + transcript_view.model = new MessageListModel (conversation.guid); + transcript_view.title = conversation.display_name; try { - Repository.get_instance().sync_conversation(conversation_guid); + Repository.get_instance().sync_conversation(conversation.guid); } catch (Error e) { GLib.warning("Failed to sync conversation: %s", e.message); } diff --git a/src/conversation-list/conversation-list-model.vala b/src/conversation-list/conversation-list-model.vala index 3fe3dd8..e762a75 100644 --- a/src/conversation-list/conversation-list-model.vala +++ b/src/conversation-list/conversation-list-model.vala @@ -18,6 +18,16 @@ public class ConversationListModel : Object, ListModel Repository.get_instance().conversations_updated.connect(load_conversations); Repository.get_instance().messages_updated.connect(load_conversations); } + + public Conversation? get_conversation(string guid) { + foreach (var conv in _conversations) { + if (conv.guid == guid) { + return conv; + } + } + + return null; + } public void load_conversations() { try { diff --git a/src/conversation-list/conversation-list-view.vala b/src/conversation-list/conversation-list-view.vala index 11bffe7..044f523 100644 --- a/src/conversation-list/conversation-list-view.vala +++ b/src/conversation-list/conversation-list-view.vala @@ -3,7 +3,7 @@ using Gtk; public class ConversationListView : Adw.Bin { - public signal void conversation_selected(string? conversation_guid); + public signal void conversation_selected(Conversation conversation); private Adw.ToolbarView container; private ListBox list_box; @@ -30,7 +30,9 @@ public class ConversationListView : Adw.Bin var conversation_row = (ConversationRow?) row; if (conversation_row != null) { selected_conversation_guid = conversation_row.conversation.guid; - conversation_selected(selected_conversation_guid); + + Conversation conversation = conversation_model.get_conversation(selected_conversation_guid); + conversation_selected(conversation); } }); diff --git a/src/transcript/message-list-view.vala b/src/transcript/message-list-view.vala index 4b37cd2..3aa1ab9 100644 --- a/src/transcript/message-list-view.vala +++ b/src/transcript/message-list-view.vala @@ -20,8 +20,14 @@ public class TranscriptView : Adw.Bin } } + public string title { + get { return title_label.label; } + set { title_label.label = value; } + } + private MessageListModel? _model = null; private Adw.ToolbarView container; + private Label title_label = new Label("Messages"); private TranscriptDrawingArea transcript_drawing_area = new TranscriptDrawingArea(); private ScrolledWindow scrolled_window = new ScrolledWindow(); @@ -37,7 +43,7 @@ public class TranscriptView : Adw.Bin container.set_content(scrolled_window); var header_bar = new Adw.HeaderBar(); - header_bar.set_title_widget(new Label("Messages")); + header_bar.set_title_widget(title_label); container.add_top_bar(header_bar); } From 3e9e8fb3d0dacff2be076a355f1298e1dfb537da Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 3 May 2025 23:39:21 -0700 Subject: [PATCH 21/63] transcriptview: reset scroll position when model changes --- src/transcript/message-list-view.vala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/transcript/message-list-view.vala b/src/transcript/message-list-view.vala index 3aa1ab9..127231c 100644 --- a/src/transcript/message-list-view.vala +++ b/src/transcript/message-list-view.vala @@ -12,6 +12,9 @@ public class TranscriptView : Adw.Bin _model = value; if (model != null) { + // Reset scroll position + scrolled_window.vadjustment = new Gtk.Adjustment(0, 0, 0, 0, 0, 0); + model.messages_changed.connect(reload_messages); model.load_messages(); } else { From d4cc3358b7bd97f3c1ca3a950e2cc3dd493647ce Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sun, 4 May 2025 00:13:47 -0700 Subject: [PATCH 22/63] reorg: message-list-view -> transcript-view --- src/meson.build | 5 ++--- .../{message-list-view.vala => transcript-view.vala} | 0 2 files changed, 2 insertions(+), 3 deletions(-) rename src/transcript/{message-list-view.vala => transcript-view.vala} (100%) diff --git a/src/meson.build b/src/meson.build index 20b668f..06bff60 100644 --- a/src/meson.build +++ b/src/meson.build @@ -28,11 +28,10 @@ sources = [ 'conversation-list/conversation-list-model.vala', 'conversation-list/conversation-row.vala', + 'transcript/message-list-model.vala', 'transcript/transcript-container-view.vala', 'transcript/transcript-drawing-area.vala', - - 'transcript/message-list-view.vala', - 'transcript/message-list-model.vala', + 'transcript/transcript-view.vala', 'transcript/layouts/bubble-layout.vala', 'transcript/layouts/chat-item-layout.vala', diff --git a/src/transcript/message-list-view.vala b/src/transcript/transcript-view.vala similarity index 100% rename from src/transcript/message-list-view.vala rename to src/transcript/transcript-view.vala From f38e2a9798ed62c76f03542006c2b0aaa0f7b5ee Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sun, 4 May 2025 00:14:00 -0700 Subject: [PATCH 23/63] Nicer app menu --- .../conversation-list-view.vala | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/conversation-list/conversation-list-view.vala b/src/conversation-list/conversation-list-view.vala index 044f523..739a1d8 100644 --- a/src/conversation-list/conversation-list-view.vala +++ b/src/conversation-list/conversation-list-view.vala @@ -30,7 +30,7 @@ public class ConversationListView : Adw.Bin var conversation_row = (ConversationRow?) row; if (conversation_row != null) { selected_conversation_guid = conversation_row.conversation.guid; - + Conversation conversation = conversation_model.get_conversation(selected_conversation_guid); conversation_selected(conversation); } @@ -42,9 +42,15 @@ public class ConversationListView : Adw.Bin // Setup application menu var app_menu = new Menu (); - app_menu.append ("Refresh", "list.refresh"); - app_menu.append ("Settings...", "win.settings"); - app_menu.append ("Quit", "app.quit"); + + var section = new Menu (); + section.append ("Refresh", "list.refresh"); + section.append ("Settings...", "win.settings"); + app_menu.append_section (null, section); + + section = new Menu (); + section.append ("Quit", "app.quit"); + app_menu.append_section (null, section); var refresh_action = new SimpleAction("refresh", null); refresh_action.activate.connect (() => { @@ -59,6 +65,8 @@ public class ConversationListView : Adw.Bin var menu_button = new Gtk.MenuButton (); menu_button.menu_model = app_menu; + menu_button.primary = true; + menu_button.icon_name = "open-menu-symbolic"; header_bar.pack_end (menu_button); // Set up model and bind to list From 4aa6e53e3a680b4e83736cde6beb9f32ed4db1ad Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sun, 4 May 2025 00:17:50 -0700 Subject: [PATCH 24/63] adjust date attribution time a bit --- src/transcript/transcript-drawing-area.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 77add9b..497cdb1 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -91,7 +91,7 @@ private class TranscriptDrawingArea : Widget _messages.foreach((message) => { // Date Annotation DateTime date = message.date; - if (last_date != null && date.difference(last_date) > (TimeSpan.MINUTE * 25)) { + if (last_date != null && date.difference(last_date) > (TimeSpan.HOUR * 1)) { var date_item = new DateItemLayout(date.to_local().format("%b %d, %Y at %H:%M"), this, max_width); items.add(date_item); last_date = date; @@ -120,4 +120,4 @@ private class TranscriptDrawingArea : Widget queue_draw(); queue_resize(); } -} \ No newline at end of file +} From f377bbb7f9747efe3797e0c1153a0a506341f35f Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sun, 4 May 2025 00:49:21 -0700 Subject: [PATCH 25/63] Change unread indicator from number to icon --- src/conversation-list/conversation-row.vala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/conversation-list/conversation-row.vala b/src/conversation-list/conversation-row.vala index d809040..bd9db4b 100644 --- a/src/conversation-list/conversation-row.vala +++ b/src/conversation-list/conversation-row.vala @@ -3,7 +3,7 @@ using Gtk; public class ConversationRow : Adw.ActionRow { public Conversation conversation; - private Label unread_badge; + private Image unread_badge; public ConversationRow(Conversation conversation) { this.conversation = conversation; @@ -23,7 +23,7 @@ public class ConversationRow : Adw.ActionRow { add_css_class("conversation-row"); - unread_badge = new Label(conversation.unread_count.to_string()); + unread_badge = new Image.from_icon_name ("media-record-symbolic"); unread_badge.add_css_class("badge"); unread_badge.add_css_class("accent"); add_prefix(unread_badge); @@ -57,4 +57,4 @@ public class ConversationRow : Adw.ActionRow { } } } -} \ No newline at end of file +} From 1ed7f5bda3eb263857d1dee6841a654ef8787f69 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 14 May 2025 17:37:23 -0700 Subject: [PATCH 26/63] Fix retain cycles --- src/service/repository.vala | 2 ++ src/transcript/message-list-model.vala | 39 ++++++++++++++------- src/transcript/transcript-drawing-area.vala | 4 +-- src/transcript/transcript-view.vala | 22 ++++++++---- 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/service/repository.vala b/src/service/repository.vala index 0793fa8..421ca94 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -24,6 +24,8 @@ public class Repository : DBusServiceProxy { } private void connect_to_repository() { + GLib.info("Connecting to repository"); + try { this.dbus_repository = Bus.get_proxy_sync(BusType.SESSION, DBUS_NAME, DBUS_PATH); this.dbus_repository.conversations_updated.connect(() => { diff --git a/src/transcript/message-list-model.vala b/src/transcript/message-list-model.vala index 037e1f2..25c58fb 100644 --- a/src/transcript/message-list-model.vala +++ b/src/transcript/message-list-model.vala @@ -5,8 +5,8 @@ public class MessageListModel : Object, ListModel { public signal void messages_changed(); - public SortedSet messages { - owned get { return _messages.read_only_view; } + public ArrayList messages { + get { return _messages; } } public bool is_group_chat { @@ -17,19 +17,34 @@ public class MessageListModel : Object, ListModel public string conversation_guid { get; private set; } - private SortedSet _messages; + private ArrayList _messages; private HashSet participants = new HashSet(); - - public MessageListModel(string conversation_guid) { - _messages = new TreeSet((a, b) => { - // Sort by date in descending order (newest first) - return a.date.compare(b.date); - }); + private ulong handler_id = 0; - Repository.get_instance().messages_updated.connect(got_messages_updated); + public MessageListModel(string conversation_guid) { + _messages = new ArrayList(); this.conversation_guid = conversation_guid; } - + + ~MessageListModel() { + // NOTE: this won't actually get destructed automatically because of a retain cycle with the signal handler. + // unwatch_updates() should be called explicitly when the model is no longer needed. + unwatch_updates(); + } + + public void watch_updates() { + if (this.handler_id == 0) { + this.handler_id = Repository.get_instance().messages_updated.connect(got_messages_updated); + } + } + + public void unwatch_updates() { + if (this.handler_id != 0) { + Repository.get_instance().disconnect(this.handler_id); + this.handler_id = 0; + } + } + public void load_messages() { try { Message[] messages = Repository.get_instance().get_messages(conversation_guid); @@ -82,6 +97,6 @@ public class MessageListModel : Object, ListModel } public Object? get_item(uint position) { - return _messages.to_array()[position]; + return _messages.get((int)position); } } \ No newline at end of file diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 497cdb1..5aa393d 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -5,7 +5,7 @@ private class TranscriptDrawingArea : Widget { public bool show_sender = true; - private SortedSet _messages = new TreeSet(); + private ArrayList _messages = new ArrayList(); private ArrayList _chat_items = new ArrayList(); private const float bubble_margin = 18.0f; @@ -14,7 +14,7 @@ private class TranscriptDrawingArea : Widget add_css_class("transcript-drawing-area"); } - public void set_messages(SortedSet messages) { + public void set_messages(ArrayList messages) { _messages = messages; recompute_message_layouts(); } diff --git a/src/transcript/transcript-view.vala b/src/transcript/transcript-view.vala index 127231c..4078749 100644 --- a/src/transcript/transcript-view.vala +++ b/src/transcript/transcript-view.vala @@ -9,16 +9,25 @@ public class TranscriptView : Adw.Bin return _model; } set { + if (_model != null) { + _model.disconnect(messages_changed_handler_id); + _model.unwatch_updates(); + } + _model = value; - if (model != null) { + if (value != null) { // Reset scroll position scrolled_window.vadjustment = new Gtk.Adjustment(0, 0, 0, 0, 0, 0); - model.messages_changed.connect(reload_messages); - model.load_messages(); + weak TranscriptView self = this; + messages_changed_handler_id = value.messages_changed.connect(() => { + self.reload_messages(); + }); + + value.load_messages(); } else { - transcript_drawing_area.set_messages(new TreeSet()); + transcript_drawing_area.set_messages(new ArrayList()); } } } @@ -34,10 +43,9 @@ public class TranscriptView : Adw.Bin private TranscriptDrawingArea transcript_drawing_area = new TranscriptDrawingArea(); private ScrolledWindow scrolled_window = new ScrolledWindow(); + private ulong messages_changed_handler_id = 0; - public TranscriptView(MessageListModel? model = null) { - this.model = model; - + public TranscriptView() { container = new Adw.ToolbarView(); set_child(container); From 1a2dad08a5b9e11e32bfe3805698e2b275917c80 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 6 Jun 2025 14:33:40 -0700 Subject: [PATCH 27/63] adds image bubble layout for attachments --- .../conversation-list-model.vala | 1 - src/meson.build | 1 + .../layouts/image-bubble-layout.vala | 98 +++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/transcript/layouts/image-bubble-layout.vala diff --git a/src/conversation-list/conversation-list-model.vala b/src/conversation-list/conversation-list-model.vala index e762a75..d50f983 100644 --- a/src/conversation-list/conversation-list-model.vala +++ b/src/conversation-list/conversation-list-model.vala @@ -89,7 +89,6 @@ public class ConversationListModel : Object, ListModel } // Update changed conversations - GLib.message("Changed conversations: %d", changed_conversations.size); foreach (var conv in changed_conversations) { // Find position of old conversation uint old_pos = 0; diff --git a/src/meson.build b/src/meson.build index 06bff60..9dcebb7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -36,6 +36,7 @@ sources = [ 'transcript/layouts/bubble-layout.vala', 'transcript/layouts/chat-item-layout.vala', 'transcript/layouts/date-item-layout.vala', + 'transcript/layouts/image-bubble-layout.vala', 'transcript/layouts/sender-annotation-layout.vala', 'transcript/layouts/text-bubble-layout.vala', diff --git a/src/transcript/layouts/image-bubble-layout.vala b/src/transcript/layouts/image-bubble-layout.vala new file mode 100644 index 0000000..53cab58 --- /dev/null +++ b/src/transcript/layouts/image-bubble-layout.vala @@ -0,0 +1,98 @@ +using Gtk; + +private class ImageBubbleLayout : BubbleLayout +{ + public string image_path; + public bool is_downloaded; + + private Graphene.Size image_size; + private Gdk.Texture? cached_texture = null; + private const float max_image_width = 300.0f; + private const float max_image_height = 400.0f; + + public ImageBubbleLayout(string image_path, bool from_me, Widget parent, float max_width, Graphene.Size? image_size = null) { + base(parent, max_width); + + this.from_me = from_me; + this.image_path = image_path; + this.is_downloaded = false; + + // Calculate image dimensions for layout + calculate_image_dimensions(image_size); + } + + private void calculate_image_dimensions(Graphene.Size? image_size) { + if (image_size != null) { + this.image_size = image_size; + return; + } + + // Try to load the image to get its dimensions + try { + var texture = Gdk.Texture.from_filename(image_path); + var original_width = (float)texture.get_width(); + var original_height = (float)texture.get_height(); + + // Calculate scaled dimensions while maintaining aspect ratio + var scale_factor = float.min( + max_image_width / original_width, + max_image_height / original_height + ); + scale_factor = float.min(scale_factor, 1.0f); // Don't scale up + + this.image_size = Graphene.Size() { width = original_width * scale_factor, height = original_height * scale_factor }; + } catch (Error e) { + // Fallback dimensions if image can't be loaded + warning("Failed to load image %s: %s", image_path, e.message); + this.image_size = Graphene.Size() { width = 200.0f, height = 150.0f }; + } + } + + private void load_image_if_needed() { + if (cached_texture != null) { + return; + } + + if (!is_downloaded) { + return; + } + + try { + cached_texture = Gdk.Texture.from_filename(image_path); + } catch (Error e) { + warning("Failed to load image %s: %s", image_path, e.message); + } + } + + public override float get_height() { + float aspect_ratio = image_size.width / image_size.height; + if (image_size.width > max_width) { + return max_width / aspect_ratio; + } + + return image_size.height; + } + + public override float get_width() { + return float.min(image_size.width, max_width); + } + + public override void draw_content(Snapshot snapshot) { + load_image_if_needed(); + + snapshot.save(); + + var image_rect = Graphene.Rect () { + origin = Graphene.Point() { x = 0, y = 0 }, + size = Graphene.Size() { width = get_width(), height = get_height() } + }; + + if (cached_texture != null) { + snapshot.append_texture(cached_texture, image_rect); + } else { + snapshot.append_color(Gdk.RGBA() { red = 0.6f, green = 0.6f, blue = 0.6f, alpha = 0.5f }, image_rect); + } + + snapshot.restore(); + } +} \ No newline at end of file From 54790d1d702ee60cee3ab3fee0aea4a330ea2f9a Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 6 Jun 2025 20:03:02 -0700 Subject: [PATCH 28/63] Implements attachments display in transcript --- src/meson.build | 3 +- src/models/attachment.vala | 74 +++++++++++++++++ src/models/message.vala | 15 ++++ src/service/interface/dbusservice.vala | 19 +++++ .../xml/net.buzzert.kordophonecd.Server.xml | 83 ++++++++++++++++--- src/service/repository.vala | 13 +++ .../layouts/image-bubble-layout.vala | 21 ++--- src/transcript/transcript-drawing-area.vala | 24 ++++++ src/transcript/transcript-view.vala | 31 +++++++ 9 files changed, 252 insertions(+), 31 deletions(-) create mode 100644 src/models/attachment.vala diff --git a/src/meson.build b/src/meson.build index 9dcebb7..6a27d2d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -40,10 +40,9 @@ sources = [ 'transcript/layouts/sender-annotation-layout.vala', 'transcript/layouts/text-bubble-layout.vala', + 'models/attachment.vala', 'models/conversation.vala', 'models/message.vala', - - ] executable('kordophone', diff --git a/src/models/attachment.vala b/src/models/attachment.vala new file mode 100644 index 0000000..05357ac --- /dev/null +++ b/src/models/attachment.vala @@ -0,0 +1,74 @@ +public class AttributionInfo : Object { + // Picture width + public int64 width; + + // Picture height + public int64 height; + + public static AttributionInfo from_variant(Variant variant) { + var attribution_info = new AttributionInfo(); + + VariantDict dict = new VariantDict(variant); + attribution_info.width = dict.lookup_value("width", VariantType.INT32)?.get_int32() ?? 0; + attribution_info.height = dict.lookup_value("height", VariantType.INT32)?.get_int32() ?? 0; + + return attribution_info; + } +} + +public class AttachmentMetadata : Object { + public AttributionInfo? attribution_info = null; + + public static AttachmentMetadata from_variant(Variant variant) { + var metadata = new AttachmentMetadata(); + + VariantDict dict = new VariantDict(variant); + metadata.attribution_info = AttributionInfo.from_variant(dict.lookup_value("attribution_info", VariantType.DICTIONARY)); + + return metadata; + } +} + +public class Attachment : Object { + public string guid; + public string path; + public string preview_path; + public bool downloaded; + public bool preview_downloaded; + public AttachmentMetadata? metadata; + + public Attachment(string guid, AttachmentMetadata? metadata) { + this.guid = guid; + this.metadata = metadata; + } + + public Attachment.from_variant(Variant variant) { + VariantIter iter; + variant.get("a{sv}", out iter); + + string key; + Variant val; + while (iter.next("{sv}", out key, out val)) { + switch (key) { + case "guid": + this.guid = val.get_string(); + break; + case "path": + this.path = val.get_string(); + break; + case "preview_path": + this.preview_path = val.get_string(); + break; + case "downloaded": + this.downloaded = val.get_boolean(); + break; + case "preview_downloaded": + this.preview_downloaded = val.get_boolean(); + break; + case "metadata": + this.metadata = AttachmentMetadata.from_variant(val); + break; + } + } + } +} diff --git a/src/models/message.vala b/src/models/message.vala index e8ab948..8590e63 100644 --- a/src/models/message.vala +++ b/src/models/message.vala @@ -6,6 +6,8 @@ public class Message : Object public string text { get; set; default = ""; } public DateTime date { get; set; default = new DateTime.now_local(); } public string sender { get; set; default = null; } + + public Attachment[] attachments { get; set; default = {}; } public bool from_me { get { @@ -25,5 +27,18 @@ public class Message : Object text = message_data["text"].get_string(); sender = message_data["sender"].get_string(); date = new DateTime.from_unix_utc(message_data["date"].get_int64()); + + // Attachments + var attachments_variant = message_data["attachments"]; + var attachments = new Gee.ArrayList(); + if (attachments_variant != null) { + for (int i = 0; i < attachments_variant.n_children(); i++) { + var attachment_variant = attachments_variant.get_child_value(i); + var attachment = new Attachment.from_variant(attachment_variant); + attachments.add(attachment); + } + } + + this.attachments = attachments.to_array(); } } \ No newline at end of file diff --git a/src/service/interface/dbusservice.vala b/src/service/interface/dbusservice.vala index 16aceb1..0cb85b6 100644 --- a/src/service/interface/dbusservice.vala +++ b/src/service/interface/dbusservice.vala @@ -52,5 +52,24 @@ namespace DBusService { [DBus (name = "MessagesUpdated")] public signal void messages_updated(string conversation_id); + + [DBus (name = "GetAttachmentInfo")] + public abstract RepositoryAttachmentInfoStruct get_attachment_info(string attachment_id) throws DBusError, IOError; + + [DBus (name = "DownloadAttachment")] + public abstract void download_attachment(string attachment_id, bool preview) throws DBusError, IOError; + + [DBus (name = "AttachmentDownloadCompleted")] + public signal void attachment_download_completed(string attachment_id); + + [DBus (name = "AttachmentDownloadFailed")] + public signal void attachment_download_failed(string attachment_id, string error_message); + } + + public struct RepositoryAttachmentInfoStruct { + public string attr1; + public string attr2; + public bool attr3; + public bool attr4; } } diff --git a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml index 8198155..2cf0e9b 100644 --- a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml +++ b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml @@ -4,7 +4,7 @@ - @@ -13,9 +13,9 @@ - + - - - - - - @@ -58,7 +58,24 @@ - + + + @@ -66,16 +83,56 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -90,6 +147,6 @@ - + diff --git a/src/service/repository.vala b/src/service/repository.vala index 421ca94..f52e938 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -4,6 +4,7 @@ using Gee; public class Repository : DBusServiceProxy { public signal void conversations_updated(); public signal void messages_updated(string conversation_guid); + public signal void attachment_downloaded(string attachment_guid); public static Repository get_instance() { if (instance == null) { @@ -36,6 +37,10 @@ public class Repository : DBusServiceProxy { messages_updated(conversation_guid); }); + this.dbus_repository.attachment_download_completed.connect((attachment_guid) => { + attachment_downloaded(attachment_guid); + }); + conversations_updated(); } catch (GLib.Error e) { warning("Failed to connect to repository: %s", e.message); @@ -87,4 +92,12 @@ public class Repository : DBusServiceProxy { dbus_repository.sync_conversation(conversation_guid); } + + public void download_attachment(string attachment_guid, bool preview) throws DBusServiceProxyError, GLib.Error { + if (dbus_repository == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); + } + + dbus_repository.download_attachment(attachment_guid, preview); + } } diff --git a/src/transcript/layouts/image-bubble-layout.vala b/src/transcript/layouts/image-bubble-layout.vala index 53cab58..2e25641 100644 --- a/src/transcript/layouts/image-bubble-layout.vala +++ b/src/transcript/layouts/image-bubble-layout.vala @@ -7,8 +7,6 @@ private class ImageBubbleLayout : BubbleLayout private Graphene.Size image_size; private Gdk.Texture? cached_texture = null; - private const float max_image_width = 300.0f; - private const float max_image_height = 400.0f; public ImageBubbleLayout(string image_path, bool from_me, Widget parent, float max_width, Graphene.Size? image_size = null) { base(parent, max_width); @@ -29,18 +27,13 @@ private class ImageBubbleLayout : BubbleLayout // Try to load the image to get its dimensions try { + warning("No image size provided, loading image to get dimensions"); + var texture = Gdk.Texture.from_filename(image_path); var original_width = (float)texture.get_width(); var original_height = (float)texture.get_height(); - // Calculate scaled dimensions while maintaining aspect ratio - var scale_factor = float.min( - max_image_width / original_width, - max_image_height / original_height - ); - scale_factor = float.min(scale_factor, 1.0f); // Don't scale up - - this.image_size = Graphene.Size() { width = original_width * scale_factor, height = original_height * scale_factor }; + this.image_size = Graphene.Size() { width = original_width, height = original_height }; } catch (Error e) { // Fallback dimensions if image can't be loaded warning("Failed to load image %s: %s", image_path, e.message); @@ -65,12 +58,8 @@ private class ImageBubbleLayout : BubbleLayout } public override float get_height() { - float aspect_ratio = image_size.width / image_size.height; - if (image_size.width > max_width) { - return max_width / aspect_ratio; - } - - return image_size.height; + var scale_factor = float.min(max_width / image_size.width, 1.0f); + return image_size.height * scale_factor; } public override float get_width() { diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 5aa393d..e7482c2 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -108,6 +108,30 @@ private class TranscriptDrawingArea : Widget text_bubble.vertical_padding = (last_sender == message.sender) ? 0.0f : 10.0f; items.add(text_bubble); + // Check for attachments. For each one, add an image layout bubble + foreach (var attachment in message.attachments) { + if (attachment.metadata != null) { + var image_size = Graphene.Size() { + width = attachment.metadata.attribution_info.width, + height = attachment.metadata.attribution_info.height + }; + + var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size); + image_layout.is_downloaded = attachment.preview_downloaded; + items.add(image_layout); + + // If the attachment isn't downloaded, queue a download since we are going to be showing it here. + // TODO: Probably would be better if we only did this for stuff in the viewport. + if (!attachment.preview_downloaded) { + try { + Repository.get_instance().download_attachment(attachment.guid, true); + } catch (GLib.Error e) { + warning("Wasn't able to message daemon about queuing attachment download: %s", e.message); + } + } + } + } + last_sender = message.sender; last_date = date; diff --git a/src/transcript/transcript-view.vala b/src/transcript/transcript-view.vala index 4078749..9d22574 100644 --- a/src/transcript/transcript-view.vala +++ b/src/transcript/transcript-view.vala @@ -44,6 +44,7 @@ public class TranscriptView : Adw.Bin private TranscriptDrawingArea transcript_drawing_area = new TranscriptDrawingArea(); private ScrolledWindow scrolled_window = new ScrolledWindow(); private ulong messages_changed_handler_id = 0; + private bool needs_reload = false; public TranscriptView() { container = new Adw.ToolbarView(); @@ -56,6 +57,36 @@ public class TranscriptView : Adw.Bin var header_bar = new Adw.HeaderBar(); header_bar.set_title_widget(title_label); container.add_top_bar(header_bar); + + Repository.get_instance().attachment_downloaded.connect((attachment_guid) => { + debug("Attachment downloaded: %s", attachment_guid); + + // See if this attachment is part of this transcript. + bool contains_attachment = false; + foreach (var message in _model.messages) { + foreach (var attachment in message.attachments) { + if (attachment.guid == attachment_guid) { + contains_attachment = true; + break; + } + } + } + + if (contains_attachment && !needs_reload) { + debug("Queueing reload of messages for attachment download"); + + needs_reload = true; + GLib.Idle.add(() => { + if (needs_reload) { + debug("Reloading messages for attachment download"); + model.load_messages(); + needs_reload = false; + } + + return false; + }, GLib.Priority.HIGH); + } + }); } private void reload_messages() { From 501bd3f6047ba3664474407db271c14fdfccb1d0 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 12 Jun 2025 17:54:09 -0700 Subject: [PATCH 29/63] Add back message list watching, support attachments without metadata --- src/transcript/transcript-drawing-area.vala | 27 +++++++++++---------- src/transcript/transcript-view.vala | 1 + 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index e7482c2..6507301 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -110,24 +110,25 @@ private class TranscriptDrawingArea : Widget // Check for attachments. For each one, add an image layout bubble foreach (var attachment in message.attachments) { + Graphene.Size? image_size = null; if (attachment.metadata != null) { - var image_size = Graphene.Size() { + image_size = Graphene.Size() { width = attachment.metadata.attribution_info.width, height = attachment.metadata.attribution_info.height - }; + }; + } - var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size); - image_layout.is_downloaded = attachment.preview_downloaded; - items.add(image_layout); + var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size); + image_layout.is_downloaded = attachment.preview_downloaded; + items.add(image_layout); - // If the attachment isn't downloaded, queue a download since we are going to be showing it here. - // TODO: Probably would be better if we only did this for stuff in the viewport. - if (!attachment.preview_downloaded) { - try { - Repository.get_instance().download_attachment(attachment.guid, true); - } catch (GLib.Error e) { - warning("Wasn't able to message daemon about queuing attachment download: %s", e.message); - } + // If the attachment isn't downloaded, queue a download since we are going to be showing it here. + // TODO: Probably would be better if we only did this for stuff in the viewport. + if (!attachment.preview_downloaded) { + try { + Repository.get_instance().download_attachment(attachment.guid, true); + } catch (GLib.Error e) { + warning("Wasn't able to message daemon about queuing attachment download: %s", e.message); } } } diff --git a/src/transcript/transcript-view.vala b/src/transcript/transcript-view.vala index 9d22574..e2555cc 100644 --- a/src/transcript/transcript-view.vala +++ b/src/transcript/transcript-view.vala @@ -26,6 +26,7 @@ public class TranscriptView : Adw.Bin }); value.load_messages(); + value.watch_updates(); } else { transcript_drawing_area.set_messages(new ArrayList()); } From 8dbe36fde1ed33884dc8b116be5d4bc2aa2f4db5 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 12 Jun 2025 18:13:59 -0700 Subject: [PATCH 30/63] Repository: add support for attachment uploading --- src/service/interface/dbusservice.vala | 6 ++++++ .../xml/net.buzzert.kordophonecd.Server.xml | 17 +++++++++++++++++ src/service/repository.vala | 13 +++++++++++++ 3 files changed, 36 insertions(+) diff --git a/src/service/interface/dbusservice.vala b/src/service/interface/dbusservice.vala index 0cb85b6..1a6d95c 100644 --- a/src/service/interface/dbusservice.vala +++ b/src/service/interface/dbusservice.vala @@ -59,11 +59,17 @@ namespace DBusService { [DBus (name = "DownloadAttachment")] public abstract void download_attachment(string attachment_id, bool preview) throws DBusError, IOError; + [DBus (name = "UploadAttachment")] + public abstract string upload_attachment(string path) throws DBusError, IOError; + [DBus (name = "AttachmentDownloadCompleted")] public signal void attachment_download_completed(string attachment_id); [DBus (name = "AttachmentDownloadFailed")] public signal void attachment_download_failed(string attachment_id, string error_message); + + [DBus (name = "AttachmentUploadCompleted")] + public signal void attachment_upload_completed(string upload_guid, string attachment_guid); } public struct RepositoryAttachmentInfoStruct { diff --git a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml index 2cf0e9b..bdc0fb2 100644 --- a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml +++ b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml @@ -120,6 +120,11 @@ "/> + + + + + @@ -133,6 +138,18 @@ + + + + + + + diff --git a/src/service/repository.vala b/src/service/repository.vala index f52e938..8753f6b 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -5,6 +5,7 @@ public class Repository : DBusServiceProxy { public signal void conversations_updated(); public signal void messages_updated(string conversation_guid); public signal void attachment_downloaded(string attachment_guid); + public signal void attachment_uploaded(string upload_guid, string attachment_guid); public static Repository get_instance() { if (instance == null) { @@ -41,6 +42,10 @@ public class Repository : DBusServiceProxy { attachment_downloaded(attachment_guid); }); + this.dbus_repository.attachment_upload_completed.connect((upload_guid, attachment_guid) => { + attachment_uploaded(upload_guid, attachment_guid); + }); + conversations_updated(); } catch (GLib.Error e) { warning("Failed to connect to repository: %s", e.message); @@ -100,4 +105,12 @@ public class Repository : DBusServiceProxy { dbus_repository.download_attachment(attachment_guid, preview); } + + public string upload_attachment(string filename) throws DBusServiceProxyError, GLib.Error { + if (dbus_repository == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); + } + + return dbus_repository.upload_attachment(filename); + } } From f3e59b99519e4b088270c57e66f4647bcc5962ad Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 12 Jun 2025 19:26:49 -0700 Subject: [PATCH 31/63] Adds ui support for attachments, results not yet connected to daemon --- src/application/main-window.vala | 7 +- src/meson.build | 1 + src/resources/style.css | 17 ++ src/transcript/attachment-preview.vala | 93 ++++++++++ src/transcript/transcript-container-view.vala | 172 +++++++++++++++++- 5 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 src/transcript/attachment-preview.vala diff --git a/src/application/main-window.vala b/src/application/main-window.vala index e53a6c9..63e0cc4 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -53,7 +53,10 @@ public class MainWindow : Adw.ApplicationWindow } } - private void on_transcript_send(string message) { + private void on_transcript_send(TranscriptContainerView view) { + var body = view.message_body; + var attachment_guids = view.attachment_guids; + if (transcript_container_view.transcript_view.model == null) { GLib.warning("No conversation selected"); return; @@ -66,7 +69,7 @@ public class MainWindow : Adw.ApplicationWindow } try { - Repository.get_instance().send_message(selected_conversation, message); + Repository.get_instance().send_message(selected_conversation, body); } catch (Error e) { GLib.warning("Failed to send message: %s", e.message); } diff --git a/src/meson.build b/src/meson.build index 6a27d2d..f1ec3f0 100644 --- a/src/meson.build +++ b/src/meson.build @@ -28,6 +28,7 @@ sources = [ 'conversation-list/conversation-list-model.vala', 'conversation-list/conversation-row.vala', + 'transcript/attachment-preview.vala', 'transcript/message-list-model.vala', 'transcript/transcript-container-view.vala', 'transcript/transcript-drawing-area.vala', diff --git a/src/resources/style.css b/src/resources/style.css index 58b7ebb..5b04e69 100644 --- a/src/resources/style.css +++ b/src/resources/style.css @@ -30,5 +30,22 @@ font-size: 1.1rem; } +.attachment-preview-row { + background-color: alpha(@window_bg_color, 0.3); + border-radius: 8px; + padding: 8px; +} +.attachment-preview { + border-radius: 8px; + overflow: hidden; + border: 1px solid alpha(@borders, 0.5); +} +.attachment-preview.completed { + border-color: @success_color; +} + +.attachment-image { + border-radius: 8px; +} diff --git a/src/transcript/attachment-preview.vala b/src/transcript/attachment-preview.vala new file mode 100644 index 0000000..d9568af --- /dev/null +++ b/src/transcript/attachment-preview.vala @@ -0,0 +1,93 @@ +using Gtk; +using Gdk; + +class AttachmentPreview : Gtk.Box { + public signal void remove_requested(); + + private File file; + private string upload_guid; + private string? attachment_guid = null; + private bool is_completed = false; + + private Overlay overlay; + private Image picture; + private Spinner spinner; + private Button remove_button; + + public AttachmentPreview(File file, string upload_guid) { + Object(orientation: Orientation.VERTICAL, spacing: 0); + this.file = file; + this.upload_guid = upload_guid; + + setup_ui(); + load_image(); + } + + private void setup_ui() { + set_size_request(100, 100); + add_css_class("attachment-preview"); + + overlay = new Overlay(); + overlay.set_size_request(100, 100); + append(overlay); + + // Image preview + picture = new Image(); + overlay.set_child(picture); + + // Loading spinner + spinner = new Spinner(); + spinner.set_halign(Align.CENTER); + spinner.set_valign(Align.CENTER); + spinner.set_size_request(24, 24); + spinner.start(); + overlay.add_overlay(spinner); + + // Remove button + remove_button = new Button(); + remove_button.set_icon_name("window-close-symbolic"); + remove_button.add_css_class("circular"); + remove_button.add_css_class("destructive-action"); + remove_button.set_halign(Align.END); + remove_button.set_valign(Align.START); + remove_button.set_margin_top(4); + remove_button.set_margin_end(4); + remove_button.set_size_request(20, 20); + remove_button.clicked.connect(() => { + remove_requested(); + }); + overlay.add_overlay(remove_button); + } + + private void load_image() { + try { + picture.set_from_file(file.get_path()); + } catch (Error e) { + warning("Failed to load image preview: %s", e.message); + + // Show a placeholder icon if image loading fails + var icon = new Image.from_icon_name("image-x-generic"); + icon.set_pixel_size(48); + overlay.set_child(icon); + } + } + + public void set_completed(string attachment_guid) { + this.attachment_guid = attachment_guid; + this.is_completed = true; + + spinner.stop(); + spinner.set_visible(false); + + // Optionally change visual state to indicate completion + add_css_class("completed"); + } + + public string? get_attachment_guid() { + return attachment_guid; + } + + public bool get_is_completed() { + return is_completed; + } +} \ No newline at end of file diff --git a/src/transcript/transcript-container-view.vala b/src/transcript/transcript-container-view.vala index 9a17edb..9b26d06 100644 --- a/src/transcript/transcript-container-view.vala +++ b/src/transcript/transcript-container-view.vala @@ -1,29 +1,67 @@ using Gtk; using Adw; +using Gee; -class TranscriptContainerView : Adw.Bin { +class TranscriptContainerView : Adw.Bin +{ public TranscriptView transcript_view; - public Entry message_entry; - public signal void on_send(string message); + public signal void on_send(TranscriptContainerView view); private Box container; private Button send_button; + private FlowBox attachment_flow_box; + + private Entry message_entry; + private HashSet pending_uploads; + private HashMap attachment_previews; + private ArrayList completed_attachments; + + public string message_body { + get { + return message_entry.text; + } + } + + public ArrayList attachment_guids { + owned get { + var attachment_guids = new ArrayList(); + completed_attachments.foreach((attachment) => { + attachment_guids.add(attachment.attachment_guid); + return true; + }); + + return attachment_guids; + } + } public TranscriptContainerView () { container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); set_child (container); + + pending_uploads = new HashSet(); + attachment_previews = new HashMap(); + completed_attachments = new ArrayList(); // Create message list view transcript_view = new TranscriptView(); transcript_view.set_vexpand(true); container.append(transcript_view); + // Create attachment preview row (initially hidden) + setup_attachment_row(); + // Create bottom box for input var input_box = new Box(Orientation.HORIZONTAL, 6); input_box.add_css_class("message-input-box"); input_box.set_valign(Align.END); input_box.set_spacing(6); container.append(input_box); + + // Setup drag and drop + setup_drag_and_drop(); + + // Connect to repository signals + Repository.get_instance().attachment_uploaded.connect(on_attachment_uploaded); // Create message entry message_entry = new Entry(); @@ -42,16 +80,138 @@ class TranscriptContainerView : Adw.Bin { send_button.clicked.connect(on_request_send); input_box.append(send_button); } + + private void setup_attachment_row() { + attachment_flow_box = new FlowBox(); + attachment_flow_box.set_max_children_per_line(6); + attachment_flow_box.set_row_spacing(6); + attachment_flow_box.set_column_spacing(6); + attachment_flow_box.halign = Align.START; + attachment_flow_box.add_css_class("attachment-preview-row"); + container.append(attachment_flow_box); + } + + private void setup_drag_and_drop() { + var drop_target = new DropTarget(typeof(File), Gdk.DragAction.COPY); + drop_target.drop.connect(on_file_dropped); + this.add_controller(drop_target); + } + + private bool on_file_dropped(Value val, double x, double y) { + if (!val.holds(typeof(File))) { + return false; + } + + var file = (File)val.get_object(); + if (file == null) { + return false; + } + + // Check if it's an image file + try { + var file_info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE, FileQueryInfoFlags.NONE); + string content_type = file_info.get_content_type(); + + if (!content_type.has_prefix("image/")) { + return false; + } + + upload_file(file); + return true; + } catch (Error e) { + warning("Failed to get file info: %s", e.message); + return false; + } + } + + private void upload_file(File file) { + try { + string upload_guid = Repository.get_instance().upload_attachment(file.get_path()); + pending_uploads.add(upload_guid); + + var preview = new AttachmentPreview(file, upload_guid); + preview.remove_requested.connect(() => { + remove_attachment(upload_guid); + }); + + attachment_previews[upload_guid] = preview; + attachment_flow_box.append(preview); + + update_attachment_row_visibility(); + } catch (Error e) { + warning("Failed to upload attachment: %s", e.message); + } + } + + private void on_attachment_uploaded(string upload_guid, string attachment_guid) { + if (attachment_previews.has_key(upload_guid)) { + var preview = attachment_previews[upload_guid]; + preview.set_completed(attachment_guid); + completed_attachments.add(new UploadedAttachment(upload_guid, attachment_guid)); + pending_uploads.remove(upload_guid); + update_send_button_sensitivity(); + } + } + + private void remove_attachment(string upload_guid) { + if (attachment_previews.has_key(upload_guid)) { + var preview = attachment_previews[upload_guid]; + attachment_flow_box.remove(preview); + attachment_previews.unset(upload_guid); + + completed_attachments.foreach((attachment) => { + if (attachment.upload_guid == upload_guid) { + completed_attachments.remove(attachment); + return false; + } + + return true; + }); + + update_attachment_row_visibility(); + update_send_button_sensitivity(); + } + } + + private void update_attachment_row_visibility() { + bool has_attachments = attachment_previews.size > 0; + attachment_flow_box.set_visible(has_attachments); + } private void on_text_changed() { - send_button.set_sensitive(message_entry.text.length > 0); + update_send_button_sensitivity(); + } + + private void update_send_button_sensitivity() { + send_button.set_sensitive(message_entry.text.length > 0 && pending_uploads.size == 0); } private void on_request_send() { - if (message_entry.text.length > 0) { - on_send(message_entry.text); + if (message_entry.text.length > 0 && pending_uploads.size == 0) { + on_send(this); + + // Clear the message entry message_entry.text = ""; + + // Clear the attachment previews + attachment_flow_box.remove_all(); + attachment_previews.clear(); + completed_attachments.clear(); + pending_uploads.clear(); + + update_send_button_sensitivity(); } } } +class UploadedAttachment +{ + public string upload_guid; + public string attachment_guid; + + public UploadedAttachment(string upload_guid, string attachment_guid) + { + this.upload_guid = upload_guid; + this.attachment_guid = attachment_guid; + } +} \ No newline at end of file From 137da5b3d11186c70efdd475e901fa0f74082b3c Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 12 Jun 2025 19:46:53 -0700 Subject: [PATCH 32/63] Finish daemon support for uploaded attachments + sending --- src/application/main-window.vala | 2 +- src/service/interface/dbusservice.vala | 2 +- .../xml/net.buzzert.kordophonecd.Server.xml | 12 +++++++++++- src/service/repository.vala | 4 ++-- src/transcript/transcript-container-view.vala | 10 ++++++++-- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/application/main-window.vala b/src/application/main-window.vala index 63e0cc4..499f30e 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -69,7 +69,7 @@ public class MainWindow : Adw.ApplicationWindow } try { - Repository.get_instance().send_message(selected_conversation, body); + Repository.get_instance().send_message(selected_conversation, body, attachment_guids.to_array()); } catch (Error e) { GLib.warning("Failed to send message: %s", e.message); } diff --git a/src/service/interface/dbusservice.vala b/src/service/interface/dbusservice.vala index 1a6d95c..f352da2 100644 --- a/src/service/interface/dbusservice.vala +++ b/src/service/interface/dbusservice.vala @@ -48,7 +48,7 @@ namespace DBusService { public abstract GLib.HashTable[] get_messages(string conversation_id, string last_message_id) throws DBusError, IOError; [DBus (name = "SendMessage")] - public abstract string send_message(string conversation_id, string text) throws DBusError, IOError; + public abstract string send_message(string conversation_id, string text, string[] attachment_guids) throws DBusError, IOError; [DBus (name = "MessagesUpdated")] public signal void messages_updated(string conversation_id); diff --git a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml index bdc0fb2..8e93ca5 100644 --- a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml +++ b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml @@ -81,10 +81,20 @@ + + + value="Sends a message to the server. Returns the outgoing message ID. + Arguments: + - conversation_id: The ID of the conversation to send the message to. + - text: The text of the message to send. + - attachment_guids: The GUIDs of the attachments to send. + + Returns: + - outgoing_message_id: The ID of the outgoing message. + "/> diff --git a/src/service/repository.vala b/src/service/repository.vala index 8753f6b..f89f757 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -82,12 +82,12 @@ public class Repository : DBusServiceProxy { return returned_messages; } - public string send_message(string conversation_guid, string message) throws DBusServiceProxyError, GLib.Error { + public string send_message(string conversation_guid, string message, string[] attachment_guids) throws DBusServiceProxyError, GLib.Error { if (dbus_repository == null) { throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); } - return dbus_repository.send_message(conversation_guid, message); + return dbus_repository.send_message(conversation_guid, message, attachment_guids); } public void sync_conversation(string conversation_guid) throws DBusServiceProxyError, GLib.Error { diff --git a/src/transcript/transcript-container-view.vala b/src/transcript/transcript-container-view.vala index 9b26d06..4da4f65 100644 --- a/src/transcript/transcript-container-view.vala +++ b/src/transcript/transcript-container-view.vala @@ -34,6 +34,12 @@ class TranscriptContainerView : Adw.Bin } } + private bool can_send { + get { + return (message_entry.text.length > 0 || completed_attachments.size > 0) && pending_uploads.size == 0; + } + } + public TranscriptContainerView () { container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); set_child (container); @@ -183,11 +189,11 @@ class TranscriptContainerView : Adw.Bin } private void update_send_button_sensitivity() { - send_button.set_sensitive(message_entry.text.length > 0 && pending_uploads.size == 0); + send_button.set_sensitive(can_send); } private void on_request_send() { - if (message_entry.text.length > 0 && pending_uploads.size == 0) { + if (can_send) { on_send(this); // Clear the message entry From 6fb88c3a0daabf3a1bf3181a9c65cce853d12151 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 12 Jun 2025 20:35:56 -0700 Subject: [PATCH 33/63] Switch from Entry to TextView for multiline, paste support for attachments --- src/application/kordophone-application.vala | 3 + src/models/message.vala | 11 ++ src/resources/style.css | 4 +- .../layouts/image-bubble-layout.vala | 56 ++++++++- src/transcript/transcript-container-view.vala | 116 +++++++++++++++--- src/transcript/transcript-drawing-area.vala | 8 +- src/transcript/transcript-view.vala | 3 + 7 files changed, 179 insertions(+), 22 deletions(-) diff --git a/src/application/kordophone-application.vala b/src/application/kordophone-application.vala index 3260af4..942bcb2 100644 --- a/src/application/kordophone-application.vala +++ b/src/application/kordophone-application.vala @@ -12,6 +12,9 @@ public class KordophoneApp : Adw.Application protected override void startup () { base.startup (); + // Set default icon theme + Gtk.Settings.get_default().set_property("gtk-icon-theme-name", "Adwaita"); + // Load CSS from resources var provider = new Gtk.CssProvider (); provider.load_from_resource ("/net/buzzert/kordophone2/style.css"); diff --git a/src/models/message.vala b/src/models/message.vala index 8590e63..7fa7886 100644 --- a/src/models/message.vala +++ b/src/models/message.vala @@ -16,6 +16,17 @@ public class Message : Object } } + public bool is_attachment_marker { + get { + uint8[] attachment_marker_str = { 0xEF, 0xBF, 0xBC, 0x00 }; + if (text.length > attachment_marker_str.length) { + return false; + } + + return (string)attachment_marker_str == text; + } + } + public Message(string text, DateTime date, string? sender) { this.text = text; this.date = date; diff --git a/src/resources/style.css b/src/resources/style.css index 5b04e69..9c29d8f 100644 --- a/src/resources/style.css +++ b/src/resources/style.css @@ -28,6 +28,9 @@ .message-input-entry { font-size: 1.1rem; + border-radius: 8px; + padding: 12px; + border: 1px solid alpha(@borders, 0.5); } .attachment-preview-row { @@ -38,7 +41,6 @@ .attachment-preview { border-radius: 8px; - overflow: hidden; border: 1px solid alpha(@borders, 0.5); } diff --git a/src/transcript/layouts/image-bubble-layout.vala b/src/transcript/layouts/image-bubble-layout.vala index 2e25641..ea3feea 100644 --- a/src/transcript/layouts/image-bubble-layout.vala +++ b/src/transcript/layouts/image-bubble-layout.vala @@ -1,5 +1,27 @@ +using Gee; using Gtk; +private class SizeCache +{ + private static SizeCache instance = null; + private HashMap size_cache = new HashMap(); + + public static SizeCache get_instance() { + if (instance == null) { + instance = new SizeCache(); + } + return instance; + } + + public Graphene.Size? get_size(string image_path) { + return size_cache.get(image_path); + } + + public void set_size(string image_path, Graphene.Size size) { + size_cache.set(image_path, size); + } +} + private class ImageBubbleLayout : BubbleLayout { public string image_path; @@ -25,6 +47,12 @@ private class ImageBubbleLayout : BubbleLayout return; } + var cached_size = SizeCache.get_instance().get_size(image_path); + if (cached_size != null) { + this.image_size = cached_size; + return; + } + // Try to load the image to get its dimensions try { warning("No image size provided, loading image to get dimensions"); @@ -34,6 +62,7 @@ private class ImageBubbleLayout : BubbleLayout var original_height = (float)texture.get_height(); this.image_size = Graphene.Size() { width = original_width, height = original_height }; + SizeCache.get_instance().set_size(image_path, this.image_size); } catch (Error e) { // Fallback dimensions if image can't be loaded warning("Failed to load image %s: %s", image_path, e.message); @@ -57,13 +86,26 @@ private class ImageBubbleLayout : BubbleLayout } } + private float intrinsic_height { + get { + var scale_factor = float.min(max_width / image_size.width, 1.0f); + return image_size.height * scale_factor; + } + } + + private float intrinsic_width { + get { + var scale_factor = float.min(max_width / image_size.width, 1.0f); + return image_size.width * scale_factor; + } + } + public override float get_height() { - var scale_factor = float.min(max_width / image_size.width, 1.0f); - return image_size.height * scale_factor; + return float.max(intrinsic_height, 100.0f); } public override float get_width() { - return float.min(image_size.width, max_width); + return float.max(intrinsic_width, 200.0f); } public override void draw_content(Snapshot snapshot) { @@ -73,9 +115,15 @@ private class ImageBubbleLayout : BubbleLayout var image_rect = Graphene.Rect () { origin = Graphene.Point() { x = 0, y = 0 }, - size = Graphene.Size() { width = get_width(), height = get_height() } + size = Graphene.Size() { width = intrinsic_width, height = intrinsic_height } }; + // Center image in the bubble (if it's smaller than the bubble) + snapshot.translate(Graphene.Point() { + x = (get_width() - intrinsic_width) / 2, + y = (get_height() - intrinsic_height) / 2 + }); + if (cached_texture != null) { snapshot.append_texture(cached_texture, image_rect); } else { diff --git a/src/transcript/transcript-container-view.vala b/src/transcript/transcript-container-view.vala index 4da4f65..1253122 100644 --- a/src/transcript/transcript-container-view.vala +++ b/src/transcript/transcript-container-view.vala @@ -1,6 +1,8 @@ using Gtk; using Adw; using Gee; +using Gdk; +using GLib; class TranscriptContainerView : Adw.Bin { @@ -11,14 +13,17 @@ class TranscriptContainerView : Adw.Bin private Button send_button; private FlowBox attachment_flow_box; - private Entry message_entry; + private TextView message_view; + private TextBuffer message_buffer; private HashSet pending_uploads; private HashMap attachment_previews; private ArrayList completed_attachments; public string message_body { - get { - return message_entry.text; + owned get { + TextIter start_iter, end_iter; + message_buffer.get_bounds(out start_iter, out end_iter); + return message_buffer.get_text(start_iter, end_iter, false); } } @@ -36,7 +41,7 @@ class TranscriptContainerView : Adw.Bin private bool can_send { get { - return (message_entry.text.length > 0 || completed_attachments.size > 0) && pending_uploads.size == 0; + return (message_body.length > 0 || completed_attachments.size > 0) && pending_uploads.size == 0; } } @@ -69,14 +74,38 @@ class TranscriptContainerView : Adw.Bin // Connect to repository signals Repository.get_instance().attachment_uploaded.connect(on_attachment_uploaded); - // Create message entry - message_entry = new Entry(); - message_entry.add_css_class("message-input-entry"); - message_entry.set_placeholder_text("Type a message..."); - message_entry.set_hexpand(true); - message_entry.changed.connect(on_text_changed); - message_entry.activate.connect(on_request_send); - input_box.append(message_entry); + // Create attach button (paperclip) + var attach_button = new Button.from_icon_name("mail-attachment"); + attach_button.set_tooltip_text("Attach file…"); + attach_button.add_css_class("flat"); + attach_button.clicked.connect(on_attach_button_clicked); + input_box.append(attach_button); + + // Create message text view (added after attachment button so button stays to the left) + message_buffer = new TextBuffer(null); + message_view = new TextView.with_buffer(message_buffer); + message_view.add_css_class("message-input-entry"); + message_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR); + message_view.set_hexpand(true); + message_view.set_vexpand(false); + message_view.set_size_request(-1, 12); // intrinsic + message_buffer.changed.connect(on_text_changed); + + // Key controller for sending on Enter (Shift+Enter for newline) + var send_key_ctrl = new EventControllerKey(); + send_key_ctrl.key_pressed.connect((keyval, keycode, state) => { + if (keyval == Gdk.Key.Return && (state & Gdk.ModifierType.SHIFT_MASK) == 0) { + on_request_send(); + return true; // consume + } + return false; + }); + message_view.add_controller(send_key_ctrl); + + // Handle paste events to detect images + message_view.paste_clipboard.connect(on_message_paste_clipboard); + + input_box.append(message_view); // Create send button send_button = new Button(); @@ -196,8 +225,8 @@ class TranscriptContainerView : Adw.Bin if (can_send) { on_send(this); - // Clear the message entry - message_entry.text = ""; + // Clear the message text + message_buffer.set_text(""); // Clear the attachment previews attachment_flow_box.remove_all(); @@ -208,6 +237,65 @@ class TranscriptContainerView : Adw.Bin update_send_button_sensitivity(); } } + + private void on_attach_button_clicked() { + var dialog = new Gtk.FileDialog(); + dialog.set_title("Select attachment"); + dialog.set_accept_label("Attach"); + dialog.set_modal(true); + + // Images only for now + var filter = new Gtk.FileFilter(); + filter.set_filter_name("Images"); + filter.add_mime_type("image/png"); + filter.add_mime_type("image/jpeg"); + filter.add_mime_type("image/gif"); + filter.add_mime_type("image/bmp"); + filter.add_mime_type("image/webp"); + filter.add_mime_type("image/svg+xml"); + filter.add_mime_type("image/tiff"); + + dialog.set_default_filter(filter); + + var parent_window = get_root() as Gtk.Window; + dialog.open.begin(parent_window, null, (obj, res) => { + try { + var file = dialog.open.end(res); + if (file != null) { + upload_file(file); + } + } catch (Error e) { + warning("Failed to open file dialog: %s", e.message); + } + }); + } + + private void on_message_paste_clipboard() { + var display = get_display(); + if (display == null) { + return; + } + + var clipboard = display.get_clipboard(); + if (clipboard == null) { + return; + } + + clipboard.read_texture_async.begin(null, (obj, res) => { + try { + var clip = obj as Gdk.Clipboard; + var texture = clip.read_texture_async.end(res); + if (texture != null) { + string tmp_path = Path.build_filename(Environment.get_tmp_dir(), "clipboard-" + Uuid.string_random() + ".png"); + texture.save_to_png(tmp_path); + var tmp_file = File.new_for_path(tmp_path); + upload_file(tmp_file); + } + } catch (Error e) { + // Ignore if clipboard does not contain image + } + }); + } } class UploadedAttachment diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 6507301..67e8524 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -104,9 +104,11 @@ private class TranscriptDrawingArea : Widget } // Text Bubble - var text_bubble = new TextBubbleLayout(message, this, max_width); - text_bubble.vertical_padding = (last_sender == message.sender) ? 0.0f : 10.0f; - items.add(text_bubble); + if (message.text.length > 0 && !message.is_attachment_marker) { + var text_bubble = new TextBubbleLayout(message, this, max_width); + text_bubble.vertical_padding = (last_sender == message.sender) ? 0.0f : 10.0f; + items.add(text_bubble); + } // Check for attachments. For each one, add an image layout bubble foreach (var attachment in message.attachments) { diff --git a/src/transcript/transcript-view.vala b/src/transcript/transcript-view.vala index e2555cc..b05170f 100644 --- a/src/transcript/transcript-view.vala +++ b/src/transcript/transcript-view.vala @@ -50,6 +50,9 @@ public class TranscriptView : Adw.Bin public TranscriptView() { container = new Adw.ToolbarView(); set_child(container); + + // Set minimum width for the transcript view + set_size_request(330, -1); scrolled_window.set_child(transcript_drawing_area); scrolled_window.add_css_class("message-list-scroller"); From 2d43b878391fa1755c81c04bed99c42d9f5e7ec8 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 12 Jun 2025 20:36:18 -0700 Subject: [PATCH 34/63] add CLAUDE.md --- CLAUDE.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..99ff308 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,58 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +```bash +# Setup build directory (first time) +meson setup builddir + +# Build project +meson compile -C builddir + +# Clean rebuild +rm -rf builddir && meson setup builddir && meson compile -C builddir + +# Run the application +./builddir/src/kordophone +``` + +## Architecture Overview + +Kordophone is a GTK4/Libadwaita messaging client written in Vala that acts as a frontend to a separate DBus daemon (`kordophonecd`). The application follows a clean separation of concerns: + +### Core Architecture +- **DBus Client**: This GTK app connects to `net.buzzert.kordophonecd` daemon over DBus +- **Split UI**: Main window has conversation list sidebar + transcript view +- **Custom Rendering**: Transcript uses Gtk.DrawingArea with snapshot API for performance +- **Repository Pattern**: All data operations go through `Repository` singleton which proxies to DBus service + +### Key Components +- **Application** (`src/application/`): App lifecycle, main window, preferences +- **Service** (`src/service/`): DBus integration, data repository, settings management +- **Conversation List** (`src/conversation-list/`): Sidebar with conversation list and search +- **Transcript** (`src/transcript/`): Message display with pluggable bubble layouts +- **Models** (`src/models/`): Data structures (Conversation, Message, Attachment) + +### DBus Interface +The app depends on a running `kordophonecd` daemon that provides: +- **Repository interface**: Conversation/message CRUD, real-time sync via signals +- **Settings interface**: Server configuration, user credentials + +### Development Notes +- Vala compiles to C, build artifacts in `builddir/` +- DBus interfaces auto-generated from XML: run `src/service/interface/generate.sh` +- Resources bundled via GResource (`src/resources/kordophone.gresource.xml`) +- Custom CSS styling in `src/resources/style.css` +- Application ID: `net.buzzert.kordophone2` + +### Message Layout System +Transcript view uses a pluggable layout system in `src/transcript/layouts/`: +- `BubbleLayout`: Base class for message bubbles +- `TextBubbleLayout`: Text messages +- `ImageBubbleLayout`: Image attachments +- `DateItemLayout`: Date separators +- `SenderAnnotationLayout`: Sender labels + +All layouts render to Cairo context via Gtk snapshot API for efficient scrolling performance. \ No newline at end of file From 269271835fc5773ae0a338aac4bf809898d3523f Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 12 Jun 2025 20:47:36 -0700 Subject: [PATCH 35/63] bug fixes --- src/application/kordophone-application.vala | 1 - src/application/preferences-window.vala | 23 +++++++++---------- src/service/settings.vala | 10 +------- src/transcript/transcript-container-view.vala | 2 ++ 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/application/kordophone-application.vala b/src/application/kordophone-application.vala index 942bcb2..986b003 100644 --- a/src/application/kordophone-application.vala +++ b/src/application/kordophone-application.vala @@ -32,7 +32,6 @@ public class KordophoneApp : Adw.Application add_action(quit_action); // Warm up dbus connections - Settings.get_instance(); Repository.get_instance(); } diff --git a/src/application/preferences-window.vala b/src/application/preferences-window.vala index 76969fa..4f11f82 100644 --- a/src/application/preferences-window.vala +++ b/src/application/preferences-window.vala @@ -37,9 +37,14 @@ public class PreferencesWindow : Adw.PreferencesDialog { password_row.title = "Password"; connection_group.add (password_row); - settings = Settings.get_instance(); + settings = new Settings(); settings.settings_ready.connect(load_settings); load_settings(); + + unowned var self = this; + closed.connect(() => { + self.save_settings(); + }); } private void load_settings() { @@ -50,21 +55,15 @@ public class PreferencesWindow : Adw.PreferencesDialog { } catch (Error e) { warning("Failed to load settings: %s", e.message); } - - setup_change_callbacks(); } - private void setup_change_callbacks() { - server_url_row.changed.connect(() => { + private void save_settings() { + try { settings.set_server_url(server_url_row.text); - }); - - username_row.changed.connect(() => { settings.set_username(username_row.text); - }); - - password_row.changed.connect(() => { settings.set_password(password_row.text); - }); + } catch (Error e) { + warning("Failed to save settings: %s", e.message); + } } } \ No newline at end of file diff --git a/src/service/settings.vala b/src/service/settings.vala index a8d5674..7016e2a 100644 --- a/src/service/settings.vala +++ b/src/service/settings.vala @@ -5,18 +5,10 @@ public class Settings : DBusServiceProxy public signal void config_changed(); public signal void settings_ready(); - public static Settings get_instance() { - if (instance == null) { - instance = new Settings(); - } - return instance; - } - - private static Settings instance = null; private DBusService.Settings? dbus_settings; private Secret.Service secret_service; - private Settings() { + public Settings() { base(); try { diff --git a/src/transcript/transcript-container-view.vala b/src/transcript/transcript-container-view.vala index 1253122..be9e08a 100644 --- a/src/transcript/transcript-container-view.vala +++ b/src/transcript/transcript-container-view.vala @@ -202,6 +202,8 @@ class TranscriptContainerView : Adw.Bin return true; }); + + pending_uploads.remove(upload_guid); update_attachment_row_visibility(); update_send_button_sensitivity(); From 1420d96a2014c6f40a22b9d9f15b5cc159d0dc0c Mon Sep 17 00:00:00 2001 From: James Magahern Date: Mon, 12 May 2025 20:46:12 -0700 Subject: [PATCH 36/63] TranscriptView: ellipsize title --- src/transcript/transcript-view.vala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/transcript/transcript-view.vala b/src/transcript/transcript-view.vala index b05170f..1c0d9bb 100644 --- a/src/transcript/transcript-view.vala +++ b/src/transcript/transcript-view.vala @@ -59,6 +59,8 @@ public class TranscriptView : Adw.Bin container.set_content(scrolled_window); var header_bar = new Adw.HeaderBar(); + title_label.single_line_mode = true; + title_label.ellipsize = Pango.EllipsizeMode.END; header_bar.set_title_widget(title_label); container.add_top_bar(header_bar); From 741932c67d14781813014964e145050a008f8526 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 13 Jun 2025 17:13:04 -0700 Subject: [PATCH 37/63] Implement resync after update monitor reconnect --- .../conversation-list-model.vala | 19 +++++++++++++++++-- .../conversation-list-view.vala | 8 +++++--- src/service/interface/dbusservice.vala | 3 +++ .../xml/net.buzzert.kordophonecd.Server.xml | 5 +++++ src/service/repository.vala | 14 ++++++++++++++ src/transcript/message-list-model.vala | 10 +++++++++- 6 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/conversation-list/conversation-list-model.vala b/src/conversation-list/conversation-list-model.vala index d50f983..dc87912 100644 --- a/src/conversation-list/conversation-list-model.vala +++ b/src/conversation-list/conversation-list-model.vala @@ -15,8 +15,23 @@ public class ConversationListModel : Object, ListModel return (int)(b.date - a.date); }); - Repository.get_instance().conversations_updated.connect(load_conversations); - Repository.get_instance().messages_updated.connect(load_conversations); + weak ConversationListModel self = this; + Repository.get_instance().conversations_updated.connect(() => { + self.load_conversations(); + }); + + Repository.get_instance().messages_updated.connect((conversation_guid) => { + self.load_conversations(); + }); + + Repository.get_instance().reconnected.connect(() => { + // Trigger a sync-list to get the latest conversations + try { + Repository.get_instance().sync_conversation_list(); + } catch (GLib.Error e) { + warning("Failed to sync conversation list: %s", e.message); + } + }); } public Conversation? get_conversation(string guid) { diff --git a/src/conversation-list/conversation-list-view.vala b/src/conversation-list/conversation-list-view.vala index 739a1d8..7cdf069 100644 --- a/src/conversation-list/conversation-list-view.vala +++ b/src/conversation-list/conversation-list-view.vala @@ -44,7 +44,7 @@ public class ConversationListView : Adw.Bin var app_menu = new Menu (); var section = new Menu (); - section.append ("Refresh", "list.refresh"); + section.append ("Manual Sync", "list.refresh"); section.append ("Settings...", "win.settings"); app_menu.append_section (null, section); @@ -54,8 +54,10 @@ public class ConversationListView : Adw.Bin var refresh_action = new SimpleAction("refresh", null); refresh_action.activate.connect (() => { - if (conversation_model != null) { - conversation_model.load_conversations (); + try { + Repository.get_instance().sync_conversation_list(); + } catch (GLib.Error e) { + warning("Failed to sync conversation list: %s", e.message); } }); diff --git a/src/service/interface/dbusservice.vala b/src/service/interface/dbusservice.vala index f352da2..63879cf 100644 --- a/src/service/interface/dbusservice.vala +++ b/src/service/interface/dbusservice.vala @@ -53,6 +53,9 @@ namespace DBusService { [DBus (name = "MessagesUpdated")] public signal void messages_updated(string conversation_id); + [DBus (name = "UpdateStreamReconnected")] + public signal void update_stream_reconnected(); + [DBus (name = "GetAttachmentInfo")] public abstract RepositoryAttachmentInfoStruct get_attachment_info(string attachment_id) throws DBusError, IOError; diff --git a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml index 8e93ca5..a0898e3 100644 --- a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml +++ b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml @@ -103,6 +103,11 @@ value="Emitted when the list of messages is updated."/> + + + + diff --git a/src/service/repository.vala b/src/service/repository.vala index f89f757..59aeab1 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -6,6 +6,7 @@ public class Repository : DBusServiceProxy { public signal void messages_updated(string conversation_guid); public signal void attachment_downloaded(string attachment_guid); public signal void attachment_uploaded(string upload_guid, string attachment_guid); + public signal void reconnected(); public static Repository get_instance() { if (instance == null) { @@ -46,11 +47,24 @@ public class Repository : DBusServiceProxy { attachment_uploaded(upload_guid, attachment_guid); }); + this.dbus_repository.update_stream_reconnected.connect(() => { + reconnected(); + }); + conversations_updated(); + reconnected(); } catch (GLib.Error e) { warning("Failed to connect to repository: %s", e.message); } } + + public void sync_conversation_list() throws DBusServiceProxyError, GLib.Error { + if (dbus_repository == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); + } + + dbus_repository.sync_conversation_list(); + } public Conversation[] get_conversations(int limit = 200) throws DBusServiceProxyError, GLib.Error { if (dbus_repository == null) { diff --git a/src/transcript/message-list-model.vala b/src/transcript/message-list-model.vala index 25c58fb..7bf111e 100644 --- a/src/transcript/message-list-model.vala +++ b/src/transcript/message-list-model.vala @@ -34,7 +34,15 @@ public class MessageListModel : Object, ListModel public void watch_updates() { if (this.handler_id == 0) { - this.handler_id = Repository.get_instance().messages_updated.connect(got_messages_updated); + weak MessageListModel self = this; + this.handler_id = Repository.get_instance().messages_updated.connect((conversation_guid) => { + self.got_messages_updated(conversation_guid); + }); + + this.handler_id = Repository.get_instance().reconnected.connect(() => { + // On reconnect, reload the messages that we're looking at now. + self.load_messages(); + }); } } From 9c013c370248715aff8bbd72d1906f69296f5cd9 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 13 Jun 2025 17:14:23 -0700 Subject: [PATCH 38/63] Strip space before sending messages --- src/application/main-window.vala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/application/main-window.vala b/src/application/main-window.vala index 499f30e..4d70318 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -57,6 +57,14 @@ public class MainWindow : Adw.ApplicationWindow var body = view.message_body; var attachment_guids = view.attachment_guids; + // Strip empty space at the beginning and end of the body + body = body.strip(); + + if (body.length == 0) { + GLib.warning("Message body is empty"); + return; + } + if (transcript_container_view.transcript_view.model == null) { GLib.warning("No conversation selected"); return; From 4d466f0d26d0d37da50f143dbd3e3954c5128f9c Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 13 Jun 2025 17:48:50 -0700 Subject: [PATCH 39/63] re-fix the issue of accumulating message list models --- src/transcript/message-list-model.vala | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/transcript/message-list-model.vala b/src/transcript/message-list-model.vala index 7bf111e..bd43b6f 100644 --- a/src/transcript/message-list-model.vala +++ b/src/transcript/message-list-model.vala @@ -19,7 +19,8 @@ public class MessageListModel : Object, ListModel private ArrayList _messages; private HashSet participants = new HashSet(); - private ulong handler_id = 0; + private ulong update_handler_id = 0; + private ulong reconnected_handler_id = 0; public MessageListModel(string conversation_guid) { _messages = new ArrayList(); @@ -33,13 +34,16 @@ public class MessageListModel : Object, ListModel } public void watch_updates() { - if (this.handler_id == 0) { + if (this.update_handler_id == 0) { weak MessageListModel self = this; - this.handler_id = Repository.get_instance().messages_updated.connect((conversation_guid) => { + this.update_handler_id = Repository.get_instance().messages_updated.connect((conversation_guid) => { self.got_messages_updated(conversation_guid); }); + } - this.handler_id = Repository.get_instance().reconnected.connect(() => { + if (this.reconnected_handler_id == 0) { + weak MessageListModel self = this; + this.reconnected_handler_id = Repository.get_instance().reconnected.connect(() => { // On reconnect, reload the messages that we're looking at now. self.load_messages(); }); @@ -47,9 +51,14 @@ public class MessageListModel : Object, ListModel } public void unwatch_updates() { - if (this.handler_id != 0) { - Repository.get_instance().disconnect(this.handler_id); - this.handler_id = 0; + if (this.update_handler_id != 0) { + Repository.get_instance().disconnect(this.update_handler_id); + this.update_handler_id = 0; + } + + if (this.reconnected_handler_id != 0) { + Repository.get_instance().disconnect(this.reconnected_handler_id); + this.reconnected_handler_id = 0; } } From 2db0e3136eb163d2c7d9dda43bf64015b32f6cf8 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 14 Jun 2025 00:14:58 -0700 Subject: [PATCH 40/63] Some metrics tweaks for my laptop --- src/transcript/layouts/bubble-layout.vala | 11 ++++++++--- .../layouts/sender-annotation-layout.vala | 2 +- src/transcript/layouts/text-bubble-layout.vala | 14 +++++++------- src/transcript/transcript-drawing-area.vala | 2 +- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/transcript/layouts/bubble-layout.vala b/src/transcript/layouts/bubble-layout.vala index a256c1a..725f2ef 100644 --- a/src/transcript/layouts/bubble-layout.vala +++ b/src/transcript/layouts/bubble-layout.vala @@ -6,15 +6,20 @@ private struct BubbleLayoutConstants { public float tail_side_offset; public float tail_bottom_padding; public float corner_radius; - public float text_padding; + public float text_x_padding; + public float text_y_padding; public BubbleLayoutConstants(float scale_factor) { + // TODO: Remove this, scale factor ignored for now. + scale_factor = 2.0f; + tail_width = 15.0f / scale_factor; tail_curve_offset = 2.5f / scale_factor; tail_side_offset = 0.0f / scale_factor; tail_bottom_padding = 4.0f / scale_factor; - corner_radius = 24.0f / scale_factor; - text_padding = 18.0f / scale_factor; + corner_radius = 34.0f / scale_factor; + text_x_padding = 31.0f / scale_factor; + text_y_padding = 8.0f / scale_factor; } } diff --git a/src/transcript/layouts/sender-annotation-layout.vala b/src/transcript/layouts/sender-annotation-layout.vala index b078d48..dac8e57 100644 --- a/src/transcript/layouts/sender-annotation-layout.vala +++ b/src/transcript/layouts/sender-annotation-layout.vala @@ -40,7 +40,7 @@ private class SenderAnnotationLayout : Object, ChatItemLayout snapshot.translate(Graphene.Point() { - x = constants.text_padding + constants.tail_width, + x = constants.text_x_padding + constants.tail_width, y = get_height() - logical_rect.height, }); diff --git a/src/transcript/layouts/text-bubble-layout.vala b/src/transcript/layouts/text-bubble-layout.vala index 6d685f2..8d1fa8f 100644 --- a/src/transcript/layouts/text-bubble-layout.vala +++ b/src/transcript/layouts/text-bubble-layout.vala @@ -19,11 +19,10 @@ private class TextBubbleLayout : BubbleLayout // Create font description from system font var font_desc = Pango.FontDescription.from_string(font_name); - - var size = font_desc.get_size(); - font_desc.set_size((int)(size)); + font_desc.set_size((int)(12 * Pango.SCALE)); layout.set_font_description(font_desc); layout.set_wrap(Pango.WrapMode.WORD_CHAR); + layout.set_line_spacing(1.18f); // Set max width layout.set_width((int)text_available_width * Pango.SCALE); @@ -31,20 +30,20 @@ private class TextBubbleLayout : BubbleLayout private float text_available_width { get { - return max_width - text_x_offset - constants.text_padding; + return max_width - text_x_offset - constants.text_x_padding; } } private float text_x_offset { get { - return from_me ? constants.text_padding : constants.tail_width + constants.text_padding; + return from_me ? constants.text_x_padding : constants.tail_width + constants.text_x_padding; } } private float text_x_padding { get { // Opposite of text_x_offset - return from_me ? constants.tail_width + constants.text_padding : constants.text_padding; + return from_me ? constants.tail_width + constants.text_x_padding : constants.text_x_padding; } } @@ -63,7 +62,7 @@ private class TextBubbleLayout : BubbleLayout Pango.Rectangle ink_rect, logical_rect; layout.get_pixel_extents(out ink_rect, out logical_rect); - return logical_rect.height + constants.corner_radius + constants.tail_bottom_padding; + return logical_rect.height + constants.corner_radius + constants.tail_bottom_padding + constants.text_y_padding; } public override float get_width() { @@ -78,6 +77,7 @@ private class TextBubbleLayout : BubbleLayout Pango.Rectangle ink_rect, logical_rect; layout.get_pixel_extents(out ink_rect, out logical_rect); + snapshot.translate(Graphene.Point() { x = text_x_offset, y = ((get_height() - constants.tail_bottom_padding) - logical_rect.height) / 2 diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 67e8524..39e9048 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -106,7 +106,7 @@ private class TranscriptDrawingArea : Widget // Text Bubble if (message.text.length > 0 && !message.is_attachment_marker) { var text_bubble = new TextBubbleLayout(message, this, max_width); - text_bubble.vertical_padding = (last_sender == message.sender) ? 0.0f : 10.0f; + text_bubble.vertical_padding = (last_sender == message.sender) ? 4.0f : 10.0f; items.add(text_bubble); } From c70ae00d5b7fbc733173dcf75aad87b35391ee1b Mon Sep 17 00:00:00 2001 From: James Magahern Date: Mon, 16 Jun 2025 20:09:56 -0700 Subject: [PATCH 41/63] transcriptview perf: only draw the items that are actually visible. --- src/transcript/transcript-drawing-area.vala | 75 ++++++++++++++++----- src/transcript/transcript-view.vala | 12 +++- 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 39e9048..dd29b9c 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -4,12 +4,25 @@ using Gee; private class TranscriptDrawingArea : Widget { public bool show_sender = true; + public Adjustment? viewport { + get { + return _viewport; + } + + set { + _viewport = value; + queue_draw(); + } + } private ArrayList _messages = new ArrayList(); private ArrayList _chat_items = new ArrayList(); + private Adjustment? _viewport = null; private const float bubble_margin = 18.0f; + private const bool debug_viewport = false; + public TranscriptDrawingArea() { add_css_class("transcript-drawing-area"); } @@ -49,35 +62,65 @@ private class TranscriptDrawingArea : Widget base.size_allocate(width, height, baseline); recompute_message_layouts(); } - + public override void snapshot(Snapshot snapshot) { - float y_offset = 0; - var container_width = get_width(); + const int viewport_y_padding = 50; + var viewport_rect = Graphene.Rect() { + origin = Graphene.Point() { x = 0, y = ((int)viewport.value - viewport_y_padding) }, + size = Graphene.Size() { width = get_width(), height = (int)viewport.page_size + (viewport_y_padding * 2) } + }; + + if (debug_viewport) { + // Draw viewport outline for debugging + var color = Gdk.RGBA() { red = 1.0f, green = 0, blue = 0, alpha = 1.0f }; + snapshot.append_border( + Gsk.RoundedRect().init_from_rect(viewport_rect, 0), + { 1, 1, 1, 1 }, + { color, color, color, color } + ); + } // Draw each item in reverse order, since the messages are in reverse order + float y_offset = 0; + int container_width = get_width(); for (int i = _chat_items.size - 1; i >= 0; i--) { var chat_item = _chat_items[i]; var item_width = chat_item.get_width(); var item_height = chat_item.get_height(); - snapshot.save(); - - // Flip the y-axis, since our parent is upside down (so newest messages are at the bottom) - snapshot.scale(1.0f, -1.0f); - - // Translate to the correct position - snapshot.translate(Graphene.Point() { + var origin = Graphene.Point() { x = (chat_item.from_me ? (container_width - item_width - bubble_margin) : bubble_margin), y = y_offset - }); + }; - // Undo the y-axis flip, origin is top left - snapshot.translate(Graphene.Point() { x = 0, y = -item_height }); + var size = Graphene.Size() { + width = item_width, + height = item_height + }; - chat_item.draw(snapshot); - snapshot.restore(); + var rect = Graphene.Rect() { + origin = origin, + size = size + }; - y_offset -= item_height + chat_item.vertical_padding; + // Skip drawing if this item is not in the viewport + if (viewport_rect.intersection(rect, null)) { + snapshot.save(); + + // Translate to the correct position + snapshot.translate(origin); + + // Flip the y-axis, since our parent is upside down (so newest messages are at the bottom) + snapshot.scale(1.0f, -1.0f); + + // Undo the y-axis flip, origin is top left + snapshot.translate(Graphene.Point() { x = 0, y = -item_height }); + + chat_item.draw(snapshot); + snapshot.restore(); + } + + y_offset += item_height + chat_item.vertical_padding; } } diff --git a/src/transcript/transcript-view.vala b/src/transcript/transcript-view.vala index 1c0d9bb..a081bb2 100644 --- a/src/transcript/transcript-view.vala +++ b/src/transcript/transcript-view.vala @@ -17,8 +17,10 @@ public class TranscriptView : Adw.Bin _model = value; if (value != null) { - // Reset scroll position - scrolled_window.vadjustment = new Gtk.Adjustment(0, 0, 0, 0, 0, 0); + // Reset scroll position by updating the existing adjustment + scrolled_window.vadjustment.value = 0; + scrolled_window.vadjustment.upper = 0; + scrolled_window.vadjustment.page_size = 0; weak TranscriptView self = this; messages_changed_handler_id = value.messages_changed.connect(() => { @@ -56,8 +58,14 @@ public class TranscriptView : Adw.Bin scrolled_window.set_child(transcript_drawing_area); scrolled_window.add_css_class("message-list-scroller"); + transcript_drawing_area.viewport = scrolled_window.vadjustment; container.set_content(scrolled_window); + // Connect to the adjustment's value_changed signal + scrolled_window.vadjustment.value_changed.connect(() => { + transcript_drawing_area.viewport = scrolled_window.vadjustment; + }); + var header_bar = new Adw.HeaderBar(); title_label.single_line_mode = true; title_label.ellipsize = Pango.EllipsizeMode.END; From 54ca00189200546c7908448346111be029d9fdf2 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 17 Jun 2025 00:47:03 -0700 Subject: [PATCH 42/63] Adds incoming bubble animations --- src/meson.build | 1 + src/models/message.vala | 21 +++- src/transcript/layouts/bubble-layout.vala | 1 + src/transcript/layouts/chat-item-layout.vala | 1 + src/transcript/layouts/date-item-layout.vala | 3 +- .../layouts/sender-annotation-layout.vala | 1 + src/transcript/message-list-model.vala | 12 +- src/transcript/transcript-drawing-area.vala | 107 +++++++++++++++++- 8 files changed, 141 insertions(+), 6 deletions(-) diff --git a/src/meson.build b/src/meson.build index f1ec3f0..f60fed6 100644 --- a/src/meson.build +++ b/src/meson.build @@ -51,5 +51,6 @@ executable('kordophone', resources, dependencies : dependencies, vala_args: ['--pkg', 'posix'], + link_args: ['-lm'], install : true ) \ No newline at end of file diff --git a/src/models/message.vala b/src/models/message.vala index 7fa7886..e3cf1d6 100644 --- a/src/models/message.vala +++ b/src/models/message.vala @@ -1,6 +1,7 @@ using GLib; +using Gee; -public class Message : Object +public class Message : Object, Comparable, Hashable { public string guid { get; set; default = ""; } public string text { get; set; default = ""; } @@ -9,6 +10,8 @@ public class Message : Object public Attachment[] attachments { get; set; default = {}; } + public bool should_animate = false; + public bool from_me { get { // Hm, this may have been accidental. @@ -52,4 +55,20 @@ public class Message : Object this.attachments = attachments.to_array(); } + + public int compare_to(Message other) { + if (guid == other.guid) { + return 0; + } + + return 1; + } + + public bool equal_to(Message other) { + return guid == other.guid; + } + + public uint hash() { + return guid.hash(); + } } \ No newline at end of file diff --git a/src/transcript/layouts/bubble-layout.vala b/src/transcript/layouts/bubble-layout.vala index 725f2ef..c812cbe 100644 --- a/src/transcript/layouts/bubble-layout.vala +++ b/src/transcript/layouts/bubble-layout.vala @@ -27,6 +27,7 @@ private abstract class BubbleLayout : Object, ChatItemLayout { public bool from_me { get; set; } public float vertical_padding { get; set; } + public string id { get; set; } protected float max_width; protected Widget parent; diff --git a/src/transcript/layouts/chat-item-layout.vala b/src/transcript/layouts/chat-item-layout.vala index e1f5a06..8623e03 100644 --- a/src/transcript/layouts/chat-item-layout.vala +++ b/src/transcript/layouts/chat-item-layout.vala @@ -4,6 +4,7 @@ interface ChatItemLayout : Object { public abstract bool from_me { get; set; } public abstract float vertical_padding { get; set; } + public abstract string id { get; set; } public abstract float get_height(); public abstract float get_width(); diff --git a/src/transcript/layouts/date-item-layout.vala b/src/transcript/layouts/date-item-layout.vala index 4e18ee2..c9661ce 100644 --- a/src/transcript/layouts/date-item-layout.vala +++ b/src/transcript/layouts/date-item-layout.vala @@ -3,6 +3,7 @@ using Gtk; class DateItemLayout : Object, ChatItemLayout { public bool from_me { get; set; } public float vertical_padding { get; set; } + public string id { get; set; } private Pango.Layout layout; private float max_width; @@ -14,7 +15,7 @@ class DateItemLayout : Object, ChatItemLayout { layout.set_font_description(Pango.FontDescription.from_string("Sans 9")); layout.set_alignment(Pango.Alignment.CENTER); } - + public float get_height() { Pango.Rectangle ink_rect, logical_rect; layout.get_pixel_extents(out ink_rect, out logical_rect); diff --git a/src/transcript/layouts/sender-annotation-layout.vala b/src/transcript/layouts/sender-annotation-layout.vala index dac8e57..d442836 100644 --- a/src/transcript/layouts/sender-annotation-layout.vala +++ b/src/transcript/layouts/sender-annotation-layout.vala @@ -6,6 +6,7 @@ private class SenderAnnotationLayout : Object, ChatItemLayout public float max_width; public bool from_me { get; set; default = false; } public float vertical_padding { get; set; default = 0.0f; } + public string id { get; set; } private Pango.Layout layout; private BubbleLayoutConstants constants; diff --git a/src/transcript/message-list-model.vala b/src/transcript/message-list-model.vala index bd43b6f..445f6a3 100644 --- a/src/transcript/message-list-model.vala +++ b/src/transcript/message-list-model.vala @@ -63,7 +63,12 @@ public class MessageListModel : Object, ListModel } public void load_messages() { + var previous_messages = new HashSet(); + previous_messages.add_all(_messages); + try { + bool first_load = _messages.size == 0; + Message[] messages = Repository.get_instance().get_messages(conversation_guid); // Clear existing set @@ -81,9 +86,14 @@ public class MessageListModel : Object, ListModel for (int i = 0; i < messages.length; i++) { var message = messages[i]; - _messages.add(message); participants.add(message.sender); + if (!first_load && !previous_messages.contains(message)) { + // This is a new message according to the UI, schedule an animation for it. + message.should_animate = true; + } + + _messages.add(message); position++; } diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index dd29b9c..883de0a 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -1,5 +1,6 @@ using Gtk; using Gee; +using Gdk; private class TranscriptDrawingArea : Widget { @@ -22,6 +23,8 @@ private class TranscriptDrawingArea : Widget private const float bubble_margin = 18.0f; private const bool debug_viewport = false; + private uint? _tick_callback_id = null; + private HashMap _animations = new HashMap(); public TranscriptDrawingArea() { add_css_class("transcript-drawing-area"); @@ -63,7 +66,7 @@ private class TranscriptDrawingArea : Widget recompute_message_layouts(); } - public override void snapshot(Snapshot snapshot) { + public override void snapshot(Gtk.Snapshot snapshot) { const int viewport_y_padding = 50; var viewport_rect = Graphene.Rect() { origin = Graphene.Point() { x = 0, y = ((int)viewport.value - viewport_y_padding) }, @@ -104,9 +107,22 @@ private class TranscriptDrawingArea : Widget }; // Skip drawing if this item is not in the viewport + float height_offset = 0.0f; if (viewport_rect.intersection(rect, null)) { snapshot.save(); + var pushed_opacity = false; + if (_animations.has_key(chat_item.id)) { + var animation = _animations[chat_item.id]; + + var item_height_offset = (float) (-item_height * (1.0 - animation.progress)); + snapshot.translate(Graphene.Point() { x = 0, y = item_height_offset }); + height_offset = item_height_offset; + + snapshot.push_opacity(animation.progress); + pushed_opacity = true; + } + // Translate to the correct position snapshot.translate(origin); @@ -117,17 +133,64 @@ private class TranscriptDrawingArea : Widget snapshot.translate(Graphene.Point() { x = 0, y = -item_height }); chat_item.draw(snapshot); + + if (pushed_opacity) { + snapshot.pop(); + } + snapshot.restore(); } - y_offset += item_height + chat_item.vertical_padding; + y_offset += item_height + chat_item.vertical_padding + height_offset; } + + animation_tick(); + } + + private bool animation_tick() { + HashSet animations_to_remove = new HashSet(); + _animations.foreach(entry => { + var animation = entry.value; + animation.tick_animation(); + + if ((animation.progress - 1.0).abs() < 0.000001) { + animations_to_remove.add(entry.key); + } + + return true; + }); + + foreach (var key in animations_to_remove) { + _animations.unset(key); + } + + if (_animations.size > 0) { + queue_draw(); + } + + return _animations.size > 0; + } + + private bool on_frame_clock_tick(Gtk.Widget widget, Gdk.FrameClock frame_clock) { + return animation_tick(); + } + + private void start_animation(string id) { + if (_animations.has_key(id)) { + return; + } + + var animation = new ChatItemAnimation(get_frame_clock()); + animation.start_animation(0.18f); + _animations.set(id, animation); + + _tick_callback_id = add_tick_callback(on_frame_clock_tick); } private void recompute_message_layouts() { var container_width = get_width(); float max_width = container_width * 0.90f; - + DateTime? last_date = null; string? last_sender = null; ArrayList items = new ArrayList(); @@ -147,8 +210,14 @@ private class TranscriptDrawingArea : Widget } // Text Bubble + var animate = message.should_animate; if (message.text.length > 0 && !message.is_attachment_marker) { var text_bubble = new TextBubbleLayout(message, this, max_width); + text_bubble.id = @"text-$(message.guid)"; + if (animate) { + start_animation(text_bubble.id); + } + text_bubble.vertical_padding = (last_sender == message.sender) ? 4.0f : 10.0f; items.add(text_bubble); } @@ -164,6 +233,11 @@ private class TranscriptDrawingArea : Widget } var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size); + image_layout.id = @"image-$(attachment.guid)"; + if (animate) { + start_animation(image_layout.id); + } + image_layout.is_downloaded = attachment.preview_downloaded; items.add(image_layout); @@ -191,3 +265,30 @@ private class TranscriptDrawingArea : Widget queue_resize(); } } + +private class ChatItemAnimation +{ + public double progress = 0.0; + private int64 start_time = 0; + private double duration = 0.0; + private Gdk.FrameClock frame_clock = null; + + public ChatItemAnimation(Gdk.FrameClock frame_clock) { + this.frame_clock = frame_clock; + progress = 0.0f; + } + + public void start_animation(double duration) { + this.duration = duration; + this.progress = 0.0; + this.start_time = frame_clock.get_frame_time(); + } + + public void tick_animation() { + progress = ease_out_quart((frame_clock.get_frame_time() - start_time) / (duration * 1000000.0)); + } + + private static double ease_out_quart(double t) { + return 1.0 - Math.pow(1.0 - t, 4); + } +} \ No newline at end of file From 16102f9f9419655c0a4e07b4759d48543adf51c4 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 17 Jun 2025 00:53:37 -0700 Subject: [PATCH 43/63] Fix animation clamping issues --- src/transcript/transcript-drawing-area.vala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 883de0a..e100d27 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -273,6 +273,9 @@ private class ChatItemAnimation private double duration = 0.0; private Gdk.FrameClock frame_clock = null; + delegate double EaseFunction(double t); + private EaseFunction ease_function = ease_out_quart; + public ChatItemAnimation(Gdk.FrameClock frame_clock) { this.frame_clock = frame_clock; progress = 0.0f; @@ -285,7 +288,9 @@ private class ChatItemAnimation } public void tick_animation() { - progress = ease_out_quart((frame_clock.get_frame_time() - start_time) / (duration * 1000000.0)); + double t = (frame_clock.get_frame_time() - start_time) / (duration * 1000000.0); + t = double.min(1.0, double.max(0.0, t)); + progress = ease_function(t); } private static double ease_out_quart(double t) { From e1c579d23b90e4fb47185ffd1746d3d41279f0f3 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 17 Jun 2025 00:57:22 -0700 Subject: [PATCH 44/63] Text size: this really should just read the default --- src/transcript/layouts/text-bubble-layout.vala | 1 - 1 file changed, 1 deletion(-) diff --git a/src/transcript/layouts/text-bubble-layout.vala b/src/transcript/layouts/text-bubble-layout.vala index 8d1fa8f..a46d0ee 100644 --- a/src/transcript/layouts/text-bubble-layout.vala +++ b/src/transcript/layouts/text-bubble-layout.vala @@ -19,7 +19,6 @@ private class TextBubbleLayout : BubbleLayout // Create font description from system font var font_desc = Pango.FontDescription.from_string(font_name); - font_desc.set_size((int)(12 * Pango.SCALE)); layout.set_font_description(font_desc); layout.set_wrap(Pango.WrapMode.WORD_CHAR); layout.set_line_spacing(1.18f); From f0b7cff2267a227e815fd93bbbe0aa5e0005804a Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 17 Jun 2025 20:52:21 -0700 Subject: [PATCH 45/63] Remove this check: attachments could have no body --- src/application/main-window.vala | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/application/main-window.vala b/src/application/main-window.vala index 4d70318..cab37bd 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -60,11 +60,6 @@ public class MainWindow : Adw.ApplicationWindow // Strip empty space at the beginning and end of the body body = body.strip(); - if (body.length == 0) { - GLib.warning("Message body is empty"); - return; - } - if (transcript_container_view.transcript_view.model == null) { GLib.warning("No conversation selected"); return; @@ -82,4 +77,4 @@ public class MainWindow : Adw.ApplicationWindow GLib.warning("Failed to send message: %s", e.message); } } -} \ No newline at end of file +} From d33b50cfb5128e44d683196d48c4dc013d1dfc6d Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 18 Jun 2025 01:36:29 -0700 Subject: [PATCH 46/63] transcript: add copy right click action --- src/transcript/transcript-drawing-area.vala | 58 +++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index e100d27..8a002f7 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -22,12 +22,37 @@ private class TranscriptDrawingArea : Widget private Adjustment? _viewport = null; private const float bubble_margin = 18.0f; + private GestureClick _click_gesture = new GestureClick(); + private Gdk.Rectangle? _click_bounding_box = null; + private const bool debug_viewport = false; private uint? _tick_callback_id = null; private HashMap _animations = new HashMap(); public TranscriptDrawingArea() { add_css_class("transcript-drawing-area"); + + weak TranscriptDrawingArea self = this; + + _click_gesture.button = Gdk.BUTTON_SECONDARY; + _click_gesture.begin.connect(() => { + self.on_right_click(); + }); + add_controller(_click_gesture); + + SimpleAction copy_action = new SimpleAction("copy", null); + copy_action.activate.connect(() => { + if (_click_bounding_box != null) { + copy_message_at(_click_bounding_box); + } else { + GLib.warning("Failed to get bounding box for right click"); + } + }); + + SimpleActionGroup action_group = new SimpleActionGroup(); + action_group.add_action(copy_action); + + insert_action_group("transcript", action_group); } public void set_messages(ArrayList messages) { @@ -147,6 +172,39 @@ private class TranscriptDrawingArea : Widget animation_tick(); } + private void on_right_click() { + var menu_model = new Menu(); + menu_model.append("Copy", "transcript.copy"); + + Gdk.Rectangle? bounding_box = null; + if (_click_gesture.get_bounding_box(out bounding_box)) { + _click_bounding_box = bounding_box; + + var menu = new PopoverMenu.from_model(menu_model); + menu.set_position(PositionType.TOP); + menu.pointing_to = bounding_box; + menu.set_parent(this); + menu.popup(); + } + } + + private void copy_message_at(Gdk.Rectangle bounding_box) { + double y_offset = 0.0; + for (int i = _chat_items.size - 1; i >= 0; i--) { + var chat_item = _chat_items[i]; + y_offset += chat_item.get_height() + chat_item.vertical_padding; + + if (y_offset > bounding_box.y) { + var text_bubble = chat_item as TextBubbleLayout; + var text = text_bubble.message.text; + + var clipboard = get_clipboard(); + clipboard.set_text(text); + break; + } + } + } + private bool animation_tick() { HashSet animations_to_remove = new HashSet(); _animations.foreach(entry => { From 4170f13092a745f1ee175f6e0382e290c18d9977 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 18 Jun 2025 01:49:33 -0700 Subject: [PATCH 47/63] fixes crash when trying to copy image --- src/transcript/layouts/bubble-layout.vala | 4 +++- src/transcript/layouts/chat-item-layout.vala | 1 + src/transcript/layouts/date-item-layout.vala | 4 ++++ src/transcript/layouts/image-bubble-layout.vala | 4 ++++ src/transcript/layouts/sender-annotation-layout.vala | 4 ++++ src/transcript/layouts/text-bubble-layout.vala | 4 ++++ src/transcript/transcript-drawing-area.vala | 6 +----- 7 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/transcript/layouts/bubble-layout.vala b/src/transcript/layouts/bubble-layout.vala index c812cbe..dbda1a6 100644 --- a/src/transcript/layouts/bubble-layout.vala +++ b/src/transcript/layouts/bubble-layout.vala @@ -28,7 +28,7 @@ private abstract class BubbleLayout : Object, ChatItemLayout public bool from_me { get; set; } public float vertical_padding { get; set; } public string id { get; set; } - + protected float max_width; protected Widget parent; protected BubbleLayoutConstants constants; @@ -63,6 +63,8 @@ private abstract class BubbleLayout : Object, ChatItemLayout public abstract void draw_content(Snapshot snapshot); + public abstract void copy(Gdk.Clipboard clipboard); + private void draw_background(Snapshot snapshot) { var width = get_width(); diff --git a/src/transcript/layouts/chat-item-layout.vala b/src/transcript/layouts/chat-item-layout.vala index 8623e03..40c5f1e 100644 --- a/src/transcript/layouts/chat-item-layout.vala +++ b/src/transcript/layouts/chat-item-layout.vala @@ -10,4 +10,5 @@ interface ChatItemLayout : Object public abstract float get_width(); public abstract void draw(Snapshot snapshot); + public abstract void copy(Gdk.Clipboard clipboard); } diff --git a/src/transcript/layouts/date-item-layout.vala b/src/transcript/layouts/date-item-layout.vala index c9661ce..b147cd0 100644 --- a/src/transcript/layouts/date-item-layout.vala +++ b/src/transcript/layouts/date-item-layout.vala @@ -47,4 +47,8 @@ class DateItemLayout : Object, ChatItemLayout { snapshot.restore(); } + + public void copy(Gdk.Clipboard clipboard) { + clipboard.set_text(layout.get_text()); + } } \ No newline at end of file diff --git a/src/transcript/layouts/image-bubble-layout.vala b/src/transcript/layouts/image-bubble-layout.vala index ea3feea..09f5f4b 100644 --- a/src/transcript/layouts/image-bubble-layout.vala +++ b/src/transcript/layouts/image-bubble-layout.vala @@ -132,4 +132,8 @@ private class ImageBubbleLayout : BubbleLayout snapshot.restore(); } + + public override void copy(Gdk.Clipboard clipboard) { + clipboard.set_texture(cached_texture); + } } \ No newline at end of file diff --git a/src/transcript/layouts/sender-annotation-layout.vala b/src/transcript/layouts/sender-annotation-layout.vala index d442836..13f6fe6 100644 --- a/src/transcript/layouts/sender-annotation-layout.vala +++ b/src/transcript/layouts/sender-annotation-layout.vala @@ -54,4 +54,8 @@ private class SenderAnnotationLayout : Object, ChatItemLayout snapshot.restore(); } + + public void copy(Gdk.Clipboard clipboard) { + clipboard.set_text(sender); + } } \ No newline at end of file diff --git a/src/transcript/layouts/text-bubble-layout.vala b/src/transcript/layouts/text-bubble-layout.vala index a46d0ee..4b3fce2 100644 --- a/src/transcript/layouts/text-bubble-layout.vala +++ b/src/transcript/layouts/text-bubble-layout.vala @@ -91,6 +91,10 @@ private class TextBubbleLayout : BubbleLayout snapshot.restore(); } + + public override void copy(Gdk.Clipboard clipboard) { + clipboard.set_text(message.text); + } } diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 8a002f7..2169cde 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -195,11 +195,7 @@ private class TranscriptDrawingArea : Widget y_offset += chat_item.get_height() + chat_item.vertical_padding; if (y_offset > bounding_box.y) { - var text_bubble = chat_item as TextBubbleLayout; - var text = text_bubble.message.text; - - var clipboard = get_clipboard(); - clipboard.set_text(text); + chat_item.copy(get_clipboard()); break; } } From a70adbb7f189dca1e33d40f334562ce8f380b3fe Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 18 Jun 2025 15:00:54 -0700 Subject: [PATCH 48/63] Implements marking conversations as read when clicked on --- src/application/main-window.vala | 1 + src/service/interface/dbusservice.vala | 3 +++ .../interface/xml/net.buzzert.kordophonecd.Server.xml | 6 ++++++ src/service/repository.vala | 8 ++++++++ 4 files changed, 18 insertions(+) diff --git a/src/application/main-window.vala b/src/application/main-window.vala index cab37bd..d580c3e 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -45,6 +45,7 @@ public class MainWindow : Adw.ApplicationWindow transcript_view.title = conversation.display_name; try { + Repository.get_instance().mark_conversation_as_read(conversation.guid); Repository.get_instance().sync_conversation(conversation.guid); } catch (Error e) { GLib.warning("Failed to sync conversation: %s", e.message); diff --git a/src/service/interface/dbusservice.vala b/src/service/interface/dbusservice.vala index 63879cf..b7b89ac 100644 --- a/src/service/interface/dbusservice.vala +++ b/src/service/interface/dbusservice.vala @@ -38,6 +38,9 @@ namespace DBusService { [DBus (name = "SyncConversation")] public abstract void sync_conversation(string conversation_id) throws DBusError, IOError; + [DBus (name = "MarkConversationAsRead")] + public abstract void mark_conversation_as_read(string conversation_id) throws DBusError, IOError; + [DBus (name = "ConversationsUpdated")] public signal void conversations_updated(); diff --git a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml index a0898e3..cdef983 100644 --- a/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml +++ b/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml @@ -43,6 +43,12 @@ value="Initiates a background sync of a single conversation with the server."/> + + + + + diff --git a/src/service/repository.vala b/src/service/repository.vala index 59aeab1..9c939c4 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -112,6 +112,14 @@ public class Repository : DBusServiceProxy { dbus_repository.sync_conversation(conversation_guid); } + public void mark_conversation_as_read(string conversation_guid) throws DBusServiceProxyError, GLib.Error { + if (dbus_repository == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); + } + + dbus_repository.mark_conversation_as_read(conversation_guid); + } + public void download_attachment(string attachment_guid, bool preview) throws DBusServiceProxyError, GLib.Error { if (dbus_repository == null) { throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); From 3b6666cfc2ab9317d42f0297dd9c792df53d5e62 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 18 Jun 2025 15:32:37 -0700 Subject: [PATCH 49/63] Mark conversation as read on movement --- src/application/main-window.vala | 81 ++++++++++++------- .../conversation-list-view.vala | 2 +- src/transcript/message-list-model.vala | 18 +++-- src/transcript/transcript-container-view.vala | 27 ++++++- src/transcript/transcript-drawing-area.vala | 22 +++++ 5 files changed, 113 insertions(+), 37 deletions(-) diff --git a/src/application/main-window.vala b/src/application/main-window.vala index d580c3e..40934e5 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -6,6 +6,9 @@ public class MainWindow : Adw.ApplicationWindow private ConversationListView conversation_list_view; private TranscriptContainerView transcript_container_view; + private EventControllerMotion _motion_controller = new EventControllerMotion(); + private bool _motion_queued = false; + public MainWindow () { Object (title: "Kordophone"); @@ -20,14 +23,59 @@ public class MainWindow : Adw.ApplicationWindow split_view.sidebar = conversation_list_page; transcript_container_view = new TranscriptContainerView (); - transcript_container_view.on_send.connect (on_transcript_send); - var transcript_page = new NavigationPage (transcript_container_view, "Transcript"); split_view.content = transcript_page; var show_settings_action = new SimpleAction ("settings", null); show_settings_action.activate.connect(show_settings); add_action(show_settings_action); + + _motion_controller.motion.connect((x, y) => { + queue_motion(); + }); + _motion_controller.set_propagation_phase(PropagationPhase.CAPTURE); + split_view.add_controller(_motion_controller); + } + + private void queue_motion() { + if (_motion_queued) { + return; + } + + if (!is_active) { + return; + } + + _motion_queued = true; + GLib.Timeout.add(500, () => { + _motion_queued = false; + on_motion(); + return false; + }); + } + + private void on_motion() { + var selected_conversation = transcript_container_view.transcript_view.model?.conversation; + if (selected_conversation == null) { + return; + } + + var list_model = conversation_list_view.conversation_model; + var conversation_in_list = list_model.get_conversation(selected_conversation.guid); + if (conversation_in_list == null) { + return; + } + + if (conversation_in_list.unread_count == 0) { + return; + } + + try { + GLib.message("Marking conversation as read on motion: %s", selected_conversation.guid); + Repository.get_instance().mark_conversation_as_read(selected_conversation.guid); + } catch (Error e) { + GLib.warning("Failed to mark conversation as read: %s", e.message); + } } private void show_settings () { @@ -40,8 +88,8 @@ public class MainWindow : Adw.ApplicationWindow if (conversation == null) { transcript_view.model = null; } else { - if (transcript_view.model == null || transcript_view.model.conversation_guid != conversation.guid) { - transcript_view.model = new MessageListModel (conversation.guid); + if (transcript_view.model == null || transcript_view.model.conversation.guid != conversation.guid) { + transcript_view.model = new MessageListModel (conversation); transcript_view.title = conversation.display_name; try { @@ -53,29 +101,4 @@ public class MainWindow : Adw.ApplicationWindow } } } - - private void on_transcript_send(TranscriptContainerView view) { - var body = view.message_body; - var attachment_guids = view.attachment_guids; - - // Strip empty space at the beginning and end of the body - body = body.strip(); - - if (transcript_container_view.transcript_view.model == null) { - GLib.warning("No conversation selected"); - return; - } - - var selected_conversation = transcript_container_view.transcript_view.model.conversation_guid; - if (selected_conversation == null) { - GLib.warning("No conversation selected"); - return; - } - - try { - Repository.get_instance().send_message(selected_conversation, body, attachment_guids.to_array()); - } catch (Error e) { - GLib.warning("Failed to send message: %s", e.message); - } - } } diff --git a/src/conversation-list/conversation-list-view.vala b/src/conversation-list/conversation-list-view.vala index 7cdf069..06bc6ce 100644 --- a/src/conversation-list/conversation-list-view.vala +++ b/src/conversation-list/conversation-list-view.vala @@ -3,13 +3,13 @@ using Gtk; public class ConversationListView : Adw.Bin { + public ConversationListModel conversation_model { get; private set; } public signal void conversation_selected(Conversation conversation); private Adw.ToolbarView container; private ListBox list_box; private ScrolledWindow scrolled_window; private Adw.HeaderBar header_bar; - private ConversationListModel conversation_model; private string? selected_conversation_guid = null; private bool selection_update_queued = false; diff --git a/src/transcript/message-list-model.vala b/src/transcript/message-list-model.vala index 445f6a3..b4cf6ec 100644 --- a/src/transcript/message-list-model.vala +++ b/src/transcript/message-list-model.vala @@ -15,16 +15,16 @@ public class MessageListModel : Object, ListModel } } - public string conversation_guid { get; private set; } + public Conversation conversation { get; private set; } private ArrayList _messages; private HashSet participants = new HashSet(); private ulong update_handler_id = 0; private ulong reconnected_handler_id = 0; - public MessageListModel(string conversation_guid) { + public MessageListModel(Conversation conversation) { _messages = new ArrayList(); - this.conversation_guid = conversation_guid; + this.conversation = conversation; } ~MessageListModel() { @@ -69,7 +69,7 @@ public class MessageListModel : Object, ListModel try { bool first_load = _messages.size == 0; - Message[] messages = Repository.get_instance().get_messages(conversation_guid); + Message[] messages = Repository.get_instance().get_messages(conversation.guid); // Clear existing set uint old_count = _messages.size; @@ -108,8 +108,16 @@ public class MessageListModel : Object, ListModel messages_changed(); } + public void mark_as_read() { + try { + Repository.get_instance().mark_conversation_as_read(conversation.guid); + } catch (Error e) { + warning("Failed to mark conversation as read: %s", e.message); + } + } + private void got_messages_updated(string conversation_guid) { - if (conversation_guid == this.conversation_guid) { + if (conversation_guid == this.conversation.guid) { load_messages(); } } diff --git a/src/transcript/transcript-container-view.vala b/src/transcript/transcript-container-view.vala index be9e08a..4d1bef6 100644 --- a/src/transcript/transcript-container-view.vala +++ b/src/transcript/transcript-container-view.vala @@ -7,7 +7,6 @@ using GLib; class TranscriptContainerView : Adw.Bin { public TranscriptView transcript_view; - public signal void on_send(TranscriptContainerView view); private Box container; private Button send_button; @@ -225,7 +224,7 @@ class TranscriptContainerView : Adw.Bin private void on_request_send() { if (can_send) { - on_send(this); + on_send(); // Clear the message text message_buffer.set_text(""); @@ -240,6 +239,30 @@ class TranscriptContainerView : Adw.Bin } } + private void on_send() { + var body = message_body; + + // Strip empty space at the beginning and end of the body + body = body.strip(); + + if (transcript_view.model == null) { + GLib.warning("No conversation selected"); + return; + } + + var selected_conversation = transcript_view.model.conversation; + if (selected_conversation == null) { + GLib.warning("No conversation selected"); + return; + } + + try { + Repository.get_instance().send_message(selected_conversation.guid, body, attachment_guids.to_array()); + } catch (Error e) { + GLib.warning("Failed to send message: %s", e.message); + } + } + private void on_attach_button_clicked() { var dialog = new Gtk.FileDialog(); dialog.set_title("Select attachment"); diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 2169cde..52a84e8 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -25,6 +25,8 @@ private class TranscriptDrawingArea : Widget private GestureClick _click_gesture = new GestureClick(); private Gdk.Rectangle? _click_bounding_box = null; + private EventControllerMotion _motion_controller = new EventControllerMotion(); + private const bool debug_viewport = false; private uint? _tick_callback_id = null; private HashMap _animations = new HashMap(); @@ -40,6 +42,22 @@ private class TranscriptDrawingArea : Widget }); add_controller(_click_gesture); + _motion_controller.motion.connect((x, y) => { + if (self == null) { + return; + } + + // Only print motion events if window is active/focused + var window = self.get_native() as Gtk.Window; + if (window == null || !window.is_active) { + return; + } + + self.on_mouse_motion(x, y); + }); + _motion_controller.set_propagation_phase(PropagationPhase.CAPTURE); + add_controller(_motion_controller); + SimpleAction copy_action = new SimpleAction("copy", null); copy_action.activate.connect(() => { if (_click_bounding_box != null) { @@ -172,6 +190,10 @@ private class TranscriptDrawingArea : Widget animation_tick(); } + private void on_mouse_motion(double x, double y) { + // TODO: Will be making temporary text views here. + } + private void on_right_click() { var menu_model = new Menu(); menu_model.append("Copy", "transcript.copy"); From ccfea2883cf1e059c40d87dfc5146e87ffe8b3fa Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 18 Jun 2025 16:50:14 -0700 Subject: [PATCH 50/63] Enables selection of bubbles using an invisible text view --- src/resources/style.css | 8 +- src/transcript/layouts/bubble-layout.vala | 4 +- src/transcript/layouts/chat-item-layout.vala | 2 +- .../layouts/text-bubble-layout.vala | 42 +++++---- src/transcript/transcript-drawing-area.vala | 52 ++++++++++- src/transcript/transcript-view.vala | 90 +++++++++++++++++-- 6 files changed, 170 insertions(+), 28 deletions(-) diff --git a/src/resources/style.css b/src/resources/style.css index 9c29d8f..37c73bf 100644 --- a/src/resources/style.css +++ b/src/resources/style.css @@ -9,7 +9,7 @@ background-color: alpha(@accent_bg_color, 0.50); } -.message-list-scroller { +.flipped-y-axis { /* Invert the y-axis, so the messages are drawn bottom-to-top */ /* Individual messages are drawn upside down in the custom renderer */ transform: scale(1, -1); @@ -51,3 +51,9 @@ .attachment-image { border-radius: 8px; } + +.hovering-text-view { + background-color: transparent; + color: transparent; + line-height: 1.18; /* TextBubbleLayout.line_height */ +} diff --git a/src/transcript/layouts/bubble-layout.vala b/src/transcript/layouts/bubble-layout.vala index dbda1a6..1251a58 100644 --- a/src/transcript/layouts/bubble-layout.vala +++ b/src/transcript/layouts/bubble-layout.vala @@ -1,6 +1,6 @@ using Gtk; -private struct BubbleLayoutConstants { +public struct BubbleLayoutConstants { public float tail_width; public float tail_curve_offset; public float tail_side_offset; @@ -23,7 +23,7 @@ private struct BubbleLayoutConstants { } } -private abstract class BubbleLayout : Object, ChatItemLayout +public abstract class BubbleLayout : Object, ChatItemLayout { public bool from_me { get; set; } public float vertical_padding { get; set; } diff --git a/src/transcript/layouts/chat-item-layout.vala b/src/transcript/layouts/chat-item-layout.vala index 40c5f1e..bfbf712 100644 --- a/src/transcript/layouts/chat-item-layout.vala +++ b/src/transcript/layouts/chat-item-layout.vala @@ -1,6 +1,6 @@ using Gtk; -interface ChatItemLayout : Object +public interface ChatItemLayout : Object { public abstract bool from_me { get; set; } public abstract float vertical_padding { get; set; } diff --git a/src/transcript/layouts/text-bubble-layout.vala b/src/transcript/layouts/text-bubble-layout.vala index 4b3fce2..103b920 100644 --- a/src/transcript/layouts/text-bubble-layout.vala +++ b/src/transcript/layouts/text-bubble-layout.vala @@ -1,10 +1,23 @@ using Gtk; -private class TextBubbleLayout : BubbleLayout +public class TextBubbleLayout : BubbleLayout { public Message message; private Pango.Layout layout; + public static float line_height { get { return 1.18f; } } + + public static Pango.FontDescription body_font { + owned get { + var settings = Gtk.Settings.get_default(); + var font_name = settings.gtk_font_name; + + // Create font description from system font + var font_desc = Pango.FontDescription.from_string(font_name); + return font_desc; + } + } + public TextBubbleLayout(Message message, Widget parent, float max_width) { base(parent, max_width); @@ -13,15 +26,10 @@ private class TextBubbleLayout : BubbleLayout layout = parent.create_pango_layout(message.text); - // Get the system font settings - var settings = Gtk.Settings.get_default(); - var font_name = settings.gtk_font_name; - - // Create font description from system font - var font_desc = Pango.FontDescription.from_string(font_name); + var font_desc = TextBubbleLayout.body_font; layout.set_font_description(font_desc); layout.set_wrap(Pango.WrapMode.WORD_CHAR); - layout.set_line_spacing(1.18f); + layout.set_line_spacing(line_height); // Set max width layout.set_width((int)text_available_width * Pango.SCALE); @@ -71,16 +79,20 @@ private class TextBubbleLayout : BubbleLayout return logical_rect.width + text_x_offset + text_x_padding; } + public Graphene.Point get_text_origin() { + Pango.Rectangle ink_rect, logical_rect; + layout.get_pixel_extents(out ink_rect, out logical_rect); + + return Graphene.Point() { + x = text_x_offset, + y = ((get_height() - constants.tail_bottom_padding) - logical_rect.height) / 2 + }; + } + public override void draw_content(Snapshot snapshot) { snapshot.save(); - Pango.Rectangle ink_rect, logical_rect; - layout.get_pixel_extents(out ink_rect, out logical_rect); - - snapshot.translate(Graphene.Point() { - x = text_x_offset, - y = ((get_height() - constants.tail_bottom_padding) - logical_rect.height) / 2 - }); + snapshot.translate(get_text_origin()); snapshot.append_layout(layout, Gdk.RGBA() { red = 1.0f, diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 52a84e8..90edda3 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -16,6 +16,9 @@ private class TranscriptDrawingArea : Widget } } + public signal void on_text_bubble_hover(VisibleTextLayout? text_bubble); + public signal void on_text_bubble_click(VisibleTextLayout? text_bubble); + private ArrayList _messages = new ArrayList(); private ArrayList _chat_items = new ArrayList(); @@ -26,6 +29,7 @@ private class TranscriptDrawingArea : Widget private Gdk.Rectangle? _click_bounding_box = null; private EventControllerMotion _motion_controller = new EventControllerMotion(); + private ArrayList _visible_text_layouts = new ArrayList(); private const bool debug_viewport = false; private uint? _tick_callback_id = null; @@ -36,9 +40,9 @@ private class TranscriptDrawingArea : Widget weak TranscriptDrawingArea self = this; - _click_gesture.button = Gdk.BUTTON_SECONDARY; + _click_gesture.button = Gdk.BUTTON_SECONDARY | Gdk.BUTTON_PRIMARY; _click_gesture.begin.connect(() => { - self.on_right_click(); + self.on_click(self._click_gesture.get_button()); }); add_controller(_click_gesture); @@ -129,6 +133,7 @@ private class TranscriptDrawingArea : Widget // Draw each item in reverse order, since the messages are in reverse order float y_offset = 0; int container_width = get_width(); + _visible_text_layouts.clear(); for (int i = _chat_items.size - 1; i >= 0; i--) { var chat_item = _chat_items[i]; var item_width = chat_item.get_width(); @@ -152,6 +157,10 @@ private class TranscriptDrawingArea : Widget // Skip drawing if this item is not in the viewport float height_offset = 0.0f; if (viewport_rect.intersection(rect, null)) { + if (chat_item is TextBubbleLayout) { + _visible_text_layouts.add(VisibleTextLayout(chat_item as TextBubbleLayout, rect)); + } + snapshot.save(); var pushed_opacity = false; @@ -190,8 +199,35 @@ private class TranscriptDrawingArea : Widget animation_tick(); } + private VisibleTextLayout? get_text_bubble_at(double x, double y) { + foreach (var layout in _visible_text_layouts) { + if (layout.rect.contains_point(Graphene.Point() { x = (float)x, y = (float)y })) { + return layout; + } + } + + return null; + } + private void on_mouse_motion(double x, double y) { - // TODO: Will be making temporary text views here. + VisibleTextLayout? hovered_text_bubble = get_text_bubble_at(x, y); + on_text_bubble_hover(hovered_text_bubble); + } + + private void on_click(uint button) { + if (button == Gdk.BUTTON_SECONDARY) { + on_right_click(); + } else if (button == Gdk.BUTTON_PRIMARY) { + on_left_click(); + } + } + + private void on_left_click() { + Gdk.Rectangle? bounding_box = null; + if (_click_gesture.get_bounding_box(out bounding_box)) { + var text_bubble = get_text_bubble_at(bounding_box.x, bounding_box.y); + on_text_bubble_click(text_bubble); + } } private void on_right_click() { @@ -372,4 +408,14 @@ private class ChatItemAnimation private static double ease_out_quart(double t) { return 1.0 - Math.pow(1.0 - t, 4); } +} + +public struct VisibleTextLayout { + public weak TextBubbleLayout text_bubble; + public Graphene.Rect rect; + + public VisibleTextLayout(TextBubbleLayout text_bubble, Graphene.Rect rect) { + this.text_bubble = text_bubble; + this.rect = rect; + } } \ No newline at end of file diff --git a/src/transcript/transcript-view.vala b/src/transcript/transcript-view.vala index a081bb2..6da5ffc 100644 --- a/src/transcript/transcript-view.vala +++ b/src/transcript/transcript-view.vala @@ -17,10 +17,7 @@ public class TranscriptView : Adw.Bin _model = value; if (value != null) { - // Reset scroll position by updating the existing adjustment - scrolled_window.vadjustment.value = 0; - scrolled_window.vadjustment.upper = 0; - scrolled_window.vadjustment.page_size = 0; + reset_for_conversation_change(); weak TranscriptView self = this; messages_changed_handler_id = value.messages_changed.connect(() => { @@ -44,11 +41,18 @@ public class TranscriptView : Adw.Bin private Adw.ToolbarView container; private Label title_label = new Label("Messages"); + private Overlay overlay = new Overlay(); private TranscriptDrawingArea transcript_drawing_area = new TranscriptDrawingArea(); private ScrolledWindow scrolled_window = new ScrolledWindow(); private ulong messages_changed_handler_id = 0; private bool needs_reload = false; + private Graphene.Point _hovering_text_view_origin = Graphene.Point() { x = 0, y = 0 }; + private TextView _hovering_text_view = new TextView(); + + private VisibleTextLayout? hovered_text_bubble = null; + private VisibleTextLayout? locked_text_bubble = null; + public TranscriptView() { container = new Adw.ToolbarView(); set_child(container); @@ -56,8 +60,26 @@ public class TranscriptView : Adw.Bin // Set minimum width for the transcript view set_size_request(330, -1); - scrolled_window.set_child(transcript_drawing_area); - scrolled_window.add_css_class("message-list-scroller"); + overlay.set_child(transcript_drawing_area); + + weak TranscriptView self = this; + overlay.get_child_position.connect((child, out allocation) => { + allocation = Gtk.Allocation() { x = 0, y = 0, width = 0, height = 0 }; + if (self.hovered_text_bubble != null) { + var rect = self.hovered_text_bubble.rect; + allocation.x = (int)(rect.origin.x + self._hovering_text_view_origin.x); + allocation.y = (int)(rect.origin.y - self._hovering_text_view_origin.y); + allocation.width = (int)rect.size.width; + allocation.height = (int)rect.size.height; + + return true; + } + + return true; + }); + + scrolled_window.set_child(overlay); + scrolled_window.add_css_class("flipped-y-axis"); transcript_drawing_area.viewport = scrolled_window.vadjustment; container.set_content(scrolled_window); @@ -72,6 +94,33 @@ public class TranscriptView : Adw.Bin header_bar.set_title_widget(title_label); container.add_top_bar(header_bar); + // This is an invisible text view that's used to handle selection. + _hovering_text_view.add_css_class("hovering-text-view"); + _hovering_text_view.add_css_class("flipped-y-axis"); + _hovering_text_view.set_editable(false); + _hovering_text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR); + overlay.add_overlay(_hovering_text_view); + + // 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); + }); + + // When the mouse hovers over a text bubble, configure the hovering text view to show the text of the bubble. + transcript_drawing_area.on_text_bubble_hover.connect((visible_text_layout) => { + if (visible_text_layout != null) { + configure_hovering_text_view(visible_text_layout); + } + }); + + // This is triggered when another bubble is currently locked, and the user clicks on a new bubble. + transcript_drawing_area.on_text_bubble_click.connect((visible_text_layout) => { + if (visible_text_layout != null) { + locked_text_bubble = null; + configure_hovering_text_view(visible_text_layout); + } + }); + Repository.get_instance().attachment_downloaded.connect((attachment_guid) => { debug("Attachment downloaded: %s", attachment_guid); @@ -103,6 +152,35 @@ public class TranscriptView : Adw.Bin }); } + private void reset_for_conversation_change() { + locked_text_bubble = null; + hovered_text_bubble = null; + _hovering_text_view.buffer.text = ""; + overlay.queue_allocate(); + + // Reset scroll position by updating the existing adjustment + scrolled_window.vadjustment.value = 0; + scrolled_window.vadjustment.upper = 0; + scrolled_window.vadjustment.page_size = 0; + } + + private void lock_text_bubble(VisibleTextLayout? visible_text_layout) { + if (visible_text_layout != null) { + locked_text_bubble = visible_text_layout; + configure_hovering_text_view(visible_text_layout); + } + } + + private void configure_hovering_text_view(VisibleTextLayout? visible_text_layout) { + hovered_text_bubble = visible_text_layout; + + 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; + overlay.queue_allocate(); + } + } + private void reload_messages() { transcript_drawing_area.show_sender = _model.is_group_chat; transcript_drawing_area.set_messages(_model.messages); From 4ebd310b7ac45202b32767e9a87da3c087f4b9fa Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 18 Jun 2025 17:01:01 -0700 Subject: [PATCH 51/63] fix bug with clearing locked selection bubble --- src/transcript/transcript-drawing-area.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 90edda3..68fa7a9 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -40,9 +40,9 @@ private class TranscriptDrawingArea : Widget weak TranscriptDrawingArea self = this; - _click_gesture.button = Gdk.BUTTON_SECONDARY | Gdk.BUTTON_PRIMARY; + _click_gesture.button = 0; _click_gesture.begin.connect(() => { - self.on_click(self._click_gesture.get_button()); + self.on_click(self._click_gesture.get_current_button()); }); add_controller(_click_gesture); From 0dece34012363c80301c3154745d48797e0926d6 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 18 Jun 2025 17:36:32 -0700 Subject: [PATCH 52/63] Adds link clicking support --- src/models/message.vala | 16 ++++++ .../layouts/text-bubble-layout.vala | 3 +- src/transcript/transcript-view.vala | 57 ++++++++++++++++++- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/models/message.vala b/src/models/message.vala index e3cf1d6..ce315d8 100644 --- a/src/models/message.vala +++ b/src/models/message.vala @@ -30,6 +30,22 @@ public class Message : Object, Comparable, Hashable } } + 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, "\\0"); + } 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; diff --git a/src/transcript/layouts/text-bubble-layout.vala b/src/transcript/layouts/text-bubble-layout.vala index 103b920..0ab77e5 100644 --- a/src/transcript/layouts/text-bubble-layout.vala +++ b/src/transcript/layouts/text-bubble-layout.vala @@ -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); diff --git a/src/transcript/transcript-view.vala b/src/transcript/transcript-view.vala index 6da5ffc..3c7336c 100644 --- a/src/transcript/transcript-view.vala +++ b/src/transcript/transcript-view.vala @@ -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(); } } From 3379198940dec6d159390278be51d9d6c2856c76 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 18 Jun 2025 18:07:59 -0700 Subject: [PATCH 53/63] Add double click gesture on image bubbles to open --- src/models/attachment.vala | 14 +++++ src/service/repository.vala | 9 +++ .../layouts/image-bubble-layout.vala | 1 + src/transcript/transcript-drawing-area.vala | 58 ++++++++++++++----- src/transcript/transcript-view.vala | 51 ++++++++++++++-- 5 files changed, 112 insertions(+), 21 deletions(-) diff --git a/src/models/attachment.vala b/src/models/attachment.vala index 05357ac..24e9596 100644 --- a/src/models/attachment.vala +++ b/src/models/attachment.vala @@ -29,6 +29,20 @@ public class AttachmentMetadata : Object { } } +public class AttachmentInfo : Object { + public string? path; + public string? preview_path; + public bool? downloaded; + public bool? preview_downloaded; + + public AttachmentInfo(string? path, string? preview_path, bool? downloaded, bool? preview_downloaded) { + this.path = path; + this.preview_path = preview_path; + this.downloaded = downloaded; + this.preview_downloaded = preview_downloaded; + } +} + public class Attachment : Object { public string guid; public string path; diff --git a/src/service/repository.vala b/src/service/repository.vala index 9c939c4..77c13c5 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -135,4 +135,13 @@ public class Repository : DBusServiceProxy { return dbus_repository.upload_attachment(filename); } + + public AttachmentInfo get_attachment_info(string attachment_guid) throws DBusServiceProxyError, GLib.Error { + if (dbus_repository == null) { + throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected"); + } + + var info = dbus_repository.get_attachment_info(attachment_guid); + return new AttachmentInfo(info.attr1, info.attr2, info.attr3, info.attr4); + } } diff --git a/src/transcript/layouts/image-bubble-layout.vala b/src/transcript/layouts/image-bubble-layout.vala index 09f5f4b..c957d86 100644 --- a/src/transcript/layouts/image-bubble-layout.vala +++ b/src/transcript/layouts/image-bubble-layout.vala @@ -26,6 +26,7 @@ private class ImageBubbleLayout : BubbleLayout { public string image_path; public bool is_downloaded; + public string? attachment_guid; private Graphene.Size image_size; private Gdk.Texture? cached_texture = null; diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 68fa7a9..6abb32c 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -16,8 +16,9 @@ private class TranscriptDrawingArea : Widget } } - public signal void on_text_bubble_hover(VisibleTextLayout? text_bubble); - public signal void on_text_bubble_click(VisibleTextLayout? text_bubble); + public signal void on_text_bubble_hover(VisibleLayout? text_bubble); + public signal void on_text_bubble_click(VisibleLayout? text_bubble); + public signal void on_image_bubble_activate(string attachment_guid); private ArrayList _messages = new ArrayList(); private ArrayList _chat_items = new ArrayList(); @@ -29,7 +30,7 @@ private class TranscriptDrawingArea : Widget private Gdk.Rectangle? _click_bounding_box = null; private EventControllerMotion _motion_controller = new EventControllerMotion(); - private ArrayList _visible_text_layouts = new ArrayList(); + private ArrayList _visible_text_layouts = new ArrayList(); private const bool debug_viewport = false; private uint? _tick_callback_id = null; @@ -41,8 +42,8 @@ private class TranscriptDrawingArea : Widget weak TranscriptDrawingArea self = this; _click_gesture.button = 0; - _click_gesture.begin.connect(() => { - self.on_click(self._click_gesture.get_current_button()); + _click_gesture.pressed.connect((n_press, x, y) => { + self.on_click(self._click_gesture.get_current_button(), n_press); }); add_controller(_click_gesture); @@ -157,8 +158,8 @@ private class TranscriptDrawingArea : Widget // Skip drawing if this item is not in the viewport float height_offset = 0.0f; if (viewport_rect.intersection(rect, null)) { - if (chat_item is TextBubbleLayout) { - _visible_text_layouts.add(VisibleTextLayout(chat_item as TextBubbleLayout, rect)); + if (chat_item is BubbleLayout) { + _visible_text_layouts.add(VisibleLayout(chat_item as BubbleLayout, rect)); } snapshot.save(); @@ -199,9 +200,10 @@ private class TranscriptDrawingArea : Widget animation_tick(); } - private VisibleTextLayout? get_text_bubble_at(double x, double y) { + private VisibleLayout? get_visible_layout_at(double x, double y) { + var point = Graphene.Point() { x = (float)x, y = (float)y }; foreach (var layout in _visible_text_layouts) { - if (layout.rect.contains_point(Graphene.Point() { x = (float)x, y = (float)y })) { + if (layout.rect.contains_point(point)) { return layout; } } @@ -209,16 +211,29 @@ private class TranscriptDrawingArea : Widget return null; } + private VisibleLayout? get_text_bubble_at(double x, double y) { + var layout = get_visible_layout_at(x, y); + if (layout != null && layout.bubble is TextBubbleLayout) { + return layout; + } + + return null; + } + private void on_mouse_motion(double x, double y) { - VisibleTextLayout? hovered_text_bubble = get_text_bubble_at(x, y); + VisibleLayout? hovered_text_bubble = get_text_bubble_at(x, y); on_text_bubble_hover(hovered_text_bubble); } - private void on_click(uint button) { + private void on_click(uint button, int n_press) { if (button == Gdk.BUTTON_SECONDARY) { on_right_click(); } else if (button == Gdk.BUTTON_PRIMARY) { on_left_click(); + + if (n_press == 2) { + on_double_click(); + } } } @@ -230,6 +245,17 @@ private class TranscriptDrawingArea : Widget } } + private void on_double_click() { + Gdk.Rectangle? bounding_box = null; + if (_click_gesture.get_bounding_box(out bounding_box)) { + var double_clicked_bubble = get_visible_layout_at(bounding_box.x, bounding_box.y); + if (double_clicked_bubble != null && double_clicked_bubble.bubble is ImageBubbleLayout) { + var image_bubble = double_clicked_bubble.bubble as ImageBubbleLayout; + on_image_bubble_activate(image_bubble.attachment_guid); + } + } + } + private void on_right_click() { var menu_model = new Menu(); menu_model.append("Copy", "transcript.copy"); @@ -346,6 +372,8 @@ private class TranscriptDrawingArea : Widget var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size); image_layout.id = @"image-$(attachment.guid)"; + image_layout.attachment_guid = attachment.guid; + if (animate) { start_animation(image_layout.id); } @@ -410,12 +438,12 @@ private class ChatItemAnimation } } -public struct VisibleTextLayout { - public weak TextBubbleLayout text_bubble; +public struct VisibleLayout { + public weak BubbleLayout bubble; public Graphene.Rect rect; - public VisibleTextLayout(TextBubbleLayout text_bubble, Graphene.Rect rect) { - this.text_bubble = text_bubble; + public VisibleLayout(BubbleLayout bubble, Graphene.Rect rect) { + this.bubble = bubble; this.rect = rect; } } \ No newline at end of file diff --git a/src/transcript/transcript-view.vala b/src/transcript/transcript-view.vala index 3c7336c..cd431bc 100644 --- a/src/transcript/transcript-view.vala +++ b/src/transcript/transcript-view.vala @@ -51,8 +51,8 @@ public class TranscriptView : Adw.Bin 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; + private VisibleLayout? hovered_text_bubble = null; + private VisibleLayout? locked_text_bubble = null; public TranscriptView() { container = new Adw.ToolbarView(); @@ -123,6 +123,10 @@ public class TranscriptView : Adw.Bin } }); + transcript_drawing_area.on_image_bubble_activate.connect((attachment_guid) => { + self.open_attachment(attachment_guid); + }); + Repository.get_instance().attachment_downloaded.connect((attachment_guid) => { debug("Attachment downloaded: %s", attachment_guid); @@ -154,6 +158,40 @@ public class TranscriptView : Adw.Bin }); } + delegate void OpenPath(string path); + private ulong attachment_downloaded_handler_id = 0; + private void open_attachment(string attachment_guid) { + OpenPath open_path = (path) => { + try { + GLib.AppInfo.launch_default_for_uri("file://" + path, null); + } catch (GLib.Error e) { + warning("Failed to open image %s: %s", path, e.message); + } + }; + + try { + var attachment_info = Repository.get_instance().get_attachment_info(attachment_guid); + if (attachment_info.downloaded == true) { + // We already have it, so open it. + open_path(attachment_info.path); + } else { + // We need to download this, then open it once the downloaded signal is emitted. + Repository.get_instance().download_attachment(attachment_guid, false); + + // TODO: Should probably indicate progress here. + + attachment_downloaded_handler_id = Repository.get_instance().attachment_downloaded.connect((guid) => { + if (guid == attachment_guid) { + open_path(attachment_info.path); + Repository.get_instance().disconnect(attachment_downloaded_handler_id); + } + }); + } + } catch (GLib.Error e) { + warning("Failed to get attachment info: %s", e.message); + } + } + private void on_hovering_text_view_clicked(TextIter location, TextMark end) { lock_text_bubble(hovered_text_bubble); @@ -213,23 +251,24 @@ public class TranscriptView : Adw.Bin scrolled_window.vadjustment.page_size = 0; } - private void lock_text_bubble(VisibleTextLayout? visible_text_layout) { + private void lock_text_bubble(VisibleLayout? visible_text_layout) { if (visible_text_layout != null) { locked_text_bubble = visible_text_layout; configure_hovering_text_view(visible_text_layout); } } - private void configure_hovering_text_view(VisibleTextLayout? visible_text_layout) { + private void configure_hovering_text_view(VisibleLayout? visible_text_layout) { hovered_text_bubble = visible_text_layout; if (locked_text_bubble == null) { - _hovering_text_view_origin = visible_text_layout.text_bubble.get_text_origin(); + TextBubbleLayout text_bubble = visible_text_layout.bubble as TextBubbleLayout; + _hovering_text_view_origin = text_bubble.get_text_origin(); _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); + _hovering_text_view.buffer.insert_markup(ref start_iter, text_bubble.message.markup, -1); overlay.queue_allocate(); } } From 9f84969ff5be3817dc20ec86beb5110ff9800999 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 18 Jun 2025 18:21:11 -0700 Subject: [PATCH 54/63] Multi-window support! --- src/application/main-window.vala | 12 ++++++++++++ src/conversation-list/conversation-list-view.vala | 11 ++++++++++- src/conversation-list/conversation-row.vala | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/application/main-window.vala b/src/application/main-window.vala index 40934e5..f649ad1 100644 --- a/src/application/main-window.vala +++ b/src/application/main-window.vala @@ -18,6 +18,7 @@ public class MainWindow : Adw.ApplicationWindow conversation_list_view = new ConversationListView (); conversation_list_view.conversation_selected.connect (conversation_selected); + conversation_list_view.conversation_activated.connect (open_conversation_in_new_window); var conversation_list_page = new NavigationPage (conversation_list_view, "Conversations"); split_view.sidebar = conversation_list_page; @@ -101,4 +102,15 @@ public class MainWindow : Adw.ApplicationWindow } } } + + private void open_conversation_in_new_window(Conversation conversation) { + var view = new TranscriptContainerView(); + view.transcript_view.model = new MessageListModel (conversation); + view.transcript_view.title = conversation.display_name; + + var window = new Adw.Window(); + window.set_default_size(750, 990); + window.set_content(view); + window.present(); + } } diff --git a/src/conversation-list/conversation-list-view.vala b/src/conversation-list/conversation-list-view.vala index 06bc6ce..ba5e952 100644 --- a/src/conversation-list/conversation-list-view.vala +++ b/src/conversation-list/conversation-list-view.vala @@ -5,6 +5,7 @@ public class ConversationListView : Adw.Bin { public ConversationListModel conversation_model { get; private set; } public signal void conversation_selected(Conversation conversation); + public signal void conversation_activated(Conversation conversation); private Adw.ToolbarView container; private ListBox list_box; @@ -24,6 +25,7 @@ public class ConversationListView : Adw.Bin list_box = new ListBox (); list_box.add_css_class ("boxed-list"); list_box.set_selection_mode (SelectionMode.SINGLE); + list_box.activate_on_single_click = false; scrolled_window.set_child (list_box); list_box.row_selected.connect ((row) => { @@ -36,6 +38,14 @@ public class ConversationListView : Adw.Bin } }); + list_box.row_activated.connect((row) => { + var conversation_row = (ConversationRow?) row; + if (conversation_row != null) { + Conversation conversation = conversation_row.conversation; + conversation_activated(conversation); + } + }); + header_bar = new Adw.HeaderBar (); header_bar.set_title_widget (new Label ("Kordophone")); container.add_top_bar (header_bar); @@ -108,7 +118,6 @@ public class ConversationListView : Adw.Bin } } } - private Widget create_conversation_row (Object item) { Conversation conversation = (Conversation) item; diff --git a/src/conversation-list/conversation-row.vala b/src/conversation-list/conversation-row.vala index bd9db4b..be13c37 100644 --- a/src/conversation-list/conversation-row.vala +++ b/src/conversation-list/conversation-row.vala @@ -7,6 +7,7 @@ public class ConversationRow : Adw.ActionRow { public ConversationRow(Conversation conversation) { this.conversation = conversation; + this.activatable = true; title = conversation.display_name.strip(); title_lines = 1; From bb74604a74ff799937f673578f8d96de7e6d501b Mon Sep 17 00:00:00 2001 From: James Magahern Date: Thu, 26 Jun 2025 18:48:52 -0700 Subject: [PATCH 55/63] transcript: show sender annotation if interrupted by date annotation --- src/transcript/transcript-drawing-area.vala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/transcript/transcript-drawing-area.vala b/src/transcript/transcript-drawing-area.vala index 6abb32c..2417f23 100644 --- a/src/transcript/transcript-drawing-area.vala +++ b/src/transcript/transcript-drawing-area.vala @@ -339,6 +339,7 @@ private class TranscriptDrawingArea : Widget var date_item = new DateItemLayout(date.to_local().format("%b %d, %Y at %H:%M"), this, max_width); items.add(date_item); last_date = date; + last_sender = null; } // Sender Annotation From 349a644b0e5aa2495046d42d89386df1c773382c Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 15 Jul 2025 18:42:05 -0700 Subject: [PATCH 56/63] Desktop/icon files, rpm dist --- build-aux/post-install.sh | 10 ++++ build-aux/resize.py | 26 +++++++++ dist/rpm/kordophone.spec | 57 +++++++++++++++++++ src/meson.build | 37 +++++++++++- src/resources/net.buzzert.kordophone.desktop | 9 +++ src/resources/net.buzzert.kordophone.png | Bin 0 -> 278788 bytes 6 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 build-aux/post-install.sh create mode 100755 build-aux/resize.py create mode 100644 dist/rpm/kordophone.spec create mode 100644 src/resources/net.buzzert.kordophone.desktop create mode 100644 src/resources/net.buzzert.kordophone.png diff --git a/build-aux/post-install.sh b/build-aux/post-install.sh new file mode 100644 index 0000000..9713074 --- /dev/null +++ b/build-aux/post-install.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +datadir=$1 + +# Package managers set this so we don't need to run +if [ -z "$DESTDIR" ]; then + echo Compiling GSettings schemas... + glib-compile-schemas ${datadir}/glib-2.0/schemas +fi + diff --git a/build-aux/resize.py b/build-aux/resize.py new file mode 100755 index 0000000..d56d78f --- /dev/null +++ b/build-aux/resize.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +import os, sys + +icon_class = [ + '16x16', + '24x24', + '32x32', + '48x48', + '256x256', + '512x512', +] + +magick = sys.argv[1] +input_file = sys.argv[2] +output = sys.argv[3] + +for iclass in icon_class: + outdir = os.path.join(output, iclass, 'apps') + outfile = os.path.join(outdir, 'net.buzzert.kordophone.png') + + os.makedirs(outdir, exist_ok=True) + os.system('{magick} {input} -resize {klass} {outfile}' + .format(magick=magick, input=input_file, klass=iclass, outfile=outfile)) + + diff --git a/dist/rpm/kordophone.spec b/dist/rpm/kordophone.spec new file mode 100644 index 0000000..044f011 --- /dev/null +++ b/dist/rpm/kordophone.spec @@ -0,0 +1,57 @@ +Name: kordophone +Version: 1.0.0 +Release: 1%{?dist} +Summary: GTK4/Libadwaita client for Kordophone + +License: GPL +URL: https://git.sr.ht/~buzzert/kordophone-2-gtk +# Source0: %{name}-%{version}.tar.gz + +BuildRequires: meson >= 0.56.0 +BuildRequires: vala +BuildRequires: gcc +BuildRequires: pkgconfig(gtk4) +BuildRequires: pkgconfig(libadwaita-1) +BuildRequires: pkgconfig(gio-2.0) +BuildRequires: pkgconfig(gee-0.8) +BuildRequires: pkgconfig(gio-unix-2.0) +BuildRequires: pkgconfig(libsecret-1) + +Requires: gtk4 +Requires: libadwaita +Requires: glib2 +Requires: libgee +Requires: libsecret +Requires: kordophoned >= 1.0.0 + +%description +A GTK4/Libadwaita Linux Client for the Kordophone client daemon. + +%prep +if [ ! -d .git ]; then + git clone --bare https://git.sr.ht/~buzzert/kordophone-2-gtk .git + git config --local --bool core.bare false + git reset --hard +fi + +%build +%meson +%meson_build + +%install +%meson_install + +%files +%{_bindir}/kordophone +%{_datadir}/applications/net.buzzert.kordophone.desktop +%{_datadir}/icons/ +%{_datadir}/icons/hicolor/16x16/apps/net.buzzert.kordophone.png +%{_datadir}/icons/hicolor/24x24/apps/net.buzzert.kordophone.png +%{_datadir}/icons/hicolor/32x32/apps/net.buzzert.kordophone.png +%{_datadir}/icons/hicolor/48x48/apps/net.buzzert.kordophone.png +%{_datadir}/icons/hicolor/256x256/apps/net.buzzert.kordophone.png +%{_datadir}/icons/hicolor/512x512/apps/net.buzzert.kordophone.png +%{_datadir}/icons/net.buzzert.kordophone.png + +%changelog +- Initial RPM package diff --git a/src/meson.build b/src/meson.build index f60fed6..f8b1e8e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,12 +8,46 @@ dependencies = [ ] gnome = import('gnome') +kp_prefix = get_option('prefix') +datadir = join_paths(kp_prefix, get_option('datadir')) resources = gnome.compile_resources( 'kordophone-resources', 'resources/kordophone.gresource.xml', source_dir: 'resources' ) +# Icons +app_icon_dirs = [ + '16x16', + '24x24', + '32x32', + '48x48', + '256x256', + '512x512', +] + +build_tools_dir = meson.source_root() / 'build-aux' + +image_magick = find_program('magick', required : true) +resizer = find_program(build_tools_dir / 'resize.py') +icons = custom_target('icons', + output: 'hicolor', + input: 'resources/net.buzzert.kordophone.png', + command: [resizer, image_magick, '@INPUT@', '@OUTPUT@'], + install: true, + install_dir: join_paths(datadir, 'icons'), +) + +# Full res icon for Desktop Entry +install_data('resources/net.buzzert.kordophone.png', + install_dir: join_paths(datadir, 'icons'), +) + +# Desktop +install_data('resources/net.buzzert.kordophone.desktop', + install_dir: join_paths(datadir, 'applications') +) + sources = [ 'application/kordophone-application.vala', 'application/main-window.vala', @@ -49,8 +83,9 @@ sources = [ executable('kordophone', sources, resources, + icons, dependencies : dependencies, vala_args: ['--pkg', 'posix'], link_args: ['-lm'], install : true -) \ No newline at end of file +) diff --git a/src/resources/net.buzzert.kordophone.desktop b/src/resources/net.buzzert.kordophone.desktop new file mode 100644 index 0000000..025218d --- /dev/null +++ b/src/resources/net.buzzert.kordophone.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Encoding=UTF-8 +Name=Kordophone +Comment=Kordophone GTK Client +Exec=kordophone +Icon=net.buzzert.kordophone.png +Terminal=false + diff --git a/src/resources/net.buzzert.kordophone.png b/src/resources/net.buzzert.kordophone.png new file mode 100644 index 0000000000000000000000000000000000000000..be1261d20be82e25f11fa5d90f6ac44625122fae GIT binary patch literal 278788 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 i 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:OvQrRA9Y|nX;E3^8Q`?$ z4{(d6N?<*?s$NfGcVG&Ir3=nkBb(O)?3rU1A(JYS=BoU_36T!AeqD5c2R~V-cuBD8 zLk3k!;*4dc2lH0q`^H;ZI1lFQoiUmi>Cs~sg+-%1WtjCtz&bzkW@^C4Ov z-gncqmf7^x=?m2q(6Lf(H~p8c1{OSD|J(er&db>{(P2(eHH7RFo1tOYOu$6g}PeG|ETFE_D8P60a<~@C^1chRK zBu^ka{DhTG^bUS48(GVKTweoQnlac)_rRdiGKC=er64 zozhy!Zrk)0>#4UCM}Y5ne4^@DYEQNvf0xD!zAweGo{W4=3;1W}J!Ur#b2edomYsM4 zxt!CKLSn`pda2tZs2?POkgEbG5Cx9veq~w33|JW!7(717zFLHteNGw;JcbQ!T)mHu zvd3!7g`&T9kbi^?dS4iNd=?ec9l(iJ#GS{@CZ$Xc@pSmSmYnUC;y+%;-|IO$ z>+3;{B(X8#bLn-iEhWk$Eff7*?TI4qqV$=7rg4wKL}F|FLv)5CT5f3ouhM}Wr=?6& zV+)RYz4;ZJ^)DBohr<;RfGl#Glrce-=jQRsiW6(Vw`#}d3V!DRmsbMZoi9at0n(&B zu4IUd8$es?Dy7Wi9%cbnvfNMSNgM_6=h^64rhHqL^o>dh1;8_^i=mHX$^1U7kA*3a zNR-;3d&t_ow2Gh14>hdt)6}~>eXCF~pM!2=xldqWHBNR!P%)B-W(M1Rah?cX6qWy( z48KBnl}%mbLob8Nzt?1HKe&b6_zzQS=|tFSnM-yx;Bo}iKH3Td$6NB59-ft^mC2P2 z$1V!K-KrY-i1zjp+A@o~`*u3=v1fP0GtFRRR^n#ZEvRPtShIf_lgJ z`bJQK{s#S+HgY368fYqy$)}JxGi#onbkt?|TlMym?gNX0*}5-1V~3W~tCeE*ne&+0 z3b7z!a8&SMG9%b9YM$N`^FnB6^H?C=DPIbREB(DO)J)O||1TYl&zPP6_J)<-XiC)l zB7b^J{JRI{6YZpJD)U3dU}%kP!ig`qj(!JO6ETKqgp{biNpc)l+jj^PQmi^nsxJIM zP#(6xBW3FgSC&?nlVgrUG7FwBs6Ol9=+M3~yqpF1@OUP9O+t9l1*t=(a5U4?5u*W1 zi%ErG!15(R*D2?rln2vg)SOxUl&&A-Mhs+dhGb$bRNR|ct*mNk8wI9xbY%tE$()IW zD*=e`tv-#!_nMG5?iHmD?_;%a?ex(nww^zzQ{%GIR{KcY^su{_g1*%t-G63`pUeNn zxo!RdmmYup@$1H_a`Cj8Bi;t4sN&CQX6?Rti~UmWOk@DkV5KgZHoCEi{(>`rVGKr0 z!unS&kB^D3lnNgaJnnYfxZt6wFO3YJMMpXkD%N(KXCJ^~am8E1l$?RM?1(!v`zK?H z&+m+8Uqamo0EQ8R0rx8=Zn(@MM1*&5ZdP~≫ujU(mIy@h)neA@qsZlNp62Tq;=| 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

X5!J&Lr%yG5=^BKt0+=$AoU3 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

XAtkz7U4(N6$>F+9b>@yF|uWy|{gKO== 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 MWp|Qoaruw`10)<10{{R3 literal 0 HcmV?d00001 From 44bc7c0cb47d9c793186d4113717f104b6e84d68 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 15 Jul 2025 18:42:28 -0700 Subject: [PATCH 57/63] v1.0.0 --- meson.build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index b6281f2..e4ab3a9 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project('kordophone', 'vala', - version : '0.1.0', + version : '1.0.0', meson_version : '>=0.56.0', default_options : ['warning_level=2'] ) -subdir('src') \ No newline at end of file +subdir('src') From c878141e6118571dc1585b8a5eabab07a713050a Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 15 Jul 2025 18:43:53 -0700 Subject: [PATCH 58/63] rpmspec: these may not be necessary. --- dist/rpm/kordophone.spec | 7 ------- 1 file changed, 7 deletions(-) diff --git a/dist/rpm/kordophone.spec b/dist/rpm/kordophone.spec index 044f011..ad44af7 100644 --- a/dist/rpm/kordophone.spec +++ b/dist/rpm/kordophone.spec @@ -45,13 +45,6 @@ fi %{_bindir}/kordophone %{_datadir}/applications/net.buzzert.kordophone.desktop %{_datadir}/icons/ -%{_datadir}/icons/hicolor/16x16/apps/net.buzzert.kordophone.png -%{_datadir}/icons/hicolor/24x24/apps/net.buzzert.kordophone.png -%{_datadir}/icons/hicolor/32x32/apps/net.buzzert.kordophone.png -%{_datadir}/icons/hicolor/48x48/apps/net.buzzert.kordophone.png -%{_datadir}/icons/hicolor/256x256/apps/net.buzzert.kordophone.png -%{_datadir}/icons/hicolor/512x512/apps/net.buzzert.kordophone.png -%{_datadir}/icons/net.buzzert.kordophone.png %changelog - Initial RPM package From 3e43bd1434e3cffd925d6fe99c1f3126ad19d69f Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 15 Jul 2025 18:58:13 -0700 Subject: [PATCH 59/63] repository: auto start service if not already running --- src/service/repository.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/repository.vala b/src/service/repository.vala index 77c13c5..7931404 100644 --- a/src/service/repository.vala +++ b/src/service/repository.vala @@ -21,7 +21,7 @@ public class Repository : DBusServiceProxy { private uint dbus_watch_id; private Repository() { - this.dbus_watch_id = Bus.watch_name(BusType.SESSION, DBUS_NAME, BusNameWatcherFlags.NONE, (name, name_owner) => { + this.dbus_watch_id = Bus.watch_name(BusType.SESSION, DBUS_NAME, BusNameWatcherFlags.AUTO_START, (name, name_owner) => { connect_to_repository(); }); } From 356a1b85b9866000fd151e9c26d009d0681953ef Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 15 Jul 2025 18:59:03 -0700 Subject: [PATCH 60/63] version bump --- dist/rpm/kordophone.spec | 2 +- meson.build | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/rpm/kordophone.spec b/dist/rpm/kordophone.spec index ad44af7..276d758 100644 --- a/dist/rpm/kordophone.spec +++ b/dist/rpm/kordophone.spec @@ -1,5 +1,5 @@ Name: kordophone -Version: 1.0.0 +Version: 1.0.1 Release: 1%{?dist} Summary: GTK4/Libadwaita client for Kordophone diff --git a/meson.build b/meson.build index e4ab3a9..317872f 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('kordophone', 'vala', - version : '1.0.0', + version : '1.0.1', meson_version : '>=0.56.0', default_options : ['warning_level=2'] ) From 5fa6c86a179f2ac4768e5f4fc6d37fe59bd88e7a Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 15 Jul 2025 19:00:11 -0700 Subject: [PATCH 61/63] adds readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..262c0ff --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# kordophone-2-gtk + +Libadwaita/GTK4 client for the Kordophone client daemon. + +# Building + +Build an RPM using `rpmbuild -ba dist/rpm/kordophone.spec` From 54b76109c2e6bcb051305f93e8258855ce53c559 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 8 Aug 2025 13:47:21 -0700 Subject: [PATCH 62/63] Fixes rpm build --- Dockerfile | 30 ++++++++++++++++++++++++++++++ dist/rpm/kordophone.spec | 5 +++-- src/meson.build | 2 +- 3 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d3bbb2b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM fedora:39 + +# Install RPM build tools and dependencies +RUN dnf update -y && dnf install -y \ + rpm-build \ + rpmdevtools \ + meson \ + vala \ + gcc \ + pkgconfig \ + gtk4-devel \ + libadwaita-devel \ + glib2-devel \ + libgee-devel \ + libsecret-devel \ + ImageMagick \ + git \ + && dnf clean all + +# Create RPM build environment +RUN rpmdev-setuptree + +# Set working directory +WORKDIR /root/rpmbuild + +# Copy spec file +COPY dist/rpm/kordophone.spec SPECS/ + +# Build command +CMD ["rpmbuild", "-ba", "SPECS/kordophone.spec"] diff --git a/dist/rpm/kordophone.spec b/dist/rpm/kordophone.spec index 276d758..89cd731 100644 --- a/dist/rpm/kordophone.spec +++ b/dist/rpm/kordophone.spec @@ -5,7 +5,6 @@ Summary: GTK4/Libadwaita client for Kordophone License: GPL URL: https://git.sr.ht/~buzzert/kordophone-2-gtk -# Source0: %{name}-%{version}.tar.gz BuildRequires: meson >= 0.56.0 BuildRequires: vala @@ -47,4 +46,6 @@ fi %{_datadir}/icons/ %changelog -- Initial RPM package +* Fri Aug 8 2025 James Magahern +- Updated rpmspec + diff --git a/src/meson.build b/src/meson.build index f8b1e8e..12ade82 100644 --- a/src/meson.build +++ b/src/meson.build @@ -28,7 +28,7 @@ app_icon_dirs = [ build_tools_dir = meson.source_root() / 'build-aux' -image_magick = find_program('magick', required : true) +image_magick = find_program('convert', required : true) resizer = find_program(build_tools_dir / 'resize.py') icons = custom_target('icons', output: 'hicolor', From 7d0dfb455aa86245231b383a92e79b3c08a12d5e Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 8 Aug 2025 15:47:45 -0700 Subject: [PATCH 63/63] Dockerfile: build with fedora 40 for adwaita --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d3bbb2b..873c2ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM fedora:39 +FROM fedora:40 # Install RPM build tools and dependencies RUN dnf update -y && dnf install -y \