summaryrefslogtreecommitdiffstats
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs220
1 files changed, 195 insertions, 25 deletions
diff --git a/src/lib.rs b/src/lib.rs
index a6dd648..2aa32ee 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -14,7 +14,7 @@ use std::{
use base64::{prelude::BASE64_STANDARD, Engine};
use chrono::{NaiveDateTime, TimeDelta};
use filamento::{
- chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::FilesMem, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage
+ chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::FileStore, files::opfs::OPFSError, files::FilesMem, files::FilesOPFS, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage
};
use futures::stream::StreamExt;
use indexmap::IndexMap;
@@ -28,7 +28,7 @@ use reactive_stores::{ArcStore, Store, StoreField};
use stylance::import_style;
use thiserror::Error;
use tokio::sync::{mpsc::{self, Receiver}, Mutex};
-use tracing::debug;
+use tracing::{debug, error};
use uuid::Uuid;
const NO_AVATAR: &str = "/assets/no-avatar.png";
@@ -40,13 +40,64 @@ pub enum AppState {
#[derive(Clone)]
pub struct Client {
- client: filamento::Client<FilesMem>,
+ client: filamento::Client<Files>,
jid: Arc<JID>,
- file_store: FilesMem,
+ 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<FilesMem>;
+ type Target = filamento::Client<Files>;
fn deref(&self) -> &Self::Target {
&self.client
@@ -89,6 +140,8 @@ pub enum LoginError {
InvalidJID(#[from] jid::ParseError),
#[error("Connection Error: {0}")]
ConnectionError(#[from] CommandError<ConnectionError>),
+ #[error("OPFS: {0}")]
+ OPFS(#[from] OPFSError),
}
#[component]
@@ -138,14 +191,26 @@ fn LoginPage(set_app: WriteSignal<AppState>, set_client: RwSignal<Option<(Client
// initialise the client
let db = Db::create_connect_and_migrate("mem.db").await.unwrap();
- let files_mem = FilesMem::new();
+ let files = if remember_me.get_untracked() {
+ 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_mem.clone());
+ 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_mem,
+ file_store: files,
};
if *connect_on_login.read_untracked() {
@@ -312,7 +377,7 @@ impl MessageSubscriptions {
}
}
-#[derive(Store)]
+#[derive(Store, Clone)]
pub struct Roster {
#[store(key: JID = |(jid, _)| jid.clone())]
contacts: HashMap<JID, MacawContact>,
@@ -464,28 +529,70 @@ fn Macaw(
});
view! {
- <Dock />
- <ChatsList />
+ <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 Dock() -> impl IntoView {
+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="dock panel">
- <div class="shortcuts">
- <div class="roster-tab dock-icon">
- <img src="/assets/caw.png" />
+ <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="chats-tab dock-icon">
- <img src="/assets/bubble.png" />
+ <div class="pins">
+ </div>
+ <div class="personal">
</div>
</div>
- <div class="pins">
- </div>
- <div class="personal">
- </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>
}
}
@@ -1149,6 +1256,7 @@ impl DerefMut for MacawUser {
}
}
+#[derive(Clone)]
struct MacawContact {
contact: Store<Contact>,
user: StateListener<JID, ArcStore<User>>,
@@ -1248,12 +1356,74 @@ fn ChatsList() -> impl IntoView {
}
}
+#[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 avatar = LocalResource::new(move || get_avatar(contact_user));
+ 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>
+ <Transition fallback=|| view! { <img class="avatar" src=NO_AVATAR /> } >
+ <img class="avatar" src=move || avatar.read().as_deref().map(|avatar| avatar.clone()).unwrap_or_default() />
+ </Transition>
+ <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_file(avatar).await {
- let data = BASE64_STANDARD.encode(data);
- format!("data:image/jpg;base64, {}", data)
+ if let Some(data) = client.file_store.get_src(avatar).await {
+ data
} else {
NO_AVATAR.to_string()
}