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()