From 8304b68a647247ed2a4731f988da6bace65f4336 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 12 Sep 2025 12:04:31 -0700 Subject: [PATCH] first attempt at trying to keep track of locally send id --- .../down.sql | 3 + .../up.sql | 7 +++ core/kordophone-db/src/repository.rs | 58 ++++++++++++++++++- core/kordophone-db/src/schema.rs | 9 +++ core/kordophoned/src/daemon/mod.rs | 47 ++++++++++++--- 5 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 core/kordophone-db/migrations/2025-09-12-000001_add_message_aliases/down.sql create mode 100644 core/kordophone-db/migrations/2025-09-12-000001_add_message_aliases/up.sql diff --git a/core/kordophone-db/migrations/2025-09-12-000001_add_message_aliases/down.sql b/core/kordophone-db/migrations/2025-09-12-000001_add_message_aliases/down.sql new file mode 100644 index 0000000..5c6c3b6 --- /dev/null +++ b/core/kordophone-db/migrations/2025-09-12-000001_add_message_aliases/down.sql @@ -0,0 +1,3 @@ +-- Drop the alias mapping table +DROP TABLE IF EXISTS `message_aliases`; + diff --git a/core/kordophone-db/migrations/2025-09-12-000001_add_message_aliases/up.sql b/core/kordophone-db/migrations/2025-09-12-000001_add_message_aliases/up.sql new file mode 100644 index 0000000..bc0584c --- /dev/null +++ b/core/kordophone-db/migrations/2025-09-12-000001_add_message_aliases/up.sql @@ -0,0 +1,7 @@ +-- Add table to map local (client) IDs to server message GUIDs +CREATE TABLE IF NOT EXISTS `message_aliases` ( + `local_id` TEXT NOT NULL PRIMARY KEY, + `server_id` TEXT NOT NULL UNIQUE, + `conversation_id` TEXT NOT NULL +); + diff --git a/core/kordophone-db/src/repository.rs b/core/kordophone-db/src/repository.rs index 0f6783b..79b6e40 100644 --- a/core/kordophone-db/src/repository.rs +++ b/core/kordophone-db/src/repository.rs @@ -307,8 +307,11 @@ impl<'a> Repository<'a> { } pub fn delete_all_messages(&mut self) -> Result<()> { - use crate::schema::messages::dsl::*; - diesel::delete(messages).execute(self.connection)?; + use crate::schema::messages::dsl as messages_dsl; + use crate::schema::message_aliases::dsl as aliases_dsl; + + diesel::delete(messages_dsl::messages).execute(self.connection)?; + diesel::delete(aliases_dsl::message_aliases).execute(self.connection)?; Ok(()) } @@ -359,6 +362,57 @@ impl<'a> Repository<'a> { ) } + /// Create or update an alias mapping between a local (client) message id and a server message id. + pub fn set_message_alias( + &mut self, + local_id_in: &str, + server_id_in: &str, + conversation_id_in: &str, + ) -> Result<()> { + use crate::schema::message_aliases::dsl::*; + diesel::replace_into(message_aliases) + .values(( + local_id.eq(local_id_in), + server_id.eq(server_id_in), + conversation_id.eq(conversation_id_in), + )) + .execute(self.connection)?; + Ok(()) + } + + /// Returns the local id for a given server id, if any. + pub fn get_local_id_for(&mut self, server_id_in: &str) -> Result> { + use crate::schema::message_aliases::dsl::*; + let result = message_aliases + .filter(server_id.eq(server_id_in)) + .select(local_id) + .first::(self.connection) + .optional()?; + Ok(result) + } + + /// Batch lookup: returns a map server_id -> local_id for the provided server ids. + pub fn get_local_ids_for( + &mut self, + server_ids_in: Vec, + ) -> Result> { + use crate::schema::message_aliases::dsl::*; + if server_ids_in.is_empty() { + return Ok(HashMap::new()); + } + + let rows: Vec<(String, String)> = message_aliases + .filter(server_id.eq_any(&server_ids_in)) + .select((server_id, local_id)) + .load::<(String, String)>(self.connection)?; + + let mut map = HashMap::new(); + for (sid, lid) in rows { + map.insert(sid, lid); + } + Ok(map) + } + /// Update the contact_id for an existing participant record. pub fn update_participant_contact( &mut self, diff --git a/core/kordophone-db/src/schema.rs b/core/kordophone-db/src/schema.rs index c2b41e5..4bb8952 100644 --- a/core/kordophone-db/src/schema.rs +++ b/core/kordophone-db/src/schema.rs @@ -44,6 +44,14 @@ diesel::table! { } } +diesel::table! { + message_aliases (local_id) { + local_id -> Text, + server_id -> Text, + conversation_id -> Text, + } +} + diesel::table! { settings (key) { key -> Text, @@ -62,5 +70,6 @@ diesel::allow_tables_to_appear_in_same_query!( conversation_participants, messages, conversation_messages, + message_aliases, settings, ); diff --git a/core/kordophoned/src/daemon/mod.rs b/core/kordophoned/src/daemon/mod.rs index ea7be0b..bb17208 100644 --- a/core/kordophoned/src/daemon/mod.rs +++ b/core/kordophoned/src/daemon/mod.rs @@ -347,7 +347,16 @@ impl Daemon { self.database .lock() .await - .with_repository(|r| r.insert_message(&conversation_id, message.into())) + .with_repository(|r| { + // 1) Insert the server message + r.insert_message(&conversation_id, message.clone().into())?; + // 2) Persist alias local -> server for stable UI ids + r.set_message_alias( + &outgoing_message.guid.to_string(), + &message.id, + &conversation_id, + ) + }) .await .unwrap(); @@ -448,18 +457,38 @@ impl Daemon { .get(&conversation_id) .unwrap_or(&empty_vec); - self.database + // Fetch DB messages and an alias map (server_id -> local_id) in one DB access. + let (db_messages, alias_map) = self + .database .lock() .await .with_repository(|r| { - r.get_messages_for_conversation(&conversation_id) - .unwrap() - .into_iter() - .map(|m| m.into()) // Convert db::Message to daemon::Message - .chain(outgoing_messages.into_iter().map(|m| m.into())) - .collect() + let msgs = r.get_messages_for_conversation(&conversation_id).unwrap(); + let ids: Vec = msgs.iter().map(|m| m.id.clone()).collect(); + let map = r.get_local_ids_for(ids).unwrap_or_default(); + (msgs, map) }) - .await + .await; + + // Convert DB messages to daemon model, substituting local_id when an alias exists. + let mut result: Vec = Vec::with_capacity( + db_messages.len() + outgoing_messages.len(), + ); + for m in db_messages.into_iter() { + let server_id = m.id.clone(); + let mut dm: Message = m.into(); + if let Some(local_id) = alias_map.get(&server_id) { + dm.id = local_id.clone(); + } + result.push(dm); + } + + // Append pending outgoing messages (these already use local_id) + for om in outgoing_messages.iter() { + result.push(om.into()); + } + + result } async fn enqueue_outgoing_message(