use std::{rc::Rc, time::Duration}; use base64::{engine::general_purpose, Engine as _}; use dioxus::prelude::*; use tracing::{debug, warn}; use super::{button::Button, icons::SearchIcon, text_input::TextInput}; use crate::{ domain::model::{common::PresenceState as DomainPresenceState, room::RoomId, space::SpaceId}, ui::{ components::{ button::{JoinButton, RejectButton}, icons::{ChatsIcon, LogoIcon, RoomsIcon, SpacesIcon}, }, hooks::use_long_press, ACCOUNT, STORE, }, }; turf::style_sheet!("src/ui/components/conversations.scss"); #[component] fn AccountAvatar(content: Option>, class_name: Option) -> Element { rsx! { if let Some(content) = content { div { class: class_name, background_image: format!("url(data:image/jpeg;base64,{})", general_purpose::STANDARD.encode(content)) } } } } #[component] fn PresenceState(state: Option, class_name: Option) -> Element { let class_name = class_name.unwrap_or("".to_string()); match state { Some(state) => { let state_class = match state { DomainPresenceState::Online => ClassName::ONLINE, DomainPresenceState::Offline => ClassName::OFFLINE, DomainPresenceState::Unavailable => ClassName::UNAVAILABLE, _ => ClassName::UNAVAILABLE, }; let classes = [class_name.as_str(), state_class].join(" "); rsx! { div { class: classes, LogoIcon {} } } } None => VNode::empty(), } } #[component] fn DisplayName(display_name: Option, class_name: Option) -> Element { match display_name { Some(display_name) => { rsx! { div { class: class_name, p { {display_name} } } } } None => VNode::empty(), } } #[component] fn Status(status: Option, class_name: Option) -> Element { let status = status.unwrap_or("Type your status".to_string()); rsx! { div { class: class_name, TextInput { placeholder: status, } } } } pub fn Account() -> Element { let avatar = use_resource(move || async move { let account = ACCOUNT.read(); let avatar = account.get_avatar().await; rsx! { AccountAvatar { class_name: ClassName::ACCOUNT_AVATAR, content: avatar.borrow().clone(), } } }); let presence_state = use_resource(move || async move { // TODO: Fetch the state from the domain rsx! { PresenceState { state: DomainPresenceState::Online, class_name: ClassName::ACCOUNT_PRESENCE_STATE, } } }); let display_name = use_resource(move || async move { let account = ACCOUNT.read(); let display_name = account.get_display_name().await; rsx! { DisplayName { class_name: ClassName::ACCOUNT_DISPLAY_NAME, display_name: display_name.borrow().clone(), } } }); let status = use_resource(move || async move { // TODO: Fetch the status from the domain rsx! { Status { class_name: ClassName::ACCOUNT_STATUS, status: "Coucou, Je suis BG92".to_string(), } } }); rsx! { div { class: ClassName::ACCOUNT, {avatar} {presence_state} {display_name} {status} div { class: ClassName::ACCOUNT_SPACES, Button { SpacesIcon {} } } div { class: ClassName::ACCOUNT_CHAT, Button { ChatsIcon {} } } div { class: ClassName::ACCOUNT_ROOM, Button { RoomsIcon {} } } } } } #[component] pub fn ConversationAvatar( room_id: RoomId, on_selected: Option>, on_pressed: Option>, ) -> Element { let long_press_duration = Duration::from_millis(500); let rooms = STORE.read().rooms(); let room = rooms.get(&room_id).unwrap().signal(); let room_id = Rc::new(room_id); let room_name = room.name(); let selected_room_id = use_signal(|| None::); let invited_badge = if room.is_invited() { rsx! { div { class: ClassName::CONVERSATION_AVATAR_INVITED_BADGE, p { "Invited" } } } } else { VNode::empty() }; let is_selected = match selected_room_id.read().as_ref() { Some(selected_room_id) => *selected_room_id == *room_id, None => false, }; let avatar = if let Some(content) = room.avatar() { let encoded = general_purpose::STANDARD.encode(content); rsx! { div { class: ClassName::CONVERSATION_AVATAR_IMAGE, background_image: format!("url(data:image/jpeg;base64,{encoded})"), } } } else { let placeholder = room_name .unwrap_or("?".to_string()) .to_uppercase() .chars() .next() .unwrap_or('?') .to_string(); debug!("Use of {} placeholder for {}", placeholder, room_id); rsx! { div { class: ClassName::CONVERSATION_AVATAR_IMAGE, {placeholder} } } }; let classes = [ ClassName::CONVERSATION_AVATAR, if is_selected { ClassName::SELECTED } else { "" }, ]; let classes_str = classes.join(" "); let on_press = { let room_id = room_id.clone(); move || { on_selected.map(|c| c.call(room_id.as_ref().clone())); } }; let on_long_press = move || { on_pressed.map(|c| c.call(room_id.as_ref().clone())); }; let long_press_hook = use_long_press(long_press_duration, on_press, on_long_press); rsx! { div { class: "{classes_str}", ..long_press_hook.handlers, {avatar} {invited_badge} } } } #[component] pub fn ConversationsCarousel( on_selected_conversation: EventHandler, on_pressed_conversation: EventHandler, ) -> Element { let mut ordered_rooms = use_signal(Vec::::new); use_effect(move || { let rooms = use_context::>>(); let rooms = rooms.read(); for room in rooms.iter() { if !ordered_rooms.peek().contains(room) { ordered_rooms.push(room.clone()); } } ordered_rooms.retain(|room| rooms.contains(room)); }); let ordered_rooms = ordered_rooms.read(); let rendered_avatars = ordered_rooms.iter().map(|room| { rsx! { ConversationAvatar { room_id: room.clone(), on_selected: on_selected_conversation, on_pressed: on_pressed_conversation, } } }); rsx! { div { class: ClassName::SPACE_CONVERSATIONS_CAROUSEL, // TODO: Needed? onscroll: move |_| { // Catch scrolling events. }, {rendered_avatars} } } } // If id is None, the Space will handle all the Conversation which have no parent (Space). #[component] pub fn Space(id: Option, on_pressed_conversation: EventHandler) -> Element { let mut selected_room_id = use_context_provider(|| Signal::new(None::)); let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::::new())); let name = if let Some(id) = id { let space = STORE.read().spaces().get(&id).unwrap().signal(); use_effect(move || { let rooms = STORE.peek().rooms(); let room_ids = space.room_ids(); for room_id in room_ids { if rooms.contains_key(&room_id) { displayed_rooms.write().push(room_id); } } }); space.name() } else { use_effect(move || { let rooms = STORE.read().rooms(); for room in rooms.values() { if room.signal().spaces().is_empty() { let room_id = room.signal().id(); displayed_rooms.write().push(room_id); } } }); Some("Home".to_string()) }; let on_selected_conversation = move |room_id: RoomId| { STORE.write().on_selected_room(room_id.clone()); selected_room_id.set(Some(room_id)); }; let mut space_classes: [&str; 2] = [ClassName::SPACE, ""]; let mut selected_room_name = "".to_string(); if let Some(room_id) = selected_room_id.read().as_ref() { space_classes[1] = ClassName::DISPLAY_CONVERSATION_NAME; if let Some(room) = STORE.read().rooms().get(room_id) { let room = room.signal(); if let Some(name) = room.name() { selected_room_name = name; } else { debug!("No name set for {} room", &room_id); selected_room_name = room_id.to_string(); } } else { warn!("No room found for the {} id", &room_id); } } let classes_str = space_classes.join(" "); rsx! { div { class: "{classes_str}", // Deselect the conversation on clicks outside of the ConversationAvatar onclick: move |_| { selected_room_id.set(None); }, div { class: ClassName::SPACE_NAME, p { {name} } } ConversationsCarousel { on_selected_conversation, on_pressed_conversation, } div { class: ClassName::SPACE_CONVERSATION_NAME, p { {selected_room_name} } } } } } #[component] pub fn Spaces(on_pressed_conversation: EventHandler) -> Element { let spaces = STORE.read().spaces(); let space_ids = spaces.keys().clone().last(); let rendered_spaces = space_ids.map(|id| { rsx! { Space { id: id.clone(), on_pressed_conversation } } }); rsx! { div { class: ClassName::SPACES, {rendered_spaces} Space { on_pressed_conversation } } } } #[component] pub fn Search() -> Element { rsx! { div { class: ClassName::SEARCH, div { class: ClassName::SEARCH_TEXT, TextInput {} } div { class: ClassName::SEARCH_BUTTON, Button { SearchIcon {} } } } } } #[component] fn ConversationOptionsMenu( room_id: RoomId, on_close: EventHandler, on_join: EventHandler, ) -> Element { let room = STORE.read().rooms().get(&room_id).unwrap().signal(); let topic = room.topic().unwrap_or("".to_string()); rsx! { div { class: ClassName::CONVERSATION_OPTIONS_MENU, div { class: ClassName::CONVERSATION_OPTIONS_MENU_INNER, div { class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_AVATAR, ConversationAvatar { room_id } } div { class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_NAME, p { {room.name()} } } div { class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_TOPIC, p { {topic} } } div { class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_CONFIG, p { "Coming soon..." } } div { class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_CLOSE_BUTTON, RejectButton { onclick: move |_| on_close(()) } } div { class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_JOIN_BUTTON, JoinButton { onclick: move |_| { on_join(()); on_close(()); } } } } } } } pub fn Conversations() -> Element { let mut room_id = use_signal(|| None::); let on_menu_close = move |_| { room_id.set(None); }; let on_menu_join = move |_| async move { let rooms = STORE.read().rooms(); if let Some(room_id) = room_id.read().to_owned() { if let Some(room) = rooms.get(&room_id) {} } }; let on_pressed_conversation = move |id: RoomId| { room_id.set(Some(id)); }; let menu = match room_id.read().as_ref() { Some(room_id) => { let room_id = room_id.clone(); rsx! { ConversationOptionsMenu { room_id, on_close: on_menu_close, on_join: on_menu_join } } } None => VNode::empty(), }; rsx! { style { {STYLE_SHEET} } div { class: ClassName::CONVERSATIONS, div { class: ClassName::CONVERSATIONS_ACCOUNT, Account {} } div { class: ClassName::CONVERSATIONS_SPACES, Spaces { on_pressed_conversation } } div { class: ClassName::CONVERSATIONS_SEARCH, Search {} } } {menu} } }