From 67ccfcb132bc67da4aa41a78f34981a3af338a9c Mon Sep 17 00:00:00 2001 From: reya Date: Sun, 22 Feb 2026 14:34:41 +0700 Subject: [PATCH] update send message and chat panel ui --- assets/icons/user-key.svg | 3 + assets/themes/catppuccin-frappe.json | 136 ------- assets/themes/catppuccin-latte.json | 136 ------- assets/themes/catppuccin-macchiato.json | 136 ------- assets/themes/catppuccin-mocha.json | 136 ------- assets/themes/flexoki.json | 136 ------- assets/themes/rose-pine-dawn.json | 136 ------- assets/themes/rose-pine-moon.json | 136 ------- assets/themes/rose-pine.json | 136 ------- crates/chat/src/lib.rs | 1 + crates/chat/src/room.rs | 504 +++++++++--------------- crates/chat_ui/src/actions.rs | 2 + crates/chat_ui/src/lib.rs | 168 ++++++-- crates/coop/src/sidebar/mod.rs | 4 +- crates/settings/src/lib.rs | 12 + crates/state/src/constants.rs | 2 +- crates/state/src/lib.rs | 68 +--- crates/theme/src/lib.rs | 3 + crates/ui/src/checkbox.rs | 318 +++++++++++---- crates/ui/src/dock_area/tab_panel.rs | 4 +- crates/ui/src/icon.rs | 83 ++-- crates/ui/src/menu/popup_menu.rs | 2 +- crates/ui/src/tab/mod.rs | 4 +- 23 files changed, 639 insertions(+), 1627 deletions(-) create mode 100644 assets/icons/user-key.svg delete mode 100644 assets/themes/catppuccin-frappe.json delete mode 100644 assets/themes/catppuccin-latte.json delete mode 100644 assets/themes/catppuccin-macchiato.json delete mode 100644 assets/themes/catppuccin-mocha.json delete mode 100644 assets/themes/flexoki.json delete mode 100644 assets/themes/rose-pine-dawn.json delete mode 100644 assets/themes/rose-pine-moon.json delete mode 100644 assets/themes/rose-pine.json diff --git a/assets/icons/user-key.svg b/assets/icons/user-key.svg new file mode 100644 index 0000000..a982679 --- /dev/null +++ b/assets/icons/user-key.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/themes/catppuccin-frappe.json b/assets/themes/catppuccin-frappe.json deleted file mode 100644 index b9b1f49..0000000 --- a/assets/themes/catppuccin-frappe.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "id": "catppuccin-frappe", - "name": "Catppuccin Frappé", - "author": "Catppuccin", - "url": "https://github.com/catppuccin/catppuccin", - "light": { - "background": "#303446", - "surface_background": "#292c3c", - "elevated_surface_background": "#232634", - "panel_background": "#303446", - "overlay": "#c6d0f51a", - "title_bar": "#00000000", - "title_bar_inactive": "#303446", - "window_border": "#626880", - "border": "#626880", - "border_variant": "#51576d", - "border_focused": "#8caaee", - "border_selected": "#8caaee", - "border_transparent": "#00000000", - "border_disabled": "#414559", - "ring": "#8caaee", - "text": "#c6d0f5", - "text_muted": "#b5bfe2", - "text_placeholder": "#a5adce", - "text_accent": "#8caaee", - "icon": "#c6d0f5", - "icon_muted": "#b5bfe2", - "icon_accent": "#8caaee", - "element_foreground": "#303446", - "element_background": "#8caaee", - "element_hover": "#8caaeee6", - "element_active": "#7e99d6", - "element_selected": "#7088be", - "element_disabled": "#8caaee4d", - "secondary_foreground": "#8caaee", - "secondary_background": "#414559", - "secondary_hover": "#8caaee1a", - "secondary_active": "#51576d", - "secondary_selected": "#51576d", - "secondary_disabled": "#8caaee4d", - "danger_foreground": "#303446", - "danger_background": "#e78284", - "danger_hover": "#e78284e6", - "danger_active": "#d07576", - "danger_selected": "#b96869", - "danger_disabled": "#e782844d", - "warning_foreground": "#303446", - "warning_background": "#e5c890", - "warning_hover": "#e5c890e6", - "warning_active": "#ceb481", - "warning_selected": "#b7a072", - "warning_disabled": "#e5c8904d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#414559", - "ghost_element_hover": "#c6d0f51a", - "ghost_element_active": "#51576d", - "ghost_element_selected": "#51576d", - "ghost_element_disabled": "#c6d0f50d", - "tab_inactive_background": "#414559", - "tab_hover_background": "#51576d", - "tab_active_background": "#626880", - "scrollbar_thumb_background": "#c6d0f533", - "scrollbar_thumb_hover_background": "#c6d0f54d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#51576d", - "drop_target_background": "#8caaee1a", - "cursor": "#99d1db", - "selection": "#99d1db40" - }, - "dark": { - "background": "#303446", - "surface_background": "#292c3c", - "elevated_surface_background": "#232634", - "panel_background": "#303446", - "overlay": "#c6d0f51a", - "title_bar": "#00000000", - "title_bar_inactive": "#303446", - "window_border": "#626880", - "border": "#626880", - "border_variant": "#51576d", - "border_focused": "#8caaee", - "border_selected": "#8caaee", - "border_transparent": "#00000000", - "border_disabled": "#414559", - "ring": "#8caaee", - "text": "#c6d0f5", - "text_muted": "#b5bfe2", - "text_placeholder": "#a5adce", - "text_accent": "#8caaee", - "icon": "#c6d0f5", - "icon_muted": "#b5bfe2", - "icon_accent": "#8caaee", - "element_foreground": "#303446", - "element_background": "#8caaee", - "element_hover": "#8caaeee6", - "element_active": "#7e99d6", - "element_selected": "#7088be", - "element_disabled": "#8caaee4d", - "secondary_foreground": "#8caaee", - "secondary_background": "#414559", - "secondary_hover": "#8caaee1a", - "secondary_active": "#51576d", - "secondary_selected": "#51576d", - "secondary_disabled": "#8caaee4d", - "danger_foreground": "#303446", - "danger_background": "#e78284", - "danger_hover": "#e78284e6", - "danger_active": "#d07576", - "danger_selected": "#b96869", - "danger_disabled": "#e782844d", - "warning_foreground": "#303446", - "warning_background": "#e5c890", - "warning_hover": "#e5c890e6", - "warning_active": "#ceb481", - "warning_selected": "#b7a072", - "warning_disabled": "#e5c8904d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#414559", - "ghost_element_hover": "#c6d0f51a", - "ghost_element_active": "#51576d", - "ghost_element_selected": "#51576d", - "ghost_element_disabled": "#c6d0f50d", - "tab_inactive_background": "#414559", - "tab_hover_background": "#51576d", - "tab_active_background": "#626880", - "scrollbar_thumb_background": "#c6d0f533", - "scrollbar_thumb_hover_background": "#c6d0f54d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#51576d", - "drop_target_background": "#8caaee1a", - "cursor": "#99d1db", - "selection": "#99d1db40" - } -} diff --git a/assets/themes/catppuccin-latte.json b/assets/themes/catppuccin-latte.json deleted file mode 100644 index b0c074b..0000000 --- a/assets/themes/catppuccin-latte.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "id": "catppuccin-latte", - "name": "Catppuccin Latte", - "author": "Catppuccin", - "url": "https://github.com/catppuccin/catppuccin", - "light": { - "background": "#eff1f5", - "surface_background": "#e6e9ef", - "elevated_surface_background": "#dce0e8", - "panel_background": "#eff1f5", - "overlay": "#4c4f691a", - "title_bar": "#00000000", - "title_bar_inactive": "#eff1f5", - "window_border": "#acb0be", - "border": "#acb0be", - "border_variant": "#bcc0cc", - "border_focused": "#1e66f5", - "border_selected": "#1e66f5", - "border_transparent": "#00000000", - "border_disabled": "#ccd0da", - "ring": "#1e66f5", - "text": "#4c4f69", - "text_muted": "#5c5f77", - "text_placeholder": "#6c6f85", - "text_accent": "#1e66f5", - "icon": "#4c4f69", - "icon_muted": "#5c5f77", - "icon_accent": "#1e66f5", - "element_foreground": "#eff1f5", - "element_background": "#1e66f5", - "element_hover": "#1e66f5e6", - "element_active": "#1b5cdc", - "element_selected": "#1852c3", - "element_disabled": "#1e66f54d", - "secondary_foreground": "#1e66f5", - "secondary_background": "#e6e9ef", - "secondary_hover": "#1e66f51a", - "secondary_active": "#dce0e8", - "secondary_selected": "#dce0e8", - "secondary_disabled": "#1e66f54d", - "danger_foreground": "#eff1f5", - "danger_background": "#d20f39", - "danger_hover": "#d20f39e6", - "danger_active": "#bc0e33", - "danger_selected": "#a60c2d", - "danger_disabled": "#d20f394d", - "warning_foreground": "#4c4f69", - "warning_background": "#df8e1d", - "warning_hover": "#df8e1de6", - "warning_active": "#c9801a", - "warning_selected": "#b47217", - "warning_disabled": "#df8e1d4d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#e6e9ef", - "ghost_element_hover": "#4c4f691a", - "ghost_element_active": "#dce0e8", - "ghost_element_selected": "#dce0e8", - "ghost_element_disabled": "#4c4f690d", - "tab_inactive_background": "#e6e9ef", - "tab_hover_background": "#dce0e8", - "tab_active_background": "#ccd0da", - "scrollbar_thumb_background": "#4c4f6933", - "scrollbar_thumb_hover_background": "#4c4f694d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#dce0e8", - "drop_target_background": "#1e66f51a", - "cursor": "#04a5e5", - "selection": "#04a5e540" - }, - "dark": { - "background": "#eff1f5", - "surface_background": "#e6e9ef", - "elevated_surface_background": "#dce0e8", - "panel_background": "#eff1f5", - "overlay": "#4c4f691a", - "title_bar": "#00000000", - "title_bar_inactive": "#eff1f5", - "window_border": "#acb0be", - "border": "#acb0be", - "border_variant": "#bcc0cc", - "border_focused": "#1e66f5", - "border_selected": "#1e66f5", - "border_transparent": "#00000000", - "border_disabled": "#ccd0da", - "ring": "#1e66f5", - "text": "#4c4f69", - "text_muted": "#5c5f77", - "text_placeholder": "#6c6f85", - "text_accent": "#1e66f5", - "icon": "#4c4f69", - "icon_muted": "#5c5f77", - "icon_accent": "#1e66f5", - "element_foreground": "#eff1f5", - "element_background": "#1e66f5", - "element_hover": "#1e66f5e6", - "element_active": "#1b5cdc", - "element_selected": "#1852c3", - "element_disabled": "#1e66f54d", - "secondary_foreground": "#1e66f5", - "secondary_background": "#e6e9ef", - "secondary_hover": "#1e66f51a", - "secondary_active": "#dce0e8", - "secondary_selected": "#dce0e8", - "secondary_disabled": "#1e66f54d", - "danger_foreground": "#eff1f5", - "danger_background": "#d20f39", - "danger_hover": "#d20f39e6", - "danger_active": "#bc0e33", - "danger_selected": "#a60c2d", - "danger_disabled": "#d20f394d", - "warning_foreground": "#4c4f69", - "warning_background": "#df8e1d", - "warning_hover": "#df8e1de6", - "warning_active": "#c9801a", - "warning_selected": "#b47217", - "warning_disabled": "#df8e1d4d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#e6e9ef", - "ghost_element_hover": "#4c4f691a", - "ghost_element_active": "#dce0e8", - "ghost_element_selected": "#dce0e8", - "ghost_element_disabled": "#4c4f690d", - "tab_inactive_background": "#e6e9ef", - "tab_hover_background": "#dce0e8", - "tab_active_background": "#ccd0da", - "scrollbar_thumb_background": "#4c4f6933", - "scrollbar_thumb_hover_background": "#4c4f694d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#dce0e8", - "drop_target_background": "#1e66f51a", - "cursor": "#04a5e5", - "selection": "#04a5e540" - } -} diff --git a/assets/themes/catppuccin-macchiato.json b/assets/themes/catppuccin-macchiato.json deleted file mode 100644 index 09cb345..0000000 --- a/assets/themes/catppuccin-macchiato.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "id": "catppuccin-macchiato", - "name": "Catppuccin Macchiato", - "author": "Catppuccin", - "url": "https://github.com/catppuccin/catppuccin", - "light": { - "background": "#24273a", - "surface_background": "#1e2030", - "elevated_surface_background": "#181926", - "panel_background": "#24273a", - "overlay": "#cad3f51a", - "title_bar": "#00000000", - "title_bar_inactive": "#24273a", - "window_border": "#5b6078", - "border": "#5b6078", - "border_variant": "#494d64", - "border_focused": "#8aadf4", - "border_selected": "#8aadf4", - "border_transparent": "#00000000", - "border_disabled": "#363a4f", - "ring": "#8aadf4", - "text": "#cad3f5", - "text_muted": "#b8c0e0", - "text_placeholder": "#a5adcb", - "text_accent": "#8aadf4", - "icon": "#cad3f5", - "icon_muted": "#b8c0e0", - "icon_accent": "#8aadf4", - "element_foreground": "#24273a", - "element_background": "#8aadf4", - "element_hover": "#8aadf4e6", - "element_active": "#7c9cdc", - "element_selected": "#6e8bc4", - "element_disabled": "#8aadf44d", - "secondary_foreground": "#8aadf4", - "secondary_background": "#363a4f", - "secondary_hover": "#8aadf41a", - "secondary_active": "#494d64", - "secondary_selected": "#494d64", - "secondary_disabled": "#8aadf44d", - "danger_foreground": "#24273a", - "danger_background": "#ed8796", - "danger_hover": "#ed8796e6", - "danger_active": "#d57a87", - "danger_selected": "#bd6d78", - "danger_disabled": "#ed87964d", - "warning_foreground": "#24273a", - "warning_background": "#eed49f", - "warning_hover": "#eed49fe6", - "warning_active": "#d6bf8f", - "warning_selected": "#beaa7f", - "warning_disabled": "#eed49f4d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#363a4f", - "ghost_element_hover": "#cad3f51a", - "ghost_element_active": "#494d64", - "ghost_element_selected": "#494d64", - "ghost_element_disabled": "#cad3f50d", - "tab_inactive_background": "#363a4f", - "tab_hover_background": "#494d64", - "tab_active_background": "#5b6078", - "scrollbar_thumb_background": "#cad3f533", - "scrollbar_thumb_hover_background": "#cad3f54d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#494d64", - "drop_target_background": "#8aadf41a", - "cursor": "#91d7e3", - "selection": "#91d7e340" - }, - "dark": { - "background": "#24273a", - "surface_background": "#1e2030", - "elevated_surface_background": "#181926", - "panel_background": "#24273a", - "overlay": "#cad3f51a", - "title_bar": "#00000000", - "title_bar_inactive": "#24273a", - "window_border": "#5b6078", - "border": "#5b6078", - "border_variant": "#494d64", - "border_focused": "#8aadf4", - "border_selected": "#8aadf4", - "border_transparent": "#00000000", - "border_disabled": "#363a4f", - "ring": "#8aadf4", - "text": "#cad3f5", - "text_muted": "#b8c0e0", - "text_placeholder": "#a5adcb", - "text_accent": "#8aadf4", - "icon": "#cad3f5", - "icon_muted": "#b8c0e0", - "icon_accent": "#8aadf4", - "element_foreground": "#24273a", - "element_background": "#8aadf4", - "element_hover": "#8aadf4e6", - "element_active": "#7c9cdc", - "element_selected": "#6e8bc4", - "element_disabled": "#8aadf44d", - "secondary_foreground": "#8aadf4", - "secondary_background": "#363a4f", - "secondary_hover": "#8aadf41a", - "secondary_active": "#494d64", - "secondary_selected": "#494d64", - "secondary_disabled": "#8aadf44d", - "danger_foreground": "#24273a", - "danger_background": "#ed8796", - "danger_hover": "#ed8796e6", - "danger_active": "#d57a87", - "danger_selected": "#bd6d78", - "danger_disabled": "#ed87964d", - "warning_foreground": "#24273a", - "warning_background": "#eed49f", - "warning_hover": "#eed49fe6", - "warning_active": "#d6bf8f", - "warning_selected": "#beaa7f", - "warning_disabled": "#eed49f4d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#363a4f", - "ghost_element_hover": "#cad3f51a", - "ghost_element_active": "#494d64", - "ghost_element_selected": "#494d64", - "ghost_element_disabled": "#cad3f50d", - "tab_inactive_background": "#363a4f", - "tab_hover_background": "#494d64", - "tab_active_background": "#5b6078", - "scrollbar_thumb_background": "#cad3f533", - "scrollbar_thumb_hover_background": "#cad3f54d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#494d64", - "drop_target_background": "#8aadf41a", - "cursor": "#91d7e3", - "selection": "#91d7e340" - } -} diff --git a/assets/themes/catppuccin-mocha.json b/assets/themes/catppuccin-mocha.json deleted file mode 100644 index 292051d..0000000 --- a/assets/themes/catppuccin-mocha.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "id": "catppuccin-mocha", - "name": "Catppuccin Mocha", - "author": "Catppuccin", - "url": "https://github.com/catppuccin/catppuccin", - "light": { - "background": "#1e1e2e", - "surface_background": "#181825", - "elevated_surface_background": "#11111b", - "panel_background": "#1e1e2e", - "overlay": "#cdd6f41a", - "title_bar": "#00000000", - "title_bar_inactive": "#1e1e2e", - "window_border": "#585b70", - "border": "#585b70", - "border_variant": "#45475a", - "border_focused": "#89b4fa", - "border_selected": "#89b4fa", - "border_transparent": "#00000000", - "border_disabled": "#313244", - "ring": "#89b4fa", - "text": "#cdd6f4", - "text_muted": "#bac2de", - "text_placeholder": "#a6adc8", - "text_accent": "#89b4fa", - "icon": "#cdd6f4", - "icon_muted": "#bac2de", - "icon_accent": "#89b4fa", - "element_foreground": "#1e1e2e", - "element_background": "#89b4fa", - "element_hover": "#89b4fae6", - "element_active": "#7ba2e1", - "element_selected": "#6d90c8", - "element_disabled": "#89b4fa4d", - "secondary_foreground": "#89b4fa", - "secondary_background": "#313244", - "secondary_hover": "#89b4fa1a", - "secondary_active": "#45475a", - "secondary_selected": "#45475a", - "secondary_disabled": "#89b4fa4d", - "danger_foreground": "#1e1e2e", - "danger_background": "#f38ba8", - "danger_hover": "#f38ba8e6", - "danger_active": "#db7d97", - "danger_selected": "#c36f86", - "danger_disabled": "#f38ba84d", - "warning_foreground": "#1e1e2e", - "warning_background": "#f9e2af", - "warning_hover": "#f9e2afe6", - "warning_active": "#e0cb9e", - "warning_selected": "#c7b48d", - "warning_disabled": "#f9e2af4d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#313244", - "ghost_element_hover": "#cdd6f41a", - "ghost_element_active": "#45475a", - "ghost_element_selected": "#45475a", - "ghost_element_disabled": "#cdd6f50d", - "tab_inactive_background": "#313244", - "tab_hover_background": "#45475a", - "tab_active_background": "#585b70", - "scrollbar_thumb_background": "#cdd6f533", - "scrollbar_thumb_hover_background": "#cdd6f54d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#45475a", - "drop_target_background": "#89b4fa1a", - "cursor": "#89dceb", - "selection": "#89dceb40" - }, - "dark": { - "background": "#1e1e2e", - "surface_background": "#181825", - "elevated_surface_background": "#11111b", - "panel_background": "#1e1e2e", - "overlay": "#cdd6f41a", - "title_bar": "#00000000", - "title_bar_inactive": "#1e1e2e", - "window_border": "#585b70", - "border": "#585b70", - "border_variant": "#45475a", - "border_focused": "#89b4fa", - "border_selected": "#89b4fa", - "border_transparent": "#00000000", - "border_disabled": "#313244", - "ring": "#89b4fa", - "text": "#cdd6f4", - "text_muted": "#bac2de", - "text_placeholder": "#a6adc8", - "text_accent": "#89b4fa", - "icon": "#cdd6f4", - "icon_muted": "#bac2de", - "icon_accent": "#89b4fa", - "element_foreground": "#1e1e2e", - "element_background": "#89b4fa", - "element_hover": "#89b4fae6", - "element_active": "#7ba2e1", - "element_selected": "#6d90c8", - "element_disabled": "#89b4fa4d", - "secondary_foreground": "#89b4fa", - "secondary_background": "#313244", - "secondary_hover": "#89b4fa1a", - "secondary_active": "#45475a", - "secondary_selected": "#45475a", - "secondary_disabled": "#89b4fa4d", - "danger_foreground": "#1e1e2e", - "danger_background": "#f38ba8", - "danger_hover": "#f38ba8e6", - "danger_active": "#db7d97", - "danger_selected": "#c36f86", - "danger_disabled": "#f38ba84d", - "warning_foreground": "#1e1e2e", - "warning_background": "#f9e2af", - "warning_hover": "#f9e2afe6", - "warning_active": "#e0cb9e", - "warning_selected": "#c7b48d", - "warning_disabled": "#f9e2af4d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#313244", - "ghost_element_hover": "#cdd6f41a", - "ghost_element_active": "#45475a", - "ghost_element_selected": "#45475a", - "ghost_element_disabled": "#cdd6f50d", - "tab_inactive_background": "#313244", - "tab_hover_background": "#45475a", - "tab_active_background": "#585b70", - "scrollbar_thumb_background": "#cdd6f533", - "scrollbar_thumb_hover_background": "#cdd6f54d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#45475a", - "drop_target_background": "#89b4fa1a", - "cursor": "#89dceb", - "selection": "#89dceb40" - } -} diff --git a/assets/themes/flexoki.json b/assets/themes/flexoki.json deleted file mode 100644 index 374c889..0000000 --- a/assets/themes/flexoki.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "id": "flexoki", - "name": "Flexoki", - "author": "Steph Ango", - "url": "https://stephango.com/flexoki", - "light": { - "background": "#FFFCF0", - "surface_background": "#F2F0E5", - "elevated_surface_background": "#E6E4D9", - "panel_background": "#FFFCF0", - "overlay": "#100F0F1a", - "title_bar": "#00000000", - "title_bar_inactive": "#FFFCF0", - "window_border": "#CECDC3", - "border": "#CECDC3", - "border_variant": "#DAD8CE", - "border_focused": "#24837B", - "border_selected": "#24837B", - "border_transparent": "#00000000", - "border_disabled": "#E6E4D9", - "ring": "#24837B", - "text": "#100F0F", - "text_muted": "#6F6E69", - "text_placeholder": "#878580", - "text_accent": "#24837B", - "icon": "#100F0F", - "icon_muted": "#6F6E69", - "icon_accent": "#24837B", - "element_foreground": "#DDF1E4", - "element_background": "#24837B", - "element_hover": "#24837Be5", - "element_active": "#20756E", - "element_selected": "#1C6861", - "element_disabled": "#24837B4c", - "secondary_foreground": "#24837B", - "secondary_background": "#E6E4D9", - "secondary_hover": "#24837B1a", - "secondary_active": "#DAD8CE", - "secondary_selected": "#DAD8CE", - "secondary_disabled": "#24837B4c", - "danger_foreground": "#FFE1D5", - "danger_background": "#AF3029", - "danger_hover": "#AF3029e5", - "danger_active": "#9E2B25", - "danger_selected": "#8D2620", - "danger_disabled": "#AF30294c", - "warning_foreground": "#FFE7CE", - "warning_background": "#BC5215", - "warning_hover": "#BC5215e5", - "warning_active": "#A94913", - "warning_selected": "#964011", - "warning_disabled": "#BC52154c", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#E6E4D9", - "ghost_element_hover": "#100F0F1a", - "ghost_element_active": "#DAD8CE", - "ghost_element_selected": "#DAD8CE", - "ghost_element_disabled": "#100F0F0d", - "tab_inactive_background": "#E6E4D9", - "tab_hover_background": "#DAD8CE", - "tab_active_background": "#CECDC3", - "scrollbar_thumb_background": "#100F0F33", - "scrollbar_thumb_hover_background": "#100F0F4d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#DAD8CE", - "drop_target_background": "#24837B1a", - "cursor": "#205EA6", - "selection": "#24837B40" - }, - "dark": { - "background": "#100F0F", - "surface_background": "#1C1B1A", - "elevated_surface_background": "#282726", - "panel_background": "#100F0F", - "overlay": "#FFFCF01a", - "title_bar": "#00000000", - "title_bar_inactive": "#100F0F", - "window_border": "#403E3C", - "border": "#403E3C", - "border_variant": "#343331", - "border_focused": "#3AA99F", - "border_selected": "#3AA99F", - "border_transparent": "#00000000", - "border_disabled": "#282726", - "ring": "#3AA99F", - "text": "#FFFCF0", - "text_muted": "#878580", - "text_placeholder": "#575653", - "text_accent": "#3AA99F", - "icon": "#FFFCF0", - "icon_muted": "#878580", - "icon_accent": "#3AA99F", - "element_foreground": "#101F1D", - "element_background": "#3AA99F", - "element_hover": "#3AA99Fe5", - "element_active": "#34988F", - "element_selected": "#2F877F", - "element_disabled": "#3AA99F4c", - "secondary_foreground": "#3AA99F", - "secondary_background": "#282726", - "secondary_hover": "#3AA99F1a", - "secondary_active": "#343331", - "secondary_selected": "#343331", - "secondary_disabled": "#3AA99F4c", - "danger_foreground": "#261312", - "danger_background": "#D14D41", - "danger_hover": "#D14D41e5", - "danger_active": "#BC453A", - "danger_selected": "#A73D33", - "danger_disabled": "#D14D414c", - "warning_foreground": "#27180E", - "warning_background": "#DA702C", - "warning_hover": "#DA702Ce5", - "warning_active": "#C46527", - "warning_selected": "#AF5A22", - "warning_disabled": "#DA702C4c", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#282726", - "ghost_element_hover": "#FFFCF01a", - "ghost_element_active": "#343331", - "ghost_element_selected": "#343331", - "ghost_element_disabled": "#FFFCF00d", - "tab_inactive_background": "#282726", - "tab_hover_background": "#343331", - "tab_active_background": "#403E3C", - "scrollbar_thumb_background": "#FFFCF033", - "scrollbar_thumb_hover_background": "#FFFCF04d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#343331", - "drop_target_background": "#3AA99F1a", - "cursor": "#4385BE", - "selection": "#3AA99F40" - } -} diff --git a/assets/themes/rose-pine-dawn.json b/assets/themes/rose-pine-dawn.json deleted file mode 100644 index 533fb54..0000000 --- a/assets/themes/rose-pine-dawn.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "id": "rose-pine-dawn", - "name": "Rosé Pine Dawn", - "author": "Rosé Pine", - "url": "https://rosepinetheme.com/", - "light": { - "background": "#faf4ed", - "surface_background": "#fffaf3", - "elevated_surface_background": "#f2e9e1", - "panel_background": "#fffaf3", - "overlay": "#5752791a", - "title_bar": "#00000000", - "title_bar_inactive": "#faf4ed", - "window_border": "#cecacd", - "border": "#cecacd", - "border_variant": "#dfdad9", - "border_focused": "#286983", - "border_selected": "#286983", - "border_transparent": "#00000000", - "border_disabled": "#f4ede8", - "ring": "#286983", - "text": "#575279", - "text_muted": "#797593", - "text_placeholder": "#9893a5", - "text_accent": "#907aa9", - "icon": "#575279", - "icon_muted": "#797593", - "icon_accent": "#907aa9", - "element_foreground": "#faf4ed", - "element_background": "#286983", - "element_hover": "#286983e6", - "element_active": "#245f76", - "element_selected": "#205569", - "element_disabled": "#2869834d", - "secondary_foreground": "#286983", - "secondary_background": "#f4ede8", - "secondary_hover": "#2869831a", - "secondary_active": "#dfdad9", - "secondary_selected": "#dfdad9", - "secondary_disabled": "#2869834d", - "danger_foreground": "#faf4ed", - "danger_background": "#b4637a", - "danger_hover": "#b4637ae6", - "danger_active": "#a2596e", - "danger_selected": "#904f62", - "danger_disabled": "#b4637a4d", - "warning_foreground": "#faf4ed", - "warning_background": "#ea9d34", - "warning_hover": "#ea9d34e6", - "warning_active": "#d38d2f", - "warning_selected": "#bc7d2a", - "warning_disabled": "#ea9d344d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#f4ede8", - "ghost_element_hover": "#5752791a", - "ghost_element_active": "#dfdad9", - "ghost_element_selected": "#dfdad9", - "ghost_element_disabled": "#5752790d", - "tab_inactive_background": "#f4ede8", - "tab_hover_background": "#dfdad9", - "tab_active_background": "#cecacd", - "scrollbar_thumb_background": "#57527933", - "scrollbar_thumb_hover_background": "#5752794d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#dfdad9", - "drop_target_background": "#2869831a", - "cursor": "#56949f", - "selection": "#56949f40" - }, - "dark": { - "background": "#faf4ed", - "surface_background": "#fffaf3", - "elevated_surface_background": "#f2e9e1", - "panel_background": "#fffaf3", - "overlay": "#5752791a", - "title_bar": "#00000000", - "title_bar_inactive": "#faf4ed", - "window_border": "#cecacd", - "border": "#cecacd", - "border_variant": "#dfdad9", - "border_focused": "#286983", - "border_selected": "#286983", - "border_transparent": "#00000000", - "border_disabled": "#f4ede8", - "ring": "#286983", - "text": "#575279", - "text_muted": "#797593", - "text_placeholder": "#9893a5", - "text_accent": "#907aa9", - "icon": "#575279", - "icon_muted": "#797593", - "icon_accent": "#907aa9", - "element_foreground": "#faf4ed", - "element_background": "#286983", - "element_hover": "#286983e6", - "element_active": "#245f76", - "element_selected": "#205569", - "element_disabled": "#2869834d", - "secondary_foreground": "#286983", - "secondary_background": "#f4ede8", - "secondary_hover": "#2869831a", - "secondary_active": "#dfdad9", - "secondary_selected": "#dfdad9", - "secondary_disabled": "#2869834d", - "danger_foreground": "#faf4ed", - "danger_background": "#b4637a", - "danger_hover": "#b4637ae6", - "danger_active": "#a2596e", - "danger_selected": "#904f62", - "danger_disabled": "#b4637a4d", - "warning_foreground": "#faf4ed", - "warning_background": "#ea9d34", - "warning_hover": "#ea9d34e6", - "warning_active": "#d38d2f", - "warning_selected": "#bc7d2a", - "warning_disabled": "#ea9d344d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#f4ede8", - "ghost_element_hover": "#5752791a", - "ghost_element_active": "#dfdad9", - "ghost_element_selected": "#dfdad9", - "ghost_element_disabled": "#5752790d", - "tab_inactive_background": "#f4ede8", - "tab_hover_background": "#dfdad9", - "tab_active_background": "#cecacd", - "scrollbar_thumb_background": "#57527933", - "scrollbar_thumb_hover_background": "#5752794d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#dfdad9", - "drop_target_background": "#2869831a", - "cursor": "#56949f", - "selection": "#56949f40" - } -} diff --git a/assets/themes/rose-pine-moon.json b/assets/themes/rose-pine-moon.json deleted file mode 100644 index 102299b..0000000 --- a/assets/themes/rose-pine-moon.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "id": "rose-pine-moon", - "name": "Rosé Pine Moon", - "author": "Rosé Pine", - "url": "https://rosepinetheme.com/", - "light": { - "background": "#232136", - "surface_background": "#2a273f", - "elevated_surface_background": "#393552", - "panel_background": "#2a273f", - "overlay": "#e0def41a", - "title_bar": "#00000000", - "title_bar_inactive": "#232136", - "window_border": "#56526e", - "border": "#56526e", - "border_variant": "#44415a", - "border_focused": "#3e8fb0", - "border_selected": "#3e8fb0", - "border_transparent": "#00000000", - "border_disabled": "#2a283e", - "ring": "#3e8fb0", - "text": "#e0def4", - "text_muted": "#908caa", - "text_placeholder": "#6e6a86", - "text_accent": "#c4a7e7", - "icon": "#e0def4", - "icon_muted": "#908caa", - "icon_accent": "#c4a7e7", - "element_foreground": "#232136", - "element_background": "#3e8fb0", - "element_hover": "#3e8fb0e6", - "element_active": "#38809d", - "element_selected": "#32718a", - "element_disabled": "#3e8fb04d", - "secondary_foreground": "#3e8fb0", - "secondary_background": "#2a283e", - "secondary_hover": "#3e8fb01a", - "secondary_active": "#44415a", - "secondary_selected": "#44415a", - "secondary_disabled": "#3e8fb04d", - "danger_foreground": "#232136", - "danger_background": "#eb6f92", - "danger_hover": "#eb6f92e6", - "danger_active": "#d46483", - "danger_selected": "#bd5974", - "danger_disabled": "#eb6f924d", - "warning_foreground": "#232136", - "warning_background": "#f6c177", - "warning_hover": "#f6c177e6", - "warning_active": "#ddae6b", - "warning_selected": "#c49b5f", - "warning_disabled": "#f6c1774d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#2a283e", - "ghost_element_hover": "#e0def41a", - "ghost_element_active": "#44415a", - "ghost_element_selected": "#44415a", - "ghost_element_disabled": "#e0def40d", - "tab_inactive_background": "#2a283e", - "tab_hover_background": "#44415a", - "tab_active_background": "#56526e", - "scrollbar_thumb_background": "#e0def433", - "scrollbar_thumb_hover_background": "#e0def44d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#44415a", - "drop_target_background": "#3e8fb01a", - "cursor": "#9ccfd8", - "selection": "#9ccfd840" - }, - "dark": { - "background": "#232136", - "surface_background": "#2a273f", - "elevated_surface_background": "#393552", - "panel_background": "#2a273f", - "overlay": "#e0def41a", - "title_bar": "#00000000", - "title_bar_inactive": "#232136", - "window_border": "#56526e", - "border": "#56526e", - "border_variant": "#44415a", - "border_focused": "#3e8fb0", - "border_selected": "#3e8fb0", - "border_transparent": "#00000000", - "border_disabled": "#2a283e", - "ring": "#3e8fb0", - "text": "#e0def4", - "text_muted": "#908caa", - "text_placeholder": "#6e6a86", - "text_accent": "#c4a7e7", - "icon": "#e0def4", - "icon_muted": "#908caa", - "icon_accent": "#c4a7e7", - "element_foreground": "#232136", - "element_background": "#3e8fb0", - "element_hover": "#3e8fb0e6", - "element_active": "#38809d", - "element_selected": "#32718a", - "element_disabled": "#3e8fb04d", - "secondary_foreground": "#3e8fb0", - "secondary_background": "#2a283e", - "secondary_hover": "#3e8fb01a", - "secondary_active": "#44415a", - "secondary_selected": "#44415a", - "secondary_disabled": "#3e8fb04d", - "danger_foreground": "#232136", - "danger_background": "#eb6f92", - "danger_hover": "#eb6f92e6", - "danger_active": "#d46483", - "danger_selected": "#bd5974", - "danger_disabled": "#eb6f924d", - "warning_foreground": "#232136", - "warning_background": "#f6c177", - "warning_hover": "#f6c177e6", - "warning_active": "#ddae6b", - "warning_selected": "#c49b5f", - "warning_disabled": "#f6c1774d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#2a283e", - "ghost_element_hover": "#e0def41a", - "ghost_element_active": "#44415a", - "ghost_element_selected": "#44415a", - "ghost_element_disabled": "#e0def40d", - "tab_inactive_background": "#2a283e", - "tab_hover_background": "#44415a", - "tab_active_background": "#56526e", - "scrollbar_thumb_background": "#e0def433", - "scrollbar_thumb_hover_background": "#e0def44d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#44415a", - "drop_target_background": "#3e8fb01a", - "cursor": "#9ccfd8", - "selection": "#9ccfd840" - } -} diff --git a/assets/themes/rose-pine.json b/assets/themes/rose-pine.json deleted file mode 100644 index 7715730..0000000 --- a/assets/themes/rose-pine.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "id": "rose-pine", - "name": "Rosé Pine", - "author": "Rosé Pine", - "url": "https://rosepinetheme.com/", - "light": { - "background": "#191724", - "surface_background": "#1f1d2e", - "elevated_surface_background": "#26233a", - "panel_background": "#1f1d2e", - "overlay": "#e0def41a", - "title_bar": "#00000000", - "title_bar_inactive": "#191724", - "window_border": "#524f67", - "border": "#524f67", - "border_variant": "#403d52", - "border_focused": "#31748f", - "border_selected": "#31748f", - "border_transparent": "#00000000", - "border_disabled": "#21202e", - "ring": "#31748f", - "text": "#e0def4", - "text_muted": "#908caa", - "text_placeholder": "#6e6a86", - "text_accent": "#c4a7e7", - "icon": "#e0def4", - "icon_muted": "#908caa", - "icon_accent": "#c4a7e7", - "element_foreground": "#191724", - "element_background": "#31748f", - "element_hover": "#31748fe6", - "element_active": "#2c6980", - "element_selected": "#275e71", - "element_disabled": "#31748f4d", - "secondary_foreground": "#31748f", - "secondary_background": "#21202e", - "secondary_hover": "#31748f1a", - "secondary_active": "#403d52", - "secondary_selected": "#403d52", - "secondary_disabled": "#31748f4d", - "danger_foreground": "#191724", - "danger_background": "#eb6f92", - "danger_hover": "#eb6f92e6", - "danger_active": "#d46483", - "danger_selected": "#bd5974", - "danger_disabled": "#eb6f924d", - "warning_foreground": "#191724", - "warning_background": "#f6c177", - "warning_hover": "#f6c177e6", - "warning_active": "#ddae6b", - "warning_selected": "#c49b5f", - "warning_disabled": "#f6c1774d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#21202e", - "ghost_element_hover": "#e0def41a", - "ghost_element_active": "#403d52", - "ghost_element_selected": "#403d52", - "ghost_element_disabled": "#e0def40d", - "tab_inactive_background": "#21202e", - "tab_hover_background": "#403d52", - "tab_active_background": "#524f67", - "scrollbar_thumb_background": "#e0def433", - "scrollbar_thumb_hover_background": "#e0def44d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#403d52", - "drop_target_background": "#31748f1a", - "cursor": "#9ccfd8", - "selection": "#9ccfd840" - }, - "dark": { - "background": "#191724", - "surface_background": "#1f1d2e", - "elevated_surface_background": "#26233a", - "panel_background": "#1f1d2e", - "overlay": "#e0def41a", - "title_bar": "#00000000", - "title_bar_inactive": "#191724", - "window_border": "#524f67", - "border": "#524f67", - "border_variant": "#403d52", - "border_focused": "#31748f", - "border_selected": "#31748f", - "border_transparent": "#00000000", - "border_disabled": "#21202e", - "ring": "#31748f", - "text": "#e0def4", - "text_muted": "#908caa", - "text_placeholder": "#6e6a86", - "text_accent": "#c4a7e7", - "icon": "#e0def4", - "icon_muted": "#908caa", - "icon_accent": "#c4a7e7", - "element_foreground": "#191724", - "element_background": "#31748f", - "element_hover": "#31748fe6", - "element_active": "#2c6980", - "element_selected": "#275e71", - "element_disabled": "#31748f4d", - "secondary_foreground": "#31748f", - "secondary_background": "#21202e", - "secondary_hover": "#31748f1a", - "secondary_active": "#403d52", - "secondary_selected": "#403d52", - "secondary_disabled": "#31748f4d", - "danger_foreground": "#191724", - "danger_background": "#eb6f92", - "danger_hover": "#eb6f92e6", - "danger_active": "#d46483", - "danger_selected": "#bd5974", - "danger_disabled": "#eb6f924d", - "warning_foreground": "#191724", - "warning_background": "#f6c177", - "warning_hover": "#f6c177e6", - "warning_active": "#ddae6b", - "warning_selected": "#c49b5f", - "warning_disabled": "#f6c1774d", - "ghost_element_background": "#00000000", - "ghost_element_background_alt": "#21202e", - "ghost_element_hover": "#e0def41a", - "ghost_element_active": "#403d52", - "ghost_element_selected": "#403d52", - "ghost_element_disabled": "#e0def40d", - "tab_inactive_background": "#21202e", - "tab_hover_background": "#403d52", - "tab_active_background": "#524f67", - "scrollbar_thumb_background": "#e0def433", - "scrollbar_thumb_hover_background": "#e0def44d", - "scrollbar_thumb_border": "#00000000", - "scrollbar_track_background": "#00000000", - "scrollbar_track_border": "#403d52", - "drop_target_background": "#31748f1a", - "cursor": "#9ccfd8", - "selection": "#9ccfd840" - } -} diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 9cad5e3..f10eb8a 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -588,6 +588,7 @@ impl ChatRegistry { room.update(cx, |this, cx| { this.push_message(message, cx); }); + self.sort(cx); } None => { // Push the new room to the front of the list diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index 029debd..c54d5d8 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -1,8 +1,9 @@ use std::cmp::Ordering; +use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error}; +use anyhow::{anyhow, Error}; use common::EventUtils; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task}; use itertools::Itertools; @@ -11,7 +12,10 @@ use person::{Person, PersonRegistry}; use settings::{RoomConfig, SignerKind}; use state::{NostrRegistry, TIMEOUT}; -use crate::{ChatRegistry, NewMessage}; +use crate::NewMessage; + +const NO_DEKEY: &str = "User hasn't set up a decoupled encryption key yet."; +const USER_NO_DEKEY: &str = "You haven't set up a decoupled encryption key or it's not available."; #[derive(Debug, Clone)] pub struct SendReport { @@ -222,6 +226,17 @@ impl Room { cx.notify(); } + /// Updates the signer kind config for the room + pub fn set_signer_kind(&mut self, kind: &SignerKind, cx: &mut Context) { + self.config.set_signer_kind(kind); + cx.notify(); + } + + /// Returns the config of the room + pub fn config(&self) -> &RoomConfig { + &self.config + } + /// Returns the members of the room pub fn members(&self) -> Vec { self.members.clone() @@ -296,10 +311,6 @@ impl Room { if new_message { self.set_created_at(created_at, cx); - // Sort chats after emitting a new message - ChatRegistry::global(cx).update(cx, |this, cx| { - this.sort(cx); - }); } } @@ -308,49 +319,88 @@ impl Room { cx.emit(RoomEvent::Reload); } + #[allow(clippy::type_complexity)] /// Get gossip relays for each member - pub fn early_connect(&self, cx: &App) -> Task> { + pub fn connect(&self, cx: &App) -> HashMap>> { let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); + let signer = nostr.read(cx).signer(); + let public_key = signer.public_key().unwrap(); let members = self.members(); - let subscription_id = SubscriptionId::new(format!("room-{}", self.id)); + let mut tasks = HashMap::new(); - cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - - for member in members.into_iter() { - if member == public_key { - continue; - }; - - // Construct a filter for messaging relays - let inbox = Filter::new() - .kind(Kind::InboxRelays) - .author(member) - .limit(1); - - // Construct a filter for announcement - let announcement = Filter::new() - .kind(Kind::Custom(10044)) - .author(member) - .limit(1); - - // Subscribe to get member's gossip relays - client - .subscribe(vec![inbox, announcement]) - .with_id(subscription_id.clone()) - .close_on( - SubscribeAutoCloseOptions::default() - .timeout(Some(Duration::from_secs(TIMEOUT))) - .exit_policy(ReqExitPolicy::ExitOnEOSE), - ) - .await?; + for member in members.into_iter() { + // Skip if member is the current user + if member == public_key { + continue; } - Ok(()) - }) + let client = nostr.read(cx).client(); + let write_relays = nostr.read(cx).write_relays(&member, cx); + + tasks.insert( + member, + cx.background_spawn(async move { + let urls = write_relays.await; + + // Return if no relays are available + if urls.is_empty() { + return Err(anyhow!( + "User has not set up any relays. You cannot send messages to them." + )); + } + + // Construct filters for inbox and announcement + let inbox_filter = Filter::new() + .kind(Kind::InboxRelays) + .author(member) + .limit(1); + let announcement_filter = Filter::new() + .kind(Kind::Custom(10044)) + .author(member) + .limit(1); + + // Create subscription targets + let target = urls + .into_iter() + .map(|relay| { + ( + relay, + vec![inbox_filter.clone(), announcement_filter.clone()], + ) + }) + .collect::>(); + + // Stream events from user's write relays + let mut stream = client + .stream_events(target) + .timeout(Duration::from_secs(TIMEOUT)) + .await?; + + let mut has_inbox = false; + let mut has_announcement = false; + + while let Some((_url, res)) = stream.next().await { + let event = res?; + + match event.kind { + Kind::InboxRelays => has_inbox = true, + Kind::Custom(10044) => has_announcement = true, + _ => {} + } + + // Early exit if both flags are found + if has_inbox && has_announcement { + break; + } + } + + Ok((has_inbox, has_announcement)) + }), + ); + } + + tasks } /// Get all messages belonging to the room @@ -418,11 +468,6 @@ impl Room { // Add all receiver tags for member in members.into_iter() { - // Skip current user - if member.public_key() == sender { - continue; - } - tags.push(Tag::from_standardized_without_cell( TagStandard::PublicKey { public_key: member.public_key(), @@ -445,61 +490,59 @@ impl Room { /// Send rumor event to all members's messaging relays pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option>> { + let config = self.config.clone(); let persons = PersonRegistry::global(cx); let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); - // Get room's config - let config = self.config.clone(); - // Get current user's public key - let sender = nostr.read(cx).signer().public_key()?; + let public_key = nostr.read(cx).signer().public_key()?; + let sender = persons.read(cx).get(&public_key, cx); // Get all members (excluding sender) let members: Vec = self .members .iter() - .filter(|public_key| public_key != &&sender) + .filter(|public_key| public_key != &&sender.public_key()) .map(|member| persons.read(cx).get(member, cx)) .collect(); Some(cx.background_spawn(async move { let signer_kind = config.signer_kind(); + let backup = config.backup(); + let user_signer = signer.get().await; let encryption_signer = signer.get_encryption_signer().await; + let mut sents = 0; let mut reports = Vec::new(); + // Process each member for member in members { let relays = member.messaging_relays(); let announcement = member.announcement(); + let public_key = member.public_key(); - // Skip if member has no messaging relays if relays.is_empty() { - reports.push(SendReport::new(member.public_key()).error("No messaging relays")); + reports.push(SendReport::new(public_key).error("No messaging relays")); continue; } - // Ensure relay connections - for url in relays.iter() { - client - .add_relay(url) - .and_connect() - .capabilities(RelayCapabilities::GOSSIP) - .await - .ok(); + // Handle encryption signer requirements + if signer_kind.encryption() { + if announcement.is_none() { + reports.push(SendReport::new(public_key).error(NO_DEKEY)); + continue; + } + if encryption_signer.is_none() { + reports.push(SendReport::new(sender.public_key()).error(USER_NO_DEKEY)); + continue; + } } - // When forced to use encryption signer, skip if receiver has no announcement - if signer_kind.encryption() && announcement.is_none() { - reports - .push(SendReport::new(member.public_key()).error("Encryption not found")); - continue; - } - - // Determine receiver and signer based on signer kind - let (receiver, signer_to_use) = match signer_kind { + // Determine receiver and signer + let (receiver, signer) = match signer_kind { SignerKind::Auto => { if let Some(announcement) = announcement { if let Some(enc_signer) = encryption_signer.as_ref() { @@ -512,272 +555,77 @@ impl Room { } } SignerKind::Encryption => { - let Some(encryption_signer) = encryption_signer.as_ref() else { - reports.push( - SendReport::new(member.public_key()).error("Encryption not found"), - ); - continue; - }; - let Some(announcement) = announcement else { - reports.push( - SendReport::new(member.public_key()) - .error("Announcement not found"), - ); - continue; - }; - (announcement.public_key(), encryption_signer.clone()) + // Safe to unwrap due to earlier checks + ( + announcement.unwrap().public_key(), + encryption_signer.as_ref().unwrap().clone(), + ) } SignerKind::User => (member.public_key(), user_signer.clone()), }; - // Create and send gift-wrapped event - match EventBuilder::gift_wrap(&signer_to_use, &receiver, rumor.clone(), []).await { - Ok(event) => { - match client - .send_event(&event) - .to(relays) - .ack_policy(AckPolicy::none()) - .await - { - Ok(output) => { - reports.push( - SendReport::new(member.public_key()) - .gift_wrap_id(event.id) - .output(output), - ); - } - Err(e) => { - reports.push( - SendReport::new(member.public_key()).error(e.to_string()), - ); - } - } - } - Err(e) => { - reports.push(SendReport::new(member.public_key()).error(e.to_string())); + match send_gift_wrap(&client, &signer, &receiver, &rumor, relays, public_key).await + { + Ok((report, _)) => { + reports.push(report); + sents += 1; } + Err(report) => reports.push(report), + } + } + + // Send backup to current user if needed + if backup && sents >= 1 { + let relays = sender.messaging_relays(); + let public_key = sender.public_key(); + let signer = encryption_signer.as_ref().unwrap_or(&user_signer); + + match send_gift_wrap(&client, signer, &public_key, &rumor, relays, public_key).await + { + Ok((report, _)) => reports.push(report), + Err(report) => reports.push(report), } } reports })) } - - /* - * /// Create a new unsigned message event - pub fn create_message( - &self, - content: &str, - replies: Vec, - cx: &App, - ) -> Task> { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - let subject = self.subject.clone(); - let content = content.to_string(); - - let mut member_and_relay_hints = HashMap::new(); - - // Populate the hashmap with member and relay hint tasks - for member in self.members.iter() { - let hint = nostr.read(cx).relay_hint(member, cx); - member_and_relay_hints.insert(member.to_owned(), hint); - } - - cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - - // List of event tags for each receiver - let mut tags = vec![]; - - for (member, task) in member_and_relay_hints.into_iter() { - // Skip current user - if member == public_key { - continue; - } - - // Get relay hint if available - let relay_url = task.await; - - // Construct a public key tag with relay hint - let tag = TagStandard::PublicKey { - public_key: member, - relay_url, - alias: None, - uppercase: false, - }; - - tags.push(Tag::from_standardized_without_cell(tag)); - } - - // Add subject tag if present - if let Some(value) = subject { - tags.push(Tag::from_standardized_without_cell(TagStandard::Subject( - value.to_string(), - ))); - } - - // Add all reply tags - for id in replies { - tags.push(Tag::event(id)) - } - - // Construct a direct message event - // - // WARNING: never sign and send this event to relays - let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content) - .tags(tags) - .build(public_key); - - // Ensure the event ID has been generated - event.ensure_id(); - - Ok(event) - }) - } - - /// Create a task to send a message to all room members - pub fn send_message( - &self, - rumor: &UnsignedEvent, - cx: &App, - ) -> Task, Error>> { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - let mut members = self.members(); - let rumor = rumor.to_owned(); - - cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let current_user = signer.get_public_key().await?; - - // Remove the current user's public key from the list of receivers - // the current user will be handled separately - members.retain(|this| this != ¤t_user); - - // Collect the send reports - let mut reports: Vec = vec![]; - - for receiver in members.into_iter() { - // Construct the gift wrap event - let event = - EventBuilder::gift_wrap(signer, &receiver, rumor.clone(), vec![]).await?; - - // Send the gift wrap event to the messaging relays - match client.send_event(&event).to_nip17().await { - Ok(output) => { - let id = output.id().to_owned(); - let auth = output.failed.iter().any(|(_, s)| s.starts_with("auth-")); - let report = SendReport::new(receiver).status(output); - let tracker = tracker().read().await; - - if auth { - // Wait for authenticated and resent event successfully - for attempt in 0..=SEND_RETRY { - // Check if event was successfully resent - if tracker.is_sent_by_coop(&id) { - let output = Output::new(id); - let report = SendReport::new(receiver).status(output); - reports.push(report); - break; - } - - // Check if retry limit exceeded - if attempt == SEND_RETRY { - reports.push(report); - break; - } - - smol::Timer::after(Duration::from_millis(1200)).await; - } - } else { - reports.push(report); - } - } - Err(e) => { - reports.push(SendReport::new(receiver).error(e.to_string())); - } - } - } - - // Construct the gift-wrapped event - let event = - EventBuilder::gift_wrap(signer, ¤t_user, rumor.clone(), vec![]).await?; - - // Only send a backup message to current user if sent successfully to others - if reports.iter().all(|r| r.is_sent_success()) { - // Send the event to the messaging relays - match client.send_event(&event).to_nip17().await { - Ok(output) => { - reports.push(SendReport::new(current_user).status(output)); - } - Err(e) => { - reports.push(SendReport::new(current_user).error(e.to_string())); - } - } - } else { - reports.push(SendReport::new(current_user).on_hold(event)); - } - - Ok(reports) - }) - } - - /// Create a task to resend a failed message - pub fn resend_message( - &self, - reports: Vec, - cx: &App, - ) -> Task, Error>> { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - cx.background_spawn(async move { - let mut resend_reports = vec![]; - - for report in reports.into_iter() { - let receiver = report.receiver; - - // Process failed events - if let Some(output) = report.status { - let id = output.id(); - let urls: Vec<&RelayUrl> = output.failed.keys().collect(); - - if let Some(event) = client.database().event_by_id(id).await? { - for url in urls.into_iter() { - let relay = client.relay(url).await?.context("Relay not found")?; - let id = relay.send_event(&event).await?; - - let resent: Output = Output { - val: id, - success: HashSet::from([url.to_owned()]), - failed: HashMap::new(), - }; - - resend_reports.push(SendReport::new(receiver).status(resent)); - } - } - } - - // Process the on hold event if it exists - if let Some(event) = report.on_hold { - // Send the event to the messaging relays - match client.send_event(&event).await { - Ok(output) => { - resend_reports.push(SendReport::new(receiver).status(output)); - } - Err(e) => { - resend_reports.push(SendReport::new(receiver).error(e.to_string())); - } - } - } - } - - Ok(resend_reports) - }) - } - */ +} + +// Helper function to send a gift-wrapped event +async fn send_gift_wrap( + client: &Client, + signer: &T, + receiver: &PublicKey, + rumor: &UnsignedEvent, + relays: &[RelayUrl], + public_key: PublicKey, +) -> Result<(SendReport, bool), SendReport> +where + T: NostrSigner + 'static, +{ + // Ensure relay connections + for url in relays { + client.add_relay(url).and_connect().await.ok(); + } + + match EventBuilder::gift_wrap(signer, receiver, rumor.clone(), []).await { + Ok(event) => { + match client + .send_event(&event) + .to(relays) + .ack_policy(AckPolicy::none()) + .await + { + Ok(output) => Ok(( + SendReport::new(public_key) + .gift_wrap_id(event.id) + .output(output), + true, + )), + Err(e) => Err(SendReport::new(public_key).error(e.to_string())), + } + } + Err(e) => Err(SendReport::new(public_key).error(e.to_string())), + } } diff --git a/crates/chat_ui/src/actions.rs b/crates/chat_ui/src/actions.rs index ab28139..5e2c267 100644 --- a/crates/chat_ui/src/actions.rs +++ b/crates/chat_ui/src/actions.rs @@ -1,12 +1,14 @@ use gpui::Action; use nostr_sdk::prelude::*; use serde::Deserialize; +use settings::SignerKind; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = chat, no_json)] pub enum Command { Insert(&'static str), ChangeSubject(&'static str), + ChangeSigner(SignerKind), } #[derive(Action, Clone, PartialEq, Eq, Deserialize)] diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 72d1229..499dd82 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -17,7 +17,7 @@ use gpui_tokio::Tokio; use itertools::Itertools; use nostr_sdk::prelude::*; use person::{Person, PersonRegistry}; -use settings::AppSettings; +use settings::{AppSettings, SignerKind}; use smallvec::{smallvec, SmallVec}; use smol::fs; use smol::lock::RwLock; @@ -41,6 +41,11 @@ use crate::text::RenderedText; mod actions; mod text; +const NO_INBOX: &str = "has not set up messaging relays. \ + They will not receive your messages."; +const NO_ANNOUNCEMENT: &str = "has not set up an encryption key. \ + You cannot send messages encrypted with an encryption key to them yet."; + pub fn init(room: WeakEntity, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| ChatPanel::new(room, window, cx)) } @@ -225,12 +230,43 @@ impl ChatPanel { } /// Get all necessary data for each member - fn connect(&mut self, _window: &mut Window, cx: &mut Context) { - let Ok(connect) = self.room.read_with(cx, |this, cx| this.early_connect(cx)) else { + fn connect(&mut self, window: &mut Window, cx: &mut Context) { + let Ok(tasks) = self.room.read_with(cx, |this, cx| this.connect(cx)) else { return; }; - self.tasks.push(cx.background_spawn(connect)); + self.tasks.push(cx.spawn_in(window, async move |this, cx| { + for (member, task) in tasks.into_iter() { + match task.await { + Ok((has_inbox, has_announcement)) => { + this.update(cx, |this, cx| { + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get(&member, cx); + + if !has_inbox { + let content = format!("{} {}", profile.name(), NO_INBOX); + let message = Message::warning(content); + + this.insert_message(message, true, cx); + } + + if !has_announcement { + let content = format!("{} {}", profile.name(), NO_ANNOUNCEMENT); + let message = Message::warning(content); + + this.insert_message(message, true, cx); + } + })?; + } + Err(e) => { + this.update(cx, |this, cx| { + this.insert_message(Message::warning(e.to_string()), true, cx); + })?; + } + }; + } + Ok(()) + })); } /// Load all messages belonging to this room @@ -339,6 +375,7 @@ impl ChatPanel { }; self.tasks.push(cx.spawn_in(window, async move |this, cx| { + // Send and get reports let outputs = task.await; // Add sent IDs to the list @@ -559,6 +596,36 @@ impl ChatPanel { persons.read(cx).get(public_key, cx) } + fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context) { + match command { + Command::Insert(content) => { + self.send_message(content, window, cx); + } + Command::ChangeSubject(subject) => { + if self + .room + .update(cx, |this, cx| { + this.set_subject(*subject, cx); + }) + .is_err() + { + window.push_notification(Notification::error("Failed to change subject"), cx); + } + } + Command::ChangeSigner(kind) => { + if self + .room + .update(cx, |this, cx| { + this.set_signer_kind(kind, cx); + }) + .is_err() + { + window.push_notification(Notification::error("Failed to change signer"), cx); + } + } + } + } + fn render_announcement(&self, ix: usize, cx: &Context) -> AnyElement { const MSG: &str = "This conversation is private. Only members can see each other's messages."; @@ -1133,23 +1200,60 @@ impl ChatPanel { items } - fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context) { - match command { - Command::Insert(content) => { - self.send_message(content, window, cx); - } - Command::ChangeSubject(subject) => { - if self - .room - .update(cx, |this, cx| { - this.set_subject(*subject, cx); - }) - .is_err() - { - window.push_notification(Notification::error("Failed to change subject"), cx); - } - } - } + fn render_encryption_menu(&self, _window: &mut Window, cx: &Context) -> impl IntoElement { + let signer_kind = self + .room + .read_with(cx, |this, _cx| this.config().signer_kind().clone()) + .ok() + .unwrap_or_default(); + + Button::new("encryption") + .icon(IconName::UserKey) + .ghost() + .large() + .dropdown_menu(move |this, _window, _cx| { + let auto = matches!(signer_kind, SignerKind::Auto); + let encryption = matches!(signer_kind, SignerKind::Encryption); + let user = matches!(signer_kind, SignerKind::User); + + this.check_side(ui::Side::Right) + .menu_with_check_and_disabled( + "Auto", + auto, + Box::new(Command::ChangeSigner(SignerKind::Auto)), + auto, + ) + .menu_with_check_and_disabled( + "Decoupled Encryption Key", + encryption, + Box::new(Command::ChangeSigner(SignerKind::Encryption)), + encryption, + ) + .menu_with_check_and_disabled( + "User Identity", + user, + Box::new(Command::ChangeSigner(SignerKind::User)), + user, + ) + }) + } + + fn render_emoji_menu(&self, _window: &Window, _cx: &Context) -> impl IntoElement { + Button::new("emoji") + .icon(IconName::Emoji) + .ghost() + .large() + .dropdown_menu_with_anchor(gpui::Corner::BottomLeft, move |this, _window, _cx| { + this.horizontal() + .menu("👍", Box::new(Command::Insert("👍"))) + .menu("👎", Box::new(Command::Insert("👎"))) + .menu("😄", Box::new(Command::Insert("😄"))) + .menu("🎉", Box::new(Command::Insert("🎉"))) + .menu("😕", Box::new(Command::Insert("😕"))) + .menu("❤️", Box::new(Command::Insert("❤️"))) + .menu("🚀", Box::new(Command::Insert("🚀"))) + .menu("👀", Box::new(Command::Insert("👀"))) + }) } } @@ -1235,26 +1339,8 @@ impl Render for ChatPanel { h_flex() .pl_1() .gap_1() - .child( - Button::new("emoji") - .icon(IconName::Emoji) - .ghost() - .large() - .dropdown_menu_with_anchor( - gpui::Corner::BottomLeft, - move |this, _window, _cx| { - this.horizontal() - .menu("👍", Box::new(Command::Insert("👍"))) - .menu("👎", Box::new(Command::Insert("👎"))) - .menu("😄", Box::new(Command::Insert("😄"))) - .menu("🎉", Box::new(Command::Insert("🎉"))) - .menu("😕", Box::new(Command::Insert("😕"))) - .menu("❤️", Box::new(Command::Insert("❤️"))) - .menu("🚀", Box::new(Command::Insert("🚀"))) - .menu("👀", Box::new(Command::Insert("👀"))) - }, - ), - ) + .child(self.render_encryption_menu(window, cx)) + .child(self.render_emoji_menu(window, cx)) .child( Button::new("send") .icon(IconName::PaperPlaneFill) diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index cbb8f8f..9c54cc1 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -16,7 +16,7 @@ use nostr_sdk::prelude::*; use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; use state::{NostrRegistry, FIND_DELAY}; -use theme::{ActiveTheme, TITLEBAR_HEIGHT}; +use theme::{ActiveTheme, TABBAR_HEIGHT}; use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::indicator::Indicator; @@ -497,7 +497,7 @@ impl Render for Sidebar { .gap_2() .child( h_flex() - .h(TITLEBAR_HEIGHT) + .h(TABBAR_HEIGHT) .border_b_1() .border_color(cx.theme().border_variant) .bg(cx.theme().elevated_surface_background) diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 977d97b..8039a6b 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -80,13 +80,25 @@ pub struct RoomConfig { } impl RoomConfig { + /// Get backup config pub fn backup(&self) -> bool { self.backup } + /// Get signer kind config pub fn signer_kind(&self) -> &SignerKind { &self.signer_kind } + + /// Set backup config + pub fn set_backup(&mut self, backup: bool) { + self.backup = backup; + } + + /// Set signer kind config + pub fn set_signer_kind(&mut self, kind: &SignerKind) { + self.signer_kind = kind.to_owned(); + } } /// Settings diff --git a/crates/state/src/constants.rs b/crates/state/src/constants.rs index 2c0d539..13585e1 100644 --- a/crates/state/src/constants.rs +++ b/crates/state/src/constants.rs @@ -42,7 +42,7 @@ pub const SEARCH_RELAYS: [&str; 1] = ["wss://antiprimal.net"]; /// Default bootstrap relays pub const BOOTSTRAP_RELAYS: [&str; 3] = [ "wss://relay.damus.io", - "wss://relay.primal.net", + "wss://nos.lol", "wss://user.kindpag.es", ]; diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index a19255a..0f20266 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -176,11 +176,7 @@ impl NostrRegistry { while let Some(notification) = notifications.next().await { if let ClientNotification::Message { - message: - RelayMessage::Event { - event, - subscription_id, - }, + message: RelayMessage::Event { event, .. }, .. } = notification { @@ -191,11 +187,6 @@ impl NostrRegistry { match event.kind { Kind::RelayList => { - // Automatically get messaging relays for each member when the user opens a room - if subscription_id.as_str().starts_with("room-") { - get_adv_events_by(&client, event.as_ref()).await?; - } - tx.send_async(event.into_owned()).await?; } Kind::InboxRelays => { @@ -773,53 +764,6 @@ impl NostrRegistry { } } -/// Automatically get messaging relays and encryption announcement from a received relay list -async fn get_adv_events_by(client: &Client, event: &Event) -> Result<(), Error> { - // Subscription options - let opts = SubscribeAutoCloseOptions::default() - .timeout(Some(Duration::from_secs(TIMEOUT))) - .exit_policy(ReqExitPolicy::ExitOnEOSE); - - // Extract write relays from event - let write_relays: Vec<&RelayUrl> = nip65::extract_relay_list(event) - .filter_map(|(url, metadata)| { - if metadata.is_none() || metadata == &Some(RelayMetadata::Write) { - Some(url) - } else { - None - } - }) - .collect(); - - // Ensure relay connections - for relay in write_relays.iter() { - client.add_relay(*relay).await?; - client.connect_relay(*relay).await?; - } - - // Construct filter for inbox relays - let inbox = Filter::new() - .kind(Kind::InboxRelays) - .author(event.pubkey) - .limit(1); - - // Construct filter for encryption announcement - let announcement = Filter::new() - .kind(Kind::Custom(10044)) - .author(event.pubkey) - .limit(1); - - // Construct target for subscription - let target = write_relays - .into_iter() - .map(|relay| (relay, vec![inbox.clone(), announcement.clone()])) - .collect::>(); - - client.subscribe(target).close_on(opts).await?; - - Ok(()) -} - /// Get or create a new app keys fn get_or_init_app_keys() -> Result { let dir = config_dir().join(".app_keys"); @@ -857,15 +801,15 @@ fn default_relay_list() -> Vec<(RelayUrl, Option)> { Some(RelayMetadata::Write), ), ( - RelayUrl::parse("wss://relay.primal.net/").unwrap(), + RelayUrl::parse("wss://relay.primal.net").unwrap(), Some(RelayMetadata::Write), ), ( - RelayUrl::parse("wss://relay.damus.io/").unwrap(), + RelayUrl::parse("wss://relay.damus.io").unwrap(), Some(RelayMetadata::Read), ), ( - RelayUrl::parse("wss://nos.lol/").unwrap(), + RelayUrl::parse("wss://nos.lol").unwrap(), Some(RelayMetadata::Read), ), ] @@ -873,8 +817,8 @@ fn default_relay_list() -> Vec<(RelayUrl, Option)> { fn default_messaging_relays() -> Vec { vec![ - //RelayUrl::parse("wss://auth.nostr1.com/").unwrap(), - RelayUrl::parse("wss://nip17.com/").unwrap(), + RelayUrl::parse("wss://nos.lol").unwrap(), + RelayUrl::parse("wss://nip17.com").unwrap(), ] } diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 5e74fcd..717280e 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -29,6 +29,9 @@ pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0); /// Defines window titlebar height pub const TITLEBAR_HEIGHT: Pixels = px(36.0); +/// Defines workspace tabbar height +pub const TABBAR_HEIGHT: Pixels = px(28.0); + /// Defines default sidebar width pub const SIDEBAR_WIDTH: Pixels = px(240.); diff --git a/crates/ui/src/checkbox.rs b/crates/ui/src/checkbox.rs index 64f2edc..0434032 100644 --- a/crates/ui/src/checkbox.rs +++ b/crates/ui/src/checkbox.rs @@ -1,49 +1,109 @@ +use std::rc::Rc; +use std::time::Duration; + use gpui::prelude::FluentBuilder as _; use gpui::{ - div, svg, App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, - SharedString, StatefulInteractiveElement as _, Styled as _, Window, + div, px, relative, rems, svg, Animation, AnimationExt, AnyElement, App, Div, ElementId, + InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, + StatefulInteractiveElement, StyleRefinement, Styled, Window, }; use theme::ActiveTheme; -use crate::{h_flex, v_flex, Disableable, IconName, Selectable}; - -type OnClick = Option>; +use crate::icon::IconNamed; +use crate::{v_flex, Disableable, IconName, Selectable, Sizable, Size, StyledExt as _}; /// A Checkbox element. +#[allow(clippy::type_complexity)] #[derive(IntoElement)] pub struct Checkbox { id: ElementId, + base: Div, + style: StyleRefinement, label: Option, + children: Vec, checked: bool, disabled: bool, - on_click: OnClick, + size: Size, + tab_stop: bool, + tab_index: isize, + on_click: Option>, } impl Checkbox { + /// Create a new Checkbox with the given id. pub fn new(id: impl Into) -> Self { Self { id: id.into(), + base: div(), + style: StyleRefinement::default(), label: None, + children: Vec::new(), checked: false, disabled: false, + size: Size::default(), on_click: None, + tab_stop: true, + tab_index: 0, } } + /// Set the label for the checkbox. pub fn label(mut self, label: impl Into) -> Self { self.label = Some(label.into()); self } + /// Set the checked state for the checkbox. pub fn checked(mut self, checked: bool) -> Self { self.checked = checked; self } + /// Set the click handler for the checkbox. + /// + /// The `&bool` parameter indicates the new checked state after the click. pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { - self.on_click = Some(Box::new(handler)); + self.on_click = Some(Rc::new(handler)); self } + + /// Set the tab stop for the checkbox, default is true. + pub fn tab_stop(mut self, tab_stop: bool) -> Self { + self.tab_stop = tab_stop; + self + } + + /// Set the tab index for the checkbox, default is 0. + pub fn tab_index(mut self, tab_index: isize) -> Self { + self.tab_index = tab_index; + self + } + + #[allow(clippy::type_complexity)] + fn handle_click( + on_click: &Option>, + checked: bool, + window: &mut Window, + cx: &mut App, + ) { + let new_checked = !checked; + if let Some(f) = on_click { + (f)(&new_checked, window, cx); + } + } +} + +impl InteractiveElement for Checkbox { + fn interactivity(&mut self) -> &mut gpui::Interactivity { + self.base.interactivity() + } +} +impl StatefulInteractiveElement for Checkbox {} + +impl Styled for Checkbox { + fn style(&mut self) -> &mut gpui::StyleRefinement { + &mut self.style + } } impl Disableable for Checkbox { @@ -63,64 +123,190 @@ impl Selectable for Checkbox { } } -impl RenderOnce for Checkbox { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let icon_color = if self.disabled { - cx.theme().icon_muted - } else { - cx.theme().icon_accent - }; - - h_flex() - .id(self.id) - .gap_2() - .items_center() - .child( - v_flex() - .flex_shrink_0() - .relative() - .rounded_sm() - .size_5() - .bg(cx.theme().elevated_surface_background) - .child( - svg() - .absolute() - .top_0p5() - .left_0p5() - .size_4() - .text_color(icon_color) - .map(|this| match self.checked { - true => this.path(IconName::Check.path()), - _ => this, - }), - ), - ) - .map(|this| { - if let Some(label) = self.label { - this.text_color(cx.theme().text_muted).child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .text_sm() - .child(label), - ) - } else { - this - } - }) - .when(self.disabled, |this| { - this.cursor_not_allowed() - .text_color(cx.theme().text_placeholder) - }) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| { - this.on_click(move |_, window, cx| { - let checked = !self.checked; - on_click(&checked, window, cx); - }) - }, - ) +impl ParentElement for Checkbox { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements); + } +} + +impl Sizable for Checkbox { + fn with_size(mut self, size: impl Into) -> Self { + self.size = size.into(); + self + } +} + +pub(crate) fn checkbox_check_icon( + id: ElementId, + size: Size, + checked: bool, + disabled: bool, + window: &mut Window, + cx: &mut App, +) -> impl IntoElement { + let toggle_state = window.use_keyed_state(id, cx, |_, _| checked); + + let color = if disabled { + cx.theme().text.opacity(0.5) + } else { + cx.theme().text + }; + + svg() + .absolute() + .top_px() + .left_px() + .map(|this| match size { + Size::XSmall => this.size_2(), + Size::Small => this.size_2p5(), + Size::Medium => this.size_3(), + Size::Large => this.size_3p5(), + _ => this.size_3(), + }) + .text_color(color) + .map(|this| match checked { + true => this.path(IconName::Check.path()), + _ => this, + }) + .map(|this| { + if !disabled && checked != *toggle_state.read(cx) { + let duration = Duration::from_secs_f64(0.25); + cx.spawn({ + let toggle_state = toggle_state.clone(); + async move |cx| { + cx.background_executor().timer(duration).await; + toggle_state.update(cx, |this, _| *this = checked); + } + }) + .detach(); + + this.with_animation( + ElementId::NamedInteger("toggle".into(), checked as u64), + Animation::new(Duration::from_secs_f64(0.25)), + move |this, delta| { + this.opacity(if checked { 1.0 * delta } else { 1.0 - delta }) + }, + ) + .into_any_element() + } else { + this.into_any_element() + } + }) +} + +impl RenderOnce for Checkbox { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let focus_handle = window + .use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle()) + .read(cx) + .clone(); + + let checked = self.checked; + let radius = cx.theme().radius.min(px(4.)); + + let border_color = if checked { + cx.theme().border_focused + } else { + cx.theme().border + }; + + let color = if self.disabled { + border_color.opacity(0.5) + } else { + border_color + }; + + div().child( + self.base + .id(self.id.clone()) + .when(!self.disabled, |this| { + this.track_focus( + &focus_handle + .tab_stop(self.tab_stop) + .tab_index(self.tab_index), + ) + }) + .h_flex() + .gap_2() + .items_start() + .line_height(relative(1.)) + .text_color(cx.theme().text) + .map(|this| match self.size { + Size::XSmall => this.text_xs(), + Size::Small => this.text_sm(), + Size::Medium => this.text_base(), + Size::Large => this.text_lg(), + _ => this, + }) + .when(self.disabled, |this| this.text_color(cx.theme().text_muted)) + .rounded(cx.theme().radius * 0.5) + .refine_style(&self.style) + .child( + div() + .relative() + .map(|this| match self.size { + Size::XSmall => this.size_3(), + Size::Small => this.size_3p5(), + Size::Medium => this.size_4(), + Size::Large => this.size(rems(1.125)), + _ => this.size_4(), + }) + .flex_shrink_0() + .border_1() + .border_color(color) + .rounded(radius) + .when(cx.theme().shadow && !self.disabled, |this| this.shadow_xs()) + .map(|this| match checked { + false => this.bg(cx.theme().background), + _ => this.bg(color), + }) + .child(checkbox_check_icon( + self.id, + self.size, + checked, + self.disabled, + window, + cx, + )), + ) + .when(self.label.is_some() || !self.children.is_empty(), |this| { + this.child( + v_flex() + .w_full() + .line_height(relative(1.2)) + .gap_1() + .map(|this| { + if let Some(label) = self.label { + this.child( + div() + .size_full() + .text_color(cx.theme().text) + .when(self.disabled, |this| { + this.text_color(cx.theme().text_muted) + }) + .line_height(relative(1.)) + .child(label), + ) + } else { + this + } + }) + .children(self.children), + ) + }) + .on_mouse_down(gpui::MouseButton::Left, |_, window, _| { + // Avoid focus on mouse down. + window.prevent_default(); + }) + .when(!self.disabled, |this| { + this.on_click({ + let on_click = self.on_click.clone(); + move |_, window, cx| { + window.prevent_default(); + Self::handle_click(&on_click, checked, window, cx); + } + }) + }), + ) } } diff --git a/crates/ui/src/dock_area/tab_panel.rs b/crates/ui/src/dock_area/tab_panel.rs index 0a2b69f..7ed3817 100644 --- a/crates/ui/src/dock_area/tab_panel.rs +++ b/crates/ui/src/dock_area/tab_panel.rs @@ -7,7 +7,7 @@ use gpui::{ MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window, }; -use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT}; +use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TABBAR_HEIGHT}; use crate::button::{Button, ButtonVariants as _}; use crate::dock_area::dock::DockPlacement; @@ -645,7 +645,7 @@ impl TabPanel { TabBar::new() .track_scroll(&self.tab_bar_scroll_handle) - .h(TITLEBAR_HEIGHT) + .h(TABBAR_HEIGHT) .when(has_extend_dock_button, |this| { this.prefix( h_flex() diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 1dca603..73bf163 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -1,12 +1,23 @@ use gpui::prelude::FluentBuilder as _; use gpui::{ - svg, AnyElement, App, AppContext, Entity, Hsla, IntoElement, Radians, Render, RenderOnce, - SharedString, StyleRefinement, Styled, Svg, Transformation, Window, + svg, AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render, + RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation, Window, }; use theme::ActiveTheme; use crate::{Sizable, Size}; +pub trait IconNamed { + /// Returns the embedded path of the icon. + fn path(self) -> SharedString; +} + +impl From for Icon { + fn from(value: T) -> Self { + Icon::build(value) + } +} + #[derive(IntoElement, Clone)] pub enum IconName { ArrowLeft, @@ -43,6 +54,7 @@ pub enum IconName { Sun, Ship, Shield, + UserKey, Upload, Usb, PanelLeft, @@ -63,7 +75,14 @@ pub enum IconName { } impl IconName { - pub fn path(self) -> SharedString { + /// Return the icon as a Entity + pub fn view(self, cx: &mut App) -> Entity { + Icon::build(self).view(cx) + } +} + +impl IconNamed for IconName { + fn path(self) -> SharedString { match self { Self::ArrowLeft => "icons/arrow-left.svg", Self::ArrowRight => "icons/arrow-right.svg", @@ -99,6 +118,7 @@ impl IconName { Self::Sun => "icons/sun.svg", Self::Ship => "icons/ship.svg", Self::Shield => "icons/shield.svg", + Self::UserKey => "icons/user-key.svg", Self::Upload => "icons/upload.svg", Self::Usb => "icons/usb.svg", Self::PanelLeft => "icons/panel-left.svg", @@ -119,17 +139,6 @@ impl IconName { } .into() } - - /// Return the icon as a Entity - pub fn view(self, window: &mut Window, cx: &mut App) -> Entity { - Icon::build(self).view(window, cx) - } -} - -impl From for Icon { - fn from(val: IconName) -> Self { - Icon::build(val) - } } impl From for AnyElement { @@ -139,7 +148,7 @@ impl From for AnyElement { } impl RenderOnce for IconName { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + fn render(self, _: &mut Window, _cx: &mut App) -> impl IntoElement { Icon::build(self) } } @@ -147,6 +156,7 @@ impl RenderOnce for IconName { #[derive(IntoElement)] pub struct Icon { base: Svg, + style: StyleRefinement, path: SharedString, text_color: Option, size: Option, @@ -157,6 +167,7 @@ impl Default for Icon { fn default() -> Self { Self { base: svg().flex_none().size_4(), + style: StyleRefinement::default(), path: "".into(), text_color: None, size: None, @@ -168,23 +179,20 @@ impl Default for Icon { impl Clone for Icon { fn clone(&self) -> Self { let mut this = Self::default().path(self.path.clone()); - if let Some(size) = self.size { - this = this.with_size(size); - } + this.style = self.style.clone(); + this.rotation = self.rotation; + this.size = self.size; + this.text_color = self.text_color; this } } -pub trait IconNamed { - fn path(&self) -> SharedString; -} - impl Icon { pub fn new(icon: impl Into) -> Self { icon.into() } - fn build(name: IconName) -> Self { + fn build(name: impl IconNamed) -> Self { Self::default().path(name.path()) } @@ -197,7 +205,7 @@ impl Icon { } /// Create a new view for the icon - pub fn view(self, _window: &mut Window, cx: &mut App) -> Entity { + pub fn view(self, cx: &mut App) -> Entity { cx.new(|_| self) } @@ -221,7 +229,7 @@ impl Icon { impl Styled for Icon { fn style(&mut self) -> &mut StyleRefinement { - self.base.style() + &mut self.style } fn text_color(mut self, color: impl Into) -> Self { @@ -240,9 +248,15 @@ impl Sizable for Icon { impl RenderOnce for Icon { fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { let text_color = self.text_color.unwrap_or_else(|| window.text_style().color); + let text_size = window.text_style().font_size.to_pixels(window.rem_size()); + let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some(); - self.base + let mut base = self.base; + *base.style() = self.style; + + base.flex_shrink_0() .text_color(text_color) + .when(!has_base_size, |this| this.size(text_size)) .when_some(self.size, |this, size| match size { Size::Size(px) => this.size(px), Size::XSmall => this.size_3(), @@ -261,16 +275,17 @@ impl From for AnyElement { } impl Render for Icon { - fn render( - &mut self, - _window: &mut gpui::Window, - cx: &mut gpui::Context, - ) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let text_color = self.text_color.unwrap_or_else(|| cx.theme().icon); + let text_size = window.text_style().font_size.to_pixels(window.rem_size()); + let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some(); - svg() - .flex_none() + let mut base = svg().flex_none(); + *base.style() = self.style.clone(); + + base.flex_shrink_0() .text_color(text_color) + .when(!has_base_size, |this| this.size(text_size)) .when_some(self.size, |this, size| match size { Size::Size(px) => this.size(px), Size::XSmall => this.size_3(), @@ -278,7 +293,7 @@ impl Render for Icon { Size::Medium => this.size_5(), Size::Large => this.size_6(), }) - .when(!self.path.is_empty(), |this| this.path(self.path.clone())) + .path(self.path.clone()) .when_some(self.rotation, |this, rotation| { this.with_transformation(Transformation::rotate(rotation)) }) diff --git a/crates/ui/src/menu/popup_menu.rs b/crates/ui/src/menu/popup_menu.rs index a092eab..d3cd9f1 100644 --- a/crates/ui/src/menu/popup_menu.rs +++ b/crates/ui/src/menu/popup_menu.rs @@ -1028,7 +1028,7 @@ impl PopupMenu { Icon::empty() }; - Some(icon.xsmall()) + Some(icon.small()) } #[inline] diff --git a/crates/ui/src/tab/mod.rs b/crates/ui/src/tab/mod.rs index e85eca7..cace2b1 100644 --- a/crates/ui/src/tab/mod.rs +++ b/crates/ui/src/tab/mod.rs @@ -3,7 +3,7 @@ use gpui::{ div, px, AnyElement, App, Div, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, StatefulInteractiveElement, Styled, Window, }; -use theme::{ActiveTheme, TITLEBAR_HEIGHT}; +use theme::{ActiveTheme, TABBAR_HEIGHT}; use crate::{Selectable, Sizable, Size}; @@ -136,7 +136,7 @@ impl RenderOnce for Tab { self.base .id(self.ix) - .h(TITLEBAR_HEIGHT) + .h(TABBAR_HEIGHT) .px_4() .relative() .flex()