diff options
Diffstat (limited to '')
-rw-r--r-- | assets/style.scss | 56 | ||||
-rw-r--r-- | src/lib.rs | 171 |
2 files changed, 222 insertions, 5 deletions
diff --git a/assets/style.scss b/assets/style.scss index 4ad2dd7..0efe7e9 100644 --- a/assets/style.scss +++ b/assets/style.scss @@ -159,9 +159,15 @@ p { padding: 8px 16px; } -.chats-list h2, .roster-list h2 { +.chats-list .header, .roster-list .header { margin-top: 0.4rem; border-bottom: 2px solid black; + display: flex; + justify-content: space-between; + align-items: center; +} + +.chats-list h2, .roster-list h2 { flex: 0 0 auto; } @@ -564,7 +570,7 @@ p { z-index: 100; } -.overlay-background { +.overlay-background, .modal-background { position: fixed; top: 0; left: 0; @@ -577,6 +583,31 @@ p { position: absolute; } +.modal { + position: relative; + z-index: 150; +} + +.modal-background { + background-color: rgba(0, 0, 0, 0.1); +} + +.modal-content { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; +} + +.modal .overlay { + x-index: 200; +} + +.modal .overlay-content { + z-index: 201; +} + .personal .overlay-content { bottom: 0; left: 100%; @@ -628,6 +659,27 @@ hr { width: 100%; } +.header .header-icon { + position: relative; + top: -0.2rem; + border-radius: 1em; + padding: 4px; +} + +.new-chat:hover, .new-chat.open { + background: #00000060; +} + +.new-chat-widget form { + border: 2px solid black; + padding: 4px; + gap: 4px; + background-color: #dcdcdc; + display: flex; + flex-direction: column; + align-items: end; +} + /* font-families */ /* thai */ @@ -1024,6 +1024,16 @@ pub fn Overlay(set_open: WriteSignal<bool>, children: Children) -> impl IntoView } #[component] +pub fn Modal(set_open: WriteSignal<bool>, children: Children) -> impl IntoView { + view! { + <div class="modal"> + <div class="modal-background" on:click=move |_| set_open.update(|state| *state = false)></div> + <div class="modal-content">{children()}</div> + </div> + } +} + +#[component] pub fn OpenChatsPanelView() -> impl IntoView { let open_chats: Store<OpenChatsPanel> = use_context().expect("no open chats panel in context"); @@ -1377,12 +1387,38 @@ impl Icon { Icon::Available16Color => 16, } } + + pub fn light(&self) -> bool { + match self { + Icon::AddContact24 => true, + Icon::Attachment24 => true, + Icon::Away16 => true, + Icon::Away16Color => false, + Icon::Bubble16 => true, + Icon::Bubble16Color => false, + Icon::Bubble24 => true, + Icon::Contact24 => true, + Icon::Delivered16 => true, + Icon::Dnd16 => true, + Icon::Dnd16Color => false, + Icon::Error16Color => false, + Icon::Forward24 => true, + Icon::Heart24 => true, + Icon::NewBubble24 => true, + Icon::Reply24 => true, + Icon::Sending16 => true, + Icon::Sent16 => true, + Icon::Chat16Color => false, + Icon::Xa16Color => false, + Icon::Available16Color => false, + } + } } #[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() /> + <img class:light=icon.light() class:icon=true style=move || format!("height: {}px; width: {}px", icon.size(), icon.size()) src=move || icon.src() /> } } @@ -1945,6 +1981,8 @@ fn ChatsList() -> impl IntoView { } }); + let (open_new_chat, set_open_new_chat) = signal(false); + // TODO: filter new messages signal let new_messages_signal: RwSignal<MessageSubscriptions> = use_context().unwrap(); let (sub_id, set_sub_id) = signal(None); @@ -1983,7 +2021,24 @@ fn ChatsList() -> impl IntoView { view! { <div class="chats-list panel"> - <h2>Chats</h2> + // TODO: update icon, tooltip on hover. + <div class="header"> + <h2>Chats</h2> + <div class="new-chat header-icon" class:open=open_new_chat > + <IconComponent icon=Icon::NewBubble24 on:click=move |_| set_open_new_chat.update(|state| *state = !*state)/> + {move || { + if *open_new_chat.read() { + view! { + <Overlay set_open=set_open_new_chat> + <NewChatWidget set_open_new_chat /> + </Overlay> + }.into_any() + } else { + view! {}.into_any() + } + }} + </div> + </div> <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 /> @@ -1993,6 +2048,116 @@ fn ChatsList() -> impl IntoView { } } +#[derive(Clone, Debug, Error)] +pub enum NewChatError { + #[error("Missing JID")] + MissingJID, + #[error("Invalid JID: {0}")] + InvalidJID(#[from] jid::ParseError), + #[error("Database: {0}")] + Db(#[from] CommandError<DatabaseError>), +} + +#[component] +fn NewChatWidget(set_open_new_chat: WriteSignal<bool>) -> impl IntoView { + let jid = RwSignal::new("".to_string()); + + // TODO: compartmentalise into error component, form component... + let (error, set_error) = signal(None::<NewChatError>); + 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 (new_chat_pending, set_new_chat_pending) = signal(false); + + let open_chats: Store<OpenChatsPanel> = + use_context().expect("no open chats panel store in context"); + let client = use_context::<Client>().expect("client not in context"); + + 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 open_chat = Action::new_local(move |_| { + let client = client.clone(); + async move { + set_new_chat_pending.set(true); + + if jid.read_untracked().is_empty() { + set_error.set(Some(NewChatError::MissingJID)); + set_new_chat_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_new_chat_pending.set(false); + return; + } + }; + + let chat_jid = jid.as_bare(); + let (chat, user) = match client.get_chat_and_user(chat_jid).await { + Ok(c) => c, + Err(e) => { + set_error.set(Some(e.into())); + set_new_chat_pending.set(false); + return; + }, + }; + + let chat = { + let user = user_state_store.store(user.jid.clone(), ArcStore::new(user)); + let chat = chat_state_store.store(chat.correspondent.clone(), ArcStore::new(chat)); + MacawChat { chat, user } + }; + open_chats.update(|open_chats| open_chats.open(chat.clone())); + set_open_new_chat.set(false); + } + }); + + let jid_input = NodeRef::<Input>::new(); + let _focus = Effect::new(move |_| { + if let Some(input) = jid_input.get() { + let _ = input.focus(); + input.set_text_content(Some("")); + // input.style("height: 0"); + // let height = input.scroll_height(); + // input.style(format!("height: {}px", height)); + } + }); + + view! { + <div class="new-chat-widget"> + <form on:submit=move |ev| { + ev.prevent_default(); + open_chat.dispatch(()); + }> + {error_message} + <input + disabled=new_chat_pending + placeholder="JID" + type="text" + node_ref=jid_input + bind:value=jid + name="jid" + id="jid" + autofocus="true" + /> + <input disabled=new_chat_pending class="button" type="submit" value="Start Chat" /> + </form> + </div> + } +} + #[component] fn RosterList() -> impl IntoView { let roster: Store<Roster> = use_context().expect("no roster in context"); @@ -2000,7 +2165,7 @@ fn RosterList() -> impl IntoView { // TODO: filter new messages signal view! { <div class="roster-list panel"> - <h2>Roster</h2> + <div class="header"><h2>Roster</h2><div class="header-icon"><IconComponent icon=Icon::AddContact24 /></div></div> <div class="roster-list-roster"> <For each=move || roster.contacts().get() key=|contact| contact.0.clone() let(contact)> <RosterListItem contact=contact.1 /> |