update send message and chat panel ui
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m26s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m20s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m26s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m20s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
This commit is contained in:
3
assets/icons/user-key.svg
Normal file
3
assets/icons/user-key.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="7.75" r="4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12.0014 12.25C7.80812 12.25 5.3732 14.9227 4.69664 18.2626C4.47735 19.3452 5.39684 20.25 6.50141 20.25H10.2515" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="19.0992" cy="15.4" r="0.9" fill="currentColor"/><path d="M18.8496 12.5498C20.5617 12.5498 21.9502 13.9383 21.9502 15.6504C21.95 17.3623 20.5616 18.75 18.8496 18.75C18.5179 18.75 18.199 18.6961 17.8994 18.5996L15.75 20.75H13.75V18.75L15.8994 16.5996C15.8032 16.3004 15.75 15.9816 15.75 15.6504C15.75 13.9384 17.1377 12.55 18.8496 12.5498Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 832 B |
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -588,6 +588,7 @@ impl ChatRegistry {
|
|||||||
room.update(cx, |this, cx| {
|
room.update(cx, |this, cx| {
|
||||||
this.push_message(message, cx);
|
this.push_message(message, cx);
|
||||||
});
|
});
|
||||||
|
self.sort(cx);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Push the new room to the front of the list
|
// Push the new room to the front of the list
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context as AnyhowContext, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use common::EventUtils;
|
use common::EventUtils;
|
||||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
|
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@@ -11,7 +12,10 @@ use person::{Person, PersonRegistry};
|
|||||||
use settings::{RoomConfig, SignerKind};
|
use settings::{RoomConfig, SignerKind};
|
||||||
use state::{NostrRegistry, TIMEOUT};
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SendReport {
|
pub struct SendReport {
|
||||||
@@ -222,6 +226,17 @@ impl Room {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the signer kind config for the room
|
||||||
|
pub fn set_signer_kind(&mut self, kind: &SignerKind, cx: &mut Context<Self>) {
|
||||||
|
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
|
/// Returns the members of the room
|
||||||
pub fn members(&self) -> Vec<PublicKey> {
|
pub fn members(&self) -> Vec<PublicKey> {
|
||||||
self.members.clone()
|
self.members.clone()
|
||||||
@@ -296,10 +311,6 @@ impl Room {
|
|||||||
|
|
||||||
if new_message {
|
if new_message {
|
||||||
self.set_created_at(created_at, cx);
|
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);
|
cx.emit(RoomEvent::Reload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
/// Get gossip relays for each member
|
/// Get gossip relays for each member
|
||||||
pub fn early_connect(&self, cx: &App) -> Task<Result<(), Error>> {
|
pub fn connect(&self, cx: &App) -> HashMap<PublicKey, Task<Result<(bool, bool), Error>>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
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 members = self.members();
|
||||||
let subscription_id = SubscriptionId::new(format!("room-{}", self.id));
|
let mut tasks = HashMap::new();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
for member in members.into_iter() {
|
||||||
let signer = client.signer().context("Signer not found")?;
|
// Skip if member is the current user
|
||||||
let public_key = signer.get_public_key().await?;
|
if member == public_key {
|
||||||
|
continue;
|
||||||
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?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
// 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
|
/// Get all messages belonging to the room
|
||||||
@@ -418,11 +468,6 @@ impl Room {
|
|||||||
|
|
||||||
// Add all receiver tags
|
// Add all receiver tags
|
||||||
for member in members.into_iter() {
|
for member in members.into_iter() {
|
||||||
// Skip current user
|
|
||||||
if member.public_key() == sender {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.push(Tag::from_standardized_without_cell(
|
tags.push(Tag::from_standardized_without_cell(
|
||||||
TagStandard::PublicKey {
|
TagStandard::PublicKey {
|
||||||
public_key: member.public_key(),
|
public_key: member.public_key(),
|
||||||
@@ -445,61 +490,59 @@ impl Room {
|
|||||||
|
|
||||||
/// Send rumor event to all members's messaging relays
|
/// Send rumor event to all members's messaging relays
|
||||||
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
|
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
|
||||||
|
let config = self.config.clone();
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
// Get room's config
|
|
||||||
let config = self.config.clone();
|
|
||||||
|
|
||||||
// Get current user's public key
|
// 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)
|
// Get all members (excluding sender)
|
||||||
let members: Vec<Person> = self
|
let members: Vec<Person> = self
|
||||||
.members
|
.members
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|public_key| public_key != &&sender)
|
.filter(|public_key| public_key != &&sender.public_key())
|
||||||
.map(|member| persons.read(cx).get(member, cx))
|
.map(|member| persons.read(cx).get(member, cx))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Some(cx.background_spawn(async move {
|
Some(cx.background_spawn(async move {
|
||||||
let signer_kind = config.signer_kind();
|
let signer_kind = config.signer_kind();
|
||||||
|
let backup = config.backup();
|
||||||
|
|
||||||
let user_signer = signer.get().await;
|
let user_signer = signer.get().await;
|
||||||
let encryption_signer = signer.get_encryption_signer().await;
|
let encryption_signer = signer.get_encryption_signer().await;
|
||||||
|
|
||||||
|
let mut sents = 0;
|
||||||
let mut reports = Vec::new();
|
let mut reports = Vec::new();
|
||||||
|
|
||||||
|
// Process each member
|
||||||
for member in members {
|
for member in members {
|
||||||
let relays = member.messaging_relays();
|
let relays = member.messaging_relays();
|
||||||
let announcement = member.announcement();
|
let announcement = member.announcement();
|
||||||
|
let public_key = member.public_key();
|
||||||
|
|
||||||
// Skip if member has no messaging relays
|
|
||||||
if relays.is_empty() {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure relay connections
|
// Handle encryption signer requirements
|
||||||
for url in relays.iter() {
|
if signer_kind.encryption() {
|
||||||
client
|
if announcement.is_none() {
|
||||||
.add_relay(url)
|
reports.push(SendReport::new(public_key).error(NO_DEKEY));
|
||||||
.and_connect()
|
continue;
|
||||||
.capabilities(RelayCapabilities::GOSSIP)
|
}
|
||||||
.await
|
if encryption_signer.is_none() {
|
||||||
.ok();
|
reports.push(SendReport::new(sender.public_key()).error(USER_NO_DEKEY));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When forced to use encryption signer, skip if receiver has no announcement
|
// Determine receiver and signer
|
||||||
if signer_kind.encryption() && announcement.is_none() {
|
let (receiver, signer) = match signer_kind {
|
||||||
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 {
|
|
||||||
SignerKind::Auto => {
|
SignerKind::Auto => {
|
||||||
if let Some(announcement) = announcement {
|
if let Some(announcement) = announcement {
|
||||||
if let Some(enc_signer) = encryption_signer.as_ref() {
|
if let Some(enc_signer) = encryption_signer.as_ref() {
|
||||||
@@ -512,272 +555,77 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SignerKind::Encryption => {
|
SignerKind::Encryption => {
|
||||||
let Some(encryption_signer) = encryption_signer.as_ref() else {
|
// Safe to unwrap due to earlier checks
|
||||||
reports.push(
|
(
|
||||||
SendReport::new(member.public_key()).error("Encryption not found"),
|
announcement.unwrap().public_key(),
|
||||||
);
|
encryption_signer.as_ref().unwrap().clone(),
|
||||||
continue;
|
)
|
||||||
};
|
|
||||||
let Some(announcement) = announcement else {
|
|
||||||
reports.push(
|
|
||||||
SendReport::new(member.public_key())
|
|
||||||
.error("Announcement not found"),
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
(announcement.public_key(), encryption_signer.clone())
|
|
||||||
}
|
}
|
||||||
SignerKind::User => (member.public_key(), user_signer.clone()),
|
SignerKind::User => (member.public_key(), user_signer.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create and send gift-wrapped event
|
match send_gift_wrap(&client, &signer, &receiver, &rumor, relays, public_key).await
|
||||||
match EventBuilder::gift_wrap(&signer_to_use, &receiver, rumor.clone(), []).await {
|
{
|
||||||
Ok(event) => {
|
Ok((report, _)) => {
|
||||||
match client
|
reports.push(report);
|
||||||
.send_event(&event)
|
sents += 1;
|
||||||
.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()));
|
|
||||||
}
|
}
|
||||||
|
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
|
reports
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/*
|
|
||||||
* /// Create a new unsigned message event
|
// Helper function to send a gift-wrapped event
|
||||||
pub fn create_message(
|
async fn send_gift_wrap<T>(
|
||||||
&self,
|
client: &Client,
|
||||||
content: &str,
|
signer: &T,
|
||||||
replies: Vec<EventId>,
|
receiver: &PublicKey,
|
||||||
cx: &App,
|
rumor: &UnsignedEvent,
|
||||||
) -> Task<Result<UnsignedEvent, Error>> {
|
relays: &[RelayUrl],
|
||||||
let nostr = NostrRegistry::global(cx);
|
public_key: PublicKey,
|
||||||
let client = nostr.read(cx).client();
|
) -> Result<(SendReport, bool), SendReport>
|
||||||
|
where
|
||||||
let subject = self.subject.clone();
|
T: NostrSigner + 'static,
|
||||||
let content = content.to_string();
|
{
|
||||||
|
// Ensure relay connections
|
||||||
let mut member_and_relay_hints = HashMap::new();
|
for url in relays {
|
||||||
|
client.add_relay(url).and_connect().await.ok();
|
||||||
// Populate the hashmap with member and relay hint tasks
|
}
|
||||||
for member in self.members.iter() {
|
|
||||||
let hint = nostr.read(cx).relay_hint(member, cx);
|
match EventBuilder::gift_wrap(signer, receiver, rumor.clone(), []).await {
|
||||||
member_and_relay_hints.insert(member.to_owned(), hint);
|
Ok(event) => {
|
||||||
}
|
match client
|
||||||
|
.send_event(&event)
|
||||||
cx.background_spawn(async move {
|
.to(relays)
|
||||||
let signer = client.signer().context("Signer not found")?;
|
.ack_policy(AckPolicy::none())
|
||||||
let public_key = signer.get_public_key().await?;
|
.await
|
||||||
|
{
|
||||||
// List of event tags for each receiver
|
Ok(output) => Ok((
|
||||||
let mut tags = vec![];
|
SendReport::new(public_key)
|
||||||
|
.gift_wrap_id(event.id)
|
||||||
for (member, task) in member_and_relay_hints.into_iter() {
|
.output(output),
|
||||||
// Skip current user
|
true,
|
||||||
if member == public_key {
|
)),
|
||||||
continue;
|
Err(e) => Err(SendReport::new(public_key).error(e.to_string())),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Get relay hint if available
|
Err(e) => Err(SendReport::new(public_key).error(e.to_string())),
|
||||||
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<Result<Vec<SendReport>, 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<SendReport> = 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<SendReport>,
|
|
||||||
cx: &App,
|
|
||||||
) -> Task<Result<Vec<SendReport>, 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<EventId> = 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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
use gpui::Action;
|
use gpui::Action;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use settings::SignerKind;
|
||||||
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
#[action(namespace = chat, no_json)]
|
#[action(namespace = chat, no_json)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
Insert(&'static str),
|
Insert(&'static str),
|
||||||
ChangeSubject(&'static str),
|
ChangeSubject(&'static str),
|
||||||
|
ChangeSigner(SignerKind),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use gpui_tokio::Tokio;
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Person, PersonRegistry};
|
use person::{Person, PersonRegistry};
|
||||||
use settings::AppSettings;
|
use settings::{AppSettings, SignerKind};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use smol::lock::RwLock;
|
use smol::lock::RwLock;
|
||||||
@@ -41,6 +41,11 @@ use crate::text::RenderedText;
|
|||||||
mod actions;
|
mod actions;
|
||||||
mod text;
|
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<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
||||||
cx.new(|cx| ChatPanel::new(room, window, cx))
|
cx.new(|cx| ChatPanel::new(room, window, cx))
|
||||||
}
|
}
|
||||||
@@ -225,12 +230,43 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get all necessary data for each member
|
/// Get all necessary data for each member
|
||||||
fn connect(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
fn connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Ok(connect) = self.room.read_with(cx, |this, cx| this.early_connect(cx)) else {
|
let Ok(tasks) = self.room.read_with(cx, |this, cx| this.connect(cx)) else {
|
||||||
return;
|
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
|
/// Load all messages belonging to this room
|
||||||
@@ -339,6 +375,7 @@ impl ChatPanel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
// Send and get reports
|
||||||
let outputs = task.await;
|
let outputs = task.await;
|
||||||
|
|
||||||
// Add sent IDs to the list
|
// Add sent IDs to the list
|
||||||
@@ -559,6 +596,36 @@ impl ChatPanel {
|
|||||||
persons.read(cx).get(public_key, cx)
|
persons.read(cx).get(public_key, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
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<Self>) -> AnyElement {
|
fn render_announcement(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||||
const MSG: &str =
|
const MSG: &str =
|
||||||
"This conversation is private. Only members can see each other's messages.";
|
"This conversation is private. Only members can see each other's messages.";
|
||||||
@@ -1133,23 +1200,60 @@ impl ChatPanel {
|
|||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
|
fn render_encryption_menu(&self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
||||||
match command {
|
let signer_kind = self
|
||||||
Command::Insert(content) => {
|
.room
|
||||||
self.send_message(content, window, cx);
|
.read_with(cx, |this, _cx| this.config().signer_kind().clone())
|
||||||
}
|
.ok()
|
||||||
Command::ChangeSubject(subject) => {
|
.unwrap_or_default();
|
||||||
if self
|
|
||||||
.room
|
Button::new("encryption")
|
||||||
.update(cx, |this, cx| {
|
.icon(IconName::UserKey)
|
||||||
this.set_subject(*subject, cx);
|
.ghost()
|
||||||
})
|
.large()
|
||||||
.is_err()
|
.dropdown_menu(move |this, _window, _cx| {
|
||||||
{
|
let auto = matches!(signer_kind, SignerKind::Auto);
|
||||||
window.push_notification(Notification::error("Failed to change subject"), cx);
|
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<Self>) -> 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()
|
h_flex()
|
||||||
.pl_1()
|
.pl_1()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(self.render_encryption_menu(window, cx))
|
||||||
Button::new("emoji")
|
.child(self.render_emoji_menu(window, cx))
|
||||||
.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(
|
.child(
|
||||||
Button::new("send")
|
Button::new("send")
|
||||||
.icon(IconName::PaperPlaneFill)
|
.icon(IconName::PaperPlaneFill)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use nostr_sdk::prelude::*;
|
|||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::{NostrRegistry, FIND_DELAY};
|
use state::{NostrRegistry, FIND_DELAY};
|
||||||
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
|
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
@@ -497,7 +497,7 @@ impl Render for Sidebar {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.h(TITLEBAR_HEIGHT)
|
.h(TABBAR_HEIGHT)
|
||||||
.border_b_1()
|
.border_b_1()
|
||||||
.border_color(cx.theme().border_variant)
|
.border_color(cx.theme().border_variant)
|
||||||
.bg(cx.theme().elevated_surface_background)
|
.bg(cx.theme().elevated_surface_background)
|
||||||
|
|||||||
@@ -80,13 +80,25 @@ pub struct RoomConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RoomConfig {
|
impl RoomConfig {
|
||||||
|
/// Get backup config
|
||||||
pub fn backup(&self) -> bool {
|
pub fn backup(&self) -> bool {
|
||||||
self.backup
|
self.backup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get signer kind config
|
||||||
pub fn signer_kind(&self) -> &SignerKind {
|
pub fn signer_kind(&self) -> &SignerKind {
|
||||||
&self.signer_kind
|
&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
|
/// Settings
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ pub const SEARCH_RELAYS: [&str; 1] = ["wss://antiprimal.net"];
|
|||||||
/// Default bootstrap relays
|
/// Default bootstrap relays
|
||||||
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://relay.primal.net",
|
"wss://nos.lol",
|
||||||
"wss://user.kindpag.es",
|
"wss://user.kindpag.es",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -176,11 +176,7 @@ impl NostrRegistry {
|
|||||||
|
|
||||||
while let Some(notification) = notifications.next().await {
|
while let Some(notification) = notifications.next().await {
|
||||||
if let ClientNotification::Message {
|
if let ClientNotification::Message {
|
||||||
message:
|
message: RelayMessage::Event { event, .. },
|
||||||
RelayMessage::Event {
|
|
||||||
event,
|
|
||||||
subscription_id,
|
|
||||||
},
|
|
||||||
..
|
..
|
||||||
} = notification
|
} = notification
|
||||||
{
|
{
|
||||||
@@ -191,11 +187,6 @@ impl NostrRegistry {
|
|||||||
|
|
||||||
match event.kind {
|
match event.kind {
|
||||||
Kind::RelayList => {
|
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?;
|
tx.send_async(event.into_owned()).await?;
|
||||||
}
|
}
|
||||||
Kind::InboxRelays => {
|
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::<HashMap<_, _>>();
|
|
||||||
|
|
||||||
client.subscribe(target).close_on(opts).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get or create a new app keys
|
/// Get or create a new app keys
|
||||||
fn get_or_init_app_keys() -> Result<Keys, Error> {
|
fn get_or_init_app_keys() -> Result<Keys, Error> {
|
||||||
let dir = config_dir().join(".app_keys");
|
let dir = config_dir().join(".app_keys");
|
||||||
@@ -857,15 +801,15 @@ fn default_relay_list() -> Vec<(RelayUrl, Option<RelayMetadata>)> {
|
|||||||
Some(RelayMetadata::Write),
|
Some(RelayMetadata::Write),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
RelayUrl::parse("wss://relay.primal.net/").unwrap(),
|
RelayUrl::parse("wss://relay.primal.net").unwrap(),
|
||||||
Some(RelayMetadata::Write),
|
Some(RelayMetadata::Write),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
RelayUrl::parse("wss://relay.damus.io/").unwrap(),
|
RelayUrl::parse("wss://relay.damus.io").unwrap(),
|
||||||
Some(RelayMetadata::Read),
|
Some(RelayMetadata::Read),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
RelayUrl::parse("wss://nos.lol/").unwrap(),
|
RelayUrl::parse("wss://nos.lol").unwrap(),
|
||||||
Some(RelayMetadata::Read),
|
Some(RelayMetadata::Read),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -873,8 +817,8 @@ fn default_relay_list() -> Vec<(RelayUrl, Option<RelayMetadata>)> {
|
|||||||
|
|
||||||
fn default_messaging_relays() -> Vec<RelayUrl> {
|
fn default_messaging_relays() -> Vec<RelayUrl> {
|
||||||
vec![
|
vec![
|
||||||
//RelayUrl::parse("wss://auth.nostr1.com/").unwrap(),
|
RelayUrl::parse("wss://nos.lol").unwrap(),
|
||||||
RelayUrl::parse("wss://nip17.com/").unwrap(),
|
RelayUrl::parse("wss://nip17.com").unwrap(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0);
|
|||||||
/// Defines window titlebar height
|
/// Defines window titlebar height
|
||||||
pub const TITLEBAR_HEIGHT: Pixels = px(36.0);
|
pub const TITLEBAR_HEIGHT: Pixels = px(36.0);
|
||||||
|
|
||||||
|
/// Defines workspace tabbar height
|
||||||
|
pub const TABBAR_HEIGHT: Pixels = px(28.0);
|
||||||
|
|
||||||
/// Defines default sidebar width
|
/// Defines default sidebar width
|
||||||
pub const SIDEBAR_WIDTH: Pixels = px(240.);
|
pub const SIDEBAR_WIDTH: Pixels = px(240.);
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,109 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, svg, App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
div, px, relative, rems, svg, Animation, AnimationExt, AnyElement, App, Div, ElementId,
|
||||||
SharedString, StatefulInteractiveElement as _, Styled as _, Window,
|
InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
|
||||||
|
StatefulInteractiveElement, StyleRefinement, Styled, Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::{h_flex, v_flex, Disableable, IconName, Selectable};
|
use crate::icon::IconNamed;
|
||||||
|
use crate::{v_flex, Disableable, IconName, Selectable, Sizable, Size, StyledExt as _};
|
||||||
type OnClick = Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>;
|
|
||||||
|
|
||||||
/// A Checkbox element.
|
/// A Checkbox element.
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Checkbox {
|
pub struct Checkbox {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
base: Div,
|
||||||
|
style: StyleRefinement,
|
||||||
label: Option<SharedString>,
|
label: Option<SharedString>,
|
||||||
|
children: Vec<AnyElement>,
|
||||||
checked: bool,
|
checked: bool,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
on_click: OnClick,
|
size: Size,
|
||||||
|
tab_stop: bool,
|
||||||
|
tab_index: isize,
|
||||||
|
on_click: Option<Rc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Checkbox {
|
impl Checkbox {
|
||||||
|
/// Create a new Checkbox with the given id.
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
|
base: div(),
|
||||||
|
style: StyleRefinement::default(),
|
||||||
label: None,
|
label: None,
|
||||||
|
children: Vec::new(),
|
||||||
checked: false,
|
checked: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
size: Size::default(),
|
||||||
on_click: None,
|
on_click: None,
|
||||||
|
tab_stop: true,
|
||||||
|
tab_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the label for the checkbox.
|
||||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||||
self.label = Some(label.into());
|
self.label = Some(label.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the checked state for the checkbox.
|
||||||
pub fn checked(mut self, checked: bool) -> Self {
|
pub fn checked(mut self, checked: bool) -> Self {
|
||||||
self.checked = checked;
|
self.checked = checked;
|
||||||
self
|
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 {
|
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
|
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<Rc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
|
||||||
|
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 {
|
impl Disableable for Checkbox {
|
||||||
@@ -63,64 +123,190 @@ impl Selectable for Checkbox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Checkbox {
|
impl ParentElement for Checkbox {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||||
let icon_color = if self.disabled {
|
self.children.extend(elements);
|
||||||
cx.theme().icon_muted
|
}
|
||||||
} else {
|
}
|
||||||
cx.theme().icon_accent
|
|
||||||
};
|
impl Sizable for Checkbox {
|
||||||
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||||
h_flex()
|
self.size = size.into();
|
||||||
.id(self.id)
|
self
|
||||||
.gap_2()
|
}
|
||||||
.items_center()
|
}
|
||||||
.child(
|
|
||||||
v_flex()
|
pub(crate) fn checkbox_check_icon(
|
||||||
.flex_shrink_0()
|
id: ElementId,
|
||||||
.relative()
|
size: Size,
|
||||||
.rounded_sm()
|
checked: bool,
|
||||||
.size_5()
|
disabled: bool,
|
||||||
.bg(cx.theme().elevated_surface_background)
|
window: &mut Window,
|
||||||
.child(
|
cx: &mut App,
|
||||||
svg()
|
) -> impl IntoElement {
|
||||||
.absolute()
|
let toggle_state = window.use_keyed_state(id, cx, |_, _| checked);
|
||||||
.top_0p5()
|
|
||||||
.left_0p5()
|
let color = if disabled {
|
||||||
.size_4()
|
cx.theme().text.opacity(0.5)
|
||||||
.text_color(icon_color)
|
} else {
|
||||||
.map(|this| match self.checked {
|
cx.theme().text
|
||||||
true => this.path(IconName::Check.path()),
|
};
|
||||||
_ => this,
|
|
||||||
}),
|
svg()
|
||||||
),
|
.absolute()
|
||||||
)
|
.top_px()
|
||||||
.map(|this| {
|
.left_px()
|
||||||
if let Some(label) = self.label {
|
.map(|this| match size {
|
||||||
this.text_color(cx.theme().text_muted).child(
|
Size::XSmall => this.size_2(),
|
||||||
div()
|
Size::Small => this.size_2p5(),
|
||||||
.w_full()
|
Size::Medium => this.size_3(),
|
||||||
.overflow_x_hidden()
|
Size::Large => this.size_3p5(),
|
||||||
.text_ellipsis()
|
_ => this.size_3(),
|
||||||
.text_sm()
|
})
|
||||||
.child(label),
|
.text_color(color)
|
||||||
)
|
.map(|this| match checked {
|
||||||
} else {
|
true => this.path(IconName::Check.path()),
|
||||||
this
|
_ => this,
|
||||||
}
|
})
|
||||||
})
|
.map(|this| {
|
||||||
.when(self.disabled, |this| {
|
if !disabled && checked != *toggle_state.read(cx) {
|
||||||
this.cursor_not_allowed()
|
let duration = Duration::from_secs_f64(0.25);
|
||||||
.text_color(cx.theme().text_placeholder)
|
cx.spawn({
|
||||||
})
|
let toggle_state = toggle_state.clone();
|
||||||
.when_some(
|
async move |cx| {
|
||||||
self.on_click.filter(|_| !self.disabled),
|
cx.background_executor().timer(duration).await;
|
||||||
|this, on_click| {
|
toggle_state.update(cx, |this, _| *this = checked);
|
||||||
this.on_click(move |_, window, cx| {
|
}
|
||||||
let checked = !self.checked;
|
})
|
||||||
on_click(&checked, window, cx);
|
.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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use gpui::{
|
|||||||
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
|
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
|
||||||
StatefulInteractiveElement, Styled, WeakEntity, Window,
|
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::button::{Button, ButtonVariants as _};
|
||||||
use crate::dock_area::dock::DockPlacement;
|
use crate::dock_area::dock::DockPlacement;
|
||||||
@@ -645,7 +645,7 @@ impl TabPanel {
|
|||||||
|
|
||||||
TabBar::new()
|
TabBar::new()
|
||||||
.track_scroll(&self.tab_bar_scroll_handle)
|
.track_scroll(&self.tab_bar_scroll_handle)
|
||||||
.h(TITLEBAR_HEIGHT)
|
.h(TABBAR_HEIGHT)
|
||||||
.when(has_extend_dock_button, |this| {
|
.when(has_extend_dock_button, |this| {
|
||||||
this.prefix(
|
this.prefix(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
svg, AnyElement, App, AppContext, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
|
svg, AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render,
|
||||||
SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::{Sizable, Size};
|
use crate::{Sizable, Size};
|
||||||
|
|
||||||
|
pub trait IconNamed {
|
||||||
|
/// Returns the embedded path of the icon.
|
||||||
|
fn path(self) -> SharedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: IconNamed> From<T> for Icon {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Icon::build(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(IntoElement, Clone)]
|
#[derive(IntoElement, Clone)]
|
||||||
pub enum IconName {
|
pub enum IconName {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
@@ -43,6 +54,7 @@ pub enum IconName {
|
|||||||
Sun,
|
Sun,
|
||||||
Ship,
|
Ship,
|
||||||
Shield,
|
Shield,
|
||||||
|
UserKey,
|
||||||
Upload,
|
Upload,
|
||||||
Usb,
|
Usb,
|
||||||
PanelLeft,
|
PanelLeft,
|
||||||
@@ -63,7 +75,14 @@ pub enum IconName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl IconName {
|
impl IconName {
|
||||||
pub fn path(self) -> SharedString {
|
/// Return the icon as a Entity<Icon>
|
||||||
|
pub fn view(self, cx: &mut App) -> Entity<Icon> {
|
||||||
|
Icon::build(self).view(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IconNamed for IconName {
|
||||||
|
fn path(self) -> SharedString {
|
||||||
match self {
|
match self {
|
||||||
Self::ArrowLeft => "icons/arrow-left.svg",
|
Self::ArrowLeft => "icons/arrow-left.svg",
|
||||||
Self::ArrowRight => "icons/arrow-right.svg",
|
Self::ArrowRight => "icons/arrow-right.svg",
|
||||||
@@ -99,6 +118,7 @@ impl IconName {
|
|||||||
Self::Sun => "icons/sun.svg",
|
Self::Sun => "icons/sun.svg",
|
||||||
Self::Ship => "icons/ship.svg",
|
Self::Ship => "icons/ship.svg",
|
||||||
Self::Shield => "icons/shield.svg",
|
Self::Shield => "icons/shield.svg",
|
||||||
|
Self::UserKey => "icons/user-key.svg",
|
||||||
Self::Upload => "icons/upload.svg",
|
Self::Upload => "icons/upload.svg",
|
||||||
Self::Usb => "icons/usb.svg",
|
Self::Usb => "icons/usb.svg",
|
||||||
Self::PanelLeft => "icons/panel-left.svg",
|
Self::PanelLeft => "icons/panel-left.svg",
|
||||||
@@ -119,17 +139,6 @@ impl IconName {
|
|||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the icon as a Entity<Icon>
|
|
||||||
pub fn view(self, window: &mut Window, cx: &mut App) -> Entity<Icon> {
|
|
||||||
Icon::build(self).view(window, cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<IconName> for Icon {
|
|
||||||
fn from(val: IconName) -> Self {
|
|
||||||
Icon::build(val)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<IconName> for AnyElement {
|
impl From<IconName> for AnyElement {
|
||||||
@@ -139,7 +148,7 @@ impl From<IconName> for AnyElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for IconName {
|
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)
|
Icon::build(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +156,7 @@ impl RenderOnce for IconName {
|
|||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Icon {
|
pub struct Icon {
|
||||||
base: Svg,
|
base: Svg,
|
||||||
|
style: StyleRefinement,
|
||||||
path: SharedString,
|
path: SharedString,
|
||||||
text_color: Option<Hsla>,
|
text_color: Option<Hsla>,
|
||||||
size: Option<Size>,
|
size: Option<Size>,
|
||||||
@@ -157,6 +167,7 @@ impl Default for Icon {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
base: svg().flex_none().size_4(),
|
base: svg().flex_none().size_4(),
|
||||||
|
style: StyleRefinement::default(),
|
||||||
path: "".into(),
|
path: "".into(),
|
||||||
text_color: None,
|
text_color: None,
|
||||||
size: None,
|
size: None,
|
||||||
@@ -168,23 +179,20 @@ impl Default for Icon {
|
|||||||
impl Clone for Icon {
|
impl Clone for Icon {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
let mut this = Self::default().path(self.path.clone());
|
let mut this = Self::default().path(self.path.clone());
|
||||||
if let Some(size) = self.size {
|
this.style = self.style.clone();
|
||||||
this = this.with_size(size);
|
this.rotation = self.rotation;
|
||||||
}
|
this.size = self.size;
|
||||||
|
this.text_color = self.text_color;
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait IconNamed {
|
|
||||||
fn path(&self) -> SharedString;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Icon {
|
impl Icon {
|
||||||
pub fn new(icon: impl Into<Icon>) -> Self {
|
pub fn new(icon: impl Into<Icon>) -> Self {
|
||||||
icon.into()
|
icon.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build(name: IconName) -> Self {
|
fn build(name: impl IconNamed) -> Self {
|
||||||
Self::default().path(name.path())
|
Self::default().path(name.path())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +205,7 @@ impl Icon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new view for the icon
|
/// Create a new view for the icon
|
||||||
pub fn view(self, _window: &mut Window, cx: &mut App) -> Entity<Icon> {
|
pub fn view(self, cx: &mut App) -> Entity<Icon> {
|
||||||
cx.new(|_| self)
|
cx.new(|_| self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +229,7 @@ impl Icon {
|
|||||||
|
|
||||||
impl Styled for Icon {
|
impl Styled for Icon {
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
self.base.style()
|
&mut self.style
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_color(mut self, color: impl Into<Hsla>) -> Self {
|
fn text_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||||
@@ -240,9 +248,15 @@ impl Sizable for Icon {
|
|||||||
impl RenderOnce for Icon {
|
impl RenderOnce for Icon {
|
||||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
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_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)
|
.text_color(text_color)
|
||||||
|
.when(!has_base_size, |this| this.size(text_size))
|
||||||
.when_some(self.size, |this, size| match size {
|
.when_some(self.size, |this, size| match size {
|
||||||
Size::Size(px) => this.size(px),
|
Size::Size(px) => this.size(px),
|
||||||
Size::XSmall => this.size_3(),
|
Size::XSmall => this.size_3(),
|
||||||
@@ -261,16 +275,17 @@ impl From<Icon> for AnyElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Icon {
|
impl Render for Icon {
|
||||||
fn render(
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
&mut self,
|
|
||||||
_window: &mut gpui::Window,
|
|
||||||
cx: &mut gpui::Context<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let text_color = self.text_color.unwrap_or_else(|| cx.theme().icon);
|
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()
|
let mut base = svg().flex_none();
|
||||||
.flex_none()
|
*base.style() = self.style.clone();
|
||||||
|
|
||||||
|
base.flex_shrink_0()
|
||||||
.text_color(text_color)
|
.text_color(text_color)
|
||||||
|
.when(!has_base_size, |this| this.size(text_size))
|
||||||
.when_some(self.size, |this, size| match size {
|
.when_some(self.size, |this, size| match size {
|
||||||
Size::Size(px) => this.size(px),
|
Size::Size(px) => this.size(px),
|
||||||
Size::XSmall => this.size_3(),
|
Size::XSmall => this.size_3(),
|
||||||
@@ -278,7 +293,7 @@ impl Render for Icon {
|
|||||||
Size::Medium => this.size_5(),
|
Size::Medium => this.size_5(),
|
||||||
Size::Large => this.size_6(),
|
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| {
|
.when_some(self.rotation, |this, rotation| {
|
||||||
this.with_transformation(Transformation::rotate(rotation))
|
this.with_transformation(Transformation::rotate(rotation))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1028,7 +1028,7 @@ impl PopupMenu {
|
|||||||
Icon::empty()
|
Icon::empty()
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(icon.xsmall())
|
Some(icon.small())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use gpui::{
|
|||||||
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||||
RenderOnce, StatefulInteractiveElement, Styled, Window,
|
RenderOnce, StatefulInteractiveElement, Styled, Window,
|
||||||
};
|
};
|
||||||
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
|
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||||
|
|
||||||
use crate::{Selectable, Sizable, Size};
|
use crate::{Selectable, Sizable, Size};
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ impl RenderOnce for Tab {
|
|||||||
|
|
||||||
self.base
|
self.base
|
||||||
.id(self.ix)
|
.id(self.ix)
|
||||||
.h(TITLEBAR_HEIGHT)
|
.h(TABBAR_HEIGHT)
|
||||||
.px_4()
|
.px_4()
|
||||||
.relative()
|
.relative()
|
||||||
.flex()
|
.flex()
|
||||||
|
|||||||
Reference in New Issue
Block a user