use std::collections::{HashMap, HashSet};
use tdlib_rs::enums::{
ChatAction, ChatList, ChatType, MessageContent, MessageSender, NotificationSettingsScope,
UserStatus,
};
use tdlib_rs::types::{
self, AccentColor, ChatFolderInfo, ChatNotificationSettings, ChatPhotoInfo, ChatPosition,
FormattedText, Message,
};
use crate::images::Thumbnail;
use crate::messages::{Sender, decode_minithumbnail, without_spoilers};
use crate::search;
use crate::secret::Secret;
use crate::service::Service;
use crate::text;
pub struct Chat {
pub title: String,
pub is_channel: bool,
pub is_private: bool,
pub unread: i32,
pub read_outbox: i64,
read_inbox: i64,
pub preview: String,
positions: HashMap<List, Position>,
pub photo: Option<ChatPhoto>,
accent: i32,
pub activity: Vec<(Sender, &'static str, Option<i32>)>,
pub peer: Option<Peer>,
notifications: ChatNotificationSettings,
pub secret_id: Option<i32>,
auto_delete: i32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum List {
#[default]
Main,
Archive,
Folder(i32),
}
impl List {
pub fn of(list: &ChatList) -> Self {
match list {
ChatList::Main => List::Main,
ChatList::Archive => List::Archive,
ChatList::Folder(f) => List::Folder(f.chat_folder_id),
}
}
pub fn tdlib(self) -> ChatList {
match self {
List::Main => ChatList::Main,
List::Archive => ChatList::Archive,
List::Folder(id) => ChatList::Folder(types::ChatListFolder { chat_folder_id: id }),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Position {
order: i64,
pinned: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tab {
pub list: List,
pub name: String,
pub unread: i32,
}
const ALL_CHATS: &str = "All";
const ARCHIVE: &str = "Archive";
impl Chat {
fn order(&self, list: List) -> i64 {
self.positions.get(&list).map_or(0, |p| p.order)
}
pub fn pinned(&self, list: List) -> bool {
self.positions.get(&list).is_some_and(|p| p.pinned)
}
fn set_position(&mut self, position: &ChatPosition) {
let list = List::of(&position.list);
if position.order == 0 {
self.positions.remove(&list);
} else {
let place = Position {
order: position.order,
pinned: position.is_pinned,
};
self.positions.insert(list, place);
}
}
#[cfg(test)]
pub fn set_pinned(&mut self, list: List, pinned: bool) {
if let Some(place) = self.positions.get_mut(&list) {
place.pinned = pinned;
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Peer {
User(i64),
BasicGroup(i64),
Supergroup(i64),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Badge {
Scam,
Fake,
Official,
}
impl Badge {
pub fn of(status: Option<&types::VerificationStatus>, official: bool) -> Option<Self> {
match status {
Some(s) if s.is_scam => Some(Badge::Scam),
Some(s) if s.is_fake => Some(Badge::Fake),
Some(s) if s.is_verified => Some(Badge::Official),
_ => official.then_some(Badge::Official),
}
}
pub fn mark(self) -> &'static str {
match self {
Badge::Scam => " SCAM",
Badge::Fake => " FAKE",
Badge::Official => " ✓",
}
}
}
#[derive(Clone)]
pub struct ChatPhoto {
pub file_id: i32,
pub path: Option<String>,
pub thumbnail: Option<Thumbnail>,
}
impl ChatPhoto {
fn new(info: &ChatPhotoInfo) -> Self {
let local = &info.small.local;
Self {
file_id: info.small.id,
path: local.is_downloading_completed.then(|| local.path.clone()),
thumbnail: decode_minithumbnail(info.minithumbnail.as_ref()),
}
}
}
const SAVED_MESSAGES: &str = "Saved Messages";
#[derive(Default)]
pub struct Chats {
by_id: HashMap<i64, Chat>,
sorted: Vec<i64>,
dirty: bool,
my_id: Option<i64>,
held: Option<i64>,
highlighted: HashSet<i64>,
filter: String,
total: usize,
accent_colors: HashMap<i32, i32>,
shown: List,
folders: Vec<(i32, String)>,
main_at: usize,
unread_in: HashMap<List, i32>,
badges: HashMap<Peer, Badge>,
usernames: HashMap<Peer, String>,
left: HashSet<i64>,
forums: HashSet<i64>,
default_mute: [i32; 3],
presence: HashMap<i64, Presence>,
bots: HashSet<i64>,
secrets: HashMap<i32, Secret>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Presence {
Online(i32),
Offline(i32),
Recently,
LastWeek,
LastMonth,
LongAgo,
}
impl Presence {
pub fn of(status: &UserStatus) -> Self {
match status {
UserStatus::Online(s) => Presence::Online(s.expires),
UserStatus::Offline(s) => Presence::Offline(s.was_online),
UserStatus::Recently(_) => Presence::Recently,
UserStatus::LastWeek(_) => Presence::LastWeek,
UserStatus::LastMonth(_) => Presence::LastMonth,
UserStatus::Empty => Presence::LongAgo,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Seen {
Bot,
Person(Presence),
}
impl Chats {
pub fn insert(&mut self, chat: types::Chat) {
let is_channel = matches!(&chat.r#type, ChatType::Supergroup(s) if s.is_channel);
let is_private = matches!(chat.r#type, ChatType::Private(_) | ChatType::Secret(_));
let peer = match &chat.r#type {
ChatType::Private(p) => Some(Peer::User(p.user_id)),
ChatType::Secret(s) => Some(Peer::User(s.user_id)),
ChatType::Supergroup(s) => Some(Peer::Supergroup(s.supergroup_id)),
ChatType::BasicGroup(b) => Some(Peer::BasicGroup(b.basic_group_id)),
};
let secret_id = match &chat.r#type {
ChatType::Secret(s) => Some(s.secret_chat_id),
_ => None,
};
let entry = Chat {
title: text::clean(&chat.title),
is_channel,
is_private,
unread: chat.unread_count,
read_outbox: chat.last_read_outbox_message_id,
read_inbox: chat.last_read_inbox_message_id,
preview: chat.last_message.as_ref().map(preview).unwrap_or_default(),
positions: HashMap::new(),
photo: chat.photo.as_ref().map(ChatPhoto::new),
accent: chat.accent_color_id,
activity: Vec::new(),
peer,
notifications: chat.notification_settings,
secret_id,
auto_delete: chat.message_auto_delete_time,
};
let entry = self.by_id.entry(chat.id).insert_entry(entry).into_mut();
for position in &chat.positions {
entry.set_position(position);
}
self.dirty = true;
}
pub fn set_position(&mut self, chat_id: i64, position: &ChatPosition) {
if let Some(chat) = self.by_id.get_mut(&chat_id) {
chat.set_position(position);
self.dirty = true;
}
}
pub fn set_last_message(
&mut self,
chat_id: i64,
message: Option<&Message>,
positions: &[ChatPosition],
) {
if let Some(chat) = self.by_id.get_mut(&chat_id) {
chat.preview = message.map(preview).unwrap_or_default();
for position in positions {
chat.set_position(position);
self.dirty = true;
}
}
}
pub fn set_action(
&mut self,
chat_id: i64,
topic: Option<i32>,
sender: &MessageSender,
action: &ChatAction,
) {
let Some(chat) = self.by_id.get_mut(&chat_id) else {
return;
};
let sender = Sender::from(sender);
let at = chat.activity.iter().position(|(s, _, _)| *s == sender);
match (at, activity(action)) {
(Some(i), Some(doing)) => chat.activity[i] = (sender, doing, topic),
(Some(i), None) => {
chat.activity.remove(i);
}
(None, Some(doing)) => chat.activity.push((sender, doing, topic)),
(None, None) => {}
}
}
pub fn set_title(&mut self, chat_id: i64, title: String) {
if let Some(chat) = self.by_id.get_mut(&chat_id) {
chat.title = text::clean(&title);
self.dirty = true;
}
}
pub fn set_unread(&mut self, chat_id: i64, unread: i32) {
if let Some(chat) = self.by_id.get_mut(&chat_id) {
chat.unread = unread;
self.dirty = true;
}
}
pub fn set_read_inbox(&mut self, chat_id: i64, message_id: i64) {
if let Some(chat) = self.by_id.get_mut(&chat_id) {
chat.read_inbox = message_id;
}
}
pub fn read_inbox(&self, chat_id: i64) -> i64 {
self.by_id.get(&chat_id).map_or(0, |c| c.read_inbox)
}
pub fn set_read_outbox(&mut self, chat_id: i64, message_id: i64) {
if let Some(chat) = self.by_id.get_mut(&chat_id) {
chat.read_outbox = message_id;
}
}
pub fn set_photo(&mut self, chat_id: i64, photo: Option<&ChatPhotoInfo>) {
if let Some(chat) = self.by_id.get_mut(&chat_id) {
chat.photo = photo.map(ChatPhoto::new);
}
}
pub fn set_accent(&mut self, chat_id: i64, accent_color_id: i32) {
if let Some(chat) = self.by_id.get_mut(&chat_id) {
chat.accent = accent_color_id;
}
}
pub fn set_accent_colors(&mut self, colors: &[AccentColor]) {
self.accent_colors = colors
.iter()
.map(|c| (c.id, c.built_in_accent_color_id))
.collect();
}
pub fn accent(&self, chat_id: i64) -> usize {
let id = self.by_id.get(&chat_id).map_or(0, |c| c.accent);
let built_in = match id {
0..7 => id,
_ => self.accent_colors.get(&id).copied().unwrap_or(id),
};
built_in.rem_euclid(7) as usize
}
pub fn opened(&mut self, chat_id: i64) {
self.held = Some(chat_id).filter(|id| self.by_id.get(id).is_some_and(|c| c.unread > 0));
self.dirty = true;
}
pub fn refresh(&mut self) {
if !self.dirty {
return;
}
let list = self.shown;
self.sorted = self
.by_id
.iter()
.filter(|(_, chat)| chat.order(list) != 0)
.map(|(&id, _)| id)
.collect();
self.total = self.sorted.len();
if !self.filter.is_empty() {
let mut sorted = std::mem::take(&mut self.sorted);
sorted.retain(|&id| {
let title = self.title(id).unwrap_or_default();
!search::find(title, &self.filter).is_empty()
});
self.sorted = sorted;
}
let (by_id, held) = (&self.by_id, self.held);
self.sorted.sort_unstable_by_key(|&id| {
let chat = &by_id[&id];
let unread = chat.unread > 0 || held == Some(id);
std::cmp::Reverse((chat.pinned(list), unread, chat.order(list), id))
});
self.dirty = false;
}
pub fn set_filter(&mut self, query: &str) {
if self.filter != query {
self.filter = query.to_string();
self.dirty = true;
}
}
pub fn filter(&self) -> &str {
&self.filter
}
pub fn total(&self) -> usize {
self.total
}
pub fn shown(&self) -> List {
self.shown
}
pub fn show(&mut self, list: List) {
if self.shown != list {
self.shown = list;
self.dirty = true;
}
}
pub fn pinned(&self, chat_id: i64) -> bool {
self.by_id
.get(&chat_id)
.is_some_and(|c| c.pinned(self.shown))
}
pub fn set_folders(&mut self, folders: &[ChatFolderInfo], main_at: i32) {
self.folders = folders
.iter()
.map(|f| {
let name = text::clean(&f.name.text.text);
(f.id, name.split_whitespace().collect::<Vec<_>>().join(" "))
})
.collect();
self.main_at = usize::try_from(main_at).unwrap_or(0);
if !self.tabs().iter().any(|t| t.list == self.shown) {
self.show(List::Main);
}
}
pub fn set_unread_in(&mut self, list: List, count: i32) {
self.unread_in.insert(list, count);
}
pub fn tabs(&self) -> Vec<Tab> {
let tab = |list, name: &str| Tab {
list,
name: name.to_string(),
unread: self.unread_in.get(&list).copied().unwrap_or(0),
};
let mut tabs: Vec<Tab> = self
.folders
.iter()
.map(|(id, name)| tab(List::Folder(*id), name))
.collect();
tabs.insert(self.main_at.min(tabs.len()), tab(List::Main, ALL_CHATS));
let archived = self
.by_id
.values()
.any(|c| c.positions.contains_key(&List::Archive));
if archived || self.shown == List::Archive {
tabs.push(tab(List::Archive, ARCHIVE));
}
if tabs.len() == 1 { Vec::new() } else { tabs }
}
pub fn next_list(&self, step: isize) -> List {
let tabs = self.tabs();
let Some(at) = tabs.iter().position(|t| t.list == self.shown) else {
return List::Main;
};
let at = (at as isize + step).rem_euclid(tabs.len() as isize);
tabs[at as usize].list
}
pub fn in_list(&self, chat_id: i64, list: List) -> bool {
self.by_id.get(&chat_id).is_some_and(|c| c.order(list) != 0)
}
pub fn set_my_id(&mut self, id: i64) {
self.my_id = Some(id);
}
pub fn my_id(&self) -> Option<i64> {
self.my_id
}
pub fn is_saved(&self, chat_id: i64) -> bool {
self.my_id == Some(chat_id)
}
pub fn is_highlighted(&self, chat_id: i64) -> bool {
self.highlighted.contains(&chat_id)
}
pub fn toggle_highlight(&mut self, chat_id: i64) -> Vec<i64> {
if !self.highlighted.remove(&chat_id) {
self.highlighted.insert(chat_id);
}
let mut ids: Vec<i64> = self.highlighted.iter().copied().collect();
ids.sort_unstable();
ids
}
pub fn set_highlighted(&mut self, chat_ids: &[i64]) {
self.highlighted = chat_ids.iter().copied().collect();
}
pub fn set_badge(&mut self, peer: Peer, badge: Option<Badge>) {
match badge {
Some(badge) => self.badges.insert(peer, badge),
None => self.badges.remove(&peer),
};
}
pub fn badge(&self, chat_id: i64) -> Option<Badge> {
let peer = self.by_id.get(&chat_id)?.peer?;
self.badges.get(&peer).copied()
}
pub fn set_username(&mut self, peer: Peer, usernames: Option<&types::Usernames>) {
let first = usernames.and_then(|u| u.active_usernames.first());
match first {
Some(name) => self.usernames.insert(peer, text::clean(name)),
None => self.usernames.remove(&peer),
};
}
pub fn username(&self, chat_id: i64) -> Option<&str> {
let peer = self.by_id.get(&chat_id)?.peer?;
self.usernames.get(&peer).map(String::as_str)
}
pub fn user_by_username(&self, username: &str) -> Option<i64> {
let mut found = self.usernames.iter().filter_map(|(peer, name)| match peer {
Peer::User(id) if name.eq_ignore_ascii_case(username) => Some(*id),
_ => None,
});
let first = found.next()?;
found.next().is_none().then_some(first)
}
pub fn user_username(&self, user_id: i64) -> Option<&str> {
self.usernames.get(&Peer::User(user_id)).map(String::as_str)
}
pub fn set_notifications(&mut self, chat_id: i64, settings: ChatNotificationSettings) {
if let Some(chat) = self.by_id.get_mut(&chat_id) {
chat.notifications = settings;
}
}
pub fn set_default_mute(&mut self, scope: &NotificationSettingsScope, mute_for: i32) {
self.default_mute[scope_index(scope)] = mute_for;
}
pub fn muted(&self, chat_id: i64) -> bool {
let Some(chat) = self.by_id.get(&chat_id) else {
return false;
};
let mute_for = if chat.notifications.use_default_mute_for {
let scope = match () {
_ if chat.is_private => NotificationSettingsScope::PrivateChats,
_ if chat.is_channel => NotificationSettingsScope::ChannelChats,
_ => NotificationSettingsScope::GroupChats,
};
self.default_mute[scope_index(&scope)]
} else {
chat.notifications.mute_for
};
mute_for > 0
}
pub fn with_mute(&self, chat_id: i64, mute: bool) -> Option<ChatNotificationSettings> {
let chat = self.by_id.get(&chat_id)?;
Some(ChatNotificationSettings {
use_default_mute_for: false,
mute_for: if mute { i32::MAX } else { 0 },
..chat.notifications.clone()
})
}
pub fn set_presence(&mut self, user_id: i64, presence: Presence) {
self.presence.insert(user_id, presence);
}
pub fn set_bot(&mut self, user_id: i64, bot: bool) {
if bot {
self.bots.insert(user_id);
} else {
self.bots.remove(&user_id);
}
}
pub fn is_bot(&self, user_id: i64) -> bool {
self.bots.contains(&user_id)
}
pub fn seen(&self, chat_id: i64) -> Option<Seen> {
if self.is_saved(chat_id) {
return None;
}
let Some(Peer::User(user_id)) = self.by_id.get(&chat_id)?.peer else {
return None;
};
if self.bots.contains(&user_id) {
return Some(Seen::Bot);
}
self.presence.get(&user_id).copied().map(Seen::Person)
}
pub fn set_member(&mut self, supergroup_id: i64, member: bool) {
if member {
self.left.remove(&supergroup_id);
} else {
self.left.insert(supergroup_id);
}
}
pub fn set_forum(&mut self, supergroup_id: i64, forum: bool) {
if forum {
self.forums.insert(supergroup_id);
} else {
self.forums.remove(&supergroup_id);
}
}
pub fn is_forum(&self, chat_id: i64) -> bool {
match self.by_id.get(&chat_id).and_then(|c| c.peer) {
Some(Peer::Supergroup(id)) => self.forums.contains(&id),
_ => false,
}
}
pub fn joined(&self, chat_id: i64) -> bool {
match self.by_id.get(&chat_id).and_then(|c| c.peer) {
Some(Peer::Supergroup(id)) => !self.left.contains(&id),
_ => true,
}
}
pub fn set_secret(&mut self, secret_id: i32, secret: Secret) {
self.secrets.insert(secret_id, secret);
}
pub fn secret(&self, chat_id: i64) -> Option<&Secret> {
let id = self.by_id.get(&chat_id)?.secret_id?;
self.secrets.get(&id)
}
pub fn has_secret_chats(&self) -> bool {
!self.secrets.is_empty()
}
pub fn is_secret(&self, chat_id: i64) -> bool {
self.by_id
.get(&chat_id)
.is_some_and(|c| c.secret_id.is_some())
}
pub fn set_auto_delete(&mut self, chat_id: i64, seconds: i32) {
if let Some(chat) = self.by_id.get_mut(&chat_id) {
chat.auto_delete = seconds;
}
}
pub fn auto_delete(&self, chat_id: i64) -> i32 {
self.by_id.get(&chat_id).map_or(0, |c| c.auto_delete)
}
pub fn person(&self, chat_id: i64) -> Option<i64> {
match self.by_id.get(&chat_id)?.peer? {
Peer::User(id) if !self.is_saved(chat_id) => Some(id),
_ => None,
}
}
pub fn listed(&self, chat_id: i64) -> bool {
self.in_list(chat_id, List::Main)
}
pub fn matching(&self, query: &str) -> Vec<i64> {
let query = query.trim();
let username = query.strip_prefix('@').unwrap_or(query);
let mut ids: Vec<i64> = self
.by_id
.iter()
.filter(|(_, chat)| chat.order(List::Main) != 0)
.map(|(&id, _)| id)
.filter(|&id| {
username.is_empty()
|| !search::find(self.title(id).unwrap_or_default(), query).is_empty()
|| self
.username(id)
.is_some_and(|name| !search::find(name, username).is_empty())
})
.collect();
ids.sort_unstable_by_key(|&id| std::cmp::Reverse((self.by_id[&id].order(List::Main), id)));
ids
}
pub fn title(&self, chat_id: i64) -> Option<&str> {
if self.is_saved(chat_id) {
return Some(SAVED_MESSAGES);
}
self.by_id.get(&chat_id).map(|c| c.title.as_str())
}
pub fn ids(&self) -> &[i64] {
&self.sorted
}
pub fn get(&self, chat_id: i64) -> Option<&Chat> {
self.by_id.get(&chat_id)
}
}
impl Chats {
pub fn add_local(&mut self, id: i64, title: &str, photo: Option<ChatPhoto>) -> &mut Chat {
let order = 1000 - self.by_id.len() as i64;
self.dirty = true;
let mut chat = Chat::local(title, &[(List::Main, order)]);
chat.photo = photo;
self.by_id.entry(id).insert_entry(chat).into_mut()
}
pub fn add_local_to(&mut self, list: List, chat_id: i64) {
if let Some(chat) = self.by_id.get_mut(&chat_id) {
let order = chat.order(List::Main);
chat.positions.insert(
list,
Position {
order,
pinned: false,
},
);
self.dirty = true;
}
}
}
impl Chat {
fn local(title: &str, lists: &[(List, i64)]) -> Self {
let positions = lists
.iter()
.map(|&(list, order)| {
let place = Position {
order,
pinned: false,
};
(list, place)
})
.collect();
Chat {
title: title.into(),
is_channel: false,
is_private: false,
unread: 0,
read_outbox: 0,
read_inbox: 0,
preview: String::new(),
positions,
photo: None,
accent: 0,
activity: Vec::new(),
peer: None,
notifications: ChatNotificationSettings::default(),
secret_id: None,
auto_delete: 0,
}
}
}
fn scope_index(scope: &NotificationSettingsScope) -> usize {
match scope {
NotificationSettingsScope::PrivateChats => 0,
NotificationSettingsScope::GroupChats => 1,
NotificationSettingsScope::ChannelChats => 2,
}
}
fn activity(action: &ChatAction) -> Option<&'static str> {
Some(match action {
ChatAction::Typing => "typing",
ChatAction::RecordingVideo => "recording a video",
ChatAction::UploadingVideo(_) => "sending a video",
ChatAction::RecordingVoiceNote => "recording a voice message",
ChatAction::UploadingVoiceNote(_) => "sending a voice message",
ChatAction::UploadingPhoto(_) => "sending a photo",
ChatAction::UploadingDocument(_) => "sending a file",
ChatAction::ChoosingSticker => "choosing a sticker",
ChatAction::ChoosingLocation => "choosing a location",
ChatAction::ChoosingContact => "choosing a contact",
ChatAction::StartPlayingGame => "playing a game",
ChatAction::RecordingVideoNote => "recording a video message",
ChatAction::UploadingVideoNote(_) => "sending a video message",
ChatAction::WatchingAnimations(_) | ChatAction::Cancel => return None,
})
}
const PREVIEW_CHARS: usize = 300;
fn preview(message: &Message) -> String {
let text = snippet(message);
if message.is_outgoing {
format!("You: {text}")
} else {
text
}
}
pub fn snippet(message: &Message) -> String {
let text = match Service::of(&message.content) {
Some(service) => format!("[{}]", service.label(Sender::from(&message.sender_id))),
None => text::clean(&content_text(&message.content)),
};
text::first_chars(&text, PREVIEW_CHARS).replace(['\n', '\t'], " ")
}
pub fn content_text(content: &MessageContent) -> String {
labeled_text(content, without_spoilers)
}
pub fn content_text_as_sent(content: &MessageContent) -> String {
labeled_text(content, |text| text.text.clone())
}
fn labeled_text(content: &MessageContent, text: impl Fn(&FormattedText) -> String) -> String {
let labeled = |label: &str, caption: &FormattedText| {
let caption = text(caption);
if caption.is_empty() {
format!("[{label}]")
} else {
format!("[{label}] {caption}")
}
};
match content {
MessageContent::MessageText(m) => text(&m.text),
MessageContent::MessagePhoto(m) => labeled("Photo", &m.caption),
MessageContent::MessageVideo(m) => labeled("Video", &m.caption),
MessageContent::MessageAnimation(m) => labeled("GIF", &m.caption),
MessageContent::MessageDocument(m) => {
labeled(&format!("File: {}", m.document.file_name), &m.caption)
}
MessageContent::MessageAudio(m) => labeled("Audio", &m.caption),
MessageContent::MessageVoiceNote(m) => labeled("Voice message", &m.caption),
MessageContent::MessageVideoNote(_) => "[Video message]".into(),
MessageContent::MessageSticker(m) => format!("[Sticker {}]", m.sticker.emoji),
MessageContent::MessagePoll(m) => labeled("Poll", &m.poll.question),
MessageContent::MessageLocation(_) => "[Location]".into(),
MessageContent::MessageContact(_) => "[Contact]".into(),
MessageContent::MessageExpiredPhoto => "[Photo expired]".into(),
MessageContent::MessageExpiredVideo => "[Video expired]".into(),
MessageContent::MessageExpiredVideoNote => "[Video message expired]".into(),
MessageContent::MessageExpiredVoiceNote => "[Voice message expired]".into(),
_ => "[Message]".into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn chats(list: &[(i64, i64, i32)]) -> Chats {
let mut chats = Chats::default();
for &(id, order, unread) in list {
let lists: &[(List, i64)] = if order == 0 {
&[]
} else {
&[(List::Main, order)]
};
let mut chat = Chat::local(&format!("chat {id}"), lists);
chat.unread = unread;
chats.by_id.insert(id, chat);
}
chats.dirty = true;
chats.refresh();
chats
}
fn user(user_id: i64) -> MessageSender {
MessageSender::User(types::MessageSenderUser { user_id })
}
#[test]
fn activity_lasts_until_tdlib_says_it_stopped() {
let mut list = chats(&[(1, 50, 0)]);
let activity = |list: &Chats| list.get(1).unwrap().activity.clone();
list.set_action(1, None, &user(7), &ChatAction::Typing);
list.set_action(1, None, &user(8), &ChatAction::ChoosingSticker);
assert_eq!(
activity(&list),
[
(Sender::User(7), "typing", None),
(Sender::User(8), "choosing a sticker", None)
]
);
let photo = types::ChatActionUploadingPhoto { progress: 10 };
list.set_action(1, None, &user(7), &ChatAction::UploadingPhoto(photo));
assert_eq!(
activity(&list)[0],
(Sender::User(7), "sending a photo", None)
);
list.set_action(1, None, &user(7), &ChatAction::Cancel);
assert_eq!(
activity(&list),
[(Sender::User(8), "choosing a sticker", None)]
);
let watching = types::ChatActionWatchingAnimations {
emoji: "🎉".into()
};
list.set_action(1, None, &user(9), &ChatAction::WatchingAnimations(watching));
assert_eq!(activity(&list).len(), 1);
}
#[test]
fn unread_chats_come_first_in_telegram_order() {
let mut list = chats(&[(1, 50, 0), (2, 40, 3), (3, 30, 0), (4, 20, 1), (5, 0, 9)]);
assert_eq!(list.ids(), [2, 4, 1, 3]);
list.set_unread(3, 2);
list.refresh();
assert_eq!(list.ids(), [2, 3, 4, 1], "a new message moves it up");
}
#[test]
fn pinned_chats_stay_on_top_then_unread_ones() {
let mut list = chats(&[(1, 50, 0), (2, 40, 3), (3, 30, 0)]);
list.by_id.get_mut(&3).unwrap().set_pinned(List::Main, true);
list.dirty = true;
list.refresh();
assert_eq!(list.ids(), [3, 2, 1]);
}
#[test]
fn a_chat_is_muted_by_its_own_setting_or_its_kinds_default() {
let mut list = chats(&[(1, 50, 0), (2, 40, 0)]);
let own = |mute_for| ChatNotificationSettings {
mute_for,
..ChatNotificationSettings::default()
};
let default = || ChatNotificationSettings {
use_default_mute_for: true,
..ChatNotificationSettings::default()
};
list.set_notifications(1, own(3600));
list.set_notifications(2, default());
assert!(list.muted(1));
assert!(!list.muted(2));
list.set_default_mute(&NotificationSettingsScope::GroupChats, i32::MAX);
assert!(list.muted(2), "groups are muted by default now");
let unmute = list.with_mute(1, false).unwrap();
assert!(!unmute.use_default_mute_for && unmute.mute_for == 0);
assert_eq!(list.with_mute(2, true).unwrap().mute_for, i32::MAX);
}
#[test]
fn an_opened_chat_keeps_its_place_until_another_is_opened() {
let mut list = chats(&[(1, 50, 0), (2, 40, 3), (3, 30, 1)]);
list.opened(3);
list.set_unread(3, 0); list.refresh();
assert_eq!(list.ids(), [2, 3, 1], "stays with the unread chats");
list.opened(2);
list.refresh();
assert_eq!(list.ids(), [2, 1, 3], "drops to its place once you move on");
}
#[test]
fn the_filter_keeps_matching_titles_in_order() {
let mut list = chats(&[(1, 50, 0), (2, 40, 3), (3, 30, 0)]);
list.set_title(1, "Alice".into());
list.set_title(2, "Bob".into());
list.set_title(3, "alina".into());
list.set_my_id(4);
let mut me = Chat::local("Eric", &[(List::Main, 10)]);
me.is_private = true;
list.by_id.insert(4, me);
list.set_filter("ALI");
list.refresh();
assert_eq!(list.ids(), [1, 3]);
assert_eq!(list.total(), 4);
list.set_filter("saved");
list.refresh();
assert_eq!(list.ids(), [4], "Saved Messages matches by its shown name");
list.set_filter("");
list.refresh();
assert_eq!(list.ids(), [2, 1, 3, 4]);
}
#[test]
fn accent_colors_past_the_built_in_seven_map_to_one_of_them() {
let mut list = chats(&[(1, 50, 0), (2, 40, 0), (3, 30, 0)]);
list.set_accent(1, 3);
list.set_accent(2, 9);
list.set_accent(3, 12);
list.set_accent_colors(&[AccentColor {
id: 9,
built_in_accent_color_id: 5,
light_theme_colors: Vec::new(),
dark_theme_colors: Vec::new(),
min_channel_chat_boost_level: 0,
}]);
assert_eq!(list.accent(1), 3);
assert_eq!(list.accent(2), 5);
assert_eq!(list.accent(3), 5, "an unknown id still gets a color");
}
fn folder(id: i32, name: &str) -> ChatFolderInfo {
ChatFolderInfo {
id,
name: types::ChatFolderName {
text: FormattedText {
text: name.into(),
entities: Vec::new(),
},
animate_custom_emoji: false,
},
..ChatFolderInfo::default()
}
}
fn position(list: List, order: i64, pinned: bool) -> ChatPosition {
ChatPosition {
list: list.tdlib(),
order,
is_pinned: pinned,
source: None,
}
}
#[test]
fn a_folder_lists_its_own_chats_pinned_then_unread_first() {
let mut list = chats(&[(1, 50, 0), (2, 40, 3), (3, 30, 0), (4, 20, 1)]);
let work = List::Folder(7);
list.set_position(1, &position(work, 10, false));
list.set_position(3, &position(work, 30, true));
list.set_position(4, &position(work, 20, false));
list.show(work);
list.refresh();
assert_eq!(list.ids(), [3, 4, 1], "its own order and pins");
assert_eq!(list.total(), 3);
assert!(list.pinned(3) && !list.pinned(1));
list.set_position(4, &position(work, 0, false));
list.refresh();
assert_eq!(list.ids(), [3, 1]);
list.show(List::Main);
list.refresh();
assert_eq!(list.ids(), [2, 4, 1, 3], "the main list as it was");
assert!(!list.pinned(3), "pinned only in the folder");
}
#[test]
fn tabs_put_all_chats_where_telegram_says_and_the_archive_last() {
let mut list = chats(&[(1, 50, 0), (2, 40, 0)]);
assert!(list.tabs().is_empty(), "no folders, no archive: no tabs");
list.set_folders(&[folder(7, "Work"), folder(8, "Fam\u{202e}ily ")], 1);
list.set_unread_in(List::Folder(8), 2);
let tabs = list.tabs();
let names: Vec<&str> = tabs.iter().map(|t| t.name.as_str()).collect();
assert_eq!(names, ["Work", "All", "Family"], "names are cleaned");
assert_eq!(tabs[2].unread, 2);
list.set_position(2, &position(List::Archive, 5, false));
let tabs = list.tabs();
assert_eq!(tabs.last().unwrap().list, List::Archive);
assert_eq!(list.next_list(1), List::Folder(8));
assert_eq!(list.next_list(-1), List::Folder(7));
list.show(List::Archive);
assert_eq!(list.next_list(1), List::Folder(7), "round the end");
list.show(List::Folder(8));
list.set_folders(&[folder(7, "Work")], 0);
assert_eq!(list.shown(), List::Main);
}
#[test]
fn highlights_toggle() {
let mut list = chats(&[(1, 50, 0), (2, 40, 0)]);
assert_eq!(list.toggle_highlight(2), [2]);
assert_eq!(list.toggle_highlight(1), [1, 2]);
assert!(list.is_highlighted(2));
assert_eq!(list.toggle_highlight(2), [1]);
assert!(!list.is_highlighted(2));
}
}