synpad 0.1.0

A full-featured Matrix chat client built with Dioxus
use dioxus::prelude::*;
use matrix_sdk::notification_settings::RoomNotificationMode;
use matrix_sdk::ruma::OwnedRoomId;
use matrix_sdk::Client;

use crate::components::modal::Modal;
use crate::state::app_state::AppState;
use crate::state::room_state::NotificationLevel;

/// Push rule actions for a room.
#[derive(Clone, Debug, PartialEq)]
pub enum PushRuleAction {
    /// Use the global default
    Default,
    /// Notify for all messages
    AllMessages,
    /// Only notify for mentions and keywords
    MentionsOnly,
    /// No notifications
    Mute,
}

pub fn notification_level_from_push_action(action: PushRuleAction) -> NotificationLevel {
    match action {
        PushRuleAction::Default => NotificationLevel::Default,
        PushRuleAction::AllMessages => NotificationLevel::AllMessages,
        PushRuleAction::MentionsOnly => NotificationLevel::MentionsAndKeywords,
        PushRuleAction::Mute => NotificationLevel::Mute,
    }
}

pub fn push_rule_action_from_notification_level(level: &NotificationLevel) -> PushRuleAction {
    match level {
        NotificationLevel::Default => PushRuleAction::Default,
        NotificationLevel::AllMessages => PushRuleAction::AllMessages,
        NotificationLevel::MentionsAndKeywords => PushRuleAction::MentionsOnly,
        NotificationLevel::Mute => PushRuleAction::Mute,
    }
}

pub fn notification_level_from_user_defined_mode(
    mode: Option<RoomNotificationMode>,
) -> NotificationLevel {
    match mode {
        Some(RoomNotificationMode::AllMessages) => NotificationLevel::AllMessages,
        Some(RoomNotificationMode::MentionsAndKeywordsOnly) => {
            NotificationLevel::MentionsAndKeywords
        }
        Some(RoomNotificationMode::Mute) => NotificationLevel::Mute,
        None => NotificationLevel::Default,
    }
}

pub async fn load_room_notification_level(
    client: &Client,
    room_id: &OwnedRoomId,
) -> Result<NotificationLevel, String> {
    let settings = client.notification_settings().await;
    Ok(notification_level_from_user_defined_mode(
        settings.get_user_defined_room_notification_mode(room_id).await,
    ))
}

pub async fn persist_room_notification_level(
    client: &Client,
    room_id: &OwnedRoomId,
    level: NotificationLevel,
) -> Result<(), String> {
    let settings = client.notification_settings().await;
    match level {
        NotificationLevel::Default => settings
            .delete_user_defined_room_rules(room_id)
            .await
            .map_err(|e| format!("Failed to reset room notifications: {e}"))?,
        NotificationLevel::AllMessages => settings
            .set_room_notification_mode(room_id, RoomNotificationMode::AllMessages)
            .await
            .map_err(|e| format!("Failed to enable notifications for all messages: {e}"))?,
        NotificationLevel::MentionsAndKeywords => settings
            .set_room_notification_mode(
                room_id,
                RoomNotificationMode::MentionsAndKeywordsOnly,
            )
            .await
            .map_err(|e| format!("Failed to keep only mentions and keywords: {e}"))?,
        NotificationLevel::Mute => settings
            .set_room_notification_mode(room_id, RoomNotificationMode::Mute)
            .await
            .map_err(|e| format!("Failed to mute room notifications: {e}"))?,
    }

    Ok(())
}

