use std::{
borrow::Borrow,
cell::RefCell,
collections::HashMap,
marker::PhantomData,
ops::{Deref, DerefMut},
rc::Rc,
str::FromStr,
sync::{Arc, RwLock, atomic::AtomicUsize},
thread::sleep,
time::{self, Duration},
};
use base64::{Engine, prelude::BASE64_STANDARD};
use chrono::{NaiveDateTime, TimeDelta};
use filamento::{
chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::{opfs::OPFSError, FileStore, FilesMem, FilesOPFS}, presence::{Presence, PresenceType, Show}, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage
};
use futures::stream::StreamExt;
use indexmap::IndexMap;
use jid::JID;
use leptos::{
ev::{Event, KeyboardEvent, SubmitEvent},
html::{self, Div, Input, Pre, Textarea},
prelude::*,
tachys::{dom::document, reactive_graph::bind::GetValue},
task::{spawn, spawn_local},
};
use reactive_stores::{ArcStore, Store, StoreField};
use stylance::import_style;
use thiserror::Error;
use tokio::sync::{
Mutex,
mpsc::{self, Receiver},
};
use tracing::{debug, error};
use uuid::Uuid;
const NO_AVATAR: &str = "/assets/no-avatar.png";
pub enum AppState {
LoggedOut,
LoggedIn,
}
#[derive(Clone)]
pub struct Client {
client: filamento::Client<Files>,
jid: Arc<JID>,
file_store: Files,
}
#[derive(Clone)]
pub enum Files {
Mem(FilesMem),
Opfs(FilesOPFS),
}
impl FileStore for Files {
type Err = OPFSError;
async fn is_stored(&self, name: &str) -> Result<bool, Self::Err> {
match self {
Files::Mem(files_mem) => Ok(files_mem.is_stored(name).await.unwrap()),
Files::Opfs(files_opfs) => Ok(files_opfs.is_stored(name).await?),
}
}
async fn store(&self, name: &str, data: &[u8]) -> Result<(), Self::Err> {
match self {
Files::Mem(files_mem) => Ok(files_mem.store(name, data).await.unwrap()),
Files::Opfs(files_opfs) => Ok(files_opfs.store(name, data).await?),
}
}
async fn delete(&self, name: &str) -> Result<(), Self::Err> {
match self {
Files::Mem(files_mem) => Ok(files_mem.delete(name).await.unwrap()),
Files::Opfs(files_opfs) => Ok(files_opfs.delete(name).await?),
}
}
}
impl Files {
pub async fn get_src(&self, file_name: &str) -> Option<String> {
match self {
Files::Mem(files_mem) => {
if let Some(data) = files_mem.get_file(file_name).await {
let data = BASE64_STANDARD.encode(data);
Some(format!("data:image/jpg;base64, {}", data))
} else {
None
}
}
Files::Opfs(files_opfs) => files_opfs.get_src(file_name).await.ok(),
}
}
}
impl Deref for Client {
type Target = filamento::Client<Files>;
fn deref(&self) -> &Self::Target {
&self.client
}
}
impl DerefMut for Client {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.client
}
}
#[component]
pub fn App() -> impl IntoView {
let (app, set_app) = signal(AppState::LoggedOut);
let client: RwSignal<Option<(Client, Receiver<UpdateMessage>)>> = RwSignal::new(None);
view! {
{move || match &*app.read() {
AppState::LoggedOut => view! { <LoginPage set_app set_client=client /> }.into_any(),
AppState::LoggedIn => {
if let Some((client, updates)) = client.write_untracked().take() {
view! { <Macaw client updates /> }.into_any()
} else {
set_app.set(AppState::LoggedOut);
view! { <LoginPage set_app set_client=client /> }.into_any()
}
}
}}
}
}
#[derive(Clone, Debug, Error)]
pub enum LoginError {
#[error("Missing Password")]
MissingPassword,
#[error("Missing JID")]
MissingJID,
#[error("Invalid JID: {0}")]
InvalidJID(#[from] jid::ParseError),
#[error("Connection Error: {0}")]
ConnectionError(#[from] CommandError<ConnectionError>),
#[error("OPFS: {0}")]
OPFS(#[from] OPFSError),
}
#[component]
fn LoginPage(
set_app: WriteSignal<AppState>,
set_client: RwSignal<Option<(Client, Receiver<UpdateMessage>)>>,
) -> impl IntoView {
let jid = RwSignal::new("".to_string());
let password = RwSignal::new("".to_string());
let remember_me = RwSignal::new(false);
let connect_on_login = RwSignal::new(true);
let (error, set_error) = signal(None::<LoginError>);
let error_message = move || {
error.with(|error| {
if let Some(error) = error {
view! { <div class="error">{error.to_string()}</div> }.into_any()
} else {
view! {}.into_any()
}
})
};
let (login_pending, set_login_pending) = signal(false);
let login = Action::new_local(move |_| {
async move {
set_login_pending.set(true);
if jid.read_untracked().is_empty() {
set_error.set(Some(LoginError::MissingJID));
set_login_pending.set(false);
return;
}
if password.read_untracked().is_empty() {
set_error.set(Some(LoginError::MissingPassword));
set_login_pending.set(false);
return;
}
let jid = match JID::from_str(&jid.read_untracked()) {
Ok(j) => j,
Err(e) => {
set_error.set(Some(e.into()));
set_login_pending.set(false);
return;
}
};
let remember_me = remember_me.get_untracked();
// initialise the client
let db = if remember_me {
debug!("creating db in opfs");
Db::create_connect_and_migrate(jid.as_bare().to_string())
.await
.unwrap()
} else {
debug!("creating db in memory");
Db::create_connect_and_migrate_memory().await.unwrap()
};
let files = if remember_me {
let opfs = FilesOPFS::new(jid.as_bare().to_string()).await;
match opfs {
Ok(f) => Files::Opfs(f),
Err(e) => {
set_error.set(Some(e.into()));
set_login_pending.set(false);
return;
}
}
} else {
Files::Mem(FilesMem::new())
};
let (client, updates) = filamento::Client::new(
jid.clone(),
password.read_untracked().clone(),
db,
files.clone(),
);
// TODO: remember_me
let client = Client {
client,
jid: Arc::new(jid),
file_store: files,
};
if *connect_on_login.read_untracked() {
match client.connect().await {
Ok(_) => {}
Err(e) => {
set_error.set(Some(e.into()));
set_login_pending.set(false);
return;
}
}
}
// debug!("before setting app state");
set_client.set(Some((client, updates)));
set_app.set(AppState::LoggedIn);
}
});
view! {
<div class="center fill">
<div id="login-form" class="panel">
<div id="hero">
<img src="/assets/macaw-icon.png" />
<h1>Macaw Instant Messenger</h1>
</div>
{error_message}
<form on:submit=move |ev| {
ev.prevent_default();
login.dispatch(());
}>
<label for="jid">JID</label>
<input
disabled=login_pending
placeholder="caw@macaw.chat"
type="text"
bind:value=jid
name="jid"
id="jid"
autofocus="true"
/>
<label for="password">Password</label>
<input
disabled=login_pending
placeholder="••••••••"
type="password"
bind:value=password
name="password"
id="password"
/>
<div>
<label for="remember_me">Remember me</label>
<input
disabled=login_pending
type="checkbox"
bind:checked=remember_me
name="remember_me"
id="remember_me"
/>
</div>
<div>
<label for="connect_on_login">Connect on login</label>
<input
disabled=login_pending
type="checkbox"
bind:checked=connect_on_login
name="connect_on_login"
id="connect_on_login"
/>
</div>
<input disabled=login_pending class="button" type="submit" value="Log In" />
</form>
</div>
</div>
}
}
pub struct MessageSubscriptions {
all: HashMap<Uuid, mpsc::Sender<(JID, MacawMessage)>>,
subset: HashMap<JID, HashMap<Uuid, mpsc::Sender<MacawMessage>>>,
}
impl MessageSubscriptions {
pub fn new() -> Self {
Self {
all: HashMap::new(),
subset: HashMap::new(),
}
}
pub async fn broadcast(&mut self, to: JID, message: MacawMessage) {
// subscriptions to all
let mut removals = Vec::new();
for (id, sender) in &self.all {
match sender.send((to.clone(), message.clone())).await {
Ok(_) => {}
Err(_) => {
removals.push(*id);
}
}
}
for removal in removals {
self.all.remove(&removal);
}
// subscriptions to specific chat
if let Some(subscribers) = self.subset.get_mut(&to) {
let mut removals = Vec::new();
for (id, sender) in &*subscribers {
match sender.send(message.clone()).await {
Ok(_) => {}
Err(_) => {
removals.push(*id);
}
}
}
for removal in removals {
subscribers.remove(&removal);
}
if subscribers.is_empty() {
self.subset.remove(&to);
}
}
}
pub fn subscribe_all(&mut self) -> (Uuid, Receiver<(JID, MacawMessage)>) {
let (send, recv) = mpsc::channel(10);
let id = Uuid::new_v4();
self.all.insert(id, send);
(id, recv)
}
pub fn subscribe_chat(&mut self, chat: JID) -> (Uuid, Receiver<MacawMessage>) {
let (send, recv) = mpsc::channel(10);
let id = Uuid::new_v4();
if let Some(chat_subscribers) = self.subset.get_mut(&chat) {
chat_subscribers.insert(id, send);
} else {
let hash_map = HashMap::from([(id, send)]);
self.subset.insert(chat, hash_map);
}
(id, recv)
}
pub fn unsubscribe_all(&mut self, sub_id: Uuid) {
self.all.remove(&sub_id);
}
pub fn unsubscribe_chat(&mut self, sub_id: Uuid, chat: JID) {
if let Some(chat_subs) = self.subset.get_mut(&chat) {
chat_subs.remove(&sub_id);
}
}
}
#[derive(Store, Clone)]
pub struct Roster {
#[store(key: JID = |(jid, _)| jid.clone())]
contacts: HashMap<JID, MacawContact>,
}
impl Roster {
pub fn new() -> Self {
Self {
contacts: HashMap::new(),
}
}
}
// TODO: multiple panels
// pub struct OpenChats {
// panels:
// }
#[derive(Store, Default)]
pub struct OpenChatsPanel {
// jid must be a chat in the chats map
chat_view: Option<JID>,
#[store(key: JID = |(jid, _)| jid.clone())]
chats: IndexMap<JID, MacawChat>,
}
pub fn open_chat(open_chats: Store<OpenChatsPanel>, chat: MacawChat) {
if let Some(jid) = &*open_chats.chat_view().read() {
if let Some((index, _jid, entry)) = open_chats.chats().write().shift_remove_full(jid) {
let new_jid = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat)
.correspondent()
.read()
.clone();
open_chats
.chats()
.write()
.insert_before(index, new_jid.clone(), chat);
*open_chats.chat_view().write() = Some(new_jid);
} else {
let new_jid = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat)
.correspondent()
.read()
.clone();
open_chats.chats().write().insert(new_jid.clone(), chat);
*open_chats.chat_view().write() = Some(new_jid);
}
} else {
let new_jid = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat)
.correspondent()
.read()
.clone();
open_chats.chats().write().insert(new_jid.clone(), chat);
*open_chats.chat_view().write() = Some(new_jid);
}
}
impl OpenChatsPanel {
pub fn open(&mut self, chat: MacawChat) {
if let Some(jid) = &mut self.chat_view {
debug!("a chat was already open");
if let Some((index, _jid, entry)) = self.chats.shift_remove_full(jid) {
let new_jid = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat)
.correspondent()
.read()
.clone();
self.chats.insert_before(index, new_jid.clone(), chat);
*&mut self.chat_view = Some(new_jid);
} else {
let new_jid = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat)
.correspondent()
.read()
.clone();
self.chats.insert(new_jid.clone(), chat);
*&mut self.chat_view = Some(new_jid);
}
} else {
let new_jid = <ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat)
.correspondent()
.read()
.clone();
self.chats.insert(new_jid.clone(), chat);
*&mut self.chat_view = Some(new_jid);
}
debug!("opened chat");
}
// TODO:
// pub fn open_in_new_tab_unfocused(&mut self) {
// }
// pub fn open_in_new_tab_focus(&mut self) {
// }
}
#[derive(Store)]
pub struct UserPresences {
#[store(key: JID = |(jid, _)| jid.clone())]
user_presences: HashMap<JID, ArcRwSignal<Presences>>,
}
impl UserPresences {
pub fn clear(&mut self) {
for (_user, presences) in &mut self.user_presences {
presences.set(Presences::new())
}
}
// TODO: should be a bare jid
pub fn get_user_presences(&mut self, user: &JID) -> ArcRwSignal<Presences> {
if let Some(presences) = self.user_presences.get(user) {
presences.clone()
} else {
let presences = Presences::new();
let signal = ArcRwSignal::new(presences);
self.user_presences.insert(user.clone(), signal.clone());
signal
}
}
}
impl UserPresences {
pub fn new() -> Self {
Self {
user_presences: HashMap::new(),
}
}
}
pub struct Presences {
/// presences are sorted by time, first by type, then by last activity.
presences: IndexMap<String, Presence>
}
impl Presences {
pub fn new() -> Self {
Self {
presences: IndexMap::new(),
}
}
/// gets the highest priority presence
pub fn presence(&self) -> Option<(String, Presence)> {
if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence {
online.show == Some(Show::DoNotDisturb)
} else {
false
}).next() {
return Some((resource.clone(), presence.clone()))
}
if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence {
online.show == Some(Show::Chat)
} else {
false
}).next() {
return Some((resource.clone(), presence.clone()))
}
if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence {
online.show == None
} else {
false
}).next() {
return Some((resource.clone(), presence.clone()))
}
if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence {
online.show == Some(Show::Away)
} else {
false
}).next() {
return Some((resource.clone(), presence.clone()))
}
if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Online(online) = &presence.presence {
online.show == Some(Show::ExtendedAway)
} else {
false
}).next() {
return Some((resource.clone(), presence.clone()))
}
if let Some((resource, presence)) = self.presences.iter().filter(|(_resource, presence)| if let PresenceType::Offline(_offline) = &presence.presence {
true
} else {
false
}).next() {
return Some((resource.clone(), presence.clone()))
} else {
None
}
}
pub fn update_presence(&mut self, resource: String, presence: Presence) {
let index = match self.presences.binary_search_by(|_, existing_presence| {
presence.timestamp
.cmp(
&existing_presence.timestamp
)
}) {
Ok(i) => i,
Err(i) => i,
};
self.presences.insert_before(
// TODO: check if this logic is correct
index,
resource,
presence,
);
}
}
#[component]
fn Macaw(
// TODO: logout
// app_state: WriteSignal<Option<essage>)>, LocalStorage>,
client: Client,
mut updates: Receiver<UpdateMessage>,
) -> impl IntoView {
provide_context(client);
let roster = Store::new(Roster::new());
provide_context(roster);
let message_subscriptions = RwSignal::new(MessageSubscriptions::new());
provide_context(message_subscriptions);
let messages_store: StateStore<Uuid, ArcStore<Message>> = StateStore::new();
provide_context(messages_store);
let chats_store: StateStore<JID, ArcStore<Chat>> = StateStore::new();
provide_context(chats_store);
let users_store: StateStore<JID, ArcStore<User>> = StateStore::new();
provide_context(users_store);
let open_chats = Store::new(OpenChatsPanel::default());
provide_context(open_chats);
let user_presences = Store::new(UserPresences::new());
provide_context(user_presences);
// TODO: get cached contacts on login before getting the updated contacts
OnceResource::new(async move {
while let Some(update) = updates.recv().await {
match update {
UpdateMessage::Online(online, items) => {
let contacts = items
.into_iter()
.map(|(contact, user)| {
(
contact.user_jid.clone(),
MacawContact::got_contact_and_user(contact, user),
)
})
.collect();
roster.contacts().set(contacts);
}
UpdateMessage::Offline(offline) => {
// when offline, will no longer receive updated user presences, consider everybody offline.
user_presences.write().clear();
}
UpdateMessage::RosterUpdate(contact, user) => {
roster.contacts().update(|roster| {
if let Some(macaw_contact) = roster.get_mut(&contact.user_jid) {
macaw_contact.set(contact);
} else {
let jid = contact.user_jid.clone();
let contact = MacawContact::got_contact_and_user(contact, user);
roster.insert(jid, contact);
}
});
}
UpdateMessage::RosterDelete(jid) => {
roster.contacts().update(|roster| {
roster.remove(&jid);
});
}
UpdateMessage::Presence { from, presence } => {
let bare_jid = from.as_bare();
if let Some(presences) = user_presences.read().user_presences.get(&bare_jid) {
if let Some(resource) = from.resourcepart {
presences.write().update_presence(resource, presence);
}
} else {
if let Some(resource) = from.resourcepart {
let mut presences = Presences::new();
presences.update_presence(resource, presence);
user_presences.write().user_presences.insert(bare_jid, ArcRwSignal::new(presences));
}
}
}
UpdateMessage::Message { to, from, message } => {
debug!("before got message");
let new_message = MacawMessage::got_message_and_user(message, from);
debug!("after got message");
spawn_local(async move {
message_subscriptions
.write()
.broadcast(to, new_message)
.await
});
debug!("after set message");
}
UpdateMessage::MessageDelivery { id, chat, delivery } => {
messages_store.modify(&id, |message| {
<ArcStore<filamento::chat::Message> as Clone>::clone(&message)
.delivery()
.set(Some(delivery))
});
}
UpdateMessage::SubscriptionRequest(jid) => {}
UpdateMessage::NickChanged { jid, nick } => {
users_store.modify(&jid, |user| {
user.update(|user| *&mut user.nick = nick.clone())
});
}
UpdateMessage::AvatarChanged { jid, id } => {
users_store.modify(&jid, |user| *&mut user.write().avatar = id.clone());
}
}
}
});
view! {
<Sidebar />
// <ChatsList />
<OpenChatsPanelView />
}
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum SidebarOpen {
Roster,
Chats,
}
pub fn toggle_open(state: &mut Option<SidebarOpen>, open: SidebarOpen) {
match state {
Some(opened) => {
if *opened == open {
*state = None
} else {
*state = Some(open)
}
}
None => *state = Some(open),
}
}
#[component]
pub fn Sidebar() -> impl IntoView {
// for what has been clicked open (in the background)
let (open, set_open) = signal(None::<SidebarOpen>);
// for what is just in the hovered state (not clicked to be pinned open yet necessarily)
let (hovered, set_hovered) = signal(None::<SidebarOpen>);
view! {
<div class="sidebar">
<div class="dock panel">
<div class="shortcuts">
<div class="roster-tab dock-item" class:focused=move || *open.read() == Some(SidebarOpen::Roster) class:hovering=move || *hovered.read() == Some(SidebarOpen::Chats) on:click=move |_| {
set_open.update(|state| toggle_open(state, SidebarOpen::Roster))
}>
<div class="dock-pill"></div>
<img src="/assets/caw.png" />
</div>
<div class="chats-tab dock-item" class:focused=move || *open.read() == Some(SidebarOpen::Chats) class:hovering=move || *hovered.read() == Some(SidebarOpen::Chats) on:click=move |_| {
set_open.update(|state| toggle_open(state, SidebarOpen::Chats))
}>
<div class="dock-pill"></div>
<img src="/assets/bubble.png" />
</div>
</div>
<div class="pins">
</div>
<div class="personal">
</div>
</div>
{move || if let Some(opened) = *open.read() {
match opened {
SidebarOpen::Roster => view! {
<RosterList />
}.into_any(),
SidebarOpen::Chats => view! {
<ChatsList />
}.into_any(),
}
} else {
view! {}.into_any()
}}
</div>
}
}
#[component]
pub fn OpenChatsPanelView() -> impl IntoView {
let open_chats: Store<OpenChatsPanel> = use_context().expect("no open chats panel in context");
// TODO: tabs
// view! {
// {move || {
// if open_chats.chats().read().len() > 1 {
// Some(
// view! {
// <For
// each=move || open_chats.chats().get()
// key=|(jid, _)| jid.clone()
// let(chat)
// ></For>
// },
// )
// } else {
// None
// }
// }}
// }
view! {
<div class="open-chat-views">
{move || {
if let Some(open_chat) = open_chats.chat_view().get() {
if let Some(open_chat) = open_chats.chats().read().get(&open_chat) {
view! { <OpenChatView chat=open_chat.clone() /> }.into_any()
} else {
view! {}.into_any()
}
} else {
view! {}.into_any()
}
}}
</div>
}
}
#[component]
pub fn OpenChatView(chat: MacawChat) -> impl IntoView {
let chat_chat: Store<Chat> =
<ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).into();
let chat_jid = move || chat_chat.correspondent().get();
view! {
<div class="open-chat-view">
<ChatViewHeader chat=chat.clone() />
<MessageHistoryBuffer chat=chat.clone() />
<ChatViewMessageComposer chat=chat_jid() />
</div>
}
}
pub fn show_to_icon(show: Show) -> Icon {
match show {
Show::Away => Icon::Away16Color,
Show::Chat => Icon::Chat16Color,
Show::DoNotDisturb => Icon::Dnd16Color,
Show::ExtendedAway => Icon::Xa16Color,
}
}
#[component]
pub fn AvatarWithPresence(user: Store<User>) -> impl IntoView {
let avatar = LocalResource::new(move || get_avatar(user));
let user_presences: Store<UserPresences> = use_context().expect("no user presences in context");
let presence = move || user_presences.write().get_user_presences(&user.read().jid).read().presence();
let show_icon = move || presence().map(|(_, presence)| {
match presence.presence {
PresenceType::Online(online) => if let Some(show) = online.show {
Some(show_to_icon(show))
} else {
Some(Icon::Available16Color)
},
PresenceType::Offline(offline) => None,
}
}).unwrap_or_default();
view! {
<div class="avatar-with-presence">
<img class="avatar" src=move || avatar.get() />
{move || if let Some(icon) = show_icon() {
view!{
<IconComponent icon=icon class:presence-show-icon=true />
}.into_any()
} else {
view! {}.into_any()
}}
</div>
}
}
#[component]
pub fn ChatViewHeader(chat: MacawChat) -> impl IntoView {
let chat_user = <ArcStore<filamento::user::User> as Clone>::clone(&chat.user).into();
let name = move || get_name(chat_user);
let jid = move || chat_user.jid().read().to_string();
view! {
<div class="chat-view-header panel">
<AvatarWithPresence user=chat_user />
<div class="user-info">
<h2 class="name">{name}</h2>
<h3>{jid}</h3>
</div>
</div>
}
}
#[component]
pub fn MessageHistoryBuffer(chat: MacawChat) -> impl IntoView {
let (messages, set_messages) = arc_signal(IndexMap::new());
let chat_chat: Store<Chat> =
<ArcStore<filamento::chat::Chat> as Clone>::clone(&chat.chat).into();
let chat_user: Store<User> =
<ArcStore<filamento::user::User> as Clone>::clone(&chat.user).into();
let load_set_messages = set_messages.clone();
let load_messages = LocalResource::new(move || {
let load_set_messages = load_set_messages.clone();
async move {
let client = use_context::<Client>().expect("client not in context");
let messages = client
.get_messages_with_users(chat_chat.correspondent().get())
.await
.map_err(|e| e.to_string());
match messages {
Ok(m) => {
let messages = m
.into_iter()
.map(|(message, message_user)| {
(
message.id,
MacawMessage::got_message_and_user(message, message_user),
)
})
.collect::<IndexMap<Uuid, _>>();
load_set_messages.set(messages);
}
Err(err) => {
error!("{err}")
// TODO: show error message at top of chats list
}
}
}
});
// TODO: filter new messages signal
let new_messages_signal: RwSignal<MessageSubscriptions> = use_context().unwrap();
let (sub_id, set_sub_id) = signal(None);
let load_new_messages_set = set_messages.clone();
let _load_new_messages = LocalResource::new(move || {
let load_new_messages_set = load_new_messages_set.clone();
async move {
load_messages.await;
let (sub_id, mut new_messages) = new_messages_signal
.write()
.subscribe_chat(chat_chat.correspondent().get());
set_sub_id.set(Some(sub_id));
while let Some(new_message) = new_messages.recv().await {
debug!("got new message in let message buffer");
let mut messages = load_new_messages_set.write();
if let Some((_, last)) = messages.last() {
if *<ArcStore<filamento::chat::Message> as Clone>::clone(&last.message)
.timestamp()
.read()
< *<ArcStore<filamento::chat::Message> as Clone>::clone(&new_message)
.timestamp()
.read()
{
messages.insert(
<ArcStore<filamento::chat::Message> as Clone>::clone(
&new_message.message,
)
.id()
.get(),
new_message,
);
debug!("set the new message in message buffer");
} else {
let index = match messages.binary_search_by(|_, value| {
<ArcStore<filamento::chat::Message> as Clone>::clone(&value.message)
.timestamp()
.read()
.cmp(
&<ArcStore<filamento::chat::Message> as Clone>::clone(
&new_message.message,
)
.timestamp()
.read(),
)
}) {
Ok(i) => i,
Err(i) => i,
};
messages.insert_before(
// TODO: check if this logic is correct
index,
<ArcStore<filamento::chat::Message> as Clone>::clone(
&new_message.message,
)
.id()
.get(),
new_message,
);
debug!("set the new message in message buffer");
}
} else {
messages.insert(
<ArcStore<filamento::chat::Message> as Clone>::clone(&new_message.message)
.id()
.get(),
new_message,
);
debug!("set the new message in message buffer");
}
}
}
});
on_cleanup(move || {
if let Some(sub_id) = sub_id.get() {
new_messages_signal
.write()
.unsubscribe_chat(sub_id, chat_chat.correspondent().get());
}
});
let each = move || {
let mut last_timestamp = NaiveDateTime::MIN;
let mut last_user: Option<JID> = None;
let mut messages = messages
.get()
.into_iter()
.map(|(id, message)| {
let message_timestamp =
<ArcStore<filamento::chat::Message> as Clone>::clone(&message.message)
.timestamp()
.read()
.naive_local();
// if message_timestamp.date() > last_timestamp.date() {
// messages_view = messages_view.push(date(message_timestamp.date()));
// }
let major = if last_user.as_ref() != Some(&message.message.read().from)
|| message_timestamp - last_timestamp > TimeDelta::minutes(3)
{
true
} else {
false
};
last_user = Some(
<ArcStore<filamento::chat::Message> as Clone>::clone(&message.message)
.from()
.get(),
);
last_timestamp = message_timestamp;
(id, (message, major, false))
})
.collect::<Vec<_>>();
if let Some((_id, (_, _, last))) = messages.last_mut() {
*last = true
}
messages.into_iter().rev()
};
view! {
<div class="messages-buffer">
<For each=each key=|message| (message.0, message.1.1, message.1.2) let(message)>
<Message message=message.1.0 major=message.1.1 r#final=message.1.2 />
</For>
</div>
}
}
#[derive(Copy, Clone)]
pub enum Icon {
AddContact24,
Attachment24,
Away16,
Away16Color,
Bubble16,
Bubble16Color,
Bubble24,
Contact24,
Delivered16,
Dnd16,
Dnd16Color,
Error16Color,
Forward24,
Heart24,
NewBubble24,
Reply24,
Sending16,
Sent16,
Chat16Color,
Xa16Color,
Available16Color,
}
pub const ICONS_SRC: &str = "/assets/icons/";
impl Icon {
pub fn src(&self) -> String {
match self {
Icon::AddContact24 => format!("{}addcontact24.svg", ICONS_SRC),
Icon::Attachment24 => format!("{}attachment24.svg", ICONS_SRC),
Icon::Away16 => format!("{}away16.svg", ICONS_SRC),
Icon::Away16Color => format!("{}away16color.svg", ICONS_SRC),
Icon::Bubble16 => format!("{}bubble16.svg", ICONS_SRC),
Icon::Bubble16Color => format!("{}bubble16color.svg", ICONS_SRC),
Icon::Bubble24 => format!("{}bubble24.svg", ICONS_SRC),
Icon::Contact24 => format!("{}contact24.svg", ICONS_SRC),
Icon::Delivered16 => format!("{}delivered16.svg", ICONS_SRC),
Icon::Dnd16 => format!("{}dnd16.svg", ICONS_SRC),
Icon::Dnd16Color => format!("{}dnd16color.svg", ICONS_SRC),
Icon::Error16Color => format!("{}error16color.svg", ICONS_SRC),
Icon::Forward24 => format!("{}forward24.svg", ICONS_SRC),
Icon::Heart24 => format!("{}heart24.svg", ICONS_SRC),
Icon::NewBubble24 => format!("{}newbubble24.svg", ICONS_SRC),
Icon::Reply24 => format!("{}reply24.svg", ICONS_SRC),
Icon::Sending16 => format!("{}sending16.svg", ICONS_SRC),
Icon::Sent16 => format!("{}sent16.svg", ICONS_SRC),
Icon::Chat16Color => format!("{}chat16color.svg", ICONS_SRC),
Icon::Xa16Color => format!("{}xa16color.svg", ICONS_SRC),
Icon::Available16Color => format!("{}available16color.svg", ICONS_SRC),
}
}
pub fn size(&self) -> isize {
match self {
Icon::AddContact24 => 24,
Icon::Attachment24 => 24,
Icon::Away16 => 16,
Icon::Away16Color => 16,
Icon::Bubble16 => 16,
Icon::Bubble16Color => 16,
Icon::Bubble24 => 24,
Icon::Contact24 => 24,
Icon::Delivered16 => 16,
Icon::Dnd16 => 16,
Icon::Dnd16Color => 16,
Icon::Error16Color => 16,
Icon::Forward24 => 24,
Icon::Heart24 => 24,
Icon::NewBubble24 => 24,
Icon::Reply24 => 24,
Icon::Sending16 => 16,
Icon::Sent16 => 16,
Icon::Chat16Color => 16,
Icon::Xa16Color => 16,
Icon::Available16Color => 16,
}
}
}
#[component]
pub fn IconComponent(icon: Icon) -> impl IntoView {
view! {
<img class:icon=true style=move || format!("height: {}px; width: {}px", icon.size(), icon.size()) src=move || icon.src() />
}
}
#[component]
pub fn Delivery(delivery: Delivery) -> impl IntoView {
match delivery {
// TODO: proper icon coloring/theming
Delivery::Sending => {
view! { <IconComponent class:visible=true class:light=true icon=Icon::Sending16 /> }
.into_any()
}
Delivery::Written => {
view! { <IconComponent class:light=true icon=Icon::Sent16 /> }.into_any()
}
// TODO: message receipts
// Delivery::Written => view! {}.into_any(),
Delivery::Sent => view! { <IconComponent class:light=true icon=Icon::Sent16 /> }.into_any(),
Delivery::Delivered => {
view! { <IconComponent class:light=true icon=Icon::Delivered16 /> }.into_any()
}
// TODO: check if there is also the icon class
Delivery::Read => {
view! { <IconComponent class:light=true class:read=true icon=Icon::Delivered16 /> }
.into_any()
}
Delivery::Failed => {
view! { <IconComponent class:visible=true class:light=true icon=Icon::Error16Color /> }
.into_any()
}
// TODO: queued icon
Delivery::Queued => {
view! { <IconComponent class:visible=true class:light=true icon=Icon::Sending16 /> }
.into_any()
}
}
}
#[component]
pub fn Message(message: MacawMessage, major: bool, r#final: bool) -> impl IntoView {
let message_message: Store<Message> =
<ArcStore<filamento::chat::Message> as Clone>::clone(&message.message).into();
let message_user = <ArcStore<filamento::user::User> as Clone>::clone(&message.user).into();
let avatar = LocalResource::new(move || get_avatar(message_user));
let name = move || get_name(message_user);
// TODO: chrono-humanize?
// TODO: if final, show delivery not only on hover.
// {move || message_message.delivery().read().map(|delivery| delivery.to_string()).unwrap_or_default()}
if major {
view! {
<div class:final=r#final class="chat-message major">
<div class="left">
<Transition fallback=|| view! { <img class="avatar" src=NO_AVATAR /> } >
<img class="avatar" src=move || avatar.get() />
</Transition>
</div>
<div class="middle">
<div class="message-info">
<div class="message-user-name">{name}</div>
<div class="message-timestamp">{move || message_message.timestamp().read().format("%H:%M").to_string()}</div>
</div>
<div class="message-text">
{move || message_message.body().read().body.clone()}
</div>
</div>
<div class="right message-delivery">{move || message_message.delivery().get().map(|delivery| view! { <Delivery class:light=true delivery /> } ) }</div>
</div>
}.into_any()
} else {
view! {
<div class:final=r#final class="chat-message minor">
<div class="left message-timestamp">
{move || message_message.timestamp().read().format("%H:%M").to_string()}
</div>
<div class="middle message-text">{move || message_message.body().read().body.clone()}</div>
<div class="right message-delivery">{move || message_message.delivery().get().map(|delivery| view! { <Delivery delivery /> } ) }</div>
</div>
}.into_any()
}
}
#[component]
pub fn ChatViewMessageComposer(chat: JID) -> impl IntoView {
let message_input: NodeRef<Div> = NodeRef::new();
// TODO: load last message draft
let new_message = RwSignal::new("".to_string());
let client: Client = use_context().expect("no client in context");
let client = RwSignal::new(client);
let (shift_pressed, set_shift_pressed) = signal(false);
let send_message = move || {
let value = chat.clone();
spawn_local(async move {
match client
.read()
.send_message(
value,
Body {
body: new_message.get(),
},
)
.await
{
Ok(_) => {
new_message.set("".to_string());
message_input
.write()
.as_ref()
.expect("message input div not mounted")
.set_text_content(Some(""));
}
Err(e) => tracing::error!("message send error: {}", e),
}
})
};
let _focus = Effect::new(move |_| {
if let Some(input) = message_input.get() {
let _ = input.focus();
// TODO: set the last draft
input.set_text_content(Some(""));
// input.style("height: 0");
// let height = input.scroll_height();
// input.style(format!("height: {}px", height));
}
});
// let on_input = move |ev: Event| {
// // let keyboard_event: KeyboardEvent = ev.try_into().unwrap();
// debug!("got input event");
// let key= event_target_value(&ev);
// new_message.set(key);
// debug!("set new message");
// };
//
// TODO: placeholder
view! {
<form
class="new-message-composer panel"
>
<div
class="text-box"
on:input:target=move |ev| new_message.set(ev.target().text_content().unwrap_or_default())
node_ref=message_input
contenteditable
on:keydown=move |ev| {
match ev.key_code() {
16 => set_shift_pressed.set(true),
13 => if !shift_pressed.get() {
ev.prevent_default();
send_message();
}
_ => {}
// debug!("shift pressed down");
}
}
on:keyup=move |ev| {
match ev.key_code() {
16 => set_shift_pressed.set(false),
_ => {}
// debug!("shift released");
}
}
></div>
// <input hidden type="submit" />
</form>
}
}
// V has to be an arc signal
#[derive(Debug)]
struct ArcStateStore<K, V> {
store: Arc<RwLock<HashMap<K, (V, usize)>>>,
}
impl<K, V> PartialEq for ArcStateStore<K, V> {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.store, &other.store)
}
}
impl<K, V> Clone for ArcStateStore<K, V> {
fn clone(&self) -> Self {
Self {
store: Arc::clone(&self.store),
}
}
}
impl<K, V> Eq for ArcStateStore<K, V> {}
impl<K, V> ArcStateStore<K, V> {
pub fn new() -> Self {
Self {
store: Arc::new(RwLock::new(HashMap::new())),
}
}
}
#[derive(Debug)]
struct StateStore<K, V, S = SyncStorage> {
inner: ArenaItem<ArcStateStore<K, V>, S>,
}
impl<K, V, S> Dispose for StateStore<K, V, S> {
fn dispose(self) {
self.inner.dispose()
}
}
impl<K, V> StateStore<K, V>
where
K: Send + Sync + 'static,
V: Send + Sync + 'static,
{
pub fn new() -> Self {
Self::new_with_storage()
}
}
impl<K, V, S> StateStore<K, V, S>
where
K: 'static,
V: 'static,
S: Storage<ArcStateStore<K, V>>,
{
pub fn new_with_storage() -> Self {
Self {
inner: ArenaItem::new_with_storage(ArcStateStore::new()),
}
}
}
impl<K, V> StateStore<K, V, LocalStorage>
where
K: 'static,
V: 'static,
{
pub fn new_local() -> Self {
Self::new_with_storage()
}
}
impl<
K: std::marker::Send + std::marker::Sync + 'static,
V: std::marker::Send + std::marker::Sync + 'static,
> From<ArcStateStore<K, V>> for StateStore<K, V>
{
fn from(value: ArcStateStore<K, V>) -> Self {
Self {
inner: ArenaItem::new_with_storage(value),
}
}
}
impl<K: 'static, V: 'static> FromLocal<ArcStateStore<K, V>> for StateStore<K, V, LocalStorage> {
fn from_local(value: ArcStateStore<K, V>) -> Self {
Self {
inner: ArenaItem::new_with_storage(value),
}
}
}
impl<K, V, S> Copy for StateStore<K, V, S> {}
impl<K, V, S> Clone for StateStore<K, V, S> {
fn clone(&self) -> Self {
*self
}
}
impl<K: Eq + std::hash::Hash + Clone, V: Clone> StateStore<K, V>
where
K: Send + Sync + 'static,
V: Send + Sync + 'static,
{
pub fn store(&self, key: K, value: V) -> StateListener<K, V> {
{
let store = self.inner.try_get_value().unwrap();
let mut store = store.store.write().unwrap();
debug!("got store");
debug!("got store 2");
if let Some((v, count)) = store.get_mut(&key) {
*v = value.clone();
*count += 1;
} else {
store.insert(key.clone(), (value.clone(), 1));
}
};
StateListener {
value,
cleaner: StateCleaner {
key,
state_store: self.clone(),
},
}
}
}
impl<K, V> StateStore<K, V>
where
K: Eq + std::hash::Hash + Send + Sync + 'static,
V: Send + Sync + 'static,
{
pub fn update(&self, key: &K, value: V) {
let store = self.inner.try_get_value().unwrap();
let mut store = store.store.write().unwrap();
if let Some((v, _)) = store.get_mut(key) {
*v = value;
}
}
pub fn modify(&self, key: &K, modify: impl Fn(&mut V)) {
let store = self.inner.try_get_value().unwrap();
let mut store = store.store.write().unwrap();
if let Some((v, _)) = store.get_mut(key) {
modify(v);
}
}
fn remove(&self, key: &K) {
// let store = self.inner.try_get_value().unwrap();
// let mut store = store.store.write().unwrap();
// if let Some((_v, count)) = store.get_mut(key) {
// *count -= 1;
// if *count == 0 {
// store.remove(key);
// debug!("dropped item from store");
// }
// }
}
}
#[derive(Clone)]
struct StateListener<K, V>
where
K: Eq + std::hash::Hash + 'static + std::marker::Send + std::marker::Sync,
V: 'static + std::marker::Send + std::marker::Sync,
{
value: V,
cleaner: StateCleaner<K, V>,
}
impl<
K: std::cmp::Eq + std::hash::Hash + std::marker::Send + std::marker::Sync,
V: std::marker::Send + std::marker::Sync,
> Deref for StateListener<K, V>
{
type Target = V;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<K: std::cmp::Eq + std::hash::Hash + Send + Sync, V: Send + Sync> DerefMut
for StateListener<K, V>
{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
struct ArcStateCleaner<K, V> {
key: K,
state_store: ArcStateStore<K, V>,
}
struct StateCleaner<K, V>
where
K: Eq + std::hash::Hash + Send + Sync + 'static,
V: Send + Sync + 'static,
{
key: K,
state_store: StateStore<K, V>,
}
impl<K, V> Clone for StateCleaner<K, V>
where
K: Eq + std::hash::Hash + Clone + Send + Sync,
V: Send + Sync,
{
fn clone(&self) -> Self {
{
let store = self.state_store.inner.try_get_value().unwrap();
let mut store = store.store.write().unwrap();
if let Some((_v, count)) = store.get_mut(&self.key) {
*count += 1;
}
}
Self {
key: self.key.clone(),
state_store: self.state_store.clone(),
}
}
}
impl<K: Eq + std::hash::Hash + Send + Sync + 'static, V: Send + Sync + 'static> Drop
for StateCleaner<K, V>
{
fn drop(&mut self) {
self.state_store.remove(&self.key);
}
}
#[derive(Clone)]
struct MacawChat {
chat: StateListener<JID, ArcStore<Chat>>,
user: StateListener<JID, ArcStore<User>>,
}
impl MacawChat {
fn got_chat_and_user(chat: Chat, user: User) -> Self {
let chat_state_store: StateStore<JID, ArcStore<Chat>> =
use_context().expect("no chat state store");
let user_state_store: StateStore<JID, ArcStore<User>> =
use_context().expect("no user state store");
let user = user_state_store.store(user.jid.clone(), ArcStore::new(user));
let chat = chat_state_store.store(chat.correspondent.clone(), ArcStore::new(chat));
Self { chat, user }
}
}
impl Deref for MacawChat {
type Target = StateListener<JID, ArcStore<Chat>>;
fn deref(&self) -> &Self::Target {
&self.chat
}
}
impl DerefMut for MacawChat {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.chat
}
}
#[derive(Clone)]
struct MacawMessage {
message: StateListener<Uuid, ArcStore<Message>>,
user: StateListener<JID, ArcStore<User>>,
}
impl MacawMessage {
fn got_message_and_user(message: Message, user: User) -> Self {
let message_state_store: StateStore<Uuid, ArcStore<Message>> =
use_context().expect("no message state store");
let user_state_store: StateStore<JID, ArcStore<User>> =
use_context().expect("no user state store");
let message = message_state_store.store(message.id, ArcStore::new(message));
let user = user_state_store.store(user.jid.clone(), ArcStore::new(user));
Self { message, user }
}
}
impl Deref for MacawMessage {
type Target = StateListener<Uuid, ArcStore<Message>>;
fn deref(&self) -> &Self::Target {
&self.message
}
}
impl DerefMut for MacawMessage {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.message
}
}
struct MacawUser {
user: StateListener<JID, Store<User>>,
}
impl Deref for MacawUser {
type Target = StateListener<JID, Store<User>>;
fn deref(&self) -> &Self::Target {
&self.user
}
}
impl DerefMut for MacawUser {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.user
}
}
#[derive(Clone)]
struct MacawContact {
contact: Store<Contact>,
user: StateListener<JID, ArcStore<User>>,
}
impl MacawContact {
fn got_contact_and_user(contact: Contact, user: User) -> Self {
let contact = Store::new(contact);
let user_state_store: StateStore<JID, ArcStore<User>> =
use_context().expect("no user state store");
let user = user_state_store.store(user.jid.clone(), ArcStore::new(user));
Self { contact, user }
}
}
impl Deref for MacawContact {
type Target = Store<Contact>;
fn deref(&self) -> &Self::Target {
&self.contact
}
}
impl DerefMut for MacawContact {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.contact
}
}
#[component]
fn ChatsList() -> impl IntoView {
let (chats, set_chats) = signal(IndexMap::new());
let load_chats = LocalResource::new(move || async move {
let client = use_context::<Client>().expect("client not in context");
let chats = client
.get_chats_ordered_with_latest_messages_and_users()
.await
.map_err(|e| e.to_string());
match chats {
Ok(c) => {
let chats = c
.into_iter()
.map(|((chat, chat_user), (message, message_user))| {
(
chat.correspondent.clone(),
(
MacawChat::got_chat_and_user(chat, chat_user),
MacawMessage::got_message_and_user(message, message_user),
),
)
})
.collect::<IndexMap<JID, _>>();
set_chats.set(chats);
}
Err(_) => {
// TODO: show error message at top of chats list
}
}
});
// TODO: filter new messages signal
let new_messages_signal: RwSignal<MessageSubscriptions> = use_context().unwrap();
let (sub_id, set_sub_id) = signal(None);
let _load_new_messages = LocalResource::new(move || async move {
load_chats.await;
let (sub_id, mut new_messages) = new_messages_signal.write().subscribe_all();
set_sub_id.set(Some(sub_id));
while let Some((to, new_message)) = new_messages.recv().await {
debug!("got new message in let");
let mut chats = set_chats.write();
if let Some((chat, _latest_message)) = chats.shift_remove(&to) {
// TODO: check if new message is actually latest message
debug!("chat existed");
debug!("new message: {}", new_message.read().body.body);
chats.insert_before(0, to, (chat.clone(), new_message));
debug!("done setting");
} else {
debug!("the chat didn't exist");
let client = use_context::<Client>().expect("client not in context");
let chat = client.get_chat(to.clone()).await.unwrap();
let user = client.get_user(to.clone()).await.unwrap();
debug!("before got chat");
let chat = MacawChat::got_chat_and_user(chat, user);
debug!("after got chat");
chats.insert_before(0, to, (chat, new_message));
debug!("done setting");
}
}
debug!("set the new message");
});
on_cleanup(move || {
if let Some(sub_id) = sub_id.get() {
new_messages_signal.write().unsubscribe_all(sub_id);
}
});
view! {
<div class="chats-list panel">
<h2>Chats</h2>
<div class="chats-list-chats">
<For each=move || chats.get() key=|chat| chat.1.1.message.read().id let(chat)>
<ChatsListItem chat=chat.1.0 message=chat.1.1 />
</For>
</div>
</div>
}
}
#[component]
fn RosterList() -> impl IntoView {
let roster: Store<Roster> = use_context().expect("no roster in context");
// TODO: filter new messages signal
view! {
<div class="roster-list panel">
<h2>Roster</h2>
<div class="roster-list-roster">
<For each=move || roster.contacts().get() key=|contact| contact.0.clone() let(contact)>
<RosterListItem contact=contact.1 />
</For>
</div>
</div>
}
}
#[component]
fn RosterListItem(contact: MacawContact) -> impl IntoView {
let contact_contact: Store<Contact> = contact.contact;
let contact_user: Store<User> =
<ArcStore<filamento::user::User> as Clone>::clone(&contact.user).into();
let name = move || get_name(contact_user);
let open_chats: Store<OpenChatsPanel> =
use_context().expect("no open chats panel store in context");
// TODO: why can this not be in the closure?????
// TODO: not good, as overwrites preexisting chat state with possibly incorrect one...
let chat = Chat {
correspondent: contact_user.jid().get(),
have_chatted: false,
};
let chat = MacawChat::got_chat_and_user(chat, contact_user.get());
let open_chat = move |_| {
debug!("opening chat");
open_chats.update(|open_chats| open_chats.open(chat.clone()));
};
let open = move || {
if let Some(open_chat) = &*open_chats.chat_view().read() {
debug!("got open chat: {:?}", open_chat);
if *open_chat == *contact_user.jid().read() {
return Open::Focused;
}
}
if let Some(_backgrounded_chat) = open_chats
.chats()
.read()
.get(contact_user.jid().read().deref())
{
return Open::Open;
}
Open::Closed
};
let focused = move || open().is_focused();
let open = move || open().is_open();
view! {
<div class="roster-list-item" class:open=move || open() class:focused=move || focused() on:click=open_chat>
<AvatarWithPresence user=contact_user />
<div class="item-info">
<h3>{name}</h3>
</div>
</div>
}
}
pub async fn get_avatar(user: Store<User>) -> String {
if let Some(avatar) = &user.read().avatar {
let client = use_context::<Client>().expect("client not in context");
if let Some(data) = client.file_store.get_src(avatar).await {
data
} else {
NO_AVATAR.to_string()
}
// TODO: enable avatar fetching
// format!("/files/{}", avatar)
} else {
NO_AVATAR.to_string()
}
}
pub fn get_name(user: Store<User>) -> String {
let roster: Store<Roster> = use_context().expect("no roster in context");
if let Some(name) = roster
.contacts()
.read()
.get(&user.read().jid)
.map(|contact| contact.read().name.clone())
.unwrap_or_default()
{
name.to_string()
} else if let Some(nick) = &user.read().nick {
nick.to_string()
} else {
user.read().jid.to_string()
}
}
pub enum Open {
/// Currently on screen
Focused,
/// Open in background somewhere (e.g. in another chat tab)
Open,
/// Closed
Closed,
}
impl Open {
pub fn is_focused(&self) -> bool {
match self {
Open::Focused => true,
Open::Open => false,
Open::Closed => false,
}
}
pub fn is_open(&self) -> bool {
match self {
Open::Focused => true,
Open::Open => true,
Open::Closed => false,
}
}
}
#[component]
fn ChatsListItem(chat: MacawChat, message: MacawMessage) -> impl IntoView {
let chat_chat: Store<Chat> = <ArcStore<Chat> as Clone>::clone(&chat.chat).into();
let chat_user: Store<User> =
<ArcStore<filamento::user::User> as Clone>::clone(&chat.user).into();
let name = move || get_name(chat_user);
// TODO: store fine-grained reactivity
let latest_message_body = move || message.get().body.body;
let open_chats: Store<OpenChatsPanel> =
use_context().expect("no open chats panel store in context");
let open_chat = move |_| {
debug!("opening chat");
open_chats.update(|open_chats| open_chats.open(chat.clone()));
};
let open = move || {
if let Some(open_chat) = &*open_chats.chat_view().read() {
debug!("got open chat: {:?}", open_chat);
if *open_chat == *chat_chat.correspondent().read() {
return Open::Focused;
}
}
if let Some(_backgrounded_chat) = open_chats
.chats()
.read()
.get(chat_chat.correspondent().read().deref())
{
return Open::Open;
}
Open::Closed
};
let focused = move || open().is_focused();
let open = move || open().is_open();
view! {
<div class="chats-list-item" class:open=move || open() class:focused=move || focused() on:click=open_chat>
<AvatarWithPresence user=chat_user />
<div class="item-info">
<h3>{name}</h3>
<p>{latest_message_body}</p>
</div>
</div>
}
}