api: add ChatListChanged and ChatListItemChanged events (#4476)

This commit is contained in:
Simon Laux
2024-04-16 00:35:19 +02:00
committed by GitHub
parent 489eae5d66
commit f9465f7512
27 changed files with 1021 additions and 12 deletions

View File

@@ -15,6 +15,7 @@ use strum_macros::EnumIter;
use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
use crate::chatlist::Chatlist;
use crate::chatlist_events;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
@@ -295,6 +296,8 @@ impl ChatId {
}
};
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, chat_id);
Ok(chat_id)
}
@@ -411,6 +414,7 @@ impl ChatId {
}
}
}
chatlist_events::emit_chatlist_changed(context);
if sync.into() {
// NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
@@ -433,6 +437,8 @@ impl ChatId {
pub(crate) async fn unblock_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
self.set_blocked(context, Blocked::Not).await?;
chatlist_events::emit_chatlist_changed(context);
if sync.into() {
let chat = Chat::load_from_db(context, self).await?;
// TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
@@ -443,6 +449,7 @@ impl ChatId {
.log_err(context)
.ok();
}
Ok(())
}
@@ -486,6 +493,7 @@ impl ChatId {
if self.set_blocked(context, Blocked::Not).await? {
context.emit_event(EventType::ChatModified(self));
chatlist_events::emit_chatlist_item_changed(context, self);
}
if sync.into() {
@@ -528,6 +536,7 @@ impl ChatId {
.await?;
context.emit_event(EventType::ChatModified(self));
chatlist_events::emit_chatlist_item_changed(context, self);
// make sure, the receivers will get all keys
self.reset_gossiped_timestamp(context).await?;
@@ -576,6 +585,7 @@ impl ChatId {
if protection_status_modified {
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
.await?;
chatlist_events::emit_chatlist_item_changed(context, self);
}
Ok(())
}
@@ -662,6 +672,8 @@ impl ChatId {
.await?;
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, self);
if sync.into() {
let chat = Chat::load_from_db(context, self).await?;
@@ -768,6 +780,7 @@ impl ChatId {
.await?;
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
context
.set_config_internal(Config::LastHousekeeping, None)
@@ -779,6 +792,7 @@ impl ChatId {
msg.text = stock_str::self_deleted_msg_body(context).await;
add_device_msg(context, None, Some(&mut msg)).await?;
}
chatlist_events::emit_chatlist_changed(context);
Ok(())
}
@@ -3103,7 +3117,9 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
.await?;
for chat_id_in_archive in chat_ids_in_archive {
context.emit_event(EventType::MsgsNoticed(chat_id_in_archive));
chatlist_events::emit_chatlist_item_changed(context, chat_id_in_archive);
}
chatlist_events::emit_chatlist_item_changed(context, DC_CHAT_ID_ARCHIVED_LINK);
} else {
let exists = context
.sql
@@ -3130,6 +3146,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
}
context.emit_event(EventType::MsgsNoticed(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
Ok(())
}
@@ -3197,6 +3214,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
for c in changed_chats {
context.emit_event(EventType::MsgsNoticed(c));
chatlist_events::emit_chatlist_item_changed(context, c);
}
Ok(())
@@ -3359,6 +3377,8 @@ pub async fn create_group_chat(
}
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, chat_id);
if protect == ProtectionStatus::Protected {
chat_id
@@ -3446,11 +3466,14 @@ pub(crate) async fn create_broadcast_list_ex(
let chat_id = ChatId::new(u32::try_from(row_id)?);
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
if sync.into() {
let id = SyncId::Grpid(grpid);
let action = SyncAction::CreateBroadcast(chat_name);
self::sync(context, id, action).await.log_err(context).ok();
}
Ok(chat_id)
}
@@ -3721,6 +3744,7 @@ pub(crate) async fn set_muted_ex(
.await
.context(format!("Failed to set mute duration for {chat_id}"))?;
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
if sync.into() {
let chat = Chat::load_from_db(context, chat_id).await?;
chat.sync(context, SyncAction::SetMuted(duration))
@@ -3881,6 +3905,7 @@ async fn rename_ex(
sync = Nosync;
}
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
success = true;
}
}
@@ -3941,6 +3966,7 @@ pub async fn set_chat_profile_image(
context.emit_msgs_changed(chat_id, msg.id);
}
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
Ok(())
}
@@ -4087,6 +4113,8 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
msg_id: msg.id,
});
msg.timestamp_sort = create_smeared_timestamp(context);
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
context.scheduler.interrupt_smtp().await;
}

View File

@@ -37,7 +37,7 @@ use crate::tools::{
duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time,
EmailAddress, SystemTime,
};
use crate::{chat, stock_str};
use crate::{chat, chatlist_events, stock_str};
/// Time during which a contact is considered as seen recently.
const SEEN_RECENTLY_SECONDS: i64 = 600;
@@ -760,6 +760,7 @@ impl Contact {
if count > 0 {
// Chat name updated
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
}
}
}
@@ -796,7 +797,9 @@ impl Contact {
Ok(row_id)
}).await?;
Ok((ContactId::new(row_id), sth_modified))
let contact_id = ContactId::new(row_id);
Ok((contact_id, sth_modified))
}
/// Add a number of contacts.
@@ -1524,6 +1527,7 @@ WHERE type=? AND id IN (
}
}
chatlist_events::emit_chatlist_changed(context);
Ok(())
}
@@ -1574,6 +1578,7 @@ pub(crate) async fn set_profile_image(
if changed {
contact.update_param(context).await?;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
chatlist_events::emit_chatlist_item_changed_for_contact_chat(context, contact_id).await;
}
Ok(())
}
@@ -1786,6 +1791,11 @@ impl RecentlySeenLoop {
// Timeout, notify about contact.
if let Some(contact_id) = contact_id {
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
chatlist_events::emit_chatlist_item_changed_for_contact_chat(
&context,
*contact_id,
)
.await;
unseen_queue.pop();
}
}
@@ -1818,6 +1828,11 @@ impl RecentlySeenLoop {
// Event is already in the past.
if let Some(contact_id) = contact_id {
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
chatlist_events::emit_chatlist_item_changed_for_contact_chat(
&context,
*contact_id,
)
.await;
}
unseen_queue.pop();
}