/// Push rules management dialog for a room.
#[component]
pub fn PushRulesDialog(room_id: String, room_name: String, on_close: EventHandler<()>) -> Element {
    let mut state = use_context::<Signal<AppState>>();
    let initial_selection = {
        let state_read = state.read();
        state_read
            .rooms
            .values()
            .find(|room| room.room_id.to_string() == room_id)
            .map(|room| push_rule_action_from_notification_level(&room.notification_level))
            .unwrap_or(PushRuleAction::Default)
    };
    let mut selected = use_signal(move || initial_selection.clone());
    let mut saving = use_signal(|| false);
    let mut save_error = use_signal(|| Option::<String>::None);

    let room_id_for_load = room_id.clone();
    use_effect(move || {
        let rid = room_id_for_load.clone();
        spawn(async move {
            let client = { state.read().client.clone() };
            let Some(client) = client else {
                return;
            };
            let Ok(room_id) = matrix_sdk::ruma::OwnedRoomId::try_from(rid.as_str()) else {
                return;
            };
            match load_room_notification_level(&client, &room_id).await {
                Ok(level) => selected.set(push_rule_action_from_notification_level(&level)),
                Err(e) => save_error.set(Some(e)),
            }
        });
    });

    let room_id_for_save = room_id.clone();
    let on_save = move |_| {
        let rule = selected.read().clone();
        let rid = room_id_for_save.clone();
        let next_level = notification_level_from_push_action(rule.clone());
        saving.set(true);
        save_error.set(None);

        spawn(async move {
            let client = { state.read().client.clone() };
            let Some(client) = client else {
                save_error.set(Some("Not logged in".to_string()));
                saving.set(false);
                return;
            };

            if let Ok(room_id) = matrix_sdk::ruma::OwnedRoomId::try_from(rid.as_str()) {
                match persist_room_notification_level(&client, &room_id, next_level.clone()).await {
                    Ok(()) => {
                        if let Some(room) = state.write().rooms.get_mut(&room_id) {
                            room.notification_level = next_level;
                        }
                    }
                    Err(e) => {
                        save_error.set(Some(e));
                    }
                }
            }
            saving.set(false);
        });
    };

    rsx! {
        Modal {
            title: format!("Notification settings: {room_name}"),
            on_close: move |_| on_close.call(()),

            div {
                class: "push-rules-dialog",

                p { class: "push-rules-dialog__description",
                    "Choose how you want to be notified about new messages in this room."
                }

                if let Some(err) = save_error.read().as_ref() {
                    p {
                        class: "push-rules-dialog__error",
                        "{err}"
                    }
                }

                div {
                    class: "push-rules-dialog__options",

                    label {
                        class: "push-rules-dialog__option",
                        input {
                            r#type: "radio",
                            name: "push_rule",
                            checked: *selected.read() == PushRuleAction::Default,
                            oninput: move |_| selected.set(PushRuleAction::Default),
                        }
                        div {
                            strong { "Default" }
                            p { "Use your global notification settings" }
                        }
                    }

                    label {
                        class: "push-rules-dialog__option",
                        input {
                            r#type: "radio",
                            name: "push_rule",
                            checked: *selected.read() == PushRuleAction::AllMessages,
                            oninput: move |_| selected.set(PushRuleAction::AllMessages),
                        }
                        div {
                            strong { "All messages" }
                            p { "Notify for every message" }
                        }
                    }

                    label {
                        class: "push-rules-dialog__option",
                        input {
                            r#type: "radio",
                            name: "push_rule",
                            checked: *selected.read() == PushRuleAction::MentionsOnly,
                            oninput: move |_| selected.set(PushRuleAction::MentionsOnly),
                        }
                        div {
                            strong { "Mentions & keywords" }
                            p { "Only notify when mentioned or keywords match" }
                        }
                    }

                    label {
                        class: "push-rules-dialog__option",
                        input {
                            r#type: "radio",
                            name: "push_rule",
                            checked: *selected.read() == PushRuleAction::Mute,
                            oninput: move |_| selected.set(PushRuleAction::Mute),
                        }
                        div {
                            strong { "Mute" }
                            p { "No notifications from this room" }
                        }
                    }
                }

                div {
                    class: "push-rules-dialog__actions",
                    button {
                        class: "btn btn--secondary",
                        onclick: move |_| on_close.call(()),
                        "Cancel"
                    }
                    button {
                        class: "btn btn--primary",
                        disabled: *saving.read(),
                        onclick: on_save,
                        if *saving.read() { "Saving..." } else { "Save" }
                    }
                }
            }
        }
    }
}