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| {
|
||||
this.push_message(message, cx);
|
||||
});
|
||||
self.sort(cx);
|
||||
}
|
||||
None => {
|
||||
// Push the new room to the front of the list
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as AnyhowContext, Error};
|
||||
use anyhow::{anyhow, Error};
|
||||
use common::EventUtils;
|
||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
|
||||
use itertools::Itertools;
|
||||
@@ -11,7 +12,10 @@ use person::{Person, PersonRegistry};
|
||||
use settings::{RoomConfig, SignerKind};
|
||||
use state::{NostrRegistry, TIMEOUT};
|
||||
|
||||
use crate::{ChatRegistry, NewMessage};
|
||||
use crate::NewMessage;
|
||||
|
||||
const NO_DEKEY: &str = "User hasn't set up a decoupled encryption key yet.";
|
||||
const USER_NO_DEKEY: &str = "You haven't set up a decoupled encryption key or it's not available.";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SendReport {
|
||||
@@ -222,6 +226,17 @@ impl Room {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Updates the signer kind config for the room
|
||||
pub fn set_signer_kind(&mut self, kind: &SignerKind, cx: &mut Context<Self>) {
|
||||
self.config.set_signer_kind(kind);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Returns the config of the room
|
||||
pub fn config(&self) -> &RoomConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Returns the members of the room
|
||||
pub fn members(&self) -> Vec<PublicKey> {
|
||||
self.members.clone()
|
||||
@@ -296,10 +311,6 @@ impl Room {
|
||||
|
||||
if new_message {
|
||||
self.set_created_at(created_at, cx);
|
||||
// Sort chats after emitting a new message
|
||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
||||
this.sort(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,49 +319,88 @@ impl Room {
|
||||
cx.emit(RoomEvent::Reload);
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// Get gossip relays for each member
|
||||
pub fn early_connect(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||
pub fn connect(&self, cx: &App) -> HashMap<PublicKey, Task<Result<(bool, bool), Error>>> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
let public_key = signer.public_key().unwrap();
|
||||
|
||||
let members = self.members();
|
||||
let subscription_id = SubscriptionId::new(format!("room-{}", self.id));
|
||||
let mut tasks = HashMap::new();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let signer = client.signer().context("Signer not found")?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
for member in members.into_iter() {
|
||||
if member == public_key {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Construct a filter for messaging relays
|
||||
let inbox = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(member)
|
||||
.limit(1);
|
||||
|
||||
// Construct a filter for announcement
|
||||
let announcement = Filter::new()
|
||||
.kind(Kind::Custom(10044))
|
||||
.author(member)
|
||||
.limit(1);
|
||||
|
||||
// Subscribe to get member's gossip relays
|
||||
client
|
||||
.subscribe(vec![inbox, announcement])
|
||||
.with_id(subscription_id.clone())
|
||||
.close_on(
|
||||
SubscribeAutoCloseOptions::default()
|
||||
.timeout(Some(Duration::from_secs(TIMEOUT)))
|
||||
.exit_policy(ReqExitPolicy::ExitOnEOSE),
|
||||
)
|
||||
.await?;
|
||||
for member in members.into_iter() {
|
||||
// Skip if member is the current user
|
||||
if member == public_key {
|
||||
continue;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
let client = nostr.read(cx).client();
|
||||
let write_relays = nostr.read(cx).write_relays(&member, cx);
|
||||
|
||||
tasks.insert(
|
||||
member,
|
||||
cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Return if no relays are available
|
||||
if urls.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"User has not set up any relays. You cannot send messages to them."
|
||||
));
|
||||
}
|
||||
|
||||
// Construct filters for inbox and announcement
|
||||
let inbox_filter = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(member)
|
||||
.limit(1);
|
||||
let announcement_filter = Filter::new()
|
||||
.kind(Kind::Custom(10044))
|
||||
.author(member)
|
||||
.limit(1);
|
||||
|
||||
// Create subscription targets
|
||||
let target = urls
|
||||
.into_iter()
|
||||
.map(|relay| {
|
||||
(
|
||||
relay,
|
||||
vec![inbox_filter.clone(), announcement_filter.clone()],
|
||||
)
|
||||
})
|
||||
.collect::<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
|
||||
@@ -418,11 +468,6 @@ impl Room {
|
||||
|
||||
// Add all receiver tags
|
||||
for member in members.into_iter() {
|
||||
// Skip current user
|
||||
if member.public_key() == sender {
|
||||
continue;
|
||||
}
|
||||
|
||||
tags.push(Tag::from_standardized_without_cell(
|
||||
TagStandard::PublicKey {
|
||||
public_key: member.public_key(),
|
||||
@@ -445,61 +490,59 @@ impl Room {
|
||||
|
||||
/// Send rumor event to all members's messaging relays
|
||||
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
|
||||
let config = self.config.clone();
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
// Get room's config
|
||||
let config = self.config.clone();
|
||||
|
||||
// Get current user's public key
|
||||
let sender = nostr.read(cx).signer().public_key()?;
|
||||
let public_key = nostr.read(cx).signer().public_key()?;
|
||||
let sender = persons.read(cx).get(&public_key, cx);
|
||||
|
||||
// Get all members (excluding sender)
|
||||
let members: Vec<Person> = self
|
||||
.members
|
||||
.iter()
|
||||
.filter(|public_key| public_key != &&sender)
|
||||
.filter(|public_key| public_key != &&sender.public_key())
|
||||
.map(|member| persons.read(cx).get(member, cx))
|
||||
.collect();
|
||||
|
||||
Some(cx.background_spawn(async move {
|
||||
let signer_kind = config.signer_kind();
|
||||
let backup = config.backup();
|
||||
|
||||
let user_signer = signer.get().await;
|
||||
let encryption_signer = signer.get_encryption_signer().await;
|
||||
|
||||
let mut sents = 0;
|
||||
let mut reports = Vec::new();
|
||||
|
||||
// Process each member
|
||||
for member in members {
|
||||
let relays = member.messaging_relays();
|
||||
let announcement = member.announcement();
|
||||
let public_key = member.public_key();
|
||||
|
||||
// Skip if member has no messaging relays
|
||||
if relays.is_empty() {
|
||||
reports.push(SendReport::new(member.public_key()).error("No messaging relays"));
|
||||
reports.push(SendReport::new(public_key).error("No messaging relays"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure relay connections
|
||||
for url in relays.iter() {
|
||||
client
|
||||
.add_relay(url)
|
||||
.and_connect()
|
||||
.capabilities(RelayCapabilities::GOSSIP)
|
||||
.await
|
||||
.ok();
|
||||
// Handle encryption signer requirements
|
||||
if signer_kind.encryption() {
|
||||
if announcement.is_none() {
|
||||
reports.push(SendReport::new(public_key).error(NO_DEKEY));
|
||||
continue;
|
||||
}
|
||||
if encryption_signer.is_none() {
|
||||
reports.push(SendReport::new(sender.public_key()).error(USER_NO_DEKEY));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// When forced to use encryption signer, skip if receiver has no announcement
|
||||
if signer_kind.encryption() && announcement.is_none() {
|
||||
reports
|
||||
.push(SendReport::new(member.public_key()).error("Encryption not found"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine receiver and signer based on signer kind
|
||||
let (receiver, signer_to_use) = match signer_kind {
|
||||
// Determine receiver and signer
|
||||
let (receiver, signer) = match signer_kind {
|
||||
SignerKind::Auto => {
|
||||
if let Some(announcement) = announcement {
|
||||
if let Some(enc_signer) = encryption_signer.as_ref() {
|
||||
@@ -512,272 +555,77 @@ impl Room {
|
||||
}
|
||||
}
|
||||
SignerKind::Encryption => {
|
||||
let Some(encryption_signer) = encryption_signer.as_ref() else {
|
||||
reports.push(
|
||||
SendReport::new(member.public_key()).error("Encryption not found"),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let Some(announcement) = announcement else {
|
||||
reports.push(
|
||||
SendReport::new(member.public_key())
|
||||
.error("Announcement not found"),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
(announcement.public_key(), encryption_signer.clone())
|
||||
// Safe to unwrap due to earlier checks
|
||||
(
|
||||
announcement.unwrap().public_key(),
|
||||
encryption_signer.as_ref().unwrap().clone(),
|
||||
)
|
||||
}
|
||||
SignerKind::User => (member.public_key(), user_signer.clone()),
|
||||
};
|
||||
|
||||
// Create and send gift-wrapped event
|
||||
match EventBuilder::gift_wrap(&signer_to_use, &receiver, rumor.clone(), []).await {
|
||||
Ok(event) => {
|
||||
match client
|
||||
.send_event(&event)
|
||||
.to(relays)
|
||||
.ack_policy(AckPolicy::none())
|
||||
.await
|
||||
{
|
||||
Ok(output) => {
|
||||
reports.push(
|
||||
SendReport::new(member.public_key())
|
||||
.gift_wrap_id(event.id)
|
||||
.output(output),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
reports.push(
|
||||
SendReport::new(member.public_key()).error(e.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
reports.push(SendReport::new(member.public_key()).error(e.to_string()));
|
||||
match send_gift_wrap(&client, &signer, &receiver, &rumor, relays, public_key).await
|
||||
{
|
||||
Ok((report, _)) => {
|
||||
reports.push(report);
|
||||
sents += 1;
|
||||
}
|
||||
Err(report) => reports.push(report),
|
||||
}
|
||||
}
|
||||
|
||||
// Send backup to current user if needed
|
||||
if backup && sents >= 1 {
|
||||
let relays = sender.messaging_relays();
|
||||
let public_key = sender.public_key();
|
||||
let signer = encryption_signer.as_ref().unwrap_or(&user_signer);
|
||||
|
||||
match send_gift_wrap(&client, signer, &public_key, &rumor, relays, public_key).await
|
||||
{
|
||||
Ok((report, _)) => reports.push(report),
|
||||
Err(report) => reports.push(report),
|
||||
}
|
||||
}
|
||||
|
||||
reports
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
* /// Create a new unsigned message event
|
||||
pub fn create_message(
|
||||
&self,
|
||||
content: &str,
|
||||
replies: Vec<EventId>,
|
||||
cx: &App,
|
||||
) -> Task<Result<UnsignedEvent, Error>> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
let subject = self.subject.clone();
|
||||
let content = content.to_string();
|
||||
|
||||
let mut member_and_relay_hints = HashMap::new();
|
||||
|
||||
// Populate the hashmap with member and relay hint tasks
|
||||
for member in self.members.iter() {
|
||||
let hint = nostr.read(cx).relay_hint(member, cx);
|
||||
member_and_relay_hints.insert(member.to_owned(), hint);
|
||||
}
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let signer = client.signer().context("Signer not found")?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
// List of event tags for each receiver
|
||||
let mut tags = vec![];
|
||||
|
||||
for (member, task) in member_and_relay_hints.into_iter() {
|
||||
// Skip current user
|
||||
if member == public_key {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get relay hint if available
|
||||
let relay_url = task.await;
|
||||
|
||||
// Construct a public key tag with relay hint
|
||||
let tag = TagStandard::PublicKey {
|
||||
public_key: member,
|
||||
relay_url,
|
||||
alias: None,
|
||||
uppercase: false,
|
||||
};
|
||||
|
||||
tags.push(Tag::from_standardized_without_cell(tag));
|
||||
}
|
||||
|
||||
// Add subject tag if present
|
||||
if let Some(value) = subject {
|
||||
tags.push(Tag::from_standardized_without_cell(TagStandard::Subject(
|
||||
value.to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
// Add all reply tags
|
||||
for id in replies {
|
||||
tags.push(Tag::event(id))
|
||||
}
|
||||
|
||||
// Construct a direct message event
|
||||
//
|
||||
// WARNING: never sign and send this event to relays
|
||||
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content)
|
||||
.tags(tags)
|
||||
.build(public_key);
|
||||
|
||||
// Ensure the event ID has been generated
|
||||
event.ensure_id();
|
||||
|
||||
Ok(event)
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a task to send a message to all room members
|
||||
pub fn send_message(
|
||||
&self,
|
||||
rumor: &UnsignedEvent,
|
||||
cx: &App,
|
||||
) -> Task<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)
|
||||
})
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// Helper function to send a gift-wrapped event
|
||||
async fn send_gift_wrap<T>(
|
||||
client: &Client,
|
||||
signer: &T,
|
||||
receiver: &PublicKey,
|
||||
rumor: &UnsignedEvent,
|
||||
relays: &[RelayUrl],
|
||||
public_key: PublicKey,
|
||||
) -> Result<(SendReport, bool), SendReport>
|
||||
where
|
||||
T: NostrSigner + 'static,
|
||||
{
|
||||
// Ensure relay connections
|
||||
for url in relays {
|
||||
client.add_relay(url).and_connect().await.ok();
|
||||
}
|
||||
|
||||
match EventBuilder::gift_wrap(signer, receiver, rumor.clone(), []).await {
|
||||
Ok(event) => {
|
||||
match client
|
||||
.send_event(&event)
|
||||
.to(relays)
|
||||
.ack_policy(AckPolicy::none())
|
||||
.await
|
||||
{
|
||||
Ok(output) => Ok((
|
||||
SendReport::new(public_key)
|
||||
.gift_wrap_id(event.id)
|
||||
.output(output),
|
||||
true,
|
||||
)),
|
||||
Err(e) => Err(SendReport::new(public_key).error(e.to_string())),
|
||||
}
|
||||
}
|
||||
Err(e) => Err(SendReport::new(public_key).error(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use gpui::Action;
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use settings::SignerKind;
|
||||
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[action(namespace = chat, no_json)]
|
||||
pub enum Command {
|
||||
Insert(&'static str),
|
||||
ChangeSubject(&'static str),
|
||||
ChangeSigner(SignerKind),
|
||||
}
|
||||
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||
|
||||
@@ -17,7 +17,7 @@ use gpui_tokio::Tokio;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{Person, PersonRegistry};
|
||||
use settings::AppSettings;
|
||||
use settings::{AppSettings, SignerKind};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::fs;
|
||||
use smol::lock::RwLock;
|
||||
@@ -41,6 +41,11 @@ use crate::text::RenderedText;
|
||||
mod actions;
|
||||
mod text;
|
||||
|
||||
const NO_INBOX: &str = "has not set up messaging relays. \
|
||||
They will not receive your messages.";
|
||||
const NO_ANNOUNCEMENT: &str = "has not set up an encryption key. \
|
||||
You cannot send messages encrypted with an encryption key to them yet.";
|
||||
|
||||
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
||||
cx.new(|cx| ChatPanel::new(room, window, cx))
|
||||
}
|
||||
@@ -225,12 +230,43 @@ impl ChatPanel {
|
||||
}
|
||||
|
||||
/// Get all necessary data for each member
|
||||
fn connect(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Ok(connect) = self.room.read_with(cx, |this, cx| this.early_connect(cx)) else {
|
||||
fn connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Ok(tasks) = self.room.read_with(cx, |this, cx| this.connect(cx)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.tasks.push(cx.background_spawn(connect));
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
for (member, task) in tasks.into_iter() {
|
||||
match task.await {
|
||||
Ok((has_inbox, has_announcement)) => {
|
||||
this.update(cx, |this, cx| {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let profile = persons.read(cx).get(&member, cx);
|
||||
|
||||
if !has_inbox {
|
||||
let content = format!("{} {}", profile.name(), NO_INBOX);
|
||||
let message = Message::warning(content);
|
||||
|
||||
this.insert_message(message, true, cx);
|
||||
}
|
||||
|
||||
if !has_announcement {
|
||||
let content = format!("{} {}", profile.name(), NO_ANNOUNCEMENT);
|
||||
let message = Message::warning(content);
|
||||
|
||||
this.insert_message(message, true, cx);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_message(Message::warning(e.to_string()), true, cx);
|
||||
})?;
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Load all messages belonging to this room
|
||||
@@ -339,6 +375,7 @@ impl ChatPanel {
|
||||
};
|
||||
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
// Send and get reports
|
||||
let outputs = task.await;
|
||||
|
||||
// Add sent IDs to the list
|
||||
@@ -559,6 +596,36 @@ impl ChatPanel {
|
||||
persons.read(cx).get(public_key, cx)
|
||||
}
|
||||
|
||||
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<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 {
|
||||
const MSG: &str =
|
||||
"This conversation is private. Only members can see each other's messages.";
|
||||
@@ -1133,23 +1200,60 @@ impl ChatPanel {
|
||||
items
|
||||
}
|
||||
|
||||
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn render_encryption_menu(&self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
||||
let signer_kind = self
|
||||
.room
|
||||
.read_with(cx, |this, _cx| this.config().signer_kind().clone())
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
Button::new("encryption")
|
||||
.icon(IconName::UserKey)
|
||||
.ghost()
|
||||
.large()
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
let auto = matches!(signer_kind, SignerKind::Auto);
|
||||
let encryption = matches!(signer_kind, SignerKind::Encryption);
|
||||
let user = matches!(signer_kind, SignerKind::User);
|
||||
|
||||
this.check_side(ui::Side::Right)
|
||||
.menu_with_check_and_disabled(
|
||||
"Auto",
|
||||
auto,
|
||||
Box::new(Command::ChangeSigner(SignerKind::Auto)),
|
||||
auto,
|
||||
)
|
||||
.menu_with_check_and_disabled(
|
||||
"Decoupled Encryption Key",
|
||||
encryption,
|
||||
Box::new(Command::ChangeSigner(SignerKind::Encryption)),
|
||||
encryption,
|
||||
)
|
||||
.menu_with_check_and_disabled(
|
||||
"User Identity",
|
||||
user,
|
||||
Box::new(Command::ChangeSigner(SignerKind::User)),
|
||||
user,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_emoji_menu(&self, _window: &Window, _cx: &Context<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()
|
||||
.pl_1()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("emoji")
|
||||
.icon(IconName::Emoji)
|
||||
.ghost()
|
||||
.large()
|
||||
.dropdown_menu_with_anchor(
|
||||
gpui::Corner::BottomLeft,
|
||||
move |this, _window, _cx| {
|
||||
this.horizontal()
|
||||
.menu("👍", Box::new(Command::Insert("👍")))
|
||||
.menu("👎", Box::new(Command::Insert("👎")))
|
||||
.menu("😄", Box::new(Command::Insert("😄")))
|
||||
.menu("🎉", Box::new(Command::Insert("🎉")))
|
||||
.menu("😕", Box::new(Command::Insert("😕")))
|
||||
.menu("❤️", Box::new(Command::Insert("❤️")))
|
||||
.menu("🚀", Box::new(Command::Insert("🚀")))
|
||||
.menu("👀", Box::new(Command::Insert("👀")))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(self.render_encryption_menu(window, cx))
|
||||
.child(self.render_emoji_menu(window, cx))
|
||||
.child(
|
||||
Button::new("send")
|
||||
.icon(IconName::PaperPlaneFill)
|
||||
|
||||
@@ -16,7 +16,7 @@ use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use state::{NostrRegistry, FIND_DELAY};
|
||||
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
@@ -497,7 +497,7 @@ impl Render for Sidebar {
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.h(TITLEBAR_HEIGHT)
|
||||
.h(TABBAR_HEIGHT)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border_variant)
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
|
||||
@@ -80,13 +80,25 @@ pub struct RoomConfig {
|
||||
}
|
||||
|
||||
impl RoomConfig {
|
||||
/// Get backup config
|
||||
pub fn backup(&self) -> bool {
|
||||
self.backup
|
||||
}
|
||||
|
||||
/// Get signer kind config
|
||||
pub fn signer_kind(&self) -> &SignerKind {
|
||||
&self.signer_kind
|
||||
}
|
||||
|
||||
/// Set backup config
|
||||
pub fn set_backup(&mut self, backup: bool) {
|
||||
self.backup = backup;
|
||||
}
|
||||
|
||||
/// Set signer kind config
|
||||
pub fn set_signer_kind(&mut self, kind: &SignerKind) {
|
||||
self.signer_kind = kind.to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings
|
||||
|
||||
@@ -42,7 +42,7 @@ pub const SEARCH_RELAYS: [&str; 1] = ["wss://antiprimal.net"];
|
||||
/// Default bootstrap relays
|
||||
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://relay.primal.net",
|
||||
"wss://nos.lol",
|
||||
"wss://user.kindpag.es",
|
||||
];
|
||||
|
||||
|
||||
@@ -176,11 +176,7 @@ impl NostrRegistry {
|
||||
|
||||
while let Some(notification) = notifications.next().await {
|
||||
if let ClientNotification::Message {
|
||||
message:
|
||||
RelayMessage::Event {
|
||||
event,
|
||||
subscription_id,
|
||||
},
|
||||
message: RelayMessage::Event { event, .. },
|
||||
..
|
||||
} = notification
|
||||
{
|
||||
@@ -191,11 +187,6 @@ impl NostrRegistry {
|
||||
|
||||
match event.kind {
|
||||
Kind::RelayList => {
|
||||
// Automatically get messaging relays for each member when the user opens a room
|
||||
if subscription_id.as_str().starts_with("room-") {
|
||||
get_adv_events_by(&client, event.as_ref()).await?;
|
||||
}
|
||||
|
||||
tx.send_async(event.into_owned()).await?;
|
||||
}
|
||||
Kind::InboxRelays => {
|
||||
@@ -773,53 +764,6 @@ impl NostrRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Automatically get messaging relays and encryption announcement from a received relay list
|
||||
async fn get_adv_events_by(client: &Client, event: &Event) -> Result<(), Error> {
|
||||
// Subscription options
|
||||
let opts = SubscribeAutoCloseOptions::default()
|
||||
.timeout(Some(Duration::from_secs(TIMEOUT)))
|
||||
.exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
|
||||
// Extract write relays from event
|
||||
let write_relays: Vec<&RelayUrl> = nip65::extract_relay_list(event)
|
||||
.filter_map(|(url, metadata)| {
|
||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Ensure relay connections
|
||||
for relay in write_relays.iter() {
|
||||
client.add_relay(*relay).await?;
|
||||
client.connect_relay(*relay).await?;
|
||||
}
|
||||
|
||||
// Construct filter for inbox relays
|
||||
let inbox = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(event.pubkey)
|
||||
.limit(1);
|
||||
|
||||
// Construct filter for encryption announcement
|
||||
let announcement = Filter::new()
|
||||
.kind(Kind::Custom(10044))
|
||||
.author(event.pubkey)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target = write_relays
|
||||
.into_iter()
|
||||
.map(|relay| (relay, vec![inbox.clone(), announcement.clone()]))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
client.subscribe(target).close_on(opts).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get or create a new app keys
|
||||
fn get_or_init_app_keys() -> Result<Keys, Error> {
|
||||
let dir = config_dir().join(".app_keys");
|
||||
@@ -857,15 +801,15 @@ fn default_relay_list() -> Vec<(RelayUrl, Option<RelayMetadata>)> {
|
||||
Some(RelayMetadata::Write),
|
||||
),
|
||||
(
|
||||
RelayUrl::parse("wss://relay.primal.net/").unwrap(),
|
||||
RelayUrl::parse("wss://relay.primal.net").unwrap(),
|
||||
Some(RelayMetadata::Write),
|
||||
),
|
||||
(
|
||||
RelayUrl::parse("wss://relay.damus.io/").unwrap(),
|
||||
RelayUrl::parse("wss://relay.damus.io").unwrap(),
|
||||
Some(RelayMetadata::Read),
|
||||
),
|
||||
(
|
||||
RelayUrl::parse("wss://nos.lol/").unwrap(),
|
||||
RelayUrl::parse("wss://nos.lol").unwrap(),
|
||||
Some(RelayMetadata::Read),
|
||||
),
|
||||
]
|
||||
@@ -873,8 +817,8 @@ fn default_relay_list() -> Vec<(RelayUrl, Option<RelayMetadata>)> {
|
||||
|
||||
fn default_messaging_relays() -> Vec<RelayUrl> {
|
||||
vec![
|
||||
//RelayUrl::parse("wss://auth.nostr1.com/").unwrap(),
|
||||
RelayUrl::parse("wss://nip17.com/").unwrap(),
|
||||
RelayUrl::parse("wss://nos.lol").unwrap(),
|
||||
RelayUrl::parse("wss://nip17.com").unwrap(),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0);
|
||||
/// Defines window titlebar height
|
||||
pub const TITLEBAR_HEIGHT: Pixels = px(36.0);
|
||||
|
||||
/// Defines workspace tabbar height
|
||||
pub const TABBAR_HEIGHT: Pixels = px(28.0);
|
||||
|
||||
/// Defines default sidebar width
|
||||
pub const SIDEBAR_WIDTH: Pixels = px(240.);
|
||||
|
||||
|
||||
@@ -1,49 +1,109 @@
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, svg, App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement as _, Styled as _, Window,
|
||||
div, px, relative, rems, svg, Animation, AnimationExt, AnyElement, App, Div, ElementId,
|
||||
InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement, StyleRefinement, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{h_flex, v_flex, Disableable, IconName, Selectable};
|
||||
|
||||
type OnClick = Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>;
|
||||
use crate::icon::IconNamed;
|
||||
use crate::{v_flex, Disableable, IconName, Selectable, Sizable, Size, StyledExt as _};
|
||||
|
||||
/// A Checkbox element.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[derive(IntoElement)]
|
||||
pub struct Checkbox {
|
||||
id: ElementId,
|
||||
base: Div,
|
||||
style: StyleRefinement,
|
||||
label: Option<SharedString>,
|
||||
children: Vec<AnyElement>,
|
||||
checked: 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 {
|
||||
/// Create a new Checkbox with the given id.
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
base: div(),
|
||||
style: StyleRefinement::default(),
|
||||
label: None,
|
||||
children: Vec::new(),
|
||||
checked: false,
|
||||
disabled: false,
|
||||
size: Size::default(),
|
||||
on_click: None,
|
||||
tab_stop: true,
|
||||
tab_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the label for the checkbox.
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the checked state for the checkbox.
|
||||
pub fn checked(mut self, checked: bool) -> Self {
|
||||
self.checked = checked;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the click handler for the checkbox.
|
||||
///
|
||||
/// The `&bool` parameter indicates the new checked state after the click.
|
||||
pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self.on_click = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tab stop for the checkbox, default is true.
|
||||
pub fn tab_stop(mut self, tab_stop: bool) -> Self {
|
||||
self.tab_stop = tab_stop;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tab index for the checkbox, default is 0.
|
||||
pub fn tab_index(mut self, tab_index: isize) -> Self {
|
||||
self.tab_index = tab_index;
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn handle_click(
|
||||
on_click: &Option<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 {
|
||||
@@ -63,64 +123,190 @@ impl Selectable for Checkbox {
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Checkbox {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let icon_color = if self.disabled {
|
||||
cx.theme().icon_muted
|
||||
} else {
|
||||
cx.theme().icon_accent
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_shrink_0()
|
||||
.relative()
|
||||
.rounded_sm()
|
||||
.size_5()
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.child(
|
||||
svg()
|
||||
.absolute()
|
||||
.top_0p5()
|
||||
.left_0p5()
|
||||
.size_4()
|
||||
.text_color(icon_color)
|
||||
.map(|this| match self.checked {
|
||||
true => this.path(IconName::Check.path()),
|
||||
_ => this,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.map(|this| {
|
||||
if let Some(label) = self.label {
|
||||
this.text_color(cx.theme().text_muted).child(
|
||||
div()
|
||||
.w_full()
|
||||
.overflow_x_hidden()
|
||||
.text_ellipsis()
|
||||
.text_sm()
|
||||
.child(label),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.when(self.disabled, |this| {
|
||||
this.cursor_not_allowed()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
})
|
||||
.when_some(
|
||||
self.on_click.filter(|_| !self.disabled),
|
||||
|this, on_click| {
|
||||
this.on_click(move |_, window, cx| {
|
||||
let checked = !self.checked;
|
||||
on_click(&checked, window, cx);
|
||||
})
|
||||
},
|
||||
)
|
||||
impl ParentElement for Checkbox {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Checkbox {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn checkbox_check_icon(
|
||||
id: ElementId,
|
||||
size: Size,
|
||||
checked: bool,
|
||||
disabled: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let toggle_state = window.use_keyed_state(id, cx, |_, _| checked);
|
||||
|
||||
let color = if disabled {
|
||||
cx.theme().text.opacity(0.5)
|
||||
} else {
|
||||
cx.theme().text
|
||||
};
|
||||
|
||||
svg()
|
||||
.absolute()
|
||||
.top_px()
|
||||
.left_px()
|
||||
.map(|this| match size {
|
||||
Size::XSmall => this.size_2(),
|
||||
Size::Small => this.size_2p5(),
|
||||
Size::Medium => this.size_3(),
|
||||
Size::Large => this.size_3p5(),
|
||||
_ => this.size_3(),
|
||||
})
|
||||
.text_color(color)
|
||||
.map(|this| match checked {
|
||||
true => this.path(IconName::Check.path()),
|
||||
_ => this,
|
||||
})
|
||||
.map(|this| {
|
||||
if !disabled && checked != *toggle_state.read(cx) {
|
||||
let duration = Duration::from_secs_f64(0.25);
|
||||
cx.spawn({
|
||||
let toggle_state = toggle_state.clone();
|
||||
async move |cx| {
|
||||
cx.background_executor().timer(duration).await;
|
||||
toggle_state.update(cx, |this, _| *this = checked);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
this.with_animation(
|
||||
ElementId::NamedInteger("toggle".into(), checked as u64),
|
||||
Animation::new(Duration::from_secs_f64(0.25)),
|
||||
move |this, delta| {
|
||||
this.opacity(if checked { 1.0 * delta } else { 1.0 - delta })
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
this.into_any_element()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl RenderOnce for Checkbox {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let focus_handle = window
|
||||
.use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
|
||||
.read(cx)
|
||||
.clone();
|
||||
|
||||
let checked = self.checked;
|
||||
let radius = cx.theme().radius.min(px(4.));
|
||||
|
||||
let border_color = if checked {
|
||||
cx.theme().border_focused
|
||||
} else {
|
||||
cx.theme().border
|
||||
};
|
||||
|
||||
let color = if self.disabled {
|
||||
border_color.opacity(0.5)
|
||||
} else {
|
||||
border_color
|
||||
};
|
||||
|
||||
div().child(
|
||||
self.base
|
||||
.id(self.id.clone())
|
||||
.when(!self.disabled, |this| {
|
||||
this.track_focus(
|
||||
&focus_handle
|
||||
.tab_stop(self.tab_stop)
|
||||
.tab_index(self.tab_index),
|
||||
)
|
||||
})
|
||||
.h_flex()
|
||||
.gap_2()
|
||||
.items_start()
|
||||
.line_height(relative(1.))
|
||||
.text_color(cx.theme().text)
|
||||
.map(|this| match self.size {
|
||||
Size::XSmall => this.text_xs(),
|
||||
Size::Small => this.text_sm(),
|
||||
Size::Medium => this.text_base(),
|
||||
Size::Large => this.text_lg(),
|
||||
_ => this,
|
||||
})
|
||||
.when(self.disabled, |this| this.text_color(cx.theme().text_muted))
|
||||
.rounded(cx.theme().radius * 0.5)
|
||||
.refine_style(&self.style)
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.map(|this| match self.size {
|
||||
Size::XSmall => this.size_3(),
|
||||
Size::Small => this.size_3p5(),
|
||||
Size::Medium => this.size_4(),
|
||||
Size::Large => this.size(rems(1.125)),
|
||||
_ => this.size_4(),
|
||||
})
|
||||
.flex_shrink_0()
|
||||
.border_1()
|
||||
.border_color(color)
|
||||
.rounded(radius)
|
||||
.when(cx.theme().shadow && !self.disabled, |this| this.shadow_xs())
|
||||
.map(|this| match checked {
|
||||
false => this.bg(cx.theme().background),
|
||||
_ => this.bg(color),
|
||||
})
|
||||
.child(checkbox_check_icon(
|
||||
self.id,
|
||||
self.size,
|
||||
checked,
|
||||
self.disabled,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(self.label.is_some() || !self.children.is_empty(), |this| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.line_height(relative(1.2))
|
||||
.gap_1()
|
||||
.map(|this| {
|
||||
if let Some(label) = self.label {
|
||||
this.child(
|
||||
div()
|
||||
.size_full()
|
||||
.text_color(cx.theme().text)
|
||||
.when(self.disabled, |this| {
|
||||
this.text_color(cx.theme().text_muted)
|
||||
})
|
||||
.line_height(relative(1.))
|
||||
.child(label),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.children(self.children),
|
||||
)
|
||||
})
|
||||
.on_mouse_down(gpui::MouseButton::Left, |_, window, _| {
|
||||
// Avoid focus on mouse down.
|
||||
window.prevent_default();
|
||||
})
|
||||
.when(!self.disabled, |this| {
|
||||
this.on_click({
|
||||
let on_click = self.on_click.clone();
|
||||
move |_, window, cx| {
|
||||
window.prevent_default();
|
||||
Self::handle_click(&on_click, checked, window, cx);
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::{
|
||||
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
|
||||
StatefulInteractiveElement, Styled, WeakEntity, Window,
|
||||
};
|
||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT};
|
||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TABBAR_HEIGHT};
|
||||
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::dock_area::dock::DockPlacement;
|
||||
@@ -645,7 +645,7 @@ impl TabPanel {
|
||||
|
||||
TabBar::new()
|
||||
.track_scroll(&self.tab_bar_scroll_handle)
|
||||
.h(TITLEBAR_HEIGHT)
|
||||
.h(TABBAR_HEIGHT)
|
||||
.when(has_extend_dock_button, |this| {
|
||||
this.prefix(
|
||||
h_flex()
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
svg, AnyElement, App, AppContext, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
|
||||
SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
||||
svg, AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render,
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{Sizable, Size};
|
||||
|
||||
pub trait IconNamed {
|
||||
/// Returns the embedded path of the icon.
|
||||
fn path(self) -> SharedString;
|
||||
}
|
||||
|
||||
impl<T: IconNamed> From<T> for Icon {
|
||||
fn from(value: T) -> Self {
|
||||
Icon::build(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub enum IconName {
|
||||
ArrowLeft,
|
||||
@@ -43,6 +54,7 @@ pub enum IconName {
|
||||
Sun,
|
||||
Ship,
|
||||
Shield,
|
||||
UserKey,
|
||||
Upload,
|
||||
Usb,
|
||||
PanelLeft,
|
||||
@@ -63,7 +75,14 @@ pub enum IconName {
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
pub fn path(self) -> SharedString {
|
||||
/// Return the icon as a Entity<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 {
|
||||
Self::ArrowLeft => "icons/arrow-left.svg",
|
||||
Self::ArrowRight => "icons/arrow-right.svg",
|
||||
@@ -99,6 +118,7 @@ impl IconName {
|
||||
Self::Sun => "icons/sun.svg",
|
||||
Self::Ship => "icons/ship.svg",
|
||||
Self::Shield => "icons/shield.svg",
|
||||
Self::UserKey => "icons/user-key.svg",
|
||||
Self::Upload => "icons/upload.svg",
|
||||
Self::Usb => "icons/usb.svg",
|
||||
Self::PanelLeft => "icons/panel-left.svg",
|
||||
@@ -119,17 +139,6 @@ impl IconName {
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Return the icon as a Entity<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 {
|
||||
@@ -139,7 +148,7 @@ impl From<IconName> for AnyElement {
|
||||
}
|
||||
|
||||
impl RenderOnce for IconName {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
fn render(self, _: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
Icon::build(self)
|
||||
}
|
||||
}
|
||||
@@ -147,6 +156,7 @@ impl RenderOnce for IconName {
|
||||
#[derive(IntoElement)]
|
||||
pub struct Icon {
|
||||
base: Svg,
|
||||
style: StyleRefinement,
|
||||
path: SharedString,
|
||||
text_color: Option<Hsla>,
|
||||
size: Option<Size>,
|
||||
@@ -157,6 +167,7 @@ impl Default for Icon {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base: svg().flex_none().size_4(),
|
||||
style: StyleRefinement::default(),
|
||||
path: "".into(),
|
||||
text_color: None,
|
||||
size: None,
|
||||
@@ -168,23 +179,20 @@ impl Default for Icon {
|
||||
impl Clone for Icon {
|
||||
fn clone(&self) -> Self {
|
||||
let mut this = Self::default().path(self.path.clone());
|
||||
if let Some(size) = self.size {
|
||||
this = this.with_size(size);
|
||||
}
|
||||
this.style = self.style.clone();
|
||||
this.rotation = self.rotation;
|
||||
this.size = self.size;
|
||||
this.text_color = self.text_color;
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IconNamed {
|
||||
fn path(&self) -> SharedString;
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn new(icon: impl Into<Icon>) -> Self {
|
||||
icon.into()
|
||||
}
|
||||
|
||||
fn build(name: IconName) -> Self {
|
||||
fn build(name: impl IconNamed) -> Self {
|
||||
Self::default().path(name.path())
|
||||
}
|
||||
|
||||
@@ -197,7 +205,7 @@ impl Icon {
|
||||
}
|
||||
|
||||
/// Create a new view for the icon
|
||||
pub fn view(self, _window: &mut Window, cx: &mut App) -> Entity<Icon> {
|
||||
pub fn view(self, cx: &mut App) -> Entity<Icon> {
|
||||
cx.new(|_| self)
|
||||
}
|
||||
|
||||
@@ -221,7 +229,7 @@ impl Icon {
|
||||
|
||||
impl Styled for Icon {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
&mut self.style
|
||||
}
|
||||
|
||||
fn text_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
@@ -240,9 +248,15 @@ impl Sizable for Icon {
|
||||
impl RenderOnce for Icon {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let text_color = self.text_color.unwrap_or_else(|| window.text_style().color);
|
||||
let text_size = window.text_style().font_size.to_pixels(window.rem_size());
|
||||
let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some();
|
||||
|
||||
self.base
|
||||
let mut base = self.base;
|
||||
*base.style() = self.style;
|
||||
|
||||
base.flex_shrink_0()
|
||||
.text_color(text_color)
|
||||
.when(!has_base_size, |this| this.size(text_size))
|
||||
.when_some(self.size, |this, size| match size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_3(),
|
||||
@@ -261,16 +275,17 @@ impl From<Icon> for AnyElement {
|
||||
}
|
||||
|
||||
impl Render for Icon {
|
||||
fn render(
|
||||
&mut self,
|
||||
_window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let text_color = self.text_color.unwrap_or_else(|| cx.theme().icon);
|
||||
let text_size = window.text_style().font_size.to_pixels(window.rem_size());
|
||||
let has_base_size = self.style.size.width.is_some() || self.style.size.height.is_some();
|
||||
|
||||
svg()
|
||||
.flex_none()
|
||||
let mut base = svg().flex_none();
|
||||
*base.style() = self.style.clone();
|
||||
|
||||
base.flex_shrink_0()
|
||||
.text_color(text_color)
|
||||
.when(!has_base_size, |this| this.size(text_size))
|
||||
.when_some(self.size, |this, size| match size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_3(),
|
||||
@@ -278,7 +293,7 @@ impl Render for Icon {
|
||||
Size::Medium => this.size_5(),
|
||||
Size::Large => this.size_6(),
|
||||
})
|
||||
.when(!self.path.is_empty(), |this| this.path(self.path.clone()))
|
||||
.path(self.path.clone())
|
||||
.when_some(self.rotation, |this, rotation| {
|
||||
this.with_transformation(Transformation::rotate(rotation))
|
||||
})
|
||||
|
||||
@@ -1028,7 +1028,7 @@ impl PopupMenu {
|
||||
Icon::empty()
|
||||
};
|
||||
|
||||
Some(icon.xsmall())
|
||||
Some(icon.small())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -3,7 +3,7 @@ use gpui::{
|
||||
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||
RenderOnce, StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||
|
||||
use crate::{Selectable, Sizable, Size};
|
||||
|
||||
@@ -136,7 +136,7 @@ impl RenderOnce for Tab {
|
||||
|
||||
self.base
|
||||
.id(self.ix)
|
||||
.h(TITLEBAR_HEIGHT)
|
||||
.h(TABBAR_HEIGHT)
|
||||
.px_4()
|
||||
.relative()
|
||||
.flex()
|
||||
|
||||
Reference in New Issue
Block a user