View File

@@ -16,6 +16,7 @@ use tokio::sync::{Mutex, Notify, RwLock};
use crate::aheader::EncryptPreference;
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{
self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR,
@@ -593,11 +594,15 @@ impl Context {
/// Emits a MsgsChanged event with specified chat and message ids
pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
chatlist_events::emit_chatlist_changed(self);
chatlist_events::emit_chatlist_item_changed(self, chat_id);
}
/// Emits an IncomingMsg event with specified chat and message ids
pub fn emit_incoming_msg(&self, chat_id: ChatId, msg_id: MsgId) {
self.emit_event(EventType::IncomingMsg { chat_id, msg_id });
chatlist_events::emit_chatlist_changed(self);
chatlist_events::emit_chatlist_item_changed(self, chat_id);
}
/// Returns a receiver for emitted events.

View File

@@ -13,7 +13,7 @@ use crate::imap::session::Session;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, Part};
use crate::tools::time;
use crate::{stock_str, EventType};
use crate::{chatlist_events, stock_str, EventType};
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
///
@@ -115,6 +115,7 @@ impl MsgId {
chat_id: msg.chat_id,
msg_id: self,
});
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
Ok(())
}
}

View File

@@ -3,6 +3,7 @@
use async_channel::{self as channel, Receiver, Sender, TrySendError};
use pin_project::pin_project;
pub(crate) mod chatlist_events;
mod payload;
pub use self::payload::EventType;

View File

