Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 156 additions & 35 deletions src/home/space_lobby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@
//! that allows the user to click on it to show the `SpaceLobby`.
//!

use std::collections::{HashMap, HashSet};
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap, HashSet};
use imbl::Vector;
use makepad_widgets::*;
use matrix_sdk::{RoomState, ruma::OwnedRoomId};
use matrix_sdk_ui::spaces::SpaceRoom;
use ruma::room::JoinRuleSummary;
use tokio::sync::mpsc::UnboundedSender;
use crate::{
home::rooms_list::RoomsListRef, shared::avatar::{AvatarState, AvatarWidgetExt, AvatarWidgetRefExt}, space_service_sync::{SpaceRequest, SpaceRoomExt, SpaceRoomListAction}, utils::{self, RoomNameId}
avatar_cache::{self, AvatarCacheEntry},
home::rooms_list::RoomsListRef,
shared::avatar::{AvatarWidgetExt, AvatarWidgetRefExt},
space_service_sync::{SpaceRequest, SpaceRoomExt, SpaceRoomListAction},
utils::{self, RoomNameId},
};


Expand Down Expand Up @@ -450,6 +454,22 @@ live_design! {
}


thread_local! {
/// A cache of UI states for each SpaceLobbyScreen, keyed by the space's room ID.
/// This allows preserving the expanded/collapsed state of subspaces across screen changes.
static SPACE_LOBBY_STATES: RefCell<BTreeMap<OwnedRoomId, SpaceLobbyUiState>> = const {
RefCell::new(BTreeMap::new())
};
}

/// The UI-side state of a SpaceLobbyScreen that should persist across hide/show cycles.
#[derive(Default)]
struct SpaceLobbyUiState {
/// The set of space IDs that are currently expanded (showing their children).
expanded_spaces: HashSet<OwnedRoomId>,
}


