Private
Public Access
1
0

Add 'gtk/' from commit '7d0dfb455aa86245231b383a92e79b3c08a12d5e'

git-subtree-dir: gtk
git-subtree-mainline: c710c6e053
git-subtree-split: 7d0dfb455a
This commit is contained in:
2025-09-06 19:34:30 -07:00
39 changed files with 3599 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
using GLib;
using Gee;
public class ConversationListModel : Object, ListModel
{
public SortedSet<Conversation> conversations {
owned get { return _conversations.read_only_view; }
}
private SortedSet<Conversation> _conversations;
public ConversationListModel() {
_conversations = new TreeSet<Conversation>((a, b) => {
// Sort by date in descending order (newest first)
return (int)(b.date - a.date);
});
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) {
foreach (var conv in _conversations) {
if (conv.guid == guid) {
return conv;
}
}
return null;
}
public void load_conversations() {
try {
Conversation[] new_conversations = Repository.get_instance().get_conversations();
// Create a map of old conversations for quick lookup
var old_conversations_map = new HashMap<string, Conversation>();
foreach (var conv in _conversations) {
old_conversations_map[conv.guid] = conv;
}
// Create a map of new conversations for quick lookup
var new_conversations_map = new HashMap<string, Conversation>();
foreach (var conv in new_conversations) {
new_conversations_map[conv.guid] = conv;
}
// Find removed conversations
var removed_positions = new ArrayList<uint>();
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<Conversation>();
var changed_conversations = new ArrayList<Conversation>();
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
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);
}
}
// 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];
}
}

View File

@@ -0,0 +1,126 @@
using Adw;
using Gtk;
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;
private ScrolledWindow scrolled_window;
private Adw.HeaderBar header_bar;
private string? selected_conversation_guid = null;
private bool selection_update_queued = false;
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);
list_box.activate_on_single_click = false;
scrolled_window.set_child (list_box);
list_box.row_selected.connect ((row) => {
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);
}
});
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);
// Setup application menu
var app_menu = new Menu ();
var section = new Menu ();
section.append ("Manual Sync", "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 (() => {
try {
Repository.get_instance().sync_conversation_list();
} catch (GLib.Error e) {
warning("Failed to sync conversation list: %s", e.message);
}
});
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;
menu_button.primary = true;
menu_button.icon_name = "open-menu-symbolic";
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);
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);
}
}

View File

@@ -0,0 +1,61 @@
using Adw;
using Gtk;
public class ConversationRow : Adw.ActionRow {
public Conversation conversation;
private Image unread_badge;
public ConversationRow(Conversation conversation) {
this.conversation = conversation;
this.activatable = true;
title = conversation.display_name.strip();
title_lines = 1;
var preview = conversation.last_message_preview
.strip()
.replace("\n", " ")
.replace("<", "\\<")
.replace(">", "\\>")
.replace("&", "&amp;");
subtitle = preview.length > 100 ? preview.substring(0, 100) : preview;
subtitle_lines = 1;
add_css_class("conversation-row");
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);
if (conversation.is_unread) {
unread_badge.opacity = 1.0;
} else {
unread_badge.opacity = 0.0;
}
// 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);
}
}
}
}