@@ -0,0 +1,635 @@
use crate::{chat::ChatId, contact::ContactId, context::Context, EventType};
/// order or content of chatlist changes (chat ids, not the actual chatlist item)
pub(crate) fn emit_chatlist_changed(context: &Context) {
context.emit_event(EventType::ChatlistChanged);
}
/// Chatlist item of a specific chat changed
pub(crate) fn emit_chatlist_item_changed(context: &Context, chat_id: ChatId) {
context.emit_event(EventType::ChatlistItemChanged {
chat_id: Some(chat_id),
});
}
/// Used when you don't know which chatlist items changed, this reloads all cached chatlist items in the UI
///
/// Avoid calling this when you can find out the affected chat ids easialy (without extra expensive db queries).
///
/// This method is not public, so you have to define and document your new case here in this file.
fn emit_unknown_chatlist_items_changed(context: &Context) {
context.emit_event(EventType::ChatlistItemChanged { chat_id: None });
}
/// update event for the 1:1 chat with the contact
/// used when recently seen changes and when profile image changes
pub(crate) async fn emit_chatlist_item_changed_for_contact_chat(
context: &Context,
contact_id: ContactId,
) {
match ChatId::lookup_by_contact(context, contact_id).await {
Ok(Some(chat_id)) => self::emit_chatlist_item_changed(context, chat_id),
Ok(None) => {}
Err(error) => context.emit_event(EventType::Error(format!(
"failed to find chat id for contact for chatlist event: {error:?}"
))),
}
}
/// update items for chats that have the contact
/// used when contact changes their name or did AEAP for example
///
/// The most common case is that the contact changed their name
/// and their name should be updated in the chatlistitems for the chats
/// where they sent the last message as there their name is shown in the summary on those
pub(crate) fn emit_chatlist_items_changed_for_contact(context: &Context, _contact_id: ContactId) {
// note:(treefit): it is too expensive to find the right chats
// so we'll just tell ui to reload every loaded item
emit_unknown_chatlist_items_changed(context)
// note:(treefit): in the future we could instead emit an extra event for this and also store contact id in the chatlistitems
// (contact id for dm chats and contact id of contact that wrote the message in the summary)
// the ui could then look for this info in the cache and only reload the needed chats.
}
/// Tests for chatlist events
///
/// Only checks if the events are emitted,
/// does not check for excess/too-many events
#[cfg(test)]
mod test_chatlist_events {
use std::{
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use crate::{
chat::{
self, create_broadcast_list, create_group_chat, set_muted, ChatId, ChatVisibility,
MuteDuration, ProtectionStatus,
},
config::Config,
constants::*,
contact::Contact,
message::{self, Message, MessageState},
reaction,
receive_imf::receive_imf,
securejoin::{get_securejoin_qr, join_securejoin},
test_utils::{TestContext, TestContextManager},
EventType,
};
use anyhow::Result;
async fn wait_for_chatlist_and_specific_item(context: &TestContext, chat_id: ChatId) {
let first_event_is_item = AtomicBool::new(false);
context
.evtracker
.get_matching(|evt| match evt {
EventType::ChatlistItemChanged {
chat_id: Some(ev_chat_id),
} => {
if ev_chat_id == &chat_id {
first_event_is_item.store(true, Ordering::Relaxed);
true
} else {
false
}
}
EventType::ChatlistChanged => true,
_ => false,
})
.await;
if first_event_is_item.load(Ordering::Relaxed) {
wait_for_chatlist(context).await;
} else {
wait_for_chatlist_specific_item(context, chat_id).await;
}
}
async fn wait_for_chatlist_specific_item(context: &TestContext, chat_id: ChatId) {
context
.evtracker
.get_matching(|evt| match evt {
EventType::ChatlistItemChanged {
chat_id: Some(ev_chat_id),
} => ev_chat_id == &chat_id,
_ => false,
})
.await;
}
async fn wait_for_chatlist_all_items(context: &TestContext) {
context
.evtracker
.get_matching(|evt| matches!(evt, EventType::ChatlistItemChanged { chat_id: None }))
.await;
}
async fn wait_for_chatlist(context: &TestContext) {
context
.evtracker
.get_matching(|evt| matches!(evt, EventType::ChatlistChanged))
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_chat_visibility() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat_id = create_group_chat(
&alice,
crate::chat::ProtectionStatus::Unprotected,
"my_group",
)
.await?;
chat_id
.set_visibility(&alice, ChatVisibility::Pinned)
.await?;
wait_for_chatlist_and_specific_item(&alice, chat_id).await;
chat_id
.set_visibility(&alice, ChatVisibility::Archived)
.await?;
wait_for_chatlist_and_specific_item(&alice, chat_id).await;
chat_id
.set_visibility(&alice, ChatVisibility::Normal)
.await?;
wait_for_chatlist_and_specific_item(&alice, chat_id).await;
Ok(())
}
/// mute a chat, archive it, then use another account to send a message to it, the counter on the archived chatlist item should change
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_archived_counter_increases_for_muted_chats() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice.create_chat(&bob).await;
let sent_msg = alice.send_text(chat.id, "moin").await;
bob.recv_msg(&sent_msg).await;
let bob_chat = bob.create_chat(&alice).await;
bob_chat
.id
.set_visibility(&bob, ChatVisibility::Archived)
.await?;
set_muted(&bob, bob_chat.id, MuteDuration::Forever).await?;
bob.evtracker.clear_events();
let sent_msg = alice.send_text(chat.id, "moin2").await;
bob.recv_msg(&sent_msg).await;
bob.evtracker
.get_matching(|evt| match evt {
EventType::ChatlistItemChanged {
chat_id: Some(chat_id),
} => chat_id.is_archived_link(),
_ => false,
})
.await;
Ok(())
}
/// Mark noticed on archive-link chatlistitem should update the unread counter on it
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_archived_counter_update_on_mark_noticed() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice.create_chat(&bob).await;
let sent_msg = alice.send_text(chat.id, "moin").await;
bob.recv_msg(&sent_msg).await;
let bob_chat = bob.create_chat(&alice).await;
bob_chat
.id
.set_visibility(&bob, ChatVisibility::Archived)
.await?;
set_muted(&bob, bob_chat.id, MuteDuration::Forever).await?;
let sent_msg = alice.send_text(chat.id, "moin2").await;
bob.recv_msg(&sent_msg).await;
bob.evtracker.clear_events();
chat::marknoticed_chat(&bob, DC_CHAT_ID_ARCHIVED_LINK).await?;
wait_for_chatlist_specific_item(&bob, DC_CHAT_ID_ARCHIVED_LINK).await;
Ok(())
}
/// Contact name update - expect all chats to update
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_contact_name_update() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_to_bob_chat = alice.create_chat(&bob).await;
let sent_msg = alice.send_text(alice_to_bob_chat.id, "hello").await;
bob.recv_msg(&sent_msg).await;
bob.evtracker.clear_events();
// set alice name then receive messagefrom her with bob
alice.set_config(Config::Displayname, Some("Alice")).await?;
let sent_msg = alice
.send_text(alice_to_bob_chat.id, "hello, I set a displayname")
.await;
bob.recv_msg(&sent_msg).await;
let alice_on_bob = bob.add_or_lookup_contact(&alice).await;
assert!(alice_on_bob.get_display_name() == "Alice");
wait_for_chatlist_all_items(&bob).await;
bob.evtracker.clear_events();
// set name
let addr = alice_on_bob.get_addr();
Contact::create(&bob, "Alice2", addr).await?;
assert!(bob.add_or_lookup_contact(&alice).await.get_display_name() == "Alice2");
wait_for_chatlist_all_items(&bob).await;
Ok(())
}
/// Contact changed avatar
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_contact_changed_avatar() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_to_bob_chat = alice.create_chat(&bob).await;
let sent_msg = alice.send_text(alice_to_bob_chat.id, "hello").await;
bob.recv_msg(&sent_msg).await;
bob.evtracker.clear_events();
// set alice avatar then receive messagefrom her with bob
let file = alice.dir.path().join("avatar.png");
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
alice
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
let sent_msg = alice
.send_text(alice_to_bob_chat.id, "hello, I have a new avatar")
.await;
bob.recv_msg(&sent_msg).await;
let alice_on_bob = bob.add_or_lookup_contact(&alice).await;
assert!(alice_on_bob.get_profile_image(&bob).await?.is_some());
wait_for_chatlist_specific_item(&bob, bob.create_chat(&alice).await.id).await;
Ok(())
}
/// Delete chat
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
alice.evtracker.clear_events();
chat.delete(&alice).await?;
wait_for_chatlist(&alice).await;
Ok(())
}
/// Create group chat
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_group_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
alice.evtracker.clear_events();
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
wait_for_chatlist_and_specific_item(&alice, chat).await;
Ok(())
}
/// Create broadcastlist
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_broadcastlist() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
alice.evtracker.clear_events();
create_broadcast_list(&alice).await?;
wait_for_chatlist(&alice).await;
Ok(())
}
/// Mute chat
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mute_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
alice.evtracker.clear_events();
chat::set_muted(&alice, chat, MuteDuration::Forever).await?;
wait_for_chatlist_specific_item(&alice, chat).await;
alice.evtracker.clear_events();
chat::set_muted(&alice, chat, MuteDuration::NotMuted).await?;
wait_for_chatlist_specific_item(&alice, chat).await;
Ok(())
}
/// Expiry of mute should also trigger an event
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "does not work yet"]
async fn test_mute_chat_expired() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let mute_duration = MuteDuration::Until(
std::time::SystemTime::now()
.checked_add(Duration::from_secs(2))
.unwrap(),
);
chat::set_muted(&alice, chat, mute_duration).await?;
alice.evtracker.clear_events();
tokio::time::sleep(Duration::from_secs(3)).await;
wait_for_chatlist_specific_item(&alice, chat).await;
Ok(())
}
/// Change chat name
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_chat_name() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
alice.evtracker.clear_events();
chat::set_chat_name(&alice, chat, "New Name").await?;
wait_for_chatlist_specific_item(&alice, chat).await;
Ok(())
}
/// Change chat profile image
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_change_chat_profile_image() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
alice.evtracker.clear_events();
let file = alice.dir.path().join("avatar.png");
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
chat::set_chat_profile_image(&alice, chat, file.to_str().unwrap()).await?;
wait_for_chatlist_specific_item(&alice, chat).await;
Ok(())
}
/// Receive group and receive name change
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_receiving_group_and_group_changes() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
.await;
let sent_msg = alice.send_text(chat, "Hello").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
wait_for_chatlist_specific_item(&bob, chat_id_for_bob).await;
chat_id_for_bob.accept(&bob).await?;
bob.evtracker.clear_events();
chat::set_chat_name(&alice, chat, "New Name").await?;
let sent_msg = alice.send_text(chat, "Hello").await;
bob.recv_msg(&sent_msg).await;
wait_for_chatlist_specific_item(&bob, chat_id_for_bob).await;
Ok(())
}
/// Accept contact request
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_accept_contact_request() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
.await;
let sent_msg = alice.send_text(chat, "Hello").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
bob.evtracker.clear_events();
chat_id_for_bob.accept(&bob).await?;
wait_for_chatlist_specific_item(&bob, chat_id_for_bob).await;
Ok(())
}
/// Block contact request
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_block_contact_request() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
.await;
let sent_msg = alice.send_text(chat, "Hello").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
bob.evtracker.clear_events();
chat_id_for_bob.block(&bob).await?;
wait_for_chatlist(&bob).await;
Ok(())
}
/// Delete message
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_message() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let message = chat::send_text_msg(&alice, chat, "Hello World".to_owned()).await?;
alice.evtracker.clear_events();
message::delete_msgs(&alice, &[message]).await?;
wait_for_chatlist_specific_item(&alice, chat).await;
Ok(())
}
/// Click on chat should remove the unread count (on msgs noticed)
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_msgs_noticed_on_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = alice
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
.await;
let sent_msg = alice.send_text(chat, "Hello").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
chat_id_for_bob.accept(&bob).await?;
let sent_msg = alice.send_text(chat, "New Message").await;
let chat_id_for_bob = bob.recv_msg(&sent_msg).await.chat_id;
assert!(chat_id_for_bob.get_fresh_msg_cnt(&bob).await? >= 1);
bob.evtracker.clear_events();
chat::marknoticed_chat(&bob, chat_id_for_bob).await?;
wait_for_chatlist_specific_item(&bob, chat_id_for_bob).await;
Ok(())
}
// Block and Unblock contact
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unblock_contact() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let contact_id = Contact::create(&alice, "example", "example@example.com").await?;
let _ = ChatId::create_for_contact(&alice, contact_id).await;
alice.evtracker.clear_events();
Contact::block(&alice, contact_id).await?;
wait_for_chatlist(&alice).await;
alice.evtracker.clear_events();
Contact::unblock(&alice, contact_id).await?;
wait_for_chatlist(&alice).await;
Ok(())
}
/// ephemeral / disappearing messages
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_update_after_ephemeral_messages() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
chat.set_ephemeral_timer(&alice, crate::ephemeral::Timer::Enabled { duration: 1 })
.await?;
let _ = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?;
alice.evtracker.clear_events();
tokio::time::sleep(Duration::from_secs(2)).await;
wait_for_chatlist_and_specific_item(&alice, chat).await;
Ok(())
}
/// AdHoc (Groups without a group ID.) group receiving
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_adhoc_group() -> Result<()> {
let alice = TestContext::new_alice().await;
let mime = br#"Subject: First thread
Message-ID: first@example.org
To: Alice <alice@example.org>, Bob <bob@example.net>
From: Claire <claire@example.org>
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
First thread."#;
alice.evtracker.clear_events();
receive_imf(&alice, mime, false).await?;
wait_for_chatlist(&alice).await;
Ok(())
}
/// Test both direction of securejoin
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_secure_join_group() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_chatid =
chat::create_group_chat(&alice.ctx, ProtectionStatus::Protected, "the chat").await?;
// Step 1: Generate QR-code, secure-join implied by chatid
let qr = get_securejoin_qr(&alice.ctx, Some(alice_chatid)).await?;
// Step 2: Bob scans QR-code, sends vg-request
bob.evtracker.clear_events();
let bob_chatid = join_securejoin(&bob.ctx, &qr).await?;
wait_for_chatlist(&bob).await;
let sent = bob.pop_sent_msg().await;
// Step 3: Alice receives vg-request, sends vg-auth-required
alice.evtracker.clear_events();
alice.recv_msg(&sent).await;
let sent = alice.pop_sent_msg().await;
// Step 4: Bob receives vg-auth-required, sends vg-request-with-auth
bob.evtracker.clear_events();
bob.recv_msg(&sent).await;
wait_for_chatlist_and_specific_item(&bob, bob_chatid).await;
let sent = bob.pop_sent_msg().await;
// Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added
alice.evtracker.clear_events();
alice.recv_msg(&sent).await;
wait_for_chatlist_and_specific_item(&alice, alice_chatid).await;
let sent = alice.pop_sent_msg().await;
// Step 7: Bob receives vg-member-added
bob.evtracker.clear_events();
bob.recv_msg(&sent).await;
wait_for_chatlist_and_specific_item(&bob, bob_chatid).await;
Ok(())
}
/// Call Resend on message
///
/// (the event is technically only needed if it is the last message in the chat, but checking that would be too expensive so the event is always emitted)
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resend_message() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let msg_id = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?;
let _ = alice.pop_sent_msg().await;
let message = Message::load_from_db(&alice, msg_id).await?;
assert_eq!(message.get_state(), MessageState::OutDelivered);
alice.evtracker.clear_events();
chat::resend_msgs(&alice, &[msg_id]).await?;
wait_for_chatlist_specific_item(&alice, chat).await;
Ok(())
}
/// test that setting a reaction emits chatlistitem update event
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reaction() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let chat = create_group_chat(&alice, ProtectionStatus::Protected, "My Group").await?;
let msg_id = chat::send_text_msg(&alice, chat, "Hello".to_owned()).await?;
let _ = alice.pop_sent_msg().await;
alice.evtracker.clear_events();
reaction::send_reaction(&alice, msg_id, "👍").await?;
let _ = alice.pop_sent_msg().await;
wait_for_chatlist_specific_item(&alice, chat).await;
Ok(())
}
}

