From e976b3db4c4245ad17e6c00643e07d58a3ef5f6b Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 30 Apr 2025 15:58:47 -0700 Subject: [PATCH] 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();