diff --git a/commands.txt b/commands.txt new file mode 100644 index 0000000000..f2dc5c892b --- /dev/null +++ b/commands.txt @@ -0,0 +1,12 @@ +dfx identity use matt +dfx canister call user_mgmt set_username '("Matt")' +dfx canister call user_mgmt get_principal '("Matt")' +dfx identity use hamish +dfx canister call user_mgmt set_username '("Hamish")' +dfx canister call user_mgmt get_principal '("Hamish")' +dfx canister call chats create_chat '(principal "lpfg6-5goq7-vn6yz-wegiu-r6goc-sxoor-wj3b2-dpyup-k6i5m-qhv5m-xae", "Hi Matt, how's it going")' +dfx canister call chats send_message '(45_292_552_032_833_753, "hello!!??")' +dfx identity use matt +dfx canister call chats list_chats +dfx canister call chats get_messages '(45_292_552_032_833_753, 0)' +dfx canister call chats send_message '(45_292_552_032_833_753, "whats up?")' \ No newline at end of file diff --git a/src/chats/chats.did b/src/chats/chats.did index 655d367eca..89cc379edf 100644 --- a/src/chats/chats.did +++ b/src/chats/chats.did @@ -1,13 +1,21 @@ -type Message = +type Message = record { id: nat64; timestamp: nat64; sent_by_me: bool; text: text; - } + }; + +type ChatSummary = + record { + id: nat64; + them: principal; + most_recent: Message; + }; service : { - create_chat: (principal) -> (opt nat64); - send_message: (nat64, text) -> (bool); + create_chat: (principal, text) -> (opt nat64); + send_message: (nat64, text) -> (opt nat64); get_messages: (nat64, nat64) -> (opt vec Message) query; + list_chats: () -> (vec ChatSummary) query; } \ No newline at end of file diff --git a/src/chats/src/api.rs b/src/chats/src/api.rs index b83f8b29ac..a53463b62f 100644 --- a/src/chats/src/api.rs +++ b/src/chats/src/api.rs @@ -1,13 +1,13 @@ use ic_cdk_macros::*; use ic_types::Principal; -use crate::domain::chat::Message; +use crate::domain::chat::{ChatId, ChatSummary, Message}; use crate::queries::*; use crate::updates::*; -use crate::domain::chat_list::ChatId; -// #[query] -// fn list_chats() -> Vec { -// } +#[query] +fn list_chats() -> Vec { + list_chats::query() +} #[query] fn get_messages(chat_id: ChatId, from_index: usize) -> Option> { @@ -15,12 +15,12 @@ fn get_messages(chat_id: ChatId, from_index: usize) -> Option> { } #[update] -fn create_chat(recipient: Principal) -> Option { - create_chat::update(recipient) +fn create_chat(recipient: Principal, text: String) -> Option { + create_chat::update(recipient, text) } #[update] -fn send_message(chat_id: ChatId, text: String) -> bool { +fn send_message(chat_id: ChatId, text: String) -> Option { send_message::update(chat_id, text) } diff --git a/src/chats/src/domain/chat.rs b/src/chats/src/domain/chat.rs index 6af0991845..773b1ccd1f 100644 --- a/src/chats/src/domain/chat.rs +++ b/src/chats/src/domain/chat.rs @@ -1,20 +1,35 @@ use ic_cdk::export::candid::CandidType; use ic_types::Principal; +use highway::{HighwayHasher, HighwayHash}; use serde::Deserialize; #[derive(CandidType, Deserialize)] pub struct Chat { + id: ChatId, user1: Principal, user2: Principal, messages: Vec } impl Chat { - pub fn new(user1: Principal, user2: Principal) -> Chat { + pub fn get_id(&self) -> ChatId { + self.id + } + + pub fn new(id: ChatId, sender: Principal, recipient: Principal, text: String, timestamp: u64) -> Chat { + + let message = MessageInternal { + id: 0, + timestamp, + sent_by_user1: true, + text + }; + Chat { - user1, - user2, - messages: Vec::new() + id, + user1: sender, + user2: recipient, + messages: vec![message] } } @@ -22,11 +37,8 @@ impl Chat { self.user1 == *user || self.user2 == *user } - pub fn push_message(&mut self, sender: &Principal, text: String, timestamp: u64) { - let id = match self.messages.last() { - Some(m) => m.id + 1, - None => 0 - }; + pub fn push_message(&mut self, sender: &Principal, text: String, timestamp: u64) -> u64 { + let id = self.messages.last().unwrap().id + 1; let message = MessageInternal { id, @@ -36,6 +48,8 @@ impl Chat { }; self.messages.push(message); + + id } pub fn get_messages(&self, me: &Principal, from_index: usize) -> Vec { @@ -45,9 +59,60 @@ impl Chat { self.messages[from_index..] .iter() - .map(|m| Message::new(m.id, m.timestamp, m.sent_by_user1 == (*me == self.user1), m.text.clone())) + .map(|m| Message::new( + m.id, + m.timestamp, + m.sent_by_user1 == (*me == self.user1), + m.text.clone())) .collect() } + + pub fn to_summary(&self, me: &Principal) -> ChatSummary { + let message = self.messages.last().unwrap(); + + ChatSummary { + id: self.id, + them: if self.user1 == *me { self.user2.clone() } else { self.user1.clone() }, + most_recent: Message::new( + message.id, + message.timestamp, + message.sent_by_user1 == (*me == self.user1), + message.text.clone()) + } + } +} + +/// TODO: We would preferably use a Uuid or u128 but these haven't yet got a CandidType implementation +#[derive(CandidType, Deserialize, PartialEq, Eq, Hash, Copy, Clone)] +pub struct ChatId(u64); + +impl ChatId { + pub fn new(user1: &Principal, user2: &Principal) -> ChatId { + let mut hasher = HighwayHasher::default(); + + if user1 < user2 { + hasher.append(user1.as_slice()); + hasher.append(user2.as_slice()); + } else { + hasher.append(user2.as_slice()); + hasher.append(user1.as_slice()); + } + + ChatId(hasher.finalize64()) + } +} + +#[derive(CandidType)] +pub struct ChatSummary { + id: ChatId, + them: Principal, + most_recent: Message, +} + +impl ChatSummary { + pub fn get_most_recent(&self) -> &Message { + &self.most_recent + } } #[derive(CandidType)] @@ -67,6 +132,10 @@ impl Message { text } } + + pub fn get_timestamp(&self) -> u64 { + self.timestamp + } } #[derive(CandidType, Deserialize)] @@ -88,7 +157,10 @@ mod tests { let user1 = Principal::from_text("yy53y-szfmc-h2gyu-zlind-wzikf-6zuqh-i4x6u-fvado-rvydl-qxwlz-oqe").unwrap(); let user2 = Principal::from_text("ups66-6ukpx-mitsu-vhso3-ixjld-5p3m5-fbq6p-bbbma-oyzvm-mjr2w-qae").unwrap(); - let mut chat = Chat::new(user1.clone(), user2.clone()); + let mut chat = Chat::new( + ChatId::new(&user1, &user2), + user1.clone(), + user2.clone()); for i in 0..10 { let text = i.to_string(); @@ -113,7 +185,10 @@ mod tests { let user1 = Principal::from_text("yy53y-szfmc-h2gyu-zlind-wzikf-6zuqh-i4x6u-fvado-rvydl-qxwlz-oqe").unwrap(); let user2 = Principal::from_text("ups66-6ukpx-mitsu-vhso3-ixjld-5p3m5-fbq6p-bbbma-oyzvm-mjr2w-qae").unwrap(); - let mut chat = Chat::new(user1.clone(), user2.clone()); + let mut chat = Chat::new( + ChatId::new(&user1, &user2), + user1.clone(), + user2.clone()); let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos() as u64; @@ -136,7 +211,10 @@ mod tests { let user1 = Principal::from_text("yy53y-szfmc-h2gyu-zlind-wzikf-6zuqh-i4x6u-fvado-rvydl-qxwlz-oqe").unwrap(); let user2 = Principal::from_text("ups66-6ukpx-mitsu-vhso3-ixjld-5p3m5-fbq6p-bbbma-oyzvm-mjr2w-qae").unwrap(); - let mut chat = Chat::new(user1.clone(), user2.clone()); + let mut chat = Chat::new( + ChatId::new(&user1, &user2), + user1.clone(), + user2.clone()); let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos() as u64; diff --git a/src/chats/src/domain/chat_list.rs b/src/chats/src/domain/chat_list.rs index 164c47b6b7..19b6d4f965 100644 --- a/src/chats/src/domain/chat_list.rs +++ b/src/chats/src/domain/chat_list.rs @@ -1,10 +1,7 @@ use std::collections::{HashMap, hash_map::Entry::{Occupied, Vacant}}; -use ic_cdk::export::candid::CandidType; use ic_types::Principal; -use highway::{HighwayHasher, HighwayHash}; -use serde::Deserialize; use shared::StableState; -use super::chat::Chat; +use super::chat::{Chat, ChatId, ChatSummary}; #[derive(Default)] pub struct ChatList { @@ -12,55 +9,66 @@ pub struct ChatList { } impl ChatList { - - pub fn create(&mut self, sender: Principal, recipient: Principal) -> Option { - + pub fn create(&mut self, sender: Principal, recipient: Principal, text: String, timestamp: u64) -> Option { let chat_id = ChatId::new(&sender, &recipient); - match self.chats.entry(chat_id) { Occupied(_) => None, Vacant(e) => { - e.insert(Chat::new(sender, recipient)); + e.insert(Chat::new(chat_id, sender, recipient, text, timestamp)); Some(chat_id) } } } - pub fn get(&self, chat_id: ChatId, on_behalf_of: &Principal) -> Option<&Chat> { - + pub fn get(&self, chat_id: ChatId, me: &Principal) -> Option<&Chat> { let chat = self.chats.get(&chat_id)?; - - if !chat.involves_user(on_behalf_of) { + if !chat.involves_user(me) { return None; } - Some(chat) } - pub fn get_mut(&mut self, chat_id: ChatId, on_behalf_of: &Principal) -> Option<&mut Chat> { - + pub fn get_mut(&mut self, chat_id: ChatId, me: &Principal) -> Option<&mut Chat> { let chat = self.chats.get_mut(&chat_id)?; - - if !chat.involves_user(on_behalf_of) { + if !chat.involves_user(me) { return None; } - Some(chat) } + + pub fn list_chats(&self, user: &Principal) -> Vec { + // For now this will iterate through every chat... + let mut list: Vec<_> = self + .chats + .values() + .filter(|chat| chat.involves_user(user)) + .map(|chat| chat.to_summary(user)) + .collect(); + + list.sort_unstable_by(|c1, c2| { + let t1 = c1.get_most_recent().get_timestamp(); + let t2 = c2.get_most_recent().get_timestamp(); + t2.cmp(&t1) + }); + + list + } } impl StableState for ChatList { - type State = Vec<(ChatId, Chat)>; + type State = Vec; - fn drain(self) -> Vec<(ChatId, Chat)> { + fn drain(self) -> Vec { self.chats .into_iter() + .map(|(_, c)| c) .collect() } - fn fill(chats: Vec<(ChatId, Chat)>) -> ChatList { + fn fill(chats: Vec) -> ChatList { let map: HashMap = chats .into_iter() + .map(|c| (c.get_id(), c)) .collect(); ChatList { @@ -68,24 +76,3 @@ impl StableState for ChatList { } } } - -/// TODO: We would preferably use a Uuid or u128 but these haven't yet got a CandidType implementation -#[derive(CandidType, Deserialize, PartialEq, Eq, Hash, Copy, Clone)] -pub struct ChatId(u64); - -impl ChatId { - fn new(user1: &Principal, user2: &Principal) -> ChatId { - let mut hasher = HighwayHasher::default(); - - if user1 < user2 { - hasher.append(user1.as_slice()); - hasher.append(user2.as_slice()); - } else { - hasher.append(user2.as_slice()); - hasher.append(user1.as_slice()); - } - - ChatId(hasher.finalize64()) - } -} - diff --git a/src/chats/src/queries/get_messages.rs b/src/chats/src/queries/get_messages.rs index bb64996d15..7a36bb60ad 100644 --- a/src/chats/src/queries/get_messages.rs +++ b/src/chats/src/queries/get_messages.rs @@ -1,12 +1,10 @@ use ic_cdk::storage; -use crate::domain::chat_list::{ChatId, ChatList}; -use crate::domain::chat::Message; +use crate::domain::chat::{ChatId, Message}; +use crate::domain::chat_list::ChatList; pub fn query(chat_id: ChatId, from_index: usize) -> Option> { - - let chat_list: &mut ChatList = storage::get_mut(); + let chat_list: &ChatList = storage::get(); let me = ic_cdk::caller(); - let chat = chat_list.get(chat_id, &me)?; Some(chat.get_messages(&me, from_index)) diff --git a/src/chats/src/queries/list_chats.rs b/src/chats/src/queries/list_chats.rs new file mode 100644 index 0000000000..e62c50d9e9 --- /dev/null +++ b/src/chats/src/queries/list_chats.rs @@ -0,0 +1,10 @@ +use ic_cdk::storage; +use crate::domain::chat_list::ChatList; +use crate::domain::chat::ChatSummary; + +pub fn query() -> Vec { + let chat_list: &ChatList = storage::get(); + let me = ic_cdk::caller(); + + chat_list.list_chats(&me) +} \ No newline at end of file diff --git a/src/chats/src/queries/mod.rs b/src/chats/src/queries/mod.rs index 4cb644c016..fcc14f2519 100644 --- a/src/chats/src/queries/mod.rs +++ b/src/chats/src/queries/mod.rs @@ -1 +1,2 @@ -pub mod get_messages; \ No newline at end of file +pub mod get_messages; +pub mod list_chats; \ No newline at end of file diff --git a/src/chats/src/updates/create_chat.rs b/src/chats/src/updates/create_chat.rs index 0cf72ef2d9..f14eb3d958 100644 --- a/src/chats/src/updates/create_chat.rs +++ b/src/chats/src/updates/create_chat.rs @@ -1,11 +1,12 @@ +use ic_cdk::{api, storage}; use ic_types::Principal; -use crate::domain::chat_list::{ChatId, ChatList}; -use ic_cdk::{storage}; - -pub fn update(recipient: Principal) -> Option { +use crate::domain::chat::ChatId; +use crate::domain::chat_list::ChatList; +pub fn update(recipient: Principal, text: String) -> Option { let chat_list: &mut ChatList = storage::get_mut(); let me = ic_cdk::caller(); + let timestamp = api::time() as u64; - chat_list.create(me, recipient) + chat_list.create(me, recipient, text, timestamp) } \ No newline at end of file diff --git a/src/chats/src/updates/send_message.rs b/src/chats/src/updates/send_message.rs index fcbbf08393..c88ead9297 100644 --- a/src/chats/src/updates/send_message.rs +++ b/src/chats/src/updates/send_message.rs @@ -1,16 +1,16 @@ -use crate::domain::chat_list::{ChatId, ChatList}; use ic_cdk::{api, storage}; +use crate::domain::chat::ChatId; +use crate::domain::chat_list::ChatList; -pub fn update(chat_id: ChatId, text: String) -> bool { - +pub fn update(chat_id: ChatId, text: String) -> Option { let chat_list: &mut ChatList = storage::get_mut(); let me = ic_cdk::caller(); if let Some(chat) = chat_list.get_mut(chat_id, &me) { let timestamp = api::time() as u64; - chat.push_message(&me, text, timestamp); - return true; + let message_id = chat.push_message(&me, text, timestamp); + return Some(message_id); } - false + None } \ No newline at end of file