View File

@@ -291,4 +291,15 @@ pub enum EventType {
///
/// This event is only emitted by the account manager
AccountsBackgroundFetchDone,
/// Inform that set of chats or the order of the chats in the chatlist has changed.
///
/// Sometimes this is emitted together with `UIChatlistItemChanged`.
ChatlistChanged,
/// Inform that a single chat list item changed and needs to be rerendered.
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
ChatlistItemChanged {
/// ID of the changed chat
chat_id: Option<ChatId>,
},
}

View File

@@ -22,6 +22,7 @@ use ratelimit::Ratelimit;
use tokio::sync::RwLock;
use crate::chat::{self, ChatId, ChatIdBlocked};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{self, Blocked, Chattype, ShowEmails};
use crate::contact::{normalize_name, Contact, ContactAddress, ContactId, Modifier, Origin};
@@ -1170,6 +1171,7 @@ impl Session {
.with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
for updated_chat_id in updated_chat_ids {
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
}
Ok(())

View File

@@ -13,8 +13,8 @@ use crate::context::Context;
use crate::events::EventType;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::stock_str;
use crate::tools::{duration_to_str, time};
use crate::{chatlist_events, stock_str};
/// Location record.
#[derive(Debug, Clone, Default)]
@@ -290,6 +290,7 @@ pub async fn send_locations_to_chat(
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
}
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
if 0 != seconds {
context.scheduler.interrupt_location().await;
}
@@ -802,6 +803,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
}
}

