Add 'gtk/' from commit '7d0dfb455aa86245231b383a92e79b3c08a12d5e'
git-subtree-dir: gtk git-subtree-mainline:c710c6e053git-subtree-split:7d0dfb455a
This commit is contained in:
150
gtk/src/conversation-list/conversation-list-model.vala
Normal file
150
gtk/src/conversation-list/conversation-list-model.vala
Normal 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];
|
||||
}
|
||||
}
|
||||
126
gtk/src/conversation-list/conversation-list-view.vala
Normal file
126
gtk/src/conversation-list/conversation-list-view.vala
Normal 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);
|
||||
}
|
||||
}
|
||||
61
gtk/src/conversation-list/conversation-row.vala
Normal file
61
gtk/src/conversation-list/conversation-row.vala
Normal 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("&", "&");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user