/// A clickable entry shown in the RoomsList that will show the space lobby when clicked.
#[derive(Live, LiveHook, Widget)]
pub struct SpaceLobbyEntry {
Expand Down Expand Up @@ -618,15 +638,13 @@ impl Widget for RoomEntry {
struct SpaceRoomInfo {
id: OwnedRoomId,
name: String,
#[allow(unused)]
topic: Option<String>,
#[allow(unused)]
room_avatar: AvatarState,
/// The avatar URI, used to fetch from cache.
avatar_uri: Option<ruma::OwnedMxcUri>,
/// Cached avatar image data, stored after fetching from avatar cache.
avatar_data: Option<std::sync::Arc<[u8]>>,
num_joined_members: u64,
#[allow(unused)]
state: Option<RoomState>,
#[allow(unused)]
join_rule: Option<JoinRuleSummary>,
/// If `Some`, this is a space. If `None`, it's a room.
children_count: Option<u64>,
}
Expand All @@ -641,10 +659,10 @@ impl From<&SpaceRoom> for SpaceRoomInfo {
id: space_room.room_id.clone(),
name: space_room.display_name.clone(),
topic: space_room.topic.clone(),
room_avatar: AvatarState::Known(space_room.avatar_url.clone()),
avatar_uri: space_room.avatar_url.clone(),
avatar_data: None,
num_joined_members: space_room.num_joined_members,
state: space_room.state,
join_rule: space_room.join_rule.clone(),
children_count: space_room.is_space().then_some(space_room.children_count),
}
}
Expand All @@ -656,10 +674,10 @@ impl From<SpaceRoom> for SpaceRoomInfo {
id: space_room.room_id,
name: space_room.display_name,
topic: space_room.topic,
room_avatar: AvatarState::Known(space_room.avatar_url),
avatar_uri: space_room.avatar_url,
avatar_data: None,
num_joined_members: space_room.num_joined_members,
state: space_room.state,
join_rule: space_room.join_rule,
}
}
}
Expand Down Expand Up @@ -719,6 +737,15 @@ impl Widget for SpaceLobbyScreen {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
self.view.handle_event(cx, event, scope);

// Handle Signal events for avatar cache updates
if let Event::Signal = event {
// Process any pending avatar updates
avatar_cache::process_avatar_updates(cx);
// Try to fetch and store avatars that we don't have yet, then redraw
self.fetch_pending_avatars(cx);
self.redraw(cx);
}

if let Event::Actions(actions) = event {
for action in actions {
if let Some(SpaceRoomListAction::DetailedChildren { space_id, children, .. }) = action.downcast_ref() {
Expand Down Expand Up @@ -792,10 +819,36 @@ impl Widget for SpaceLobbyScreen {

// Below, draw things that are common to child rooms and subspaces.
item.label(ids!(content.name_label)).set_text(cx, &info.name);
// TODO: query (and update) room/space avatar from the avatar_cache

// Display avatar from stored data, or fetch from cache, or show initials
let avatar_ref = item.avatar(ids!(avatar));
let first_char = utils::user_name_first_letter(&info.name);
avatar_ref.show_text(cx, None, None, first_char.unwrap_or("#"));
let mut drew_avatar = false;

// First try using stored avatar data
if let Some(ref data) = info.avatar_data {
drew_avatar = avatar_ref.show_image(
cx,
None,
|cx, img| utils::load_png_or_jpg(&img, cx, data),
).is_ok();
}
// If no stored data, try fetching from cache
if !drew_avatar {
if let Some(ref uri) = info.avatar_uri {
if let AvatarCacheEntry::Loaded(data) = avatar_cache::get_or_fetch_avatar(cx, uri.clone()) {
drew_avatar = avatar_ref.show_image(
cx,
None,
|cx, img| utils::load_png_or_jpg(&img, cx, &data),
).is_ok();
}
}
}
// Fallback to initials
if !drew_avatar {
avatar_ref.show_text(cx, None, None, first_char.unwrap_or("#"));
}

if let Some(mut lines) = item.widget(ids!(tree_lines)).borrow_mut::<TreeLines>() {
lines.draw_bg.level = *level as f32;
Expand All @@ -804,27 +857,49 @@ impl Widget for SpaceLobbyScreen {
lines.draw_bg.indent_width = 44.0; // Hardcoded to match
}

// Build the info label with join status, member count, and topic
// Note: Public/Private is intentionally not shown per-item to reduce clutter
let info_label = item.label(ids!(content.info_label));
if let Some(c) = info.children_count && c > 0 {
info_label.set_text(cx, &format!(
"{} {} | ~{} {}",
info.num_joined_members,
match info.num_joined_members {
1 => "member",
_ => "members",
},
c,
match c {
1 => "room",
_ => "rooms",
}
));
} else {
match info.num_joined_members {
1 => info_label.set_text(cx, "1 member"),
n => info_label.set_text(cx, &format!("{n} members")),
let mut info_parts = Vec::new();

// Add join status for rooms we haven't joined
if let Some(state) = &info.state {
match state {
RoomState::Joined => { /* Don't show "Joined" - it's implied */ }
RoomState::Left => info_parts.push("Left".to_string()),
RoomState::Invited => info_parts.push("Invited".to_string()),
RoomState::Knocked => info_parts.push("Knocked".to_string()),
RoomState::Banned => info_parts.push("Banned".to_string()),
}
};
}

// Add member count
info_parts.push(format!(
"{} {}",
info.num_joined_members,
if info.num_joined_members == 1 { "member" } else { "members" }
));

// Add children count for spaces
if let Some(c) = info.children_count {
if c > 0 {
info_parts.push(format!(
"~{} {}",
c,
if c == 1 { "room" } else { "rooms" }
));
}
}

// Add topic if available (Label handles truncation via wrap: Ellipsis)
if let Some(topic) = &info.topic {
let topic = topic.trim();
if !topic.is_empty() {
info_parts.push(topic.to_string());
}
}

info_label.set_text(cx, &info_parts.join(" | "));

item
}
Expand Down Expand Up @@ -987,6 +1062,36 @@ impl SpaceLobbyScreen {
}
}

/// Fetches avatars from the cache for any tree entries that don't have avatar data yet.
fn fetch_pending_avatars(&mut self, cx: &mut Cx) {
for entry in &mut self.tree_entries {
if let TreeEntry::Item { info, .. } = entry {
// Only fetch if we have a URI but no data yet
if info.avatar_data.is_none() {
if let Some(ref uri) = info.avatar_uri {
if let AvatarCacheEntry::Loaded(data) = avatar_cache::get_or_fetch_avatar(cx, uri.clone()) {
info.avatar_data = Some(data);
}
}
}
}
}
}

/// Saves the current UI state to the cache. Call this when the screen is being hidden.
pub fn save_current_state(&mut self) {
if let Some(current_space) = &self.space_name_id {
SPACE_LOBBY_STATES.with_borrow_mut(|states| {
states.insert(
current_space.room_id().clone(),
SpaceLobbyUiState {
expanded_spaces: self.expanded_spaces.clone(),
},
);
});
}
}

pub fn set_displayed_space(&mut self, cx: &mut Cx, space_name_id: &RoomNameId) {
let space_name = space_name_id.to_string();
let parent_name = self.view.label(ids!(header.parent_space_row.parent_name));
Expand All @@ -998,6 +1103,9 @@ impl SpaceLobbyScreen {
return;
}

// Save the current UI state before switching to a new space
self.save_current_state();

self.space_name_id = Some(space_name_id.clone());
let rooms_list_ref = cx.get_global::<RoomsListRef>();
if let Some(sender) = rooms_list_ref.get_space_request_sender() {
Expand All @@ -1011,9 +1119,16 @@ impl SpaceLobbyScreen {
}

self.tree_entries.clear();
self.expanded_spaces.clear();
self.is_loading = true;

// Restore UI state if we've viewed this space before, otherwise start fresh
self.expanded_spaces = SPACE_LOBBY_STATES.with_borrow(|states| {
states
.get(space_name_id.room_id())
.map(|state| state.expanded_spaces.clone())
.unwrap_or_default()
});

// Set parent avatar
let avatar_ref = self.view.avatar(ids!(header.parent_space_row.parent_avatar));
let first_char = utils::user_name_first_letter(&space_name);
Expand All @@ -1027,4 +1142,10 @@ impl SpaceLobbyScreenRef {
let Some(mut inner) = self.borrow_mut() else { return };
inner.set_displayed_space(cx, space_name_id);
}

/// Saves the current UI state. Call this when the screen is being hidden or destroyed.
pub fn save_current_state(&self) {
let Some(mut inner) = self.borrow_mut() else { return };
inner.save_current_state();
}
}