View File

@@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
use crate::blob::BlobObject;
use crate::chat::{Chat, ChatId};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
@@ -138,6 +139,7 @@ WHERE id=?;
chat_id,
msg_id: self,
});
chatlist_events::emit_chatlist_item_changed(context, chat_id);
Ok(())
}
@@ -1527,9 +1529,12 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
for modified_chat_id in modified_chat_ids {
context.emit_msgs_changed(modified_chat_id, MsgId::new(0));
chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
}
if !msg_ids.is_empty() {
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
// Run housekeeping to delete unused blobs.
context
.set_config_internal(Config::LastHousekeeping, None)
@@ -1664,6 +1669,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
for updated_chat_id in updated_chat_ids {
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
}
Ok(())
@@ -1724,6 +1730,7 @@ pub(crate) async fn set_msg_failed(
chat_id: msg.chat_id,
msg_id: msg.id,
});
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
Ok(())
}

View File

@@ -32,13 +32,12 @@ use crate::message::{
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::simplify::{simplify, SimplifiedText};
use crate::stock_str;
use crate::sync::SyncItems;
use crate::tools::{
create_smeared_timestamp, get_filemeta, parse_receive_headers, smeared_time,
strip_rtlo_characters, truncate_by_lines,
};
use crate::{location, tools};
use crate::{chatlist_events, location, stock_str, tools};
/// A parsed MIME message.
///
@@ -2153,6 +2152,8 @@ async fn handle_mdn(
{
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await?;
context.emit_event(EventType::MsgRead { chat_id, msg_id });
// note(treefit): only matters if it is the last message in chat (but probably too expensive to check, debounce also solves it)
chatlist_events::emit_chatlist_item_changed(context, chat_id);
}
Ok(())
}

View File

@@ -17,7 +17,7 @@ use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::message::Message;
use crate::mimeparser::SystemMessage;
use crate::sql::Sql;
use crate::stock_str;
use crate::{chatlist_events, stock_str};
/// Type of the public key stored inside the peerstate.
#[derive(Debug)]
@@ -722,6 +722,9 @@ impl Peerstate {
.await?;
}
chatlist_events::emit_chatlist_changed(context);
// update the chats the contact is part of
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
Ok(())
}

