569 lines
15 KiB
Rust
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}
|
|
}
|
|
}
|