kordophone-db: adds support for the Messages table
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
-- This file should undo anything in `up.sql`
|
-- This file should undo anything in `up.sql`
|
||||||
DROP TABLE IF EXISTS `participants`;
|
|
||||||
DROP TABLE IF EXISTS `conversation_participants`;
|
DROP TABLE IF EXISTS `conversation_participants`;
|
||||||
|
DROP TABLE IF EXISTS `messages`;
|
||||||
|
DROP TABLE IF EXISTS `conversation_messages`;
|
||||||
|
DROP TABLE IF EXISTS `participants`;
|
||||||
DROP TABLE IF EXISTS `conversations`;
|
DROP TABLE IF EXISTS `conversations`;
|
||||||
@@ -1,15 +1,29 @@
|
|||||||
-- Your SQL goes here
|
-- Your SQL goes here
|
||||||
CREATE TABLE `participants`(
|
|
||||||
`id` INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
`display_name` TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE `conversation_participants`(
|
CREATE TABLE `conversation_participants`(
|
||||||
`conversation_id` TEXT NOT NULL,
|
`conversation_id` TEXT NOT NULL,
|
||||||
`participant_id` INTEGER NOT NULL,
|
`participant_id` INTEGER NOT NULL,
|
||||||
PRIMARY KEY(`conversation_id`, `participant_id`)
|
PRIMARY KEY(`conversation_id`, `participant_id`)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE `messages`(
|
||||||
|
`id` TEXT NOT NULL PRIMARY KEY,
|
||||||
|
`text` TEXT NOT NULL,
|
||||||
|
`sender_participant_id` INTEGER,
|
||||||
|
`date` TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE `conversation_messages`(
|
||||||
|
`conversation_id` TEXT NOT NULL,
|
||||||
|
`message_id` TEXT NOT NULL,
|
||||||
|
PRIMARY KEY(`conversation_id`, `message_id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE `participants`(
|
||||||
|
`id` INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
`display_name` TEXT,
|
||||||
|
`is_me` BOOL NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE `conversations`(
|
CREATE TABLE `conversations`(
|
||||||
`id` TEXT NOT NULL PRIMARY KEY,
|
`id` TEXT NOT NULL PRIMARY KEY,
|
||||||
`unread_count` BIGINT NOT NULL,
|
`unread_count` BIGINT NOT NULL,
|
||||||
@@ -3,11 +3,18 @@ use diesel::{prelude::*, sqlite::Sqlite};
|
|||||||
use diesel::query_dsl::BelongingToDsl;
|
use diesel::query_dsl::BelongingToDsl;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::models::Participant;
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{
|
models::{
|
||||||
Conversation,
|
Conversation,
|
||||||
|
Message,
|
||||||
db::conversation::Record as ConversationRecord,
|
db::conversation::Record as ConversationRecord,
|
||||||
db::participant::{Record as ParticipantRecord, ConversationParticipant},
|
db::participant::{
|
||||||
|
ConversationParticipant,
|
||||||
|
Record as ParticipantRecord,
|
||||||
|
InsertableRecord as InsertableParticipantRecord
|
||||||
|
},
|
||||||
|
db::message::Record as MessageRecord,
|
||||||
},
|
},
|
||||||
schema,
|
schema,
|
||||||
};
|
};
|
||||||
@@ -24,6 +31,14 @@ impl ChatDatabase {
|
|||||||
Self::new(":memory:")
|
Self::new(":memory:")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to get the last inserted row ID
|
||||||
|
// This is a workaround since the Sqlite backend doesn't support `RETURNING`
|
||||||
|
// Huge caveat with this is that it depends on whatever the last insert was, prevents concurrent inserts.
|
||||||
|
fn last_insert_id(&mut self) -> Result<i32> {
|
||||||
|
Ok(diesel::select(diesel::dsl::sql::<diesel::sql_types::Integer>("last_insert_rowid()"))
|
||||||
|
.get_result(&mut self.db)?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new(db_path: &str) -> Result<Self> {
|
pub fn new(db_path: &str) -> Result<Self> {
|
||||||
let mut db = SqliteConnection::establish(db_path)?;
|
let mut db = SqliteConnection::establish(db_path)?;
|
||||||
db.run_pending_migrations(MIGRATIONS)
|
db.run_pending_migrations(MIGRATIONS)
|
||||||
@@ -111,4 +126,88 @@ impl ChatDatabase {
|
|||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_message(&mut self, conversation_guid: &str, message: Message) -> Result<()> {
|
||||||
|
use crate::schema::messages::dsl::*;
|
||||||
|
use crate::schema::conversation_messages::dsl::*;
|
||||||
|
|
||||||
|
// Handle participant if message has a remote sender
|
||||||
|
let sender = message.sender.clone();
|
||||||
|
let mut db_message: MessageRecord = message.into();
|
||||||
|
db_message.sender_participant_id = self.get_or_create_participant(&sender);
|
||||||
|
|
||||||
|
diesel::replace_into(messages)
|
||||||
|
.values(&db_message)
|
||||||
|
.execute(&mut self.db)?;
|
||||||
|
|
||||||
|
diesel::replace_into(conversation_messages)
|
||||||
|
.values((
|
||||||
|
conversation_id.eq(conversation_guid),
|
||||||
|
message_id.eq(&db_message.id),
|
||||||
|
))
|
||||||
|
.execute(&mut self.db)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_messages_for_conversation(&mut self, conversation_guid: &str) -> Result<Vec<Message>> {
|
||||||
|
use crate::schema::messages::dsl::*;
|
||||||
|
use crate::schema::conversation_messages::dsl::*;
|
||||||
|
use crate::schema::participants::dsl::*;
|
||||||
|
|
||||||
|
let message_records = conversation_messages
|
||||||
|
.filter(conversation_id.eq(conversation_guid))
|
||||||
|
.inner_join(messages)
|
||||||
|
.select(MessageRecord::as_select())
|
||||||
|
.order_by(schema::messages::date.asc())
|
||||||
|
.load::<MessageRecord>(&mut self.db)?;
|
||||||
|
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for message_record in message_records {
|
||||||
|
let mut message: Message = message_record.clone().into();
|
||||||
|
|
||||||
|
// If there's a sender_participant_id, load the participant info
|
||||||
|
if let Some(pid) = message_record.sender_participant_id {
|
||||||
|
let participant = participants
|
||||||
|
.find(pid)
|
||||||
|
.first::<ParticipantRecord>(&mut self.db)?;
|
||||||
|
message.sender = participant.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_or_create_participant(&mut self, participant: &Participant) -> Option<i32> {
|
||||||
|
match participant {
|
||||||
|
Participant::Me => None,
|
||||||
|
Participant::Remote { display_name: p_name, .. } => {
|
||||||
|
use crate::schema::participants::dsl::*;
|
||||||
|
|
||||||
|
let existing_participant = participants
|
||||||
|
.filter(display_name.eq(p_name))
|
||||||
|
.first::<ParticipantRecord>(&mut self.db)
|
||||||
|
.optional()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if let Some(participant) = existing_participant {
|
||||||
|
return Some(participant.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let participant_record = InsertableParticipantRecord {
|
||||||
|
display_name: Some(participant.display_name()),
|
||||||
|
is_me: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(participants)
|
||||||
|
.values(&participant_record)
|
||||||
|
.execute(&mut self.db)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.last_insert_id().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
40
kordophone-db/src/models/db/message.rs
Normal file
40
kordophone-db/src/models/db/message.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use diesel::prelude::*;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use crate::models::{Message, Participant};
|
||||||
|
|
||||||
|
#[derive(Queryable, Selectable, Insertable, AsChangeset, Clone, Identifiable)]
|
||||||
|
#[diesel(table_name = crate::schema::messages)]
|
||||||
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
|
pub struct Record {
|
||||||
|
pub id: String,
|
||||||
|
pub sender_participant_id: Option<i32>,
|
||||||
|
pub text: String,
|
||||||
|
pub date: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Message> for Record {
|
||||||
|
fn from(message: Message) -> Self {
|
||||||
|
Self {
|
||||||
|
id: message.id,
|
||||||
|
sender_participant_id: match message.sender {
|
||||||
|
Participant::Me => None,
|
||||||
|
Participant::Remote { id, .. } => id,
|
||||||
|
},
|
||||||
|
text: message.text,
|
||||||
|
date: message.date,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Record> for Message {
|
||||||
|
fn from(record: Record) -> Self {
|
||||||
|
Self {
|
||||||
|
id: record.id,
|
||||||
|
// We'll set the proper sender later when loading participant info
|
||||||
|
sender: Participant::Me,
|
||||||
|
text: record.text,
|
||||||
|
date: record.date,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod conversation;
|
pub mod conversation;
|
||||||
pub mod participant;
|
pub mod participant;
|
||||||
|
pub mod message;
|
||||||
@@ -6,19 +6,28 @@ use crate::schema::conversation_participants;
|
|||||||
#[diesel(table_name = crate::schema::participants)]
|
#[diesel(table_name = crate::schema::participants)]
|
||||||
pub struct Record {
|
pub struct Record {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub display_name: String
|
pub display_name: Option<String>,
|
||||||
|
pub is_me: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
#[diesel(table_name = crate::schema::participants)]
|
#[diesel(table_name = crate::schema::participants)]
|
||||||
pub struct InsertableRecord {
|
pub struct InsertableRecord {
|
||||||
pub display_name: String
|
pub display_name: Option<String>,
|
||||||
|
pub is_me: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Participant> for InsertableRecord {
|
impl From<Participant> for InsertableRecord {
|
||||||
fn from(participant: Participant) -> Self {
|
fn from(participant: Participant) -> Self {
|
||||||
InsertableRecord {
|
match participant {
|
||||||
display_name: participant.display_name
|
Participant::Me => InsertableRecord {
|
||||||
|
display_name: None,
|
||||||
|
is_me: true,
|
||||||
|
},
|
||||||
|
Participant::Remote { display_name, .. } => InsertableRecord {
|
||||||
|
display_name: Some(display_name),
|
||||||
|
is_me: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,17 +44,30 @@ pub struct ConversationParticipant {
|
|||||||
|
|
||||||
impl From<Record> for Participant {
|
impl From<Record> for Participant {
|
||||||
fn from(record: Record) -> Self {
|
fn from(record: Record) -> Self {
|
||||||
Participant {
|
if record.is_me {
|
||||||
display_name: record.display_name
|
Participant::Me
|
||||||
|
} else {
|
||||||
|
Participant::Remote {
|
||||||
|
id: Some(record.id),
|
||||||
|
display_name: record.display_name.unwrap_or_default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Participant> for Record {
|
impl From<Participant> for Record {
|
||||||
fn from(participant: Participant) -> Self {
|
fn from(participant: Participant) -> Self {
|
||||||
Record {
|
match participant {
|
||||||
id: 0, // This will be set by the database
|
Participant::Me => Record {
|
||||||
display_name: participant.display_name,
|
id: 0, // This will be set by the database
|
||||||
|
display_name: None,
|
||||||
|
is_me: true,
|
||||||
|
},
|
||||||
|
Participant::Remote { display_name, .. } => Record {
|
||||||
|
id: 0, // This will be set by the database
|
||||||
|
display_name: Some(display_name),
|
||||||
|
is_me: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
84
kordophone-db/src/models/message.rs
Normal file
84
kordophone-db/src/models/message.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use chrono::{DateTime, NaiveDateTime};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::models::participant::Participant;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Message {
|
||||||
|
pub id: String,
|
||||||
|
pub sender: Participant,
|
||||||
|
pub text: String,
|
||||||
|
pub date: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
pub fn builder() -> MessageBuilder {
|
||||||
|
MessageBuilder::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<kordophone::model::Message> for Message {
|
||||||
|
fn from(value: kordophone::model::Message) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.guid,
|
||||||
|
sender: match value.sender {
|
||||||
|
Some(sender) => Participant::Remote {
|
||||||
|
id: None,
|
||||||
|
display_name: sender,
|
||||||
|
},
|
||||||
|
None => Participant::Me,
|
||||||
|
},
|
||||||
|
text: value.text,
|
||||||
|
date: DateTime::from_timestamp(
|
||||||
|
value.date.unix_timestamp(),
|
||||||
|
value.date.unix_timestamp_nanos()
|
||||||
|
.try_into()
|
||||||
|
.unwrap_or(0),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.naive_local()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MessageBuilder {
|
||||||
|
id: Option<String>,
|
||||||
|
sender: Option<Participant>,
|
||||||
|
text: Option<String>,
|
||||||
|
date: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBuilder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: None,
|
||||||
|
sender: None,
|
||||||
|
text: None,
|
||||||
|
date: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sender(mut self, sender: Participant) -> Self {
|
||||||
|
self.sender = Some(sender);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text(mut self, text: String) -> Self {
|
||||||
|
self.text = Some(text);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn date(mut self, date: NaiveDateTime) -> Self {
|
||||||
|
self.date = Some(date);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> Message {
|
||||||
|
Message {
|
||||||
|
id: self.id.unwrap_or_else(|| Uuid::new_v4().to_string()),
|
||||||
|
sender: self.sender.unwrap_or(Participant::Me),
|
||||||
|
text: self.text.unwrap_or_default(),
|
||||||
|
date: self.date.unwrap_or_else(|| chrono::Utc::now().naive_utc()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
pub mod conversation;
|
pub mod conversation;
|
||||||
pub mod participant;
|
pub mod participant;
|
||||||
|
pub mod message;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
|
||||||
pub use conversation::Conversation;
|
pub use conversation::Conversation;
|
||||||
pub use participant::Participant;
|
pub use participant::Participant;
|
||||||
|
pub use message::Message;
|
||||||
@@ -1,16 +1,35 @@
|
|||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Participant {
|
pub enum Participant {
|
||||||
pub display_name: String,
|
Me,
|
||||||
|
Remote {
|
||||||
|
id: Option<i32>,
|
||||||
|
display_name: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for Participant {
|
impl From<String> for Participant {
|
||||||
fn from(display_name: String) -> Self {
|
fn from(display_name: String) -> Self {
|
||||||
Participant { display_name }
|
Participant::Remote {
|
||||||
|
id: None,
|
||||||
|
display_name,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for Participant {
|
impl From<&str> for Participant {
|
||||||
fn from(display_name: &str) -> Self {
|
fn from(display_name: &str) -> Self {
|
||||||
Participant { display_name: display_name.to_string() }
|
Participant::Remote {
|
||||||
|
id: None,
|
||||||
|
display_name: display_name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Participant {
|
||||||
|
pub fn display_name(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Participant::Me => "(Me)".to_string(),
|
||||||
|
Participant::Remote { display_name, .. } => display_name.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// When this file changes, run the following command to generate a new migration:
|
||||||
|
// DATABASE_URL=/tmp/db.sql diesel migration generate --diff-schema create_conversations
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
conversations (id) {
|
conversations (id) {
|
||||||
id -> Text,
|
id -> Text,
|
||||||
@@ -11,7 +14,8 @@ diesel::table! {
|
|||||||
diesel::table! {
|
diesel::table! {
|
||||||
participants (id) {
|
participants (id) {
|
||||||
id -> Integer,
|
id -> Integer,
|
||||||
display_name -> Text,
|
display_name -> Nullable<Text>,
|
||||||
|
is_me -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +26,26 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
messages (id) {
|
||||||
|
id -> Text, // guid
|
||||||
|
text -> Text,
|
||||||
|
sender_participant_id -> Nullable<Integer>,
|
||||||
|
date -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
conversation_messages (conversation_id, message_id) {
|
||||||
|
conversation_id -> Text, // guid
|
||||||
|
message_id -> Text, // guid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::joinable!(conversation_participants -> conversations (conversation_id));
|
diesel::joinable!(conversation_participants -> conversations (conversation_id));
|
||||||
diesel::joinable!(conversation_participants -> participants (participant_id));
|
diesel::joinable!(conversation_participants -> participants (participant_id));
|
||||||
diesel::allow_tables_to_appear_in_same_query!(conversations, participants, conversation_participants);
|
diesel::allow_tables_to_appear_in_same_query!(conversations, participants, conversation_participants);
|
||||||
|
|
||||||
|
diesel::joinable!(conversation_messages -> conversations (conversation_id));
|
||||||
|
diesel::joinable!(conversation_messages -> messages (message_id));
|
||||||
|
diesel::allow_tables_to_appear_in_same_query!(conversations, messages, conversation_messages);
|
||||||
@@ -2,10 +2,28 @@ use crate::{
|
|||||||
chat_database::ChatDatabase,
|
chat_database::ChatDatabase,
|
||||||
models::{
|
models::{
|
||||||
conversation::{Conversation, ConversationBuilder},
|
conversation::{Conversation, ConversationBuilder},
|
||||||
participant::Participant
|
participant::Participant,
|
||||||
|
message::Message,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to compare participants ignoring database IDs
|
||||||
|
fn participants_equal_ignoring_id(a: &Participant, b: &Participant) -> bool {
|
||||||
|
match (a, b) {
|
||||||
|
(Participant::Me, Participant::Me) => true,
|
||||||
|
(Participant::Remote { display_name: name_a, .. },
|
||||||
|
Participant::Remote { display_name: name_b, .. }) => name_a == name_b,
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn participants_vec_equal_ignoring_id(a: &[Participant], b: &[Participant]) -> bool {
|
||||||
|
if a.len() != b.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
a.iter().zip(b.iter()).all(|(a, b)| participants_equal_ignoring_id(a, b))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_database_init() {
|
fn test_database_init() {
|
||||||
let _ = ChatDatabase::new_in_memory().unwrap();
|
let _ = ChatDatabase::new_in_memory().unwrap();
|
||||||
@@ -62,7 +80,7 @@ fn test_conversation_participants() {
|
|||||||
let read_conversation = db.get_conversation_by_guid(&guid).unwrap().unwrap();
|
let read_conversation = db.get_conversation_by_guid(&guid).unwrap().unwrap();
|
||||||
let read_participants = read_conversation.participants;
|
let read_participants = read_conversation.participants;
|
||||||
|
|
||||||
assert_eq!(participants, read_participants);
|
assert!(participants_vec_equal_ignoring_id(&participants, &read_participants));
|
||||||
|
|
||||||
// Try making another conversation with the same participants
|
// Try making another conversation with the same participants
|
||||||
let conversation = ConversationBuilder::new()
|
let conversation = ConversationBuilder::new()
|
||||||
@@ -75,7 +93,7 @@ fn test_conversation_participants() {
|
|||||||
let read_conversation = db.get_conversation_by_guid(&guid).unwrap().unwrap();
|
let read_conversation = db.get_conversation_by_guid(&guid).unwrap().unwrap();
|
||||||
let read_participants: Vec<Participant> = read_conversation.participants;
|
let read_participants: Vec<Participant> = read_conversation.participants;
|
||||||
|
|
||||||
assert_eq!(participants, read_participants);
|
assert!(participants_vec_equal_ignoring_id(&participants, &read_participants));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -112,6 +130,97 @@ fn test_all_conversations_with_participants() {
|
|||||||
let conv1 = all_conversations.iter().find(|c| c.guid == guid1).unwrap();
|
let conv1 = all_conversations.iter().find(|c| c.guid == guid1).unwrap();
|
||||||
let conv2 = all_conversations.iter().find(|c| c.guid == guid2).unwrap();
|
let conv2 = all_conversations.iter().find(|c| c.guid == guid2).unwrap();
|
||||||
|
|
||||||
assert_eq!(conv1.participants, participants1);
|
assert!(participants_vec_equal_ignoring_id(&conv1.participants, &participants1));
|
||||||
assert_eq!(conv2.participants, participants2);
|
assert!(participants_vec_equal_ignoring_id(&conv2.participants, &participants2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_messages() {
|
||||||
|
let mut db = ChatDatabase::new_in_memory().unwrap();
|
||||||
|
|
||||||
|
// First create a conversation with participants
|
||||||
|
let participants = vec!["Alice".into(), "Bob".into()];
|
||||||
|
let conversation = ConversationBuilder::new()
|
||||||
|
.display_name("Test Chat")
|
||||||
|
.participants(participants)
|
||||||
|
.build();
|
||||||
|
let conversation_id = conversation.guid.clone();
|
||||||
|
|
||||||
|
db.insert_conversation(conversation).unwrap();
|
||||||
|
|
||||||
|
// Create and insert a message from Me
|
||||||
|
let message1 = Message::builder()
|
||||||
|
.text("Hello everyone!".to_string())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Create and insert a message from a remote participant
|
||||||
|
let message2 = Message::builder()
|
||||||
|
.text("Hi there!".to_string())
|
||||||
|
.sender("Alice".into())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Insert both messages
|
||||||
|
db.insert_message(&conversation_id, message1.clone()).unwrap();
|
||||||
|
db.insert_message(&conversation_id, message2.clone()).unwrap();
|
||||||
|
|
||||||
|
// Retrieve messages
|
||||||
|
let messages = db.get_messages_for_conversation(&conversation_id).unwrap();
|
||||||
|
assert_eq!(messages.len(), 2);
|
||||||
|
|
||||||
|
// Verify first message (from Me)
|
||||||
|
let retrieved_message1 = messages.iter().find(|m| m.id == message1.id).unwrap();
|
||||||
|
assert_eq!(retrieved_message1.text, "Hello everyone!");
|
||||||
|
assert!(matches!(retrieved_message1.sender, Participant::Me));
|
||||||
|
|
||||||
|
// Verify second message (from Alice)
|
||||||
|
let retrieved_message2 = messages.iter().find(|m| m.id == message2.id).unwrap();
|
||||||
|
assert_eq!(retrieved_message2.text, "Hi there!");
|
||||||
|
if let Participant::Remote { display_name, .. } = &retrieved_message2.sender {
|
||||||
|
assert_eq!(display_name, "Alice");
|
||||||
|
} else {
|
||||||
|
panic!("Expected Remote participant. Got: {:?}", retrieved_message2.sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_message_ordering() {
|
||||||
|
let mut db = ChatDatabase::new_in_memory().unwrap();
|
||||||
|
|
||||||
|
// Create a conversation
|
||||||
|
let conversation = ConversationBuilder::new()
|
||||||
|
.display_name("Test Chat")
|
||||||
|
.build();
|
||||||
|
let conversation_id = conversation.guid.clone();
|
||||||
|
db.insert_conversation(conversation).unwrap();
|
||||||
|
|
||||||
|
// Create messages with specific timestamps
|
||||||
|
let now = chrono::Utc::now().naive_utc();
|
||||||
|
let message1 = Message::builder()
|
||||||
|
.text("First message".to_string())
|
||||||
|
.date(now)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let message2 = Message::builder()
|
||||||
|
.text("Second message".to_string())
|
||||||
|
.date(now + chrono::Duration::minutes(1))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let message3 = Message::builder()
|
||||||
|
.text("Third message".to_string())
|
||||||
|
.date(now + chrono::Duration::minutes(2))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Insert messages
|
||||||
|
db.insert_message(&conversation_id, message1).unwrap();
|
||||||
|
db.insert_message(&conversation_id, message2).unwrap();
|
||||||
|
db.insert_message(&conversation_id, message3).unwrap();
|
||||||
|
|
||||||
|
// Retrieve messages and verify order
|
||||||
|
let messages = db.get_messages_for_conversation(&conversation_id).unwrap();
|
||||||
|
assert_eq!(messages.len(), 3);
|
||||||
|
|
||||||
|
// Messages should be ordered by date
|
||||||
|
for i in 1..messages.len() {
|
||||||
|
assert!(messages[i].date > messages[i-1].date);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user