View File

@@ -21,6 +21,7 @@ use std::fmt;
use anyhow::Result;
use crate::chat::{send_msg, Chat, ChatId};
use crate::chatlist_events;
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
@@ -214,6 +215,7 @@ async fn set_msg_id_reaction(
msg_id,
contact_id,
});
chatlist_events::emit_chatlist_item_changed(context, chat_id);
Ok(())
}

View File

@@ -22,7 +22,6 @@ use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
use crate::location;
use crate::log::LogExt;
use crate::message::{
self, rfc724_mid_exists, rfc724_mid_exists_and, Message, MessageState, MessengerMessage, MsgId,
@@ -40,6 +39,7 @@ use crate::sync::Sync::*;
use crate::tools::{
self, buf_compress, extract_grpid_from_rfc724_mid, strip_rtlo_characters, validate_id,
};
use crate::{chatlist_events, location};
use crate::{contact, imap};
/// This is the struct that is returned after receiving one email (aka MIME message).
@@ -1903,6 +1903,8 @@ async fn create_or_lookup_group(
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
context.emit_event(EventType::ChatModified(new_chat_id));
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
}
if let Some(chat_id) = chat_id {
@@ -2216,6 +2218,7 @@ async fn apply_group_changes(
if send_event_chat_modified {
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
}
Ok((group_changes_msgs, better_msg))
}
@@ -2518,6 +2521,8 @@ async fn create_adhoc_group(
chat::add_to_chat_contacts_table(context, new_chat_id, member_ids).await?;
context.emit_event(EventType::ChatModified(new_chat_id));
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
Ok(Some(new_chat_id))
}

View File

@@ -5,6 +5,7 @@ use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::Blocked;
use crate::contact::{Contact, ContactId, Origin};
@@ -680,6 +681,7 @@ async fn secure_connection_established(
)
.await?;
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
Ok(())
}

View File

@@ -1022,6 +1022,11 @@ impl EventTracker {
self.get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. }))
.await;
}
/// Clears event queue
pub fn clear_events(&self) {
while self.try_recv().is_ok() {}
}
}
/// Gets a specific message from a chat and asserts that the chat has a specific length.