use dioxus::prelude::*;
use matrix_sdk::ruma::OwnedRoomId;
use crate::components::modal::Modal;
use crate::components::tabs::{TabItem, Tabs};
use crate::state::app_state::AppState;
#[derive(Clone, PartialEq)]
struct SpaceChildInfo {
room_id: String,
display_name: String,
is_suggested: bool,
}
#[component]
pub fn SpaceSettingsDialog(
space_id: String,
space_name: String,
on_close: EventHandler<()>,
) -> Element {
let _state = use_context::<Signal<AppState>>();
let mut active_tab = use_signal(|| "general".to_string());
let tab_items = vec![
TabItem { id: "general".to_string(), label: "General".to_string() },
TabItem { id: "rooms".to_string(), label: "Rooms".to_string() },
TabItem { id: "advanced".to_string(), label: "Advanced".to_string() },
];
let current_tab = active_tab.read().clone();
rsx! {
Modal {
title: format!("Space Settings - {space_name}"),
on_close: move |_| on_close.call(()),
wide: true,
div {
class: "space-settings-dialog",
Tabs {
tabs: tab_items,
active_tab: current_tab.clone(),
on_change: move |tab_id: String| active_tab.set(tab_id),
}
div {
style: "padding: 16px 0;",
match current_tab.as_str() {
"general" => rsx! {
SpaceGeneralTab {
space_id: space_id.clone(),
}
},
"rooms" => rsx! {
SpaceRoomsTab {
space_id: space_id.clone(),
}
},
"advanced" => rsx! {
SpaceAdvancedTab {
space_id: space_id.clone(),
}
},
_ => rsx! { div { "Unknown tab" } },
}
}
}
}
}
}
#[component]
fn SpaceGeneralTab(space_id: String) -> Element {
let state = use_context::<Signal<AppState>>();
let mut new_name = use_signal(|| String::new());
let mut new_topic = use_signal(|| String::new());
let mut is_saving = use_signal(|| false);
let mut save_result = use_signal(|| Option::<Result<(), String>>::None);
let mut initialized = use_signal(|| false);
if !*initialized.read() {
initialized.set(true);
let s = state.read();
if let Ok(rid) = <&str as TryInto<OwnedRoomId>>::try_into(space_id.as_str()) {
if let Some(room) = s.rooms.get(&rid) {
new_name.set(room.display_name.clone());
new_topic.set(room.topic.clone().unwrap_or_default());
}
}
}
let save_id = space_id.clone();
let on_save = move |_| {
let name = new_name.read().clone();
let topic = new_topic.read().clone();
let sid = save_id.clone();
is_saving.set(true);
save_result.set(None);
spawn(async move {
let client = { state.read().client.clone() };
let Some(client) = client else {
save_result.set(Some(Err("Not logged in".to_string())));
is_saving.set(false);
return;
};
let Ok(room_id) = <&str as TryInto<OwnedRoomId>>::try_into(sid.as_str()) else {
save_result.set(Some(Err("Invalid space ID".to_string())));
is_saving.set(false);
return;
};
let Some(room) = client.get_room(&room_id) else {
save_result.set(Some(Err("Space not found".to_string())));
is_saving.set(false);
return;
};
use matrix_sdk::ruma::events::room::name::RoomNameEventContent;
let name_content = RoomNameEventContent::new(name);
if let Err(e) = room.send_state_event(name_content).await {
save_result.set(Some(Err(format!("Failed to update name: {e}"))));
is_saving.set(false);
return;
}
use matrix_sdk::ruma::events::room::topic::RoomTopicEventContent;
let topic_content = RoomTopicEventContent::new(topic);
if let Err(e) = room.send_state_event(topic_content).await {
save_result.set(Some(Err(format!("Failed to update topic: {e}"))));
is_saving.set(false);
return;
}
save_result.set(Some(Ok(())));
is_saving.set(false);
});
};
let is_busy = *is_saving.read();
let result_guard = save_result.read();
let result_msg = match &*result_guard {
Some(Ok(())) => Some(("Settings saved!".to_string(), true)),
Some(Err(e)) => Some((e.clone(), false)),
None => None,
};
drop(result_guard);
rsx! {
div {
class: "space-settings__general",
if let Some((ref msg, is_success)) = result_msg {
{
let style = if is_success {
"padding: 8px 12px; background: var(--success-bg, #1a3a2a); color: var(--success-text, #4caf50); border-radius: 6px; margin-bottom: 12px;"
} else {
"padding: 8px 12px; background: var(--error-bg, #3a1a1a); color: var(--error-text, #f44336); border-radius: 6px; margin-bottom: 12px;"
};
rsx! {
div {
style: "{style}",
"{msg}"
}
}
}
}
div {
style: "margin-bottom: 16px;",
label {
style: "display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; color: var(--text-secondary, #888);",
"Space Name"
}
input {
style: "width: 100%; padding: 8px 12px; border: 1px solid var(--border-color, #333); border-radius: 6px; background: var(--bg-primary, #0d0d1a); color: var(--text-primary, #e0e0e0); font-size: 14px;",
r#type: "text",
value: "{new_name}",
oninput: move |evt| new_name.set(evt.value()),
disabled: is_busy,
}
}
div {
style: "margin-bottom: 16px;",
label {
style: "display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; color: var(--text-secondary, #888);",
"Topic"
}
textarea {
style: "width: 100%; padding: 8px 12px; border: 1px solid var(--border-color, #333); border-radius: 6px; background: var(--bg-primary, #0d0d1a); color: var(--text-primary, #e0e0e0); font-size: 14px; resize: vertical; font-family: inherit;",
value: "{new_topic}",
oninput: move |evt| new_topic.set(evt.value()),
disabled: is_busy,
rows: "3",
}
}
button {
style: "padding: 8px 20px; border: none; border-radius: 6px; background: var(--accent-color, #4a9eff); color: white; cursor: pointer;",
onclick: on_save,
disabled: is_busy,
if is_busy { "Saving..." } else { "Save Changes" }
}
}
}
}
#[component]
fn SpaceRoomsTab(space_id: String) -> Element {
let state = use_context::<Signal<AppState>>();
let mut children = use_signal(Vec::<SpaceChildInfo>::new);
let mut is_loading = use_signal(|| true);
let mut show_add_room = use_signal(|| false);
let mut add_room_id = use_signal(|| String::new());
let mut add_error = use_signal(|| Option::<String>::None);
let mut is_adding = use_signal(|| false);
let load_sid = space_id.clone();
use_effect(move || {
let sid = load_sid.clone();
spawn(async move {
let client = { state.read().client.clone() };
let Some(client) = client else {
is_loading.set(false);
return;
};
let Ok(room_id) = <&str as TryInto<OwnedRoomId>>::try_into(sid.as_str()) else {
is_loading.set(false);
return;
};
let Some(_room) = client.get_room(&room_id) else {
is_loading.set(false);
return;
};
let s = state.read();
let mut child_list = Vec::new();
for (rid, room_info) in s.rooms.iter() {
if room_info.parent_spaces.contains(&room_id) {
child_list.push(SpaceChildInfo {
room_id: rid.to_string(),
display_name: room_info.display_name.clone(),
is_suggested: false,
});
}
}
children.set(child_list);
is_loading.set(false);
});
});
let add_sid = space_id.clone();
let on_add_room = move |_| {
let child_room = add_room_id.read().clone();
if child_room.trim().is_empty() {
add_error.set(Some("Room ID is required".to_string()));
return;
}
is_adding.set(true);
add_error.set(None);
let sid = add_sid.clone();
spawn(async move {
let client = { state.read().client.clone() };
let Some(client) = client else {
add_error.set(Some("Not logged in".to_string()));
is_adding.set(false);
return;
};
let Ok(space_room_id) = <&str as TryInto<OwnedRoomId>>::try_into(sid.as_str()) else {
add_error.set(Some("Invalid space ID".to_string()));
is_adding.set(false);
return;
};
let Ok(child_room_id) = <&str as TryInto<OwnedRoomId>>::try_into(child_room.as_str()) else {
add_error.set(Some("Invalid room ID format. Use !room_id:server".to_string()));
is_adding.set(false);
return;
};
let Some(space) = client.get_room(&space_room_id) else {
add_error.set(Some("Space not found".to_string()));
is_adding.set(false);
return;
};
use matrix_sdk::ruma::events::space::child::SpaceChildEventContent;
let server_name = child_room_id.server_name()
.map(|s| s.to_owned());
let via = if let Some(sn) = server_name {
vec![sn]
} else {
Vec::new()
};
let content = SpaceChildEventContent::new(via);
match space.send_state_event_for_key(&child_room_id, content).await {
Ok(_) => {
tracing::info!("Added {child_room_id} to space {space_room_id}");
let display = client.get_room(&child_room_id)
.and_then(|r| r.cached_display_name().map(|n| n.to_string()))
.unwrap_or_else(|| child_room.clone());
let mut ch = children.write();
ch.push(SpaceChildInfo {
room_id: child_room_id.to_string(),
display_name: display,
is_suggested: false,
});
drop(ch);
add_room_id.set(String::new());
show_add_room.set(false);
}
Err(e) => {
add_error.set(Some(format!("Failed to add room: {e}")));
}
}
is_adding.set(false);
});
};
let is_busy = *is_adding.read();
rsx! {
div {
class: "space-settings__rooms",
if *is_loading.read() {
div {
style: "text-align: center; padding: 16px; color: var(--text-secondary, #888);",
div { class: "spinner spinner--small" }
p { "Loading rooms..." }
}
}
if !*is_loading.read() {
div {
div {
style: "display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;",
span {
style: "font-size: 14px; color: var(--text-secondary, #888);",
"{children.read().len()} rooms in this space"
}
button {
style: "padding: 6px 12px; border: 1px solid var(--accent-color, #4a9eff); border-radius: 6px; background: transparent; color: var(--accent-color, #4a9eff); cursor: pointer; font-size: 13px;",
onclick: move |_| {
let current = *show_add_room.read();
show_add_room.set(!current);
},
"+ Add Room"
}
}
if *show_add_room.read() {
div {
style: "padding: 12px; background: var(--bg-secondary, #1a1a2e); border-radius: 6px; margin-bottom: 12px;",
if let Some(ref err) = *add_error.read() {
div {
style: "padding: 6px 10px; background: var(--error-bg, #3a1a1a); color: var(--error-text, #f44336); border-radius: 4px; margin-bottom: 8px; font-size: 13px;",
"{err}"
}
}
div {
style: "display: flex; gap: 8px;",
input {
style: "flex: 1; padding: 8px 10px; border: 1px solid var(--border-color, #333); border-radius: 6px; background: var(--bg-primary, #0d0d1a); color: var(--text-primary, #e0e0e0); font-size: 14px;",
r#type: "text",
placeholder: "!room_id:server.name",
value: "{add_room_id}",
oninput: move |evt| add_room_id.set(evt.value()),
disabled: is_busy,
}
button {
style: "padding: 8px 14px; border: none; border-radius: 6px; background: var(--accent-color, #4a9eff); color: white; cursor: pointer;",
onclick: on_add_room,
disabled: add_room_id.read().trim().is_empty() || is_busy,
if is_busy { "Adding..." } else { "Add" }
}
}
}
}
div {
style: "border: 1px solid var(--border-color, #333); border-radius: 6px; overflow: hidden;",
if children.read().is_empty() {
p {
style: "padding: 16px; text-align: center; color: var(--text-secondary, #888); margin: 0;",
"No rooms in this space yet."
}
}
for child in children.read().iter() {
{
let child_name = child.display_name.clone();
let child_rid = child.room_id.clone();
let remove_sid = space_id.clone();
let remove_rid = child.room_id.clone();
rsx! {
div {
style: "display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid var(--border-color, #222);",
div {
span {
style: "font-size: 14px;",
"{child_name}"
}
span {
style: "font-size: 11px; color: var(--text-secondary, #666); margin-left: 8px;",
"{child_rid}"
}
}
button {
style: "padding: 4px 10px; border: 1px solid var(--error-text, #f44336); border-radius: 4px; background: transparent; color: var(--error-text, #f44336); cursor: pointer; font-size: 12px;",
title: "Remove from space",
onclick: move |_| {
let sid = remove_sid.clone();
let rid = remove_rid.clone();
spawn(async move {
let client = { state.read().client.clone() };
let Some(client) = client else { return };
let Ok(space_room_id) = <&str as TryInto<OwnedRoomId>>::try_into(sid.as_str()) else { return };
let Ok(child_room_id) = <&str as TryInto<OwnedRoomId>>::try_into(rid.as_str()) else { return };
let Some(space) = client.get_room(&space_room_id) else { return };
use matrix_sdk::ruma::events::space::child::SpaceChildEventContent;
let content = SpaceChildEventContent::new(Vec::new());
if let Err(e) = space.send_state_event_for_key(&child_room_id, content).await {
tracing::error!("Failed to remove room from space: {e}");
} else {
tracing::info!("Removed {child_room_id} from space {space_room_id}");
let mut ch = children.write();
ch.retain(|c| c.room_id != rid);
}
});
},
"Remove"
}
}
}
}
}
}
}
}
}
}
}
#[component]
fn SpaceAdvancedTab(space_id: String) -> Element {
let mut state = use_context::<Signal<AppState>>();
let mut is_leaving = use_signal(|| false);
let leave_sid = space_id.clone();
let on_leave = move |_| {
is_leaving.set(true);
let sid = leave_sid.clone();
spawn(async move {
let client = { state.read().client.clone() };
let Some(client) = client else {
is_leaving.set(false);
return;
};
let Ok(room_id) = <&str as TryInto<OwnedRoomId>>::try_into(sid.as_str()) else {
is_leaving.set(false);
return;
};
let Some(room) = client.get_room(&room_id) else {
is_leaving.set(false);
return;
};
match room.leave().await {
Ok(_) => {
tracing::info!("Left space {room_id}");
let mut w = state.write();
w.rooms.remove(&room_id);
w.room_filter = crate::state::room_state::RoomFilter::All;
w.update_sorted_rooms();
}
Err(e) => {
tracing::error!("Failed to leave space: {e}");
}
}
is_leaving.set(false);
});
};
rsx! {
div {
class: "space-settings__advanced",
div {
style: "margin-bottom: 20px;",
label {
style: "display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; color: var(--text-secondary, #888);",
"Space ID"
}
code {
style: "display: block; padding: 8px 12px; background: var(--bg-secondary, #1a1a2e); border-radius: 6px; font-size: 13px; word-break: break-all;",
"{space_id}"
}
}
div {
style: "border-top: 1px solid var(--border-color, #333); padding-top: 16px;",
h4 {
style: "color: var(--error-text, #f44336); margin-bottom: 8px; font-size: 14px;",
"Danger Zone"
}
button {
style: "padding: 8px 20px; border: 1px solid var(--error-text, #f44336); border-radius: 6px; background: transparent; color: var(--error-text, #f44336); cursor: pointer;",
disabled: *is_leaving.read(),
onclick: on_leave,
if *is_leaving.read() { "Leaving..." } else { "Leave Space" }
}
}
}
}
}