use dioxus::prelude::*;
use matrix_sdk::ruma::OwnedRoomId;
use crate::components::tabs::{TabItem, Tabs};
use crate::state::app_state::AppState;
static SETTINGS_TABS: &[(&str, &str)] = &[
("general", "General"),
("security", "Security"),
("roles", "Roles & Permissions"),
("aliases", "Addresses"),
("advanced", "Advanced"),
];
#[component]
pub fn RoomSettingsDialog(room_id: String, on_close: EventHandler<()>) -> Element {
let _state = use_context::<Signal<AppState>>();
let mut active_tab = use_signal(|| "general".to_string());
let tabs: Vec<TabItem> = SETTINGS_TABS
.iter()
.map(|(id, label)| TabItem {
id: id.to_string(),
label: label.to_string(),
})
.collect();
let current_tab = active_tab.read().clone();
rsx! {
div {
class: "modal-overlay",
onclick: move |_| on_close.call(()),
div {
class: "modal-dialog room-settings-dialog",
onclick: move |evt| evt.stop_propagation(),
div {
class: "modal-dialog__header",
h2 { "Room Settings" }
button {
class: "modal-dialog__close",
onclick: move |_| on_close.call(()),
"✕"
}
}
Tabs {
tabs: tabs,
active_tab: current_tab.clone(),
on_change: move |tab: String| active_tab.set(tab),
}
div {
class: "room-settings-dialog__content",
match current_tab.as_str() {
"general" => rsx! {
GeneralTab { room_id: room_id.clone() }
},
"security" => rsx! {
SecurityTab { room_id: room_id.clone() }
},
"roles" => rsx! {
RolesTab { room_id: room_id.clone() }
},
"aliases" => rsx! {
AliasesTab { room_id: room_id.clone() }
},
"advanced" => rsx! {
AdvancedTab { room_id: room_id.clone() }
},
_ => rsx! {},
}
}
}
}
}
}
#[component]
fn GeneralTab(room_id: String) -> Element {
let state = use_context::<Signal<AppState>>();
let (current_name, current_topic) = {
let s = state.read();
let room_id_parsed: Result<OwnedRoomId, _> = room_id.as_str().try_into();
match room_id_parsed.ok().and_then(|id| s.rooms.get(&id)) {
Some(r) => (r.display_name.clone(), r.topic.clone().unwrap_or_default()),
None => (String::new(), String::new()),
}
};
let mut name_input = use_signal(|| current_name.clone());
let mut topic_input = use_signal(|| current_topic.clone());
let mut is_saving = use_signal(|| false);
let mut save_result = use_signal(|| Option::<Result<(), String>>::None);
let rid = room_id.clone();
let on_save = move |_| {
let name = name_input.read().clone();
let topic = topic_input.read().clone();
let rid = rid.clone();
is_saving.set(true);
save_result.set(None);
spawn(async move {
let result = save_general_settings(state, &rid, &name, &topic).await;
save_result.set(Some(result));
is_saving.set(false);
});
};
rsx! {
div {
class: "room-settings-tab",
if let Some(ref result) = *save_result.read() {
match result {
Ok(()) => rsx! {
div { class: "room-settings-tab__success", "Settings saved." }
},
Err(e) => rsx! {
div { class: "room-settings-tab__error", "{e}" }
},
}
}
div {
class: "room-settings-tab__field",
label { "Room Name" }
input {
r#type: "text",
value: "{name_input}",
oninput: move |evt| name_input.set(evt.value()),
disabled: *is_saving.read(),
}
}
div {
class: "room-settings-tab__field",
label { "Topic" }
textarea {
value: "{topic_input}",
oninput: move |evt| topic_input.set(evt.value()),
disabled: *is_saving.read(),
rows: "3",
}
}
div {
class: "room-settings-tab__actions",
button {
class: "btn btn--primary",
onclick: on_save,
disabled: *is_saving.read(),
if *is_saving.read() { "Saving..." } else { "Save" }
}
}
}
}
}
async fn save_general_settings(
state: Signal<AppState>,
room_id_str: &str,
name: &str,
topic: &str,
) -> Result<(), String> {
let client = { state.read().client.clone() };
let client = client.ok_or_else(|| "Not logged in".to_string())?;
let room_id: OwnedRoomId = room_id_str
.try_into()
.map_err(|e| format!("Invalid room ID: {e}"))?;
let room = client
.get_room(&room_id)
.ok_or_else(|| format!("Room not found: {room_id}"))?;
use matrix_sdk::ruma::events::room::name::RoomNameEventContent;
let name_content = RoomNameEventContent::new(name.to_string());
room.send_state_event(name_content)
.await
.map_err(|e| format!("Failed to set room name: {e}"))?;
use matrix_sdk::ruma::events::room::topic::RoomTopicEventContent;
let topic_content = RoomTopicEventContent::new(topic.to_string());
room.send_state_event(topic_content)
.await
.map_err(|e| format!("Failed to set topic: {e}"))?;
Ok(())
}
#[component]
fn SecurityTab(room_id: String) -> Element {
let state = use_context::<Signal<AppState>>();
let mut is_saving = use_signal(|| false);
let mut save_result = use_signal(|| Option::<Result<(), String>>::None);
let (is_encrypted, _is_public) = {
let s = state.read();
let room_id_parsed: Result<OwnedRoomId, _> = room_id.as_str().try_into();
match room_id_parsed.ok().and_then(|id| s.rooms.get(&id)) {
Some(r) => (r.is_encrypted, false),
None => (false, false),
}
};
let mut join_rule = use_signal(|| "invite".to_string());
let mut history_vis = use_signal(|| "shared".to_string());
let rid = room_id.clone();
let on_save_join_rule = move |_| {
let rule = join_rule.read().clone();
let rid = rid.clone();
is_saving.set(true);
save_result.set(None);
spawn(async move {
let result = save_join_rule(state, &rid, &rule).await;
save_result.set(Some(result));
is_saving.set(false);
});
};
let rid2 = room_id.clone();
let on_save_history = move |_| {
let vis = history_vis.read().clone();
let rid = rid2.clone();
is_saving.set(true);
save_result.set(None);
spawn(async move {
let result = save_history_visibility(state, &rid, &vis).await;
save_result.set(Some(result));
is_saving.set(false);
});
};
rsx! {
div {
class: "room-settings-tab",
if let Some(ref result) = *save_result.read() {
match result {
Ok(()) => rsx! {
div { class: "room-settings-tab__success", "Settings saved." }
},
Err(e) => rsx! {
div { class: "room-settings-tab__error", "{e}" }
},
}
}
div {
class: "room-settings-tab__field",
label { "Encryption" }
p {
class: "room-settings-tab__value",
if is_encrypted {
"Enabled (cannot be disabled)"
} else {
"Not enabled"
}
}
}
div {
class: "room-settings-tab__field",
label { "Who can join" }
select {
value: "{join_rule}",
oninput: move |evt| join_rule.set(evt.value()),
disabled: *is_saving.read(),
option { value: "invite", "Invite only" }
option { value: "public", "Anyone (public)" }
option { value: "knock", "Ask to join (knock)" }
}
button {
class: "btn btn--small btn--primary",
onclick: on_save_join_rule,
disabled: *is_saving.read(),
"Apply"
}
}
div {
class: "room-settings-tab__field",
label { "History visibility" }
select {
value: "{history_vis}",
oninput: move |evt| history_vis.set(evt.value()),
disabled: *is_saving.read(),
option { value: "shared", "Members only (since they joined)" }
option { value: "invited", "Members only (since they were invited)" }
option { value: "joined", "Members only (since they joined, strict)" }
option { value: "world_readable", "Anyone" }
}
button {
class: "btn btn--small btn--primary",
onclick: on_save_history,
disabled: *is_saving.read(),
"Apply"
}
}
}
}
}
async fn save_join_rule(
state: Signal<AppState>,
room_id_str: &str,
rule: &str,
) -> Result<(), String> {
let client = { state.read().client.clone() };
let client = client.ok_or_else(|| "Not logged in".to_string())?;
let room_id: OwnedRoomId = room_id_str
.try_into()
.map_err(|e| format!("Invalid room ID: {e}"))?;
let room = client
.get_room(&room_id)
.ok_or_else(|| format!("Room not found: {room_id}"))?;
use matrix_sdk::ruma::events::room::join_rules::{JoinRule, RoomJoinRulesEventContent};
let join_rule = match rule {
"public" => JoinRule::Public,
"knock" => JoinRule::Knock,
_ => JoinRule::Invite,
};
let content = RoomJoinRulesEventContent::new(join_rule);
room.send_state_event(content)
.await
.map_err(|e| format!("Failed to set join rule: {e}"))?;
Ok(())
}
async fn save_history_visibility(
state: Signal<AppState>,
room_id_str: &str,
visibility: &str,
) -> Result<(), String> {
let client = { state.read().client.clone() };
let client = client.ok_or_else(|| "Not logged in".to_string())?;
let room_id: OwnedRoomId = room_id_str
.try_into()
.map_err(|e| format!("Invalid room ID: {e}"))?;
let room = client
.get_room(&room_id)
.ok_or_else(|| format!("Room not found: {room_id}"))?;
use matrix_sdk::ruma::events::room::history_visibility::{
HistoryVisibility, RoomHistoryVisibilityEventContent,
};
let vis = match visibility {
"invited" => HistoryVisibility::Invited,
"joined" => HistoryVisibility::Joined,
"world_readable" => HistoryVisibility::WorldReadable,
_ => HistoryVisibility::Shared,
};
let content = RoomHistoryVisibilityEventContent::new(vis);
room.send_state_event(content)
.await
.map_err(|e| format!("Failed to set history visibility: {e}"))?;
Ok(())
}
#[component]
fn RolesTab(room_id: String) -> Element {
let state = use_context::<Signal<AppState>>();
let mut members = use_signal(Vec::<(String, String, i64)>::new);
let mut is_loading = use_signal(|| true);
let rid = room_id.clone();
use_effect(move || {
let rid = rid.clone();
spawn(async move {
let loaded = load_members_with_power(state, &rid).await;
members.set(loaded);
is_loading.set(false);
});
});
rsx! {
div {
class: "room-settings-tab",
h4 { "Power Levels" }
p {
class: "room-settings-tab__hint",
"Higher power level = more permissions. Default for new members is 0."
}
if *is_loading.read() {
div { class: "spinner spinner--small" }
}
div {
class: "room-settings-tab__member-list",
for (user_id, display_name, power) in members.read().iter() {
{
let power_val = *power;
let role_label = if power_val >= 100 {
" (Admin)"
} else if power_val >= 50 {
" (Mod)"
} else {
""
};
let power_class = if power_val >= 100 {
"room-settings-tab__power room-settings-tab__power--admin"
} else if power_val >= 50 {
"room-settings-tab__power room-settings-tab__power--mod"
} else {
"room-settings-tab__power"
};
let power_text = format!("{power_val}{role_label}");
rsx! {
div {
class: "room-settings-tab__member-row",
span { class: "room-settings-tab__member-name", "{display_name}" }
span { class: "room-settings-tab__member-id", "{user_id}" }
span {
class: power_class,
"{power_text}"
}
}
}
}
}
}
}
}
}
async fn load_members_with_power(
state: Signal<AppState>,
room_id_str: &str,
) -> Vec<(String, String, i64)> {
let client = { state.read().client.clone() };
let Some(client) = client else {
return Vec::new();
};
let Ok(room_id) = <&str as TryInto<OwnedRoomId>>::try_into(room_id_str) else {
return Vec::new();
};
let Some(room) = client.get_room(&room_id) else {
return Vec::new();
};
let members = match room.members(matrix_sdk::RoomMemberships::JOIN).await {
Ok(m) => m,
Err(_) => return Vec::new(),
};
let mut result: Vec<(String, String, i64)> = members
.iter()
.map(|m| {
let user_id = m.user_id().to_string();
let display_name = m.display_name().unwrap_or_else(|| m.user_id().localpart()).to_string();
use matrix_sdk::ruma::events::room::power_levels::UserPowerLevel;
let power: i64 = match m.power_level() {
UserPowerLevel::Infinite => 9999,
UserPowerLevel::Int(n) => i64::from(n),
_ => 0,
};
(user_id, display_name, power)
})
.collect();
result.sort_by(|a, b| b.2.cmp(&a.2));
result
}
#[component]
fn AliasesTab(room_id: String) -> Element {
let state = use_context::<Signal<AppState>>();
let mut aliases = use_signal(Vec::<String>::new);
let mut canonical_alias = use_signal(|| Option::<String>::None);
let mut new_alias = use_signal(|| String::new());
let mut is_loading = use_signal(|| true);
let mut action_status = use_signal(|| Option::<Result<String, String>>::None);
let room_id_sig = use_signal(|| room_id.clone());
let rid = room_id.clone();
use_effect(move || {
let rid = rid.clone();
spawn(async move {
let client = { state.read().client.clone() };
if let Some(client) = client {
if let Ok(room_id) = <&str as TryInto<OwnedRoomId>>::try_into(rid.as_str()) {
if let Some(room) = client.get_room(&room_id) {
if let Some(alias) = room.canonical_alias() {
canonical_alias.set(Some(alias.to_string()));
}
let alt = room.alt_aliases();
aliases.set(alt.iter().map(|a| a.to_string()).collect());
}
}
}
is_loading.set(false);
});
});
rsx! {
div {
class: "room-settings-tab",
h4 { "Room Addresses" }
p {
class: "room-settings-tab__hint",
"Room addresses allow others to find and join this room."
}
if *is_loading.read() {
div { class: "spinner spinner--small" }
}
if let Some(ref result) = *action_status.read() {
match result {
Ok(msg) => rsx! {
div { class: "room-settings-tab__success", "{msg}" }
},
Err(e) => rsx! {
div { class: "room-settings-tab__error", "{e}" }
},
}
}
div {
class: "room-settings-tab__field",
label { "Main address" }
if let Some(ref alias) = *canonical_alias.read() {
span { class: "room-settings-tab__code", "{alias}" }
} else {
span { class: "room-settings-tab__hint", "No main address set" }
}
}
div {
class: "room-settings-tab__field",
label { "Other addresses" }
if aliases.read().is_empty() {
p { class: "room-settings-tab__hint", "No other addresses" }
}
for alias in aliases.read().iter() {
{
let alias_display = alias.clone();
rsx! {
div {
class: "room-settings-tab__alias-row",
style: "display: flex; align-items: center; gap: 8px; padding: 4px 0;",
span { "{alias_display}" }
}
}
}
}
}
div {
class: "room-settings-tab__field",
label { "Add address" }
div {
style: "display: flex; gap: 8px; align-items: center;",
input {
r#type: "text",
placeholder: "#alias:server.org",
value: "{new_alias}",
oninput: move |evt| new_alias.set(evt.value()),
}
button {
class: "btn btn--primary btn--sm",
disabled: new_alias.read().is_empty(),
onclick: move |_| {
let alias_str = new_alias.read().clone();
let rid_str = room_id_sig.read().clone();
spawn(async move {
let client = { state.read().client.clone() };
if let Some(client) = client {
use matrix_sdk::ruma::api::client::alias::create_alias::v3::Request;
use matrix_sdk::ruma::{OwnedRoomAliasId, OwnedRoomId};
if let (Ok(alias_id), Ok(room_id)) = (
OwnedRoomAliasId::try_from(alias_str.as_str()),
OwnedRoomId::try_from(rid_str.as_str()),
) {
let request = Request::new(alias_id, room_id);
match client.send(request).await {
Ok(_) => {
action_status.set(Some(Ok(format!("Added alias: {alias_str}"))));
let mut current = aliases.read().clone();
current.push(alias_str);
aliases.set(current);
new_alias.set(String::new());
}
Err(e) => {
action_status.set(Some(Err(format!("Failed to add alias: {e}"))));
}
}
} else {
action_status.set(Some(Err("Invalid alias format. Use #alias:server.org".to_string())));
}
}
});
},
"Add"
}
}
}
}
}
}
#[component]
fn AdvancedTab(room_id: String) -> Element {
let mut state = use_context::<Signal<AppState>>();
let mut is_leaving = use_signal(|| false);
let mut leave_error = use_signal(|| Option::<String>::None);
let rid = room_id.clone();
let on_leave = move |_| {
let rid = rid.clone();
is_leaving.set(true);
leave_error.set(None);
spawn(async move {
match leave_room(state, &rid).await {
Ok(()) => {
tracing::info!("Left room {rid}");
let mut s = state.write();
s.active_room_id = None;
s.current_view = crate::state::app_state::AppView::Home;
}
Err(e) => {
tracing::error!("Failed to leave room: {e}");
leave_error.set(Some(e));
}
}
is_leaving.set(false);
});
};
rsx! {
div {
class: "room-settings-tab",
div {
class: "room-settings-tab__field",
label { "Internal Room ID" }
code {
class: "room-settings-tab__code",
"{room_id}"
}
}
div {
class: "room-settings-tab__danger",
h4 { "Danger Zone" }
if let Some(err) = leave_error.read().as_ref() {
div { class: "room-settings-tab__error", "{err}" }
}
button {
class: "btn btn--danger",
onclick: on_leave,
disabled: *is_leaving.read(),
if *is_leaving.read() { "Leaving..." } else { "Leave Room" }
}
}
}
}
}
async fn leave_room(
state: Signal<AppState>,
room_id_str: &str,
) -> Result<(), String> {
let client = { state.read().client.clone() };
let client = client.ok_or_else(|| "Not logged in".to_string())?;
let room_id: OwnedRoomId = room_id_str
.try_into()
.map_err(|e| format!("Invalid room ID: {e}"))?;
let room = client
.get_room(&room_id)
.ok_or_else(|| format!("Room not found: {room_id}"))?;
room.leave()
.await
.map_err(|e| format!("Failed to leave room: {e}"))?;
Ok(())
}