Add a first Conversations component

This commit is contained in:
2024-05-26 10:45:09 +02:00
parent ff0ac7f982
commit 5194899de0
3 changed files with 810 additions and 0 deletions

View File

@@ -0,0 +1,502 @@
use base64::{engine::general_purpose, Engine as _};
use dioxus::prelude::*;
use tracing::{debug, trace, warn};
use super::{button::Button, icons::SearchIcon, text_input::TextInput};
use crate::{
base::{ACCOUNT, STORE},
domain::model::{common::PresenceState as DomainPresenceState, room::RoomId, space::SpaceId},
ui::components::icons::{ChatsIcon, LogoIcon, RoomsIcon, SpacesIcon},
};
turf::style_sheet!("src/ui/components/conversations.scss");
#[component]
fn AccountAvatar(content: Option<Vec<u8>>, class_name: Option<String>) -> Element {
match content {
Some(content) => {
let encoded = general_purpose::STANDARD.encode(content);
rsx! {
div {
class: class_name,
background_image: format!("url(data:image/jpeg;base64,{encoded})")
}
}
}
// TODO: Manage acount without avatar
None => None,
}
}
#[component]
fn PresenceState(state: Option<DomainPresenceState>, class_name: Option<String>) -> 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 => None,
}
}
#[component]
fn DisplayName(display_name: Option<String>, class_name: Option<String>) -> Element {
match display_name {
Some(display_name) => {
rsx! {
div {
class: class_name,
p {
{display_name},
}
}
}
}
None => None,
}
}
#[component]
fn Status(status: Option<String>, class_name: Option<String>) -> 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! {
style { {STYLE_SHEET} },
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_clicked: EventHandler<RoomId>) -> Element {
let rooms = STORE.read().rooms();
let toto = rooms.get(&room_id).unwrap();
let room = toto.signal();
let room_id = room.id();
let room_name = room.name();
let selected_room_id = use_context::<Signal<Option<RoomId>>>();
let invited_badge = if room.is_invited() {
rsx! {
div {
class: ClassName::CONVERSATION_AVATAR_INVITED_BADGE,
p {
"Invited",
}
}
}
} else {
None
};
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(Some(content)) = &*avatar.read() {
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(" ");
rsx! {
div {
class: "{classes_str}",
onclick: move |evt| {
on_clicked.call(room_id.clone());
evt.stop_propagation();
},
{avatar}
{invited_badge}
}
}
}
#[component]
pub fn ConversationsCarousel(on_selected_conversation: EventHandler<RoomId>) -> Element {
let mut ordered_rooms = use_signal(Vec::<RoomId>::new);
use_effect(move || {
let rooms = use_context::<Signal<Vec<RoomId>>>();
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_clicked: on_selected_conversation,
}
}
});
rsx! {
div {
class: ClassName::SPACE_CONVERSATIONS_CAROUSEL,
// TODO: Needed?
onscroll: move |_| {
// Catch scrolling events.
},
{rendered_avatars},
}
}
}
#[component]
pub fn Space(id: SpaceId) -> Element {
let space = STORE.read().spaces().get(&id).unwrap().signal();
let name = space.name();
let mut selected_room_id = use_context_provider(|| Signal::new(None::<RoomId>));
let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::<RoomId>::new()));
use_effect(move || {
// let rooms = STORE.read().rooms();
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);
}
}
});
let on_selected_conversation = move |room_id: RoomId| {
trace!("");
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! {
style { {STYLE_SHEET} },
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,
},
div {
class: ClassName::SPACE_CONVERSATION_NAME,
p {
{selected_room_name},
}
}
}
}
}
#[component]
pub fn HomeSpace() -> Element {
let name = "Home";
let mut selected_room_id = use_context_provider(|| Signal::new(None::<RoomId>));
let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::<RoomId>::new()));
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);
}
}
});
let on_selected_conversation = move |room_id: RoomId| {
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! {
style { {STYLE_SHEET} },
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,
},
div {
class: ClassName::SPACE_CONVERSATION_NAME,
p {
{selected_room_name},
}
}
}
}
}
pub fn Spaces() -> 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() }
}
});
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::SPACES,
{rendered_spaces},
HomeSpace {},
}
}
}
pub fn Search() -> Element {
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::SEARCH,
TextInput {}
div {
class: ClassName::SEARCH_BUTTON,
Button {
SearchIcon {}
}
}
}
}
}
pub fn Conversations() -> Element {
rsx! {
style { {STYLE_SHEET} },
div {
class: ClassName::CONVERSATIONS,
div {
class: ClassName::CONVERSATIONS_ACCOUNT,
Account {},
},
div {
class: ClassName::CONVERSATIONS_SPACES,
Spaces {},
},
div {
class: ClassName::CONVERSATIONS_SEARCH,
Search {},
},
}
}
}