Files
beau-gosse-du-92/src/ui/components/conversations.rs
Adrien 691dc7572a
All checks were successful
ci/woodpecker/pr/validate Pipeline was successful
🚨 Fix some clippy warnings
2024-09-23 22:16:55 +02:00

569 lines
15 KiB
Rust

use std::{rc::Rc, time::Duration};
use base64::{engine::general_purpose, Engine as _};
use dioxus::prelude::*;
use futures_util::StreamExt;
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<Vec<u8>>, class_name: Option<String>) -> 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<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 => VNode::empty(),
}
}
#[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 => VNode::empty(),
}
}
#[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! {
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<EventHandler<RoomId>>,
on_pressed: Option<EventHandler<RoomId>>,
) -> 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::<RoomId>);
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 || {
if let Some(c) = on_selected {
c.call(room_id.as_ref().clone())
}
}
};
let on_long_press = move || {
if let Some(c) = on_pressed {
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<RoomId>,
on_pressed_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_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<SpaceId>, on_pressed_conversation: EventHandler<RoomId>) -> Element {
let mut selected_room_id = use_context_provider(|| Signal::new(None::<RoomId>));
let mut displayed_rooms = use_context_provider(|| Signal::new(Vec::<RoomId>::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<RoomId>) -> 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 {}
}
}
}
}
}
#[derive(PartialEq)]
enum ConversationOptionsMenuActions {
Join(RoomId),
Close,
}
#[component]
fn ConversationOptionsMenu(
room_id: RoomId,
callbacks: Coroutine<ConversationOptionsMenuActions>,
) -> Element {
let room = STORE.read().rooms().get(&room_id).unwrap().signal();
let topic = room.topic().unwrap_or("<No topic set>".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: room_id.clone() }
}
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 |_| {
callbacks.send(ConversationOptionsMenuActions::Close);
}
}
}
div {
class: ClassName::CONVERSATION_OPTIONS_MENU_INNER_JOIN_BUTTON,
JoinButton {
onclick: move |_| {
callbacks.send(ConversationOptionsMenuActions::Join(room_id.clone()));
callbacks.send(ConversationOptionsMenuActions::Close);
}
}
}
}
}
}
}
pub fn Conversations() -> Element {
let mut room_id = use_signal(|| None::<RoomId>);
let on_pressed_conversation = move |id: RoomId| {
room_id.set(Some(id));
};
let callbacks = use_coroutine(
move |mut rx: UnboundedReceiver<ConversationOptionsMenuActions>| async move {
while let Some(action) = rx.next().await {
match action {
ConversationOptionsMenuActions::Join(room_id) => {
let rooms = STORE.read().rooms();
if let Some(room) = rooms.get(&room_id) {
room.join().await;
}
}
ConversationOptionsMenuActions::Close => {
room_id.set(None);
}
}
}
},
);
let menu = match room_id.read().as_ref() {
Some(room_id) => {
let room_id = room_id.clone();
rsx! {
ConversationOptionsMenu { room_id, callbacks }
}
}
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}
}
}