Compare commits
9 Commits
v1.0.0-bet
...
chat-patch
| Author | SHA1 | Date | |
|---|---|---|---|
| fec148dcb5 | |||
| 1d57a2deab | |||
| aa26c9ccba | |||
| ff61c28a76 | |||
| 069eae8145 | |||
| ccbcc644db | |||
| 15c5ce7677 | |||
| 40d726c986 | |||
| fe4eb7df74 |
539
Cargo.lock
generated
539
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,14 +4,14 @@ members = ["crates/*"]
|
||||
default-members = ["crates/coop"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.0.0-beta"
|
||||
edition = "2021"
|
||||
version = "1.0.0-beta1"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
# GPUI
|
||||
gpui = { git = "https://github.com/zed-industries/zed" }
|
||||
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland", "runtime_shaders"] }
|
||||
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland"] }
|
||||
gpui_linux = { git = "https://github.com/zed-industries/zed" }
|
||||
gpui_windows = { git = "https://github.com/zed-industries/zed" }
|
||||
gpui_macos = { git = "https://github.com/zed-industries/zed" }
|
||||
@@ -20,8 +20,10 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||
|
||||
# Nostr
|
||||
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr-memory = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr-blossom = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr-gossip-sqlite = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr-sdk = { git = "https://github.com/rust-nostr/nostr" }
|
||||
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] }
|
||||
|
||||
|
||||
3
assets/icons/input.svg
Normal file
3
assets/icons/input.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M11.75 6.75H19.25C20.3546 6.75 21.25 7.64543 21.25 8.75V15.25C21.25 16.3546 20.3546 17.25 19.25 17.25H11.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.75 6.75H4.75C3.64543 6.75 2.75 7.64543 2.75 8.75V15.25C2.75 16.3546 3.64543 17.25 4.75 17.25H5.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.75 3.75V20.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 604 B |
144
assets/themes/aurora.json
Normal file
144
assets/themes/aurora.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"id": "aurora",
|
||||
"name": "Aurora",
|
||||
"author": "Coop",
|
||||
"url": "https://github.com/lumehq/coop",
|
||||
"light": {
|
||||
"background": "#fdfcfeff",
|
||||
"surface_background": "#f8f8ffff",
|
||||
"elevated_surface_background": "#f0f1feff",
|
||||
"panel_background": "#fdfcfeff",
|
||||
"overlay": "#211f4300",
|
||||
"title_bar": "#f0f1feff",
|
||||
"title_bar_inactive": "#fdfcfeff",
|
||||
"window_border": "#dadcffff",
|
||||
"border": "#dadcffff",
|
||||
"border_variant": "#cbcdffff",
|
||||
"border_focused": "#5b5bd6ff",
|
||||
"border_selected": "#5b5bd6ff",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#e6e7ffff",
|
||||
"ring": "#5151cdff",
|
||||
"text": "#1f2d5cff",
|
||||
"text_muted": "#5753c6ff",
|
||||
"text_placeholder": "#9b9ef0ff",
|
||||
"text_accent": "#5b5bd6ff",
|
||||
"text_danger": "#e54d2eff",
|
||||
"text_warning": "#f76b15ff",
|
||||
"icon": "#5753c6ff",
|
||||
"icon_muted": "#9b9ef0ff",
|
||||
"icon_accent": "#5151cdff",
|
||||
"element_foreground": "#ffffffff",
|
||||
"element_background": "#5b5bd6ff",
|
||||
"element_hover": "#5151cdff",
|
||||
"element_active": "#6e56cfff",
|
||||
"element_selected": "#654dc4ff",
|
||||
"element_disabled": "#5b5bd64d",
|
||||
"secondary_foreground": "#1f2d5cff",
|
||||
"secondary_background": "#f0f1feff",
|
||||
"secondary_hover": "#e6e7ffff",
|
||||
"secondary_active": "#dadcffff",
|
||||
"secondary_selected": "#dadcffff",
|
||||
"secondary_disabled": "#5b5bd64d",
|
||||
"danger_foreground": "#ffffffff",
|
||||
"danger_background": "#feebe7ff",
|
||||
"danger_hover": "#ffcdc2ff",
|
||||
"danger_active": "#fdbdafff",
|
||||
"danger_selected": "#fdbdafff",
|
||||
"danger_disabled": "#e54d2e4d",
|
||||
"warning_foreground": "#ffffffff",
|
||||
"warning_background": "#fff7edff",
|
||||
"warning_hover": "#ffd19aff",
|
||||
"warning_active": "#ffc182ff",
|
||||
"warning_selected": "#ffc182ff",
|
||||
"warning_disabled": "#f76b154d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#f0f1feff",
|
||||
"ghost_element_hover": "#211f430d",
|
||||
"ghost_element_active": "#211f431a",
|
||||
"ghost_element_selected": "#211f431a",
|
||||
"ghost_element_disabled": "#211f4305",
|
||||
"tab_background": "#f0f1feff",
|
||||
"tab_foreground": "#5753c6ff",
|
||||
"tab_hover_background": "#211f430d",
|
||||
"tab_active_background": "#fdfcfeff",
|
||||
"tab_active_foreground": "#1f2d5cff",
|
||||
"scrollbar_thumb_background": "#211f431a",
|
||||
"scrollbar_thumb_hover_background": "#211f4326",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#00000000",
|
||||
"drop_target_background": "#5b5bd61a",
|
||||
"cursor": "#5b5bd6ff",
|
||||
"selection": "#5b5bd640"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#14121fff",
|
||||
"surface_background": "#1b1525ff",
|
||||
"elevated_surface_background": "#291f43ff",
|
||||
"panel_background": "#14121fff",
|
||||
"overlay": "#baa7ff1a",
|
||||
"title_bar": "#291f43ff",
|
||||
"title_bar_inactive": "#14121fff",
|
||||
"window_border": "#473876ff",
|
||||
"border": "#473876ff",
|
||||
"border_variant": "#3c2e69ff",
|
||||
"border_focused": "#7d66d9ff",
|
||||
"border_selected": "#7d66d9ff",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#33255bff",
|
||||
"ring": "#6e56cfff",
|
||||
"text": "#e2ddfeff",
|
||||
"text_muted": "#baa7ffff",
|
||||
"text_placeholder": "#6958adff",
|
||||
"text_accent": "#baa7ffff",
|
||||
"text_danger": "#ff977dff",
|
||||
"text_warning": "#ffa057ff",
|
||||
"icon": "#baa7ffff",
|
||||
"icon_muted": "#6958adff",
|
||||
"icon_accent": "#6e56cfff",
|
||||
"element_foreground": "#14121fff",
|
||||
"element_background": "#7d66d9ff",
|
||||
"element_hover": "#baa7ffff",
|
||||
"element_active": "#6e56cfff",
|
||||
"element_selected": "#654dc4ff",
|
||||
"element_disabled": "#7d66d94d",
|
||||
"secondary_foreground": "#e2ddfeff",
|
||||
"secondary_background": "#291f43ff",
|
||||
"secondary_hover": "#33255bff",
|
||||
"secondary_active": "#3c2e69ff",
|
||||
"secondary_selected": "#3c2e69ff",
|
||||
"secondary_disabled": "#7d66d94d",
|
||||
"danger_foreground": "#181111ff",
|
||||
"danger_background": "#391714ff",
|
||||
"danger_hover": "#5e1c16ff",
|
||||
"danger_active": "#6e2920ff",
|
||||
"danger_selected": "#6e2920ff",
|
||||
"danger_disabled": "#ff977d4d",
|
||||
"warning_foreground": "#17120eff",
|
||||
"warning_background": "#331e0bff",
|
||||
"warning_hover": "#562800ff",
|
||||
"warning_active": "#66350cff",
|
||||
"warning_selected": "#66350cff",
|
||||
"warning_disabled": "#ffa0574d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#291f43ff",
|
||||
"ghost_element_hover": "#baa7ff0d",
|
||||
"ghost_element_active": "#baa7ff1a",
|
||||
"ghost_element_selected": "#baa7ff1a",
|
||||
"ghost_element_disabled": "#baa7ff05",
|
||||
"tab_background": "#291f43ff",
|
||||
"tab_foreground": "#baa7ffff",
|
||||
"tab_hover_background": "#baa7ff0d",
|
||||
"tab_active_background": "#14121fff",
|
||||
"tab_active_foreground": "#e2ddfeff",
|
||||
"scrollbar_thumb_background": "#baa7ff1a",
|
||||
"scrollbar_thumb_hover_background": "#baa7ff26",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#00000000",
|
||||
"drop_target_background": "#baa7ff1a",
|
||||
"cursor": "#baa7ffff",
|
||||
"selection": "#baa7ff40"
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,76 @@
|
||||
{
|
||||
"id": "catppuccin-frappe",
|
||||
"name": "Catppuccin Frappé",
|
||||
"author": "Catppuccin",
|
||||
"url": "https://github.com/catppuccin/catppuccin",
|
||||
"author": "Catppuccin Org (ported by Coop)",
|
||||
"url": "https://catppuccin.com",
|
||||
"light": {
|
||||
"background": "#303446",
|
||||
"surface_background": "#292c3c",
|
||||
"elevated_surface_background": "#232634",
|
||||
"panel_background": "#303446",
|
||||
"overlay": "#c6d0f51a",
|
||||
"title_bar": "#292c3c",
|
||||
"title_bar_inactive": "#232634",
|
||||
"window_border": "#737994",
|
||||
"border": "#626880",
|
||||
"border_variant": "#51576d",
|
||||
"title_bar": "#232634",
|
||||
"title_bar_inactive": "#303446",
|
||||
"window_border": "#51576d",
|
||||
"border": "#51576d",
|
||||
"border_variant": "#414559",
|
||||
"border_focused": "#8caaee",
|
||||
"border_selected": "#8caaee",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#414559",
|
||||
"ring": "#8caaee",
|
||||
"border_transparent": "#c6d0f500",
|
||||
"border_disabled": "#292c3c",
|
||||
"ring": "#babbf1",
|
||||
"text": "#c6d0f5",
|
||||
"text_muted": "#b5bfe2",
|
||||
"text_placeholder": "#a5adce",
|
||||
"text_muted": "#a5adce",
|
||||
"text_placeholder": "#838ba7",
|
||||
"text_accent": "#8caaee",
|
||||
"icon": "#b5bfe2",
|
||||
"icon_muted": "#a5adce",
|
||||
"icon_accent": "#8caaee",
|
||||
"element_foreground": "#232634",
|
||||
"text_danger": "#e78284",
|
||||
"text_warning": "#ef9f76",
|
||||
"icon": "#a5adce",
|
||||
"icon_muted": "#838ba7",
|
||||
"icon_accent": "#babbf1",
|
||||
"element_foreground": "#303446",
|
||||
"element_background": "#8caaee",
|
||||
"element_hover": "#babbf1",
|
||||
"element_active": "#7e99d6",
|
||||
"element_selected": "#7088bf",
|
||||
"element_active": "#99d1db",
|
||||
"element_selected": "#85c1dc",
|
||||
"element_disabled": "#8caaee4d",
|
||||
"secondary_foreground": "#7088bf",
|
||||
"secondary_background": "#292c3c",
|
||||
"secondary_hover": "#8caaee33",
|
||||
"secondary_active": "#232634",
|
||||
"secondary_selected": "#232634",
|
||||
"secondary_foreground": "#c6d0f5",
|
||||
"secondary_background": "#414559",
|
||||
"secondary_hover": "#51576d",
|
||||
"secondary_active": "#626880",
|
||||
"secondary_selected": "#626880",
|
||||
"secondary_disabled": "#8caaee4d",
|
||||
"danger_foreground": "#232634",
|
||||
"danger_foreground": "#303446",
|
||||
"danger_background": "#e78284",
|
||||
"danger_hover": "#ea999c",
|
||||
"danger_active": "#d07576",
|
||||
"danger_selected": "#b96869",
|
||||
"danger_active": "#ef9f76",
|
||||
"danger_selected": "#e5c890",
|
||||
"danger_disabled": "#e782844d",
|
||||
"warning_foreground": "#232634",
|
||||
"warning_background": "#e5c890",
|
||||
"warning_hover": "#ef9f76",
|
||||
"warning_active": "#ceb482",
|
||||
"warning_selected": "#b7a074",
|
||||
"warning_disabled": "#e5c8904d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#414559",
|
||||
"ghost_element_hover": "#c6d0f533",
|
||||
"ghost_element_active": "#51576d",
|
||||
"ghost_element_selected": "#51576d",
|
||||
"ghost_element_disabled": "#c6d0f50d",
|
||||
"tab_inactive_background": "#292c3c",
|
||||
"tab_inactive_foreground": "#b5bfe2",
|
||||
"warning_foreground": "#303446",
|
||||
"warning_background": "#ef9f76",
|
||||
"warning_hover": "#e5c890",
|
||||
"warning_active": "#a6d189",
|
||||
"warning_selected": "#81c8be",
|
||||
"warning_disabled": "#ef9f764d",
|
||||
"ghost_element_background": "#c6d0f500",
|
||||
"ghost_element_background_alt": "#292c3c",
|
||||
"ghost_element_hover": "#c6d0f50d",
|
||||
"ghost_element_active": "#c6d0f51a",
|
||||
"ghost_element_selected": "#c6d0f51a",
|
||||
"ghost_element_disabled": "#c6d0f505",
|
||||
"tab_background": "#232634",
|
||||
"tab_foreground": "#a5adce",
|
||||
"tab_hover_background": "#c6d0f50d",
|
||||
"tab_active_background": "#303446",
|
||||
"tab_active_foreground": "#c6d0f5",
|
||||
"tab_hover_foreground": "#babbf1",
|
||||
"scrollbar_thumb_background": "#c6d0f533",
|
||||
"scrollbar_thumb_hover_background": "#c6d0f580",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#51576d",
|
||||
"scrollbar_thumb_background": "#c6d0f51a",
|
||||
"scrollbar_thumb_hover_background": "#c6d0f526",
|
||||
"scrollbar_thumb_border": "#c6d0f500",
|
||||
"scrollbar_track_background": "#c6d0f500",
|
||||
"scrollbar_track_border": "#c6d0f500",
|
||||
"drop_target_background": "#8caaee1a",
|
||||
"cursor": "#f2d5cf",
|
||||
"selection": "#949cbb40"
|
||||
"cursor": "#8caaee",
|
||||
"selection": "#8caaee40"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#303446",
|
||||
@@ -76,65 +78,67 @@
|
||||
"elevated_surface_background": "#232634",
|
||||
"panel_background": "#303446",
|
||||
"overlay": "#c6d0f51a",
|
||||
"title_bar": "#292c3c",
|
||||
"title_bar_inactive": "#232634",
|
||||
"window_border": "#737994",
|
||||
"border": "#626880",
|
||||
"border_variant": "#51576d",
|
||||
"title_bar": "#232634",
|
||||
"title_bar_inactive": "#303446",
|
||||
"window_border": "#51576d",
|
||||
"border": "#51576d",
|
||||
"border_variant": "#414559",
|
||||
"border_focused": "#8caaee",
|
||||
"border_selected": "#8caaee",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#414559",
|
||||
"ring": "#8caaee",
|
||||
"border_transparent": "#c6d0f500",
|
||||
"border_disabled": "#292c3c",
|
||||
"ring": "#babbf1",
|
||||
"text": "#c6d0f5",
|
||||
"text_muted": "#b5bfe2",
|
||||
"text_placeholder": "#a5adce",
|
||||
"text_muted": "#a5adce",
|
||||
"text_placeholder": "#838ba7",
|
||||
"text_accent": "#8caaee",
|
||||
"icon": "#b5bfe2",
|
||||
"icon_muted": "#a5adce",
|
||||
"icon_accent": "#8caaee",
|
||||
"element_foreground": "#232634",
|
||||
"text_danger": "#e78284",
|
||||
"text_warning": "#ef9f76",
|
||||
"icon": "#a5adce",
|
||||
"icon_muted": "#838ba7",
|
||||
"icon_accent": "#babbf1",
|
||||
"element_foreground": "#303446",
|
||||
"element_background": "#8caaee",
|
||||
"element_hover": "#babbf1",
|
||||
"element_active": "#7e99d6",
|
||||
"element_selected": "#7088bf",
|
||||
"element_active": "#99d1db",
|
||||
"element_selected": "#85c1dc",
|
||||
"element_disabled": "#8caaee4d",
|
||||
"secondary_foreground": "#7088bf",
|
||||
"secondary_background": "#292c3c",
|
||||
"secondary_hover": "#8caaee33",
|
||||
"secondary_active": "#232634",
|
||||
"secondary_selected": "#232634",
|
||||
"secondary_foreground": "#c6d0f5",
|
||||
"secondary_background": "#414559",
|
||||
"secondary_hover": "#51576d",
|
||||
"secondary_active": "#626880",
|
||||
"secondary_selected": "#626880",
|
||||
"secondary_disabled": "#8caaee4d",
|
||||
"danger_foreground": "#232634",
|
||||
"danger_foreground": "#303446",
|
||||
"danger_background": "#e78284",
|
||||
"danger_hover": "#ea999c",
|
||||
"danger_active": "#d07576",
|
||||
"danger_selected": "#b96869",
|
||||
"danger_active": "#ef9f76",
|
||||
"danger_selected": "#e5c890",
|
||||
"danger_disabled": "#e782844d",
|
||||
"warning_foreground": "#232634",
|
||||
"warning_background": "#e5c890",
|
||||
"warning_hover": "#ef9f76",
|
||||
"warning_active": "#ceb482",
|
||||
"warning_selected": "#b7a074",
|
||||
"warning_disabled": "#e5c8904d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#414559",
|
||||
"ghost_element_hover": "#c6d0f533",
|
||||
"ghost_element_active": "#51576d",
|
||||
"ghost_element_selected": "#51576d",
|
||||
"ghost_element_disabled": "#c6d0f50d",
|
||||
"tab_inactive_background": "#292c3c",
|
||||
"tab_inactive_foreground": "#b5bfe2",
|
||||
"warning_foreground": "#303446",
|
||||
"warning_background": "#ef9f76",
|
||||
"warning_hover": "#e5c890",
|
||||
"warning_active": "#a6d189",
|
||||
"warning_selected": "#81c8be",
|
||||
"warning_disabled": "#ef9f764d",
|
||||
"ghost_element_background": "#c6d0f500",
|
||||
"ghost_element_background_alt": "#292c3c",
|
||||
"ghost_element_hover": "#c6d0f50d",
|
||||
"ghost_element_active": "#c6d0f51a",
|
||||
"ghost_element_selected": "#c6d0f51a",
|
||||
"ghost_element_disabled": "#c6d0f505",
|
||||
"tab_background": "#232634",
|
||||
"tab_foreground": "#a5adce",
|
||||
"tab_hover_background": "#c6d0f50d",
|
||||
"tab_active_background": "#303446",
|
||||
"tab_active_foreground": "#c6d0f5",
|
||||
"tab_hover_foreground": "#babbf1",
|
||||
"scrollbar_thumb_background": "#c6d0f533",
|
||||
"scrollbar_thumb_hover_background": "#c6d0f580",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#51576d",
|
||||
"scrollbar_thumb_background": "#c6d0f51a",
|
||||
"scrollbar_thumb_hover_background": "#c6d0f526",
|
||||
"scrollbar_thumb_border": "#c6d0f500",
|
||||
"scrollbar_track_background": "#c6d0f500",
|
||||
"scrollbar_track_border": "#c6d0f500",
|
||||
"drop_target_background": "#8caaee1a",
|
||||
"cursor": "#f2d5cf",
|
||||
"selection": "#949cbb40"
|
||||
"cursor": "#8caaee",
|
||||
"selection": "#8caaee40"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,76 @@
|
||||
{
|
||||
"id": "catppuccin-latte",
|
||||
"name": "Catppuccin Latte",
|
||||
"author": "Catppuccin",
|
||||
"url": "https://github.com/catppuccin/catppuccin",
|
||||
"author": "Catppuccin Org (ported by Coop)",
|
||||
"url": "https://catppuccin.com",
|
||||
"light": {
|
||||
"background": "#eff1f5",
|
||||
"surface_background": "#e6e9ef",
|
||||
"elevated_surface_background": "#dce0e8",
|
||||
"panel_background": "#eff1f5",
|
||||
"overlay": "#4c4f691a",
|
||||
"title_bar": "#e6e9ef",
|
||||
"title_bar_inactive": "#dce0e8",
|
||||
"window_border": "#9ca0b0",
|
||||
"border": "#acb0be",
|
||||
"border_variant": "#bcc0cc",
|
||||
"title_bar": "#dce0e8",
|
||||
"title_bar_inactive": "#eff1f5",
|
||||
"window_border": "#bcc0cc",
|
||||
"border": "#bcc0cc",
|
||||
"border_variant": "#ccd0da",
|
||||
"border_focused": "#1e66f5",
|
||||
"border_selected": "#1e66f5",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#ccd0da",
|
||||
"ring": "#1e66f5",
|
||||
"border_transparent": "#4c4f6900",
|
||||
"border_disabled": "#e6e9ef",
|
||||
"ring": "#7287fd",
|
||||
"text": "#4c4f69",
|
||||
"text_muted": "#5c5f77",
|
||||
"text_placeholder": "#6c6f85",
|
||||
"text_muted": "#6c6f85",
|
||||
"text_placeholder": "#8c8fa1",
|
||||
"text_accent": "#1e66f5",
|
||||
"icon": "#5c5f77",
|
||||
"icon_muted": "#6c6f85",
|
||||
"icon_accent": "#1e66f5",
|
||||
"text_danger": "#d20f39",
|
||||
"text_warning": "#fe640b",
|
||||
"icon": "#6c6f85",
|
||||
"icon_muted": "#8c8fa1",
|
||||
"icon_accent": "#7287fd",
|
||||
"element_foreground": "#eff1f5",
|
||||
"element_background": "#1e66f5",
|
||||
"element_hover": "#8839ef",
|
||||
"element_active": "#1c5ce0",
|
||||
"element_selected": "#1a52cc",
|
||||
"element_hover": "#7287fd",
|
||||
"element_active": "#04a5e5",
|
||||
"element_selected": "#209fb5",
|
||||
"element_disabled": "#1e66f54d",
|
||||
"secondary_foreground": "#1a52cc",
|
||||
"secondary_background": "#e6e9ef",
|
||||
"secondary_hover": "#8839ef33",
|
||||
"secondary_active": "#dce0e8",
|
||||
"secondary_selected": "#dce0e8",
|
||||
"secondary_foreground": "#4c4f69",
|
||||
"secondary_background": "#ccd0da",
|
||||
"secondary_hover": "#bcc0cc",
|
||||
"secondary_active": "#acb0be",
|
||||
"secondary_selected": "#acb0be",
|
||||
"secondary_disabled": "#1e66f54d",
|
||||
"danger_foreground": "#eff1f5",
|
||||
"danger_background": "#d20f39",
|
||||
"danger_hover": "#e64553",
|
||||
"danger_active": "#bd0d33",
|
||||
"danger_selected": "#a80b2d",
|
||||
"danger_active": "#fe640b",
|
||||
"danger_selected": "#df8e1d",
|
||||
"danger_disabled": "#d20f394d",
|
||||
"warning_foreground": "#4c4f69",
|
||||
"warning_background": "#df8e1d",
|
||||
"warning_hover": "#fe640b",
|
||||
"warning_active": "#c9801a",
|
||||
"warning_selected": "#b47217",
|
||||
"warning_disabled": "#df8e1d4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#ccd0da",
|
||||
"ghost_element_hover": "#4c4f6933",
|
||||
"ghost_element_active": "#bcc0cc",
|
||||
"ghost_element_selected": "#bcc0cc",
|
||||
"ghost_element_disabled": "#4c4f690d",
|
||||
"tab_inactive_background": "#e6e9ef",
|
||||
"tab_inactive_foreground": "#5c5f77",
|
||||
"warning_foreground": "#eff1f5",
|
||||
"warning_background": "#fe640b",
|
||||
"warning_hover": "#df8e1d",
|
||||
"warning_active": "#40a02b",
|
||||
"warning_selected": "#179299",
|
||||
"warning_disabled": "#fe640b4d",
|
||||
"ghost_element_background": "#4c4f6900",
|
||||
"ghost_element_background_alt": "#e6e9ef",
|
||||
"ghost_element_hover": "#4c4f690d",
|
||||
"ghost_element_active": "#4c4f691a",
|
||||
"ghost_element_selected": "#4c4f691a",
|
||||
"ghost_element_disabled": "#4c4f6905",
|
||||
"tab_background": "#e6e9ef",
|
||||
"tab_foreground": "#6c6f85",
|
||||
"tab_hover_background": "#4c4f690d",
|
||||
"tab_active_background": "#eff1f5",
|
||||
"tab_active_foreground": "#4c4f69",
|
||||
"tab_hover_foreground": "#8839ef",
|
||||
"scrollbar_thumb_background": "#4c4f6933",
|
||||
"scrollbar_thumb_hover_background": "#4c4f6980",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#bcc0cc",
|
||||
"scrollbar_thumb_background": "#4c4f691a",
|
||||
"scrollbar_thumb_hover_background": "#4c4f6926",
|
||||
"scrollbar_thumb_border": "#4c4f6900",
|
||||
"scrollbar_track_background": "#4c4f6900",
|
||||
"scrollbar_track_border": "#4c4f6900",
|
||||
"drop_target_background": "#1e66f51a",
|
||||
"cursor": "#dc8a78",
|
||||
"selection": "#7c7f9340"
|
||||
"cursor": "#1e66f5",
|
||||
"selection": "#1e66f540"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#eff1f5",
|
||||
@@ -76,65 +78,67 @@
|
||||
"elevated_surface_background": "#dce0e8",
|
||||
"panel_background": "#eff1f5",
|
||||
"overlay": "#4c4f691a",
|
||||
"title_bar": "#e6e9ef",
|
||||
"title_bar_inactive": "#dce0e8",
|
||||
"window_border": "#9ca0b0",
|
||||
"border": "#acb0be",
|
||||
"border_variant": "#bcc0cc",
|
||||
"title_bar": "#dce0e8",
|
||||
"title_bar_inactive": "#eff1f5",
|
||||
"window_border": "#bcc0cc",
|
||||
"border": "#bcc0cc",
|
||||
"border_variant": "#ccd0da",
|
||||
"border_focused": "#1e66f5",
|
||||
"border_selected": "#1e66f5",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#ccd0da",
|
||||
"ring": "#1e66f5",
|
||||
"border_transparent": "#4c4f6900",
|
||||
"border_disabled": "#e6e9ef",
|
||||
"ring": "#7287fd",
|
||||
"text": "#4c4f69",
|
||||
"text_muted": "#5c5f77",
|
||||
"text_placeholder": "#6c6f85",
|
||||
"text_muted": "#6c6f85",
|
||||
"text_placeholder": "#8c8fa1",
|
||||
"text_accent": "#1e66f5",
|
||||
"icon": "#5c5f77",
|
||||
"icon_muted": "#6c6f85",
|
||||
"icon_accent": "#1e66f5",
|
||||
"text_danger": "#d20f39",
|
||||
"text_warning": "#fe640b",
|
||||
"icon": "#6c6f85",
|
||||
"icon_muted": "#8c8fa1",
|
||||
"icon_accent": "#7287fd",
|
||||
"element_foreground": "#eff1f5",
|
||||
"element_background": "#1e66f5",
|
||||
"element_hover": "#8839ef",
|
||||
"element_active": "#1c5ce0",
|
||||
"element_selected": "#1a52cc",
|
||||
"element_hover": "#7287fd",
|
||||
"element_active": "#04a5e5",
|
||||
"element_selected": "#209fb5",
|
||||
"element_disabled": "#1e66f54d",
|
||||
"secondary_foreground": "#1a52cc",
|
||||
"secondary_background": "#e6e9ef",
|
||||
"secondary_hover": "#8839ef33",
|
||||
"secondary_active": "#dce0e8",
|
||||
"secondary_selected": "#dce0e8",
|
||||
"secondary_foreground": "#4c4f69",
|
||||
"secondary_background": "#ccd0da",
|
||||
"secondary_hover": "#bcc0cc",
|
||||
"secondary_active": "#acb0be",
|
||||
"secondary_selected": "#acb0be",
|
||||
"secondary_disabled": "#1e66f54d",
|
||||
"danger_foreground": "#eff1f5",
|
||||
"danger_background": "#d20f39",
|
||||
"danger_hover": "#e64553",
|
||||
"danger_active": "#bd0d33",
|
||||
"danger_selected": "#a80b2d",
|
||||
"danger_active": "#fe640b",
|
||||
"danger_selected": "#df8e1d",
|
||||
"danger_disabled": "#d20f394d",
|
||||
"warning_foreground": "#4c4f69",
|
||||
"warning_background": "#df8e1d",
|
||||
"warning_hover": "#fe640b",
|
||||
"warning_active": "#c9801a",
|
||||
"warning_selected": "#b47217",
|
||||
"warning_disabled": "#df8e1d4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#ccd0da",
|
||||
"ghost_element_hover": "#4c4f6933",
|
||||
"ghost_element_active": "#bcc0cc",
|
||||
"ghost_element_selected": "#bcc0cc",
|
||||
"ghost_element_disabled": "#4c4f690d",
|
||||
"tab_inactive_background": "#e6e9ef",
|
||||
"tab_inactive_foreground": "#5c5f77",
|
||||
"warning_foreground": "#eff1f5",
|
||||
"warning_background": "#fe640b",
|
||||
"warning_hover": "#df8e1d",
|
||||
"warning_active": "#40a02b",
|
||||
"warning_selected": "#179299",
|
||||
"warning_disabled": "#fe640b4d",
|
||||
"ghost_element_background": "#4c4f6900",
|
||||
"ghost_element_background_alt": "#e6e9ef",
|
||||
"ghost_element_hover": "#4c4f690d",
|
||||
"ghost_element_active": "#4c4f691a",
|
||||
"ghost_element_selected": "#4c4f691a",
|
||||
"ghost_element_disabled": "#4c4f6905",
|
||||
"tab_background": "#e6e9ef",
|
||||
"tab_foreground": "#6c6f85",
|
||||
"tab_hover_background": "#4c4f690d",
|
||||
"tab_active_background": "#eff1f5",
|
||||
"tab_active_foreground": "#4c4f69",
|
||||
"tab_hover_foreground": "#8839ef",
|
||||
"scrollbar_thumb_background": "#4c4f6933",
|
||||
"scrollbar_thumb_hover_background": "#4c4f6980",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#bcc0cc",
|
||||
"scrollbar_thumb_background": "#4c4f691a",
|
||||
"scrollbar_thumb_hover_background": "#4c4f6926",
|
||||
"scrollbar_thumb_border": "#4c4f6900",
|
||||
"scrollbar_track_background": "#4c4f6900",
|
||||
"scrollbar_track_border": "#4c4f6900",
|
||||
"drop_target_background": "#1e66f51a",
|
||||
"cursor": "#dc8a78",
|
||||
"selection": "#7c7f9340"
|
||||
"cursor": "#1e66f5",
|
||||
"selection": "#1e66f540"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,76 @@
|
||||
{
|
||||
"id": "catppuccin-macchiato",
|
||||
"name": "Catppuccin Macchiato",
|
||||
"author": "Catppuccin",
|
||||
"url": "https://github.com/catppuccin/catppuccin",
|
||||
"author": "Catppuccin Org (ported by Coop)",
|
||||
"url": "https://catppuccin.com",
|
||||
"light": {
|
||||
"background": "#24273a",
|
||||
"surface_background": "#1e2030",
|
||||
"elevated_surface_background": "#181926",
|
||||
"panel_background": "#24273a",
|
||||
"overlay": "#cad3f51a",
|
||||
"title_bar": "#1e2030",
|
||||
"title_bar_inactive": "#181926",
|
||||
"window_border": "#6e738d",
|
||||
"border": "#5b6078",
|
||||
"border_variant": "#494d64",
|
||||
"title_bar": "#181926",
|
||||
"title_bar_inactive": "#24273a",
|
||||
"window_border": "#494d64",
|
||||
"border": "#494d64",
|
||||
"border_variant": "#363a4f",
|
||||
"border_focused": "#8aadf4",
|
||||
"border_selected": "#8aadf4",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#363a4f",
|
||||
"ring": "#8aadf4",
|
||||
"border_transparent": "#cad3f500",
|
||||
"border_disabled": "#1e2030",
|
||||
"ring": "#b7bdf8",
|
||||
"text": "#cad3f5",
|
||||
"text_muted": "#b8c0e0",
|
||||
"text_placeholder": "#a5adcb",
|
||||
"text_muted": "#a5adcb",
|
||||
"text_placeholder": "#8087a2",
|
||||
"text_accent": "#8aadf4",
|
||||
"icon": "#b8c0e0",
|
||||
"icon_muted": "#a5adcb",
|
||||
"icon_accent": "#8aadf4",
|
||||
"element_foreground": "#181926",
|
||||
"text_danger": "#ed8796",
|
||||
"text_warning": "#f5a97f",
|
||||
"icon": "#a5adcb",
|
||||
"icon_muted": "#8087a2",
|
||||
"icon_accent": "#b7bdf8",
|
||||
"element_foreground": "#24273a",
|
||||
"element_background": "#8aadf4",
|
||||
"element_hover": "#b7bdf8",
|
||||
"element_active": "#7c9cdc",
|
||||
"element_selected": "#6e8bc5",
|
||||
"element_active": "#91d7e3",
|
||||
"element_selected": "#7dc4e4",
|
||||
"element_disabled": "#8aadf44d",
|
||||
"secondary_foreground": "#6e8bc5",
|
||||
"secondary_background": "#1e2030",
|
||||
"secondary_hover": "#8aadf433",
|
||||
"secondary_active": "#181926",
|
||||
"secondary_selected": "#181926",
|
||||
"secondary_foreground": "#cad3f5",
|
||||
"secondary_background": "#363a4f",
|
||||
"secondary_hover": "#494d64",
|
||||
"secondary_active": "#5b6078",
|
||||
"secondary_selected": "#5b6078",
|
||||
"secondary_disabled": "#8aadf44d",
|
||||
"danger_foreground": "#181926",
|
||||
"danger_foreground": "#24273a",
|
||||
"danger_background": "#ed8796",
|
||||
"danger_hover": "#ee99a0",
|
||||
"danger_active": "#d57a87",
|
||||
"danger_selected": "#be6d78",
|
||||
"danger_active": "#f5a97f",
|
||||
"danger_selected": "#eed49f",
|
||||
"danger_disabled": "#ed87964d",
|
||||
"warning_foreground": "#181926",
|
||||
"warning_background": "#eed49f",
|
||||
"warning_hover": "#f5a97f",
|
||||
"warning_active": "#d6bf8f",
|
||||
"warning_selected": "#beaa7f",
|
||||
"warning_disabled": "#eed49f4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#363a4f",
|
||||
"ghost_element_hover": "#cad3f533",
|
||||
"ghost_element_active": "#494d64",
|
||||
"ghost_element_selected": "#494d64",
|
||||
"ghost_element_disabled": "#cad3f50d",
|
||||
"tab_inactive_background": "#1e2030",
|
||||
"tab_inactive_foreground": "#b8c0e0",
|
||||
"warning_foreground": "#24273a",
|
||||
"warning_background": "#f5a97f",
|
||||
"warning_hover": "#eed49f",
|
||||
"warning_active": "#a6da95",
|
||||
"warning_selected": "#8bd5ca",
|
||||
"warning_disabled": "#f5a97f4d",
|
||||
"ghost_element_background": "#cad3f500",
|
||||
"ghost_element_background_alt": "#1e2030",
|
||||
"ghost_element_hover": "#cad3f50d",
|
||||
"ghost_element_active": "#cad3f51a",
|
||||
"ghost_element_selected": "#cad3f51a",
|
||||
"ghost_element_disabled": "#cad3f505",
|
||||
"tab_background": "#181926",
|
||||
"tab_foreground": "#a5adcb",
|
||||
"tab_hover_background": "#cad3f50d",
|
||||
"tab_active_background": "#24273a",
|
||||
"tab_active_foreground": "#cad3f5",
|
||||
"tab_hover_foreground": "#b7bdf8",
|
||||
"scrollbar_thumb_background": "#cad3f533",
|
||||
"scrollbar_thumb_hover_background": "#cad3f580",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#494d64",
|
||||
"scrollbar_thumb_background": "#cad3f51a",
|
||||
"scrollbar_thumb_hover_background": "#cad3f526",
|
||||
"scrollbar_thumb_border": "#cad3f500",
|
||||
"scrollbar_track_background": "#cad3f500",
|
||||
"scrollbar_track_border": "#cad3f500",
|
||||
"drop_target_background": "#8aadf41a",
|
||||
"cursor": "#f4dbd6",
|
||||
"selection": "#939ab740"
|
||||
"cursor": "#8aadf4",
|
||||
"selection": "#8aadf440"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#24273a",
|
||||
@@ -76,65 +78,67 @@
|
||||
"elevated_surface_background": "#181926",
|
||||
"panel_background": "#24273a",
|
||||
"overlay": "#cad3f51a",
|
||||
"title_bar": "#1e2030",
|
||||
"title_bar_inactive": "#181926",
|
||||
"window_border": "#6e738d",
|
||||
"border": "#5b6078",
|
||||
"border_variant": "#494d64",
|
||||
"title_bar": "#181926",
|
||||
"title_bar_inactive": "#24273a",
|
||||
"window_border": "#494d64",
|
||||
"border": "#494d64",
|
||||
"border_variant": "#363a4f",
|
||||
"border_focused": "#8aadf4",
|
||||
"border_selected": "#8aadf4",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#363a4f",
|
||||
"ring": "#8aadf4",
|
||||
"border_transparent": "#cad3f500",
|
||||
"border_disabled": "#1e2030",
|
||||
"ring": "#b7bdf8",
|
||||
"text": "#cad3f5",
|
||||
"text_muted": "#b8c0e0",
|
||||
"text_placeholder": "#a5adcb",
|
||||
"text_muted": "#a5adcb",
|
||||
"text_placeholder": "#8087a2",
|
||||
"text_accent": "#8aadf4",
|
||||
"icon": "#b8c0e0",
|
||||
"icon_muted": "#a5adcb",
|
||||
"icon_accent": "#8aadf4",
|
||||
"element_foreground": "#181926",
|
||||
"text_danger": "#ed8796",
|
||||
"text_warning": "#f5a97f",
|
||||
"icon": "#a5adcb",
|
||||
"icon_muted": "#8087a2",
|
||||
"icon_accent": "#b7bdf8",
|
||||
"element_foreground": "#24273a",
|
||||
"element_background": "#8aadf4",
|
||||
"element_hover": "#b7bdf8",
|
||||
"element_active": "#7c9cdc",
|
||||
"element_selected": "#6e8bc5",
|
||||
"element_active": "#91d7e3",
|
||||
"element_selected": "#7dc4e4",
|
||||
"element_disabled": "#8aadf44d",
|
||||
"secondary_foreground": "#6e8bc5",
|
||||
"secondary_background": "#1e2030",
|
||||
"secondary_hover": "#8aadf433",
|
||||
"secondary_active": "#181926",
|
||||
"secondary_selected": "#181926",
|
||||
"secondary_foreground": "#cad3f5",
|
||||
"secondary_background": "#363a4f",
|
||||
"secondary_hover": "#494d64",
|
||||
"secondary_active": "#5b6078",
|
||||
"secondary_selected": "#5b6078",
|
||||
"secondary_disabled": "#8aadf44d",
|
||||
"danger_foreground": "#181926",
|
||||
"danger_foreground": "#24273a",
|
||||
"danger_background": "#ed8796",
|
||||
"danger_hover": "#ee99a0",
|
||||
"danger_active": "#d57a87",
|
||||
"danger_selected": "#be6d78",
|
||||
"danger_active": "#f5a97f",
|
||||
"danger_selected": "#eed49f",
|
||||
"danger_disabled": "#ed87964d",
|
||||
"warning_foreground": "#181926",
|
||||
"warning_background": "#eed49f",
|
||||
"warning_hover": "#f5a97f",
|
||||
"warning_active": "#d6bf8f",
|
||||
"warning_selected": "#beaa7f",
|
||||
"warning_disabled": "#eed49f4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#363a4f",
|
||||
"ghost_element_hover": "#cad3f533",
|
||||
"ghost_element_active": "#494d64",
|
||||
"ghost_element_selected": "#494d64",
|
||||
"ghost_element_disabled": "#cad3f50d",
|
||||
"tab_inactive_background": "#1e2030",
|
||||
"tab_inactive_foreground": "#b8c0e0",
|
||||
"warning_foreground": "#24273a",
|
||||
"warning_background": "#f5a97f",
|
||||
"warning_hover": "#eed49f",
|
||||
"warning_active": "#a6da95",
|
||||
"warning_selected": "#8bd5ca",
|
||||
"warning_disabled": "#f5a97f4d",
|
||||
"ghost_element_background": "#cad3f500",
|
||||
"ghost_element_background_alt": "#1e2030",
|
||||
"ghost_element_hover": "#cad3f50d",
|
||||
"ghost_element_active": "#cad3f51a",
|
||||
"ghost_element_selected": "#cad3f51a",
|
||||
"ghost_element_disabled": "#cad3f505",
|
||||
"tab_background": "#181926",
|
||||
"tab_foreground": "#a5adcb",
|
||||
"tab_hover_background": "#cad3f50d",
|
||||
"tab_active_background": "#24273a",
|
||||
"tab_active_foreground": "#cad3f5",
|
||||
"tab_hover_foreground": "#b7bdf8",
|
||||
"scrollbar_thumb_background": "#cad3f533",
|
||||
"scrollbar_thumb_hover_background": "#cad3f580",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#494d64",
|
||||
"scrollbar_thumb_background": "#cad3f51a",
|
||||
"scrollbar_thumb_hover_background": "#cad3f526",
|
||||
"scrollbar_thumb_border": "#cad3f500",
|
||||
"scrollbar_track_background": "#cad3f500",
|
||||
"scrollbar_track_border": "#cad3f500",
|
||||
"drop_target_background": "#8aadf41a",
|
||||
"cursor": "#f4dbd6",
|
||||
"selection": "#939ab740"
|
||||
"cursor": "#8aadf4",
|
||||
"selection": "#8aadf440"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,76 @@
|
||||
{
|
||||
"id": "catppuccin-mocha",
|
||||
"name": "Catppuccin Mocha",
|
||||
"author": "Catppuccin",
|
||||
"url": "https://github.com/catppuccin/catppuccin",
|
||||
"author": "Catppuccin Org (ported by Coop)",
|
||||
"url": "https://catppuccin.com",
|
||||
"light": {
|
||||
"background": "#1e1e2e",
|
||||
"surface_background": "#181825",
|
||||
"elevated_surface_background": "#11111b",
|
||||
"panel_background": "#1e1e2e",
|
||||
"overlay": "#cdd6f41a",
|
||||
"title_bar": "#181825",
|
||||
"title_bar_inactive": "#11111b",
|
||||
"window_border": "#6c7086",
|
||||
"border": "#585b70",
|
||||
"border_variant": "#45475a",
|
||||
"title_bar": "#11111b",
|
||||
"title_bar_inactive": "#1e1e2e",
|
||||
"window_border": "#45475a",
|
||||
"border": "#45475a",
|
||||
"border_variant": "#313244",
|
||||
"border_focused": "#89b4fa",
|
||||
"border_selected": "#89b4fa",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#313244",
|
||||
"ring": "#89b4fa",
|
||||
"border_transparent": "#cdd6f400",
|
||||
"border_disabled": "#181825",
|
||||
"ring": "#b4befe",
|
||||
"text": "#cdd6f4",
|
||||
"text_muted": "#bac2de",
|
||||
"text_placeholder": "#a6adc8",
|
||||
"text_muted": "#a6adc8",
|
||||
"text_placeholder": "#7f849c",
|
||||
"text_accent": "#89b4fa",
|
||||
"icon": "#bac2de",
|
||||
"icon_muted": "#a6adc8",
|
||||
"icon_accent": "#89b4fa",
|
||||
"element_foreground": "#11111b",
|
||||
"text_danger": "#f38ba8",
|
||||
"text_warning": "#fab387",
|
||||
"icon": "#a6adc8",
|
||||
"icon_muted": "#7f849c",
|
||||
"icon_accent": "#b4befe",
|
||||
"element_foreground": "#1e1e2e",
|
||||
"element_background": "#89b4fa",
|
||||
"element_hover": "#b4befe",
|
||||
"element_active": "#7ba2e1",
|
||||
"element_selected": "#6d90c9",
|
||||
"element_active": "#89dceb",
|
||||
"element_selected": "#74c7ec",
|
||||
"element_disabled": "#89b4fa4d",
|
||||
"secondary_foreground": "#6d90c9",
|
||||
"secondary_background": "#181825",
|
||||
"secondary_hover": "#89b4fa33",
|
||||
"secondary_active": "#11111b",
|
||||
"secondary_selected": "#11111b",
|
||||
"secondary_foreground": "#cdd6f4",
|
||||
"secondary_background": "#313244",
|
||||
"secondary_hover": "#45475a",
|
||||
"secondary_active": "#585b70",
|
||||
"secondary_selected": "#585b70",
|
||||
"secondary_disabled": "#89b4fa4d",
|
||||
"danger_foreground": "#11111b",
|
||||
"danger_foreground": "#1e1e2e",
|
||||
"danger_background": "#f38ba8",
|
||||
"danger_hover": "#eba0ac",
|
||||
"danger_active": "#db7d98",
|
||||
"danger_selected": "#c46f88",
|
||||
"danger_active": "#fab387",
|
||||
"danger_selected": "#f9e2af",
|
||||
"danger_disabled": "#f38ba84d",
|
||||
"warning_foreground": "#11111b",
|
||||
"warning_background": "#f9e2af",
|
||||
"warning_hover": "#fab387",
|
||||
"warning_active": "#e0cb9e",
|
||||
"warning_selected": "#c8b48d",
|
||||
"warning_disabled": "#f9e2af4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#313244",
|
||||
"ghost_element_hover": "#cdd6f433",
|
||||
"ghost_element_active": "#45475a",
|
||||
"ghost_element_selected": "#45475a",
|
||||
"ghost_element_disabled": "#cdd6f40d",
|
||||
"tab_inactive_background": "#181825",
|
||||
"tab_inactive_foreground": "#bac2de",
|
||||
"warning_foreground": "#1e1e2e",
|
||||
"warning_background": "#fab387",
|
||||
"warning_hover": "#f9e2af",
|
||||
"warning_active": "#a6e3a1",
|
||||
"warning_selected": "#94e2d5",
|
||||
"warning_disabled": "#fab3874d",
|
||||
"ghost_element_background": "#cdd6f400",
|
||||
"ghost_element_background_alt": "#181825",
|
||||
"ghost_element_hover": "#cdd6f40d",
|
||||
"ghost_element_active": "#cdd6f41a",
|
||||
"ghost_element_selected": "#cdd6f41a",
|
||||
"ghost_element_disabled": "#cdd6f405",
|
||||
"tab_background": "#11111b",
|
||||
"tab_foreground": "#a6adc8",
|
||||
"tab_hover_background": "#cdd6f40d",
|
||||
"tab_active_background": "#1e1e2e",
|
||||
"tab_active_foreground": "#cdd6f4",
|
||||
"tab_hover_foreground": "#b4befe",
|
||||
"scrollbar_thumb_background": "#cdd6f433",
|
||||
"scrollbar_thumb_hover_background": "#cdd6f580",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#45475a",
|
||||
"scrollbar_thumb_background": "#cdd6f41a",
|
||||
"scrollbar_thumb_hover_background": "#cdd6f426",
|
||||
"scrollbar_thumb_border": "#cdd6f400",
|
||||
"scrollbar_track_background": "#cdd6f400",
|
||||
"scrollbar_track_border": "#cdd6f400",
|
||||
"drop_target_background": "#89b4fa1a",
|
||||
"cursor": "#f5e0dc",
|
||||
"selection": "#9399b240"
|
||||
"cursor": "#89b4fa",
|
||||
"selection": "#89b4fa40"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#1e1e2e",
|
||||
@@ -76,65 +78,67 @@
|
||||
"elevated_surface_background": "#11111b",
|
||||
"panel_background": "#1e1e2e",
|
||||
"overlay": "#cdd6f41a",
|
||||
"title_bar": "#181825",
|
||||
"title_bar_inactive": "#11111b",
|
||||
"window_border": "#6c7086",
|
||||
"border": "#585b70",
|
||||
"border_variant": "#45475a",
|
||||
"title_bar": "#11111b",
|
||||
"title_bar_inactive": "#1e1e2e",
|
||||
"window_border": "#45475a",
|
||||
"border": "#45475a",
|
||||
"border_variant": "#313244",
|
||||
"border_focused": "#89b4fa",
|
||||
"border_selected": "#89b4fa",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#313244",
|
||||
"ring": "#89b4fa",
|
||||
"border_transparent": "#cdd6f400",
|
||||
"border_disabled": "#181825",
|
||||
"ring": "#b4befe",
|
||||
"text": "#cdd6f4",
|
||||
"text_muted": "#bac2de",
|
||||
"text_placeholder": "#a6adc8",
|
||||
"text_muted": "#a6adc8",
|
||||
"text_placeholder": "#7f849c",
|
||||
"text_accent": "#89b4fa",
|
||||
"icon": "#bac2de",
|
||||
"icon_muted": "#a6adc8",
|
||||
"icon_accent": "#89b4fa",
|
||||
"element_foreground": "#11111b",
|
||||
"text_danger": "#f38ba8",
|
||||
"text_warning": "#fab387",
|
||||
"icon": "#a6adc8",
|
||||
"icon_muted": "#7f849c",
|
||||
"icon_accent": "#b4befe",
|
||||
"element_foreground": "#1e1e2e",
|
||||
"element_background": "#89b4fa",
|
||||
"element_hover": "#b4befe",
|
||||
"element_active": "#7ba2e1",
|
||||
"element_selected": "#6d90c9",
|
||||
"element_active": "#89dceb",
|
||||
"element_selected": "#74c7ec",
|
||||
"element_disabled": "#89b4fa4d",
|
||||
"secondary_foreground": "#6d90c9",
|
||||
"secondary_background": "#181825",
|
||||
"secondary_hover": "#89b4fa33",
|
||||
"secondary_active": "#11111b",
|
||||
"secondary_selected": "#11111b",
|
||||
"secondary_foreground": "#cdd6f4",
|
||||
"secondary_background": "#313244",
|
||||
"secondary_hover": "#45475a",
|
||||
"secondary_active": "#585b70",
|
||||
"secondary_selected": "#585b70",
|
||||
"secondary_disabled": "#89b4fa4d",
|
||||
"danger_foreground": "#11111b",
|
||||
"danger_foreground": "#1e1e2e",
|
||||
"danger_background": "#f38ba8",
|
||||
"danger_hover": "#eba0ac",
|
||||
"danger_active": "#db7d98",
|
||||
"danger_selected": "#c46f88",
|
||||
"danger_active": "#fab387",
|
||||
"danger_selected": "#f9e2af",
|
||||
"danger_disabled": "#f38ba84d",
|
||||
"warning_foreground": "#11111b",
|
||||
"warning_background": "#f9e2af",
|
||||
"warning_hover": "#fab387",
|
||||
"warning_active": "#e0cb9e",
|
||||
"warning_selected": "#c8b48d",
|
||||
"warning_disabled": "#f9e2af4d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#313244",
|
||||
"ghost_element_hover": "#cdd6f433",
|
||||
"ghost_element_active": "#45475a",
|
||||
"ghost_element_selected": "#45475a",
|
||||
"ghost_element_disabled": "#cdd6f40d",
|
||||
"tab_inactive_background": "#181825",
|
||||
"tab_inactive_foreground": "#bac2de",
|
||||
"warning_foreground": "#1e1e2e",
|
||||
"warning_background": "#fab387",
|
||||
"warning_hover": "#f9e2af",
|
||||
"warning_active": "#a6e3a1",
|
||||
"warning_selected": "#94e2d5",
|
||||
"warning_disabled": "#fab3874d",
|
||||
"ghost_element_background": "#cdd6f400",
|
||||
"ghost_element_background_alt": "#181825",
|
||||
"ghost_element_hover": "#cdd6f40d",
|
||||
"ghost_element_active": "#cdd6f41a",
|
||||
"ghost_element_selected": "#cdd6f41a",
|
||||
"ghost_element_disabled": "#cdd6f405",
|
||||
"tab_background": "#11111b",
|
||||
"tab_foreground": "#a6adc8",
|
||||
"tab_hover_background": "#cdd6f40d",
|
||||
"tab_active_background": "#1e1e2e",
|
||||
"tab_active_foreground": "#cdd6f4",
|
||||
"tab_hover_foreground": "#b4befe",
|
||||
"scrollbar_thumb_background": "#cdd6f433",
|
||||
"scrollbar_thumb_hover_background": "#cdd6f580",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#45475a",
|
||||
"scrollbar_thumb_background": "#cdd6f41a",
|
||||
"scrollbar_thumb_hover_background": "#cdd6f426",
|
||||
"scrollbar_thumb_border": "#cdd6f400",
|
||||
"scrollbar_track_background": "#cdd6f400",
|
||||
"scrollbar_track_border": "#cdd6f400",
|
||||
"drop_target_background": "#89b4fa1a",
|
||||
"cursor": "#f5e0dc",
|
||||
"selection": "#9399b240"
|
||||
"cursor": "#89b4fa",
|
||||
"selection": "#89b4fa40"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +1,144 @@
|
||||
{
|
||||
"id": "flexoki",
|
||||
"name": "Flexoki",
|
||||
"author": "Stephan Ango",
|
||||
"author": "Steph Ango (ported by Coop)",
|
||||
"url": "https://stephango.com/flexoki",
|
||||
"light": {
|
||||
"background": "#FFFCF0",
|
||||
"surface_background": "#F2F0E5",
|
||||
"elevated_surface_background": "#E6E4D9",
|
||||
"panel_background": "#FFFCF0",
|
||||
"overlay": "#100F0F1a",
|
||||
"title_bar": "#F2F0E5",
|
||||
"title_bar_inactive": "#E6E4D9",
|
||||
"window_border": "#B7B5AC",
|
||||
"overlay": "#100F0F1A",
|
||||
"title_bar": "#E6E4D9",
|
||||
"title_bar_inactive": "#FFFCF0",
|
||||
"window_border": "#CECDC3",
|
||||
"border": "#CECDC3",
|
||||
"border_variant": "#DAD8CE",
|
||||
"border_focused": "#205EA6",
|
||||
"border_selected": "#205EA6",
|
||||
"border_transparent": "#00000000",
|
||||
"border_focused": "#24837B",
|
||||
"border_selected": "#24837B",
|
||||
"border_transparent": "#100F0F00",
|
||||
"border_disabled": "#E6E4D9",
|
||||
"ring": "#205EA6",
|
||||
"ring": "#3AA99F",
|
||||
"text": "#100F0F",
|
||||
"text_muted": "#6F6E69",
|
||||
"text_placeholder": "#9F9D96",
|
||||
"text_accent": "#205EA6",
|
||||
"text_placeholder": "#B7B5AC",
|
||||
"text_accent": "#24837B",
|
||||
"text_danger": "#AF3029",
|
||||
"text_warning": "#BC5215",
|
||||
"icon": "#6F6E69",
|
||||
"icon_muted": "#9F9D96",
|
||||
"icon_accent": "#205EA6",
|
||||
"icon_muted": "#B7B5AC",
|
||||
"icon_accent": "#3AA99F",
|
||||
"element_foreground": "#FFFCF0",
|
||||
"element_background": "#205EA6",
|
||||
"element_hover": "#1A4F8C",
|
||||
"element_active": "#163B66",
|
||||
"element_selected": "#133051",
|
||||
"element_disabled": "#205EA64d",
|
||||
"secondary_foreground": "#163B66",
|
||||
"secondary_background": "#F2F0E5",
|
||||
"secondary_hover": "#205EA61a",
|
||||
"secondary_active": "#E6E4D9",
|
||||
"secondary_selected": "#E6E4D9",
|
||||
"secondary_disabled": "#205EA64d",
|
||||
"element_background": "#24837B",
|
||||
"element_hover": "#3AA99F",
|
||||
"element_active": "#1C1B1A",
|
||||
"element_selected": "#100F0F",
|
||||
"element_disabled": "#24837B4D",
|
||||
"secondary_foreground": "#100F0F",
|
||||
"secondary_background": "#E6E4D9",
|
||||
"secondary_hover": "#DAD8CE",
|
||||
"secondary_active": "#CECDC3",
|
||||
"secondary_selected": "#CECDC3",
|
||||
"secondary_disabled": "#24837B4D",
|
||||
"danger_foreground": "#FFFCF0",
|
||||
"danger_background": "#D14D41",
|
||||
"danger_hover": "#C03E35",
|
||||
"danger_active": "#AF3029",
|
||||
"danger_selected": "#942822",
|
||||
"danger_disabled": "#D14D414d",
|
||||
"warning_foreground": "#100F0F",
|
||||
"warning_background": "#D0A215",
|
||||
"warning_hover": "#BE9207",
|
||||
"warning_active": "#AD8301",
|
||||
"warning_selected": "#8E6B01",
|
||||
"warning_disabled": "#D0A2154d",
|
||||
"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": "#F2F0E5",
|
||||
"tab_inactive_foreground": "#6F6E69",
|
||||
"danger_background": "#AF3029",
|
||||
"danger_hover": "#D14D41",
|
||||
"danger_active": "#1C1B1A",
|
||||
"danger_selected": "#100F0F",
|
||||
"danger_disabled": "#AF30294D",
|
||||
"warning_foreground": "#FFFCF0",
|
||||
"warning_background": "#BC5215",
|
||||
"warning_hover": "#DA702C",
|
||||
"warning_active": "#1C1B1A",
|
||||
"warning_selected": "#100F0F",
|
||||
"warning_disabled": "#BC52154D",
|
||||
"ghost_element_background": "#100F0F00",
|
||||
"ghost_element_background_alt": "#F2F0E5",
|
||||
"ghost_element_hover": "#100F0F0D",
|
||||
"ghost_element_active": "#100F0F1A",
|
||||
"ghost_element_selected": "#100F0F1A",
|
||||
"ghost_element_disabled": "#100F0F05",
|
||||
"tab_background": "#E6E4D9",
|
||||
"tab_foreground": "#6F6E69",
|
||||
"tab_hover_background": "#100F0F0D",
|
||||
"tab_active_background": "#FFFCF0",
|
||||
"tab_active_foreground": "#100F0F",
|
||||
"tab_hover_foreground": "#205EA6",
|
||||
"scrollbar_thumb_background": "#100F0F33",
|
||||
"scrollbar_thumb_hover_background": "#100F0F4d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#DAD8CE",
|
||||
"drop_target_background": "#205EA61a",
|
||||
"cursor": "#205EA6",
|
||||
"selection": "#205EA640"
|
||||
"scrollbar_thumb_background": "#100F0F1A",
|
||||
"scrollbar_thumb_hover_background": "#100F0F26",
|
||||
"scrollbar_thumb_border": "#100F0F00",
|
||||
"scrollbar_track_background": "#100F0F00",
|
||||
"scrollbar_track_border": "#100F0F00",
|
||||
"drop_target_background": "#24837B1A",
|
||||
"cursor": "#24837B",
|
||||
"selection": "#24837B40"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#100F0F",
|
||||
"surface_background": "#1C1B1A",
|
||||
"elevated_surface_background": "#282726",
|
||||
"panel_background": "#100F0F",
|
||||
"overlay": "#FFFCF01a",
|
||||
"title_bar": "#1C1B1A",
|
||||
"title_bar_inactive": "#282726",
|
||||
"window_border": "#575653",
|
||||
"overlay": "#FFFCF01A",
|
||||
"title_bar": "#282726",
|
||||
"title_bar_inactive": "#100F0F",
|
||||
"window_border": "#403E3C",
|
||||
"border": "#403E3C",
|
||||
"border_variant": "#343331",
|
||||
"border_focused": "#4385BE",
|
||||
"border_selected": "#4385BE",
|
||||
"border_transparent": "#00000000",
|
||||
"border_focused": "#3AA99F",
|
||||
"border_selected": "#3AA99F",
|
||||
"border_transparent": "#100F0F00",
|
||||
"border_disabled": "#282726",
|
||||
"ring": "#4385BE",
|
||||
"text": "#FFFCF0",
|
||||
"ring": "#24837B",
|
||||
"text": "#CECDC3",
|
||||
"text_muted": "#878580",
|
||||
"text_placeholder": "#6F6E69",
|
||||
"text_accent": "#4385BE",
|
||||
"text_placeholder": "#575653",
|
||||
"text_accent": "#3AA99F",
|
||||
"text_danger": "#D14D41",
|
||||
"text_warning": "#DA702C",
|
||||
"icon": "#878580",
|
||||
"icon_muted": "#6F6E69",
|
||||
"icon_accent": "#4385BE",
|
||||
"icon_muted": "#575653",
|
||||
"icon_accent": "#24837B",
|
||||
"element_foreground": "#100F0F",
|
||||
"element_background": "#4385BE",
|
||||
"element_hover": "#3171B2",
|
||||
"element_active": "#205EA6",
|
||||
"element_selected": "#1A4F8C",
|
||||
"element_disabled": "#4385BE4d",
|
||||
"secondary_foreground": "#205EA6",
|
||||
"element_background": "#3AA99F",
|
||||
"element_hover": "#24837B",
|
||||
"element_active": "#CECDC3",
|
||||
"element_selected": "#F2F0E5",
|
||||
"element_disabled": "#3AA99F4D",
|
||||
"secondary_foreground": "#CECDC3",
|
||||
"secondary_background": "#1C1B1A",
|
||||
"secondary_hover": "#4385BE1a",
|
||||
"secondary_active": "#282726",
|
||||
"secondary_selected": "#282726",
|
||||
"secondary_disabled": "#4385BE4d",
|
||||
"secondary_hover": "#282726",
|
||||
"secondary_active": "#343331",
|
||||
"secondary_selected": "#343331",
|
||||
"secondary_disabled": "#3AA99F4D",
|
||||
"danger_foreground": "#100F0F",
|
||||
"danger_background": "#E8705F",
|
||||
"danger_hover": "#D14D41",
|
||||
"danger_active": "#C03E35",
|
||||
"danger_selected": "#AF3029",
|
||||
"danger_disabled": "#E8705F4d",
|
||||
"danger_background": "#D14D41",
|
||||
"danger_hover": "#AF3029",
|
||||
"danger_active": "#CECDC3",
|
||||
"danger_selected": "#F2F0E5",
|
||||
"danger_disabled": "#D14D414D",
|
||||
"warning_foreground": "#100F0F",
|
||||
"warning_background": "#DFB431",
|
||||
"warning_hover": "#D0A215",
|
||||
"warning_active": "#BE9207",
|
||||
"warning_selected": "#AD8301",
|
||||
"warning_disabled": "#DFB4314d",
|
||||
"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": "#1C1B1A",
|
||||
"tab_inactive_foreground": "#878580",
|
||||
"warning_background": "#DA702C",
|
||||
"warning_hover": "#BC5215",
|
||||
"warning_active": "#CECDC3",
|
||||
"warning_selected": "#F2F0E5",
|
||||
"warning_disabled": "#DA702C4D",
|
||||
"ghost_element_background": "#100F0F00",
|
||||
"ghost_element_background_alt": "#1C1B1A",
|
||||
"ghost_element_hover": "#FFFCF00D",
|
||||
"ghost_element_active": "#FFFCF01A",
|
||||
"ghost_element_selected": "#FFFCF01A",
|
||||
"ghost_element_disabled": "#FFFCF005",
|
||||
"tab_background": "#282726",
|
||||
"tab_foreground": "#878580",
|
||||
"tab_hover_background": "#FFFCF00D",
|
||||
"tab_active_background": "#100F0F",
|
||||
"tab_active_foreground": "#FFFCF0",
|
||||
"tab_hover_foreground": "#4385BE",
|
||||
"scrollbar_thumb_background": "#FFFCF033",
|
||||
"scrollbar_thumb_hover_background": "#FFFCF04d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#343331",
|
||||
"drop_target_background": "#4385BE1a",
|
||||
"cursor": "#4385BE",
|
||||
"selection": "#4385BE40"
|
||||
"tab_active_foreground": "#CECDC3",
|
||||
"scrollbar_thumb_background": "#FFFCF01A",
|
||||
"scrollbar_thumb_hover_background": "#FFFCF026",
|
||||
"scrollbar_thumb_border": "#100F0F00",
|
||||
"scrollbar_track_background": "#100F0F00",
|
||||
"scrollbar_track_border": "#100F0F00",
|
||||
"drop_target_background": "#3AA99F1A",
|
||||
"cursor": "#3AA99F",
|
||||
"selection": "#3AA99F40"
|
||||
}
|
||||
}
|
||||
|
||||
144
assets/themes/forest.json
Normal file
144
assets/themes/forest.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"id": "forest",
|
||||
"name": "Forest",
|
||||
"author": "Coop",
|
||||
"url": "https://github.com/lumehq/coop",
|
||||
"light": {
|
||||
"background": "#fbfefcff",
|
||||
"surface_background": "#f4fbf6ff",
|
||||
"elevated_surface_background": "#e9f6e9ff",
|
||||
"panel_background": "#fbfefcff",
|
||||
"overlay": "#193b2d1a",
|
||||
"title_bar": "#e9f6e9ff",
|
||||
"title_bar_inactive": "#fbfefcff",
|
||||
"window_border": "#c4e8d1ff",
|
||||
"border": "#c4e8d1ff",
|
||||
"border_variant": "#b2ddb5ff",
|
||||
"border_focused": "#30a46cff",
|
||||
"border_selected": "#30a46cff",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#e0f3e6ff",
|
||||
"ring": "#2b9a66ff",
|
||||
"text": "#193b2dff",
|
||||
"text_muted": "#2f7c57ff",
|
||||
"text_placeholder": "#8eceaaff",
|
||||
"text_accent": "#30a46cff",
|
||||
"text_danger": "#e54d2eff",
|
||||
"text_warning": "#f76b15ff",
|
||||
"icon": "#2f7c57ff",
|
||||
"icon_muted": "#8eceaaff",
|
||||
"icon_accent": "#2b9a66ff",
|
||||
"element_foreground": "#ffffffff",
|
||||
"element_background": "#30a46cff",
|
||||
"element_hover": "#2b9a66ff",
|
||||
"element_active": "#2a7e3bff",
|
||||
"element_selected": "#218358ff",
|
||||
"element_disabled": "#30a46c4d",
|
||||
"secondary_foreground": "#193b2dff",
|
||||
"secondary_background": "#e9f6e9ff",
|
||||
"secondary_hover": "#daf1dbff",
|
||||
"secondary_active": "#c4e8d1ff",
|
||||
"secondary_selected": "#c4e8d1ff",
|
||||
"secondary_disabled": "#30a46c4d",
|
||||
"danger_foreground": "#ffffffff",
|
||||
"danger_background": "#feebe7ff",
|
||||
"danger_hover": "#ffcdc2ff",
|
||||
"danger_active": "#fdbdafff",
|
||||
"danger_selected": "#fdbdafff",
|
||||
"danger_disabled": "#e54d2e4d",
|
||||
"warning_foreground": "#ffffffff",
|
||||
"warning_background": "#fff7edff",
|
||||
"warning_hover": "#ffd19aff",
|
||||
"warning_active": "#ffc182ff",
|
||||
"warning_selected": "#ffc182ff",
|
||||
"warning_disabled": "#f76b154d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#e9f6e9ff",
|
||||
"ghost_element_hover": "#193b2d0d",
|
||||
"ghost_element_active": "#193b2d1a",
|
||||
"ghost_element_selected": "#193b2d1a",
|
||||
"ghost_element_disabled": "#193b2d05",
|
||||
"tab_background": "#e9f6e9ff",
|
||||
"tab_foreground": "#2f7c57ff",
|
||||
"tab_hover_background": "#193b2d0d",
|
||||
"tab_active_background": "#fbfefcff",
|
||||
"tab_active_foreground": "#193b2dff",
|
||||
"scrollbar_thumb_background": "#193b2d1a",
|
||||
"scrollbar_thumb_hover_background": "#193b2d26",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#00000000",
|
||||
"drop_target_background": "#30a46c1a",
|
||||
"cursor": "#30a46cff",
|
||||
"selection": "#30a46c40"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#0e1512ff",
|
||||
"surface_background": "#121b17ff",
|
||||
"elevated_surface_background": "#132d21ff",
|
||||
"panel_background": "#0e1512ff",
|
||||
"overlay": "#b1f1cb1a",
|
||||
"title_bar": "#132d21ff",
|
||||
"title_bar_inactive": "#0e1512ff",
|
||||
"window_border": "#20573eff",
|
||||
"border": "#20573eff",
|
||||
"border_variant": "#174933ff",
|
||||
"border_focused": "#33b074ff",
|
||||
"border_selected": "#33b074ff",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#113b29ff",
|
||||
"ring": "#33b074ff",
|
||||
"text": "#b1f1cbff",
|
||||
"text_muted": "#71d083ff",
|
||||
"text_placeholder": "#2f7c57ff",
|
||||
"text_accent": "#3dd68cff",
|
||||
"text_danger": "#ff977dff",
|
||||
"text_warning": "#ffa057ff",
|
||||
"icon": "#71d083ff",
|
||||
"icon_muted": "#2f7c57ff",
|
||||
"icon_accent": "#33b074ff",
|
||||
"element_foreground": "#0e1512ff",
|
||||
"element_background": "#3dd68cff",
|
||||
"element_hover": "#71d083ff",
|
||||
"element_active": "#33b074ff",
|
||||
"element_selected": "#30a46cff",
|
||||
"element_disabled": "#3dd68c4d",
|
||||
"secondary_foreground": "#b1f1cbff",
|
||||
"secondary_background": "#132d21ff",
|
||||
"secondary_hover": "#113b29ff",
|
||||
"secondary_active": "#174933ff",
|
||||
"secondary_selected": "#174933ff",
|
||||
"secondary_disabled": "#3dd68c4d",
|
||||
"danger_foreground": "#181111ff",
|
||||
"danger_background": "#391714ff",
|
||||
"danger_hover": "#5e1c16ff",
|
||||
"danger_active": "#6e2920ff",
|
||||
"danger_selected": "#6e2920ff",
|
||||
"danger_disabled": "#ff977d4d",
|
||||
"warning_foreground": "#17120eff",
|
||||
"warning_background": "#331e0bff",
|
||||
"warning_hover": "#562800ff",
|
||||
"warning_active": "#66350cff",
|
||||
"warning_selected": "#66350cff",
|
||||
"warning_disabled": "#ffa0574d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#132d21ff",
|
||||
"ghost_element_hover": "#b1f1cb0d",
|
||||
"ghost_element_active": "#b1f1cb1a",
|
||||
"ghost_element_selected": "#b1f1cb1a",
|
||||
"ghost_element_disabled": "#b1f1cb05",
|
||||
"tab_background": "#132d21ff",
|
||||
"tab_foreground": "#71d083ff",
|
||||
"tab_hover_background": "#b1f1cb0d",
|
||||
"tab_active_background": "#0e1512ff",
|
||||
"tab_active_foreground": "#b1f1cbff",
|
||||
"scrollbar_thumb_background": "#b1f1cb1a",
|
||||
"scrollbar_thumb_hover_background": "#b1f1cb26",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#00000000",
|
||||
"drop_target_background": "#3dd68c1a",
|
||||
"cursor": "#3dd68cff",
|
||||
"selection": "#3dd68c40"
|
||||
}
|
||||
}
|
||||
144
assets/themes/ocean.json
Normal file
144
assets/themes/ocean.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"id": "ocean",
|
||||
"name": "Ocean",
|
||||
"author": "Coop",
|
||||
"url": "https://github.com/lumehq/coop",
|
||||
"light": {
|
||||
"background": "#fafefeff",
|
||||
"surface_background": "#f2fbfaff",
|
||||
"elevated_surface_background": "#e6f7f7ff",
|
||||
"panel_background": "#fafefeff",
|
||||
"overlay": "#00333f1a",
|
||||
"title_bar": "#e6f7f7ff",
|
||||
"title_bar_inactive": "#fafefeff",
|
||||
"window_border": "#cce5e9ff",
|
||||
"border": "#cce5e9ff",
|
||||
"border_variant": "#b8dde3ff",
|
||||
"border_focused": "#00a2c7ff",
|
||||
"border_selected": "#00a2c7ff",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#e0f0f2ff",
|
||||
"ring": "#0797b9ff",
|
||||
"text": "#0d3c48ff",
|
||||
"text_muted": "#107d98ff",
|
||||
"text_placeholder": "#60b3d7ff",
|
||||
"text_accent": "#00a2c7ff",
|
||||
"text_danger": "#e54d2eff",
|
||||
"text_warning": "#f76b15ff",
|
||||
"icon": "#107d98ff",
|
||||
"icon_muted": "#60b3d7ff",
|
||||
"icon_accent": "#0797b9ff",
|
||||
"element_foreground": "#ffffffff",
|
||||
"element_background": "#00a2c7ff",
|
||||
"element_hover": "#0797b9ff",
|
||||
"element_active": "#12667eff",
|
||||
"element_selected": "#0d4a5cff",
|
||||
"element_disabled": "#00a2c74d",
|
||||
"secondary_foreground": "#0d4a5cff",
|
||||
"secondary_background": "#ddf9f2ff",
|
||||
"secondary_hover": "#c8f4e9ff",
|
||||
"secondary_active": "#b3ecdeff",
|
||||
"secondary_selected": "#b3ecdeff",
|
||||
"secondary_disabled": "#00a2c74d",
|
||||
"danger_foreground": "#ffffffff",
|
||||
"danger_background": "#feebe7ff",
|
||||
"danger_hover": "#ffcdc2ff",
|
||||
"danger_active": "#fdbdafff",
|
||||
"danger_selected": "#fdbdafff",
|
||||
"danger_disabled": "#e54d2e4d",
|
||||
"warning_foreground": "#ffffffff",
|
||||
"warning_background": "#fff7edff",
|
||||
"warning_hover": "#ffd19aff",
|
||||
"warning_active": "#ffc182ff",
|
||||
"warning_selected": "#ffc182ff",
|
||||
"warning_disabled": "#f76b154d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#e6f7f7ff",
|
||||
"ghost_element_hover": "#00333f0d",
|
||||
"ghost_element_active": "#00333f1a",
|
||||
"ghost_element_selected": "#00333f1a",
|
||||
"ghost_element_disabled": "#00333f05",
|
||||
"tab_background": "#e6f7f7ff",
|
||||
"tab_foreground": "#107d98ff",
|
||||
"tab_hover_background": "#00333f0d",
|
||||
"tab_active_background": "#fafefeff",
|
||||
"tab_active_foreground": "#0d3c48ff",
|
||||
"scrollbar_thumb_background": "#00333f1a",
|
||||
"scrollbar_thumb_hover_background": "#00333f26",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#00000000",
|
||||
"drop_target_background": "#00a2c71a",
|
||||
"cursor": "#00a2c7ff",
|
||||
"selection": "#00a2c740"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#0b161aff",
|
||||
"surface_background": "#101b20ff",
|
||||
"elevated_surface_background": "#082c36ff",
|
||||
"panel_background": "#0b161aff",
|
||||
"overlay": "#c2f3ff1a",
|
||||
"title_bar": "#082c36ff",
|
||||
"title_bar_inactive": "#0b161aff",
|
||||
"window_border": "#1b537bff",
|
||||
"border": "#1b537bff",
|
||||
"border_variant": "#154467ff",
|
||||
"border_focused": "#00a2c7ff",
|
||||
"border_selected": "#00a2c7ff",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#112840ff",
|
||||
"ring": "#23afd0ff",
|
||||
"text": "#b6ecf7ff",
|
||||
"text_muted": "#4ccce6ff",
|
||||
"text_placeholder": "#197caeff",
|
||||
"text_accent": "#7ce2feff",
|
||||
"text_danger": "#ff977dff",
|
||||
"text_warning": "#ffa057ff",
|
||||
"icon": "#4ccce6ff",
|
||||
"icon_muted": "#197caeff",
|
||||
"icon_accent": "#23afd0ff",
|
||||
"element_foreground": "#0b161aff",
|
||||
"element_background": "#7ce2feff",
|
||||
"element_hover": "#a8eeffff",
|
||||
"element_active": "#23afd0ff",
|
||||
"element_selected": "#00a2c7ff",
|
||||
"element_disabled": "#7ce2fe4d",
|
||||
"secondary_foreground": "#adf0ddff",
|
||||
"secondary_background": "#0d2d2aff",
|
||||
"secondary_hover": "#023b37ff",
|
||||
"secondary_active": "#084843ff",
|
||||
"secondary_selected": "#084843ff",
|
||||
"secondary_disabled": "#7ce2fe4d",
|
||||
"danger_foreground": "#181111ff",
|
||||
"danger_background": "#391714ff",
|
||||
"danger_hover": "#5e1c16ff",
|
||||
"danger_active": "#6e2920ff",
|
||||
"danger_selected": "#6e2920ff",
|
||||
"danger_disabled": "#ff977d4d",
|
||||
"warning_foreground": "#17120eff",
|
||||
"warning_background": "#331e0bff",
|
||||
"warning_hover": "#562800ff",
|
||||
"warning_active": "#66350cff",
|
||||
"warning_selected": "#66350cff",
|
||||
"warning_disabled": "#ffa0574d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#082c36ff",
|
||||
"ghost_element_hover": "#c2f3ff0d",
|
||||
"ghost_element_active": "#c2f3ff1a",
|
||||
"ghost_element_selected": "#c2f3ff1a",
|
||||
"ghost_element_disabled": "#c2f3ff05",
|
||||
"tab_background": "#082c36ff",
|
||||
"tab_foreground": "#4ccce6ff",
|
||||
"tab_hover_background": "#c2f3ff0d",
|
||||
"tab_active_background": "#0b161aff",
|
||||
"tab_active_foreground": "#b6ecf7ff",
|
||||
"scrollbar_thumb_background": "#c2f3ff1a",
|
||||
"scrollbar_thumb_hover_background": "#c2f3ff26",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#00000000",
|
||||
"drop_target_background": "#7ce2fe1a",
|
||||
"cursor": "#7ce2feff",
|
||||
"selection": "#7ce2fe40"
|
||||
}
|
||||
}
|
||||
@@ -1,140 +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": "#faf4ed",
|
||||
"overlay": "#5752791a",
|
||||
"title_bar": "#fffaf3",
|
||||
"title_bar_inactive": "#f2e9e1",
|
||||
"window_border": "#cecacd",
|
||||
"border": "#dfdad9",
|
||||
"border_variant": "#f4ede8",
|
||||
"border_focused": "#907aa9",
|
||||
"border_selected": "#907aa9",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#f2e9e1",
|
||||
"ring": "#907aa9",
|
||||
"text": "#575279",
|
||||
"text_muted": "#797593",
|
||||
"text_placeholder": "#9893a5",
|
||||
"text_accent": "#907aa9",
|
||||
"icon": "#797593",
|
||||
"icon_muted": "#9893a5",
|
||||
"icon_accent": "#907aa9",
|
||||
"element_foreground": "#faf4ed",
|
||||
"element_background": "#907aa9",
|
||||
"element_hover": "#907aa9e6",
|
||||
"element_active": "#826b95",
|
||||
"element_selected": "#745c81",
|
||||
"element_disabled": "#907aa94d",
|
||||
"secondary_foreground": "#745c81",
|
||||
"secondary_background": "#fffaf3",
|
||||
"secondary_hover": "#907aa91a",
|
||||
"secondary_active": "#f2e9e1",
|
||||
"secondary_selected": "#f2e9e1",
|
||||
"secondary_disabled": "#907aa94d",
|
||||
"danger_foreground": "#faf4ed",
|
||||
"danger_background": "#b4637a",
|
||||
"danger_hover": "#a7586e",
|
||||
"danger_active": "#9a4d62",
|
||||
"danger_selected": "#8d4256",
|
||||
"danger_disabled": "#b4637a4d",
|
||||
"warning_foreground": "#575279",
|
||||
"warning_background": "#ea9d34",
|
||||
"warning_hover": "#d98e2f",
|
||||
"warning_active": "#c87f2a",
|
||||
"warning_selected": "#b77025",
|
||||
"warning_disabled": "#ea9d344d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#f2e9e1",
|
||||
"ghost_element_hover": "#5752791a",
|
||||
"ghost_element_active": "#dfdad9",
|
||||
"ghost_element_selected": "#dfdad9",
|
||||
"ghost_element_disabled": "#5752790d",
|
||||
"tab_inactive_background": "#fffaf3",
|
||||
"tab_inactive_foreground": "#797593",
|
||||
"tab_active_background": "#faf4ed",
|
||||
"tab_active_foreground": "#575279",
|
||||
"tab_hover_foreground": "#907aa9",
|
||||
"scrollbar_thumb_background": "#57527933",
|
||||
"scrollbar_thumb_hover_background": "#5752794d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#dfdad9",
|
||||
"drop_target_background": "#907aa91a",
|
||||
"cursor": "#907aa9",
|
||||
"selection": "#907aa940"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#faf4ed",
|
||||
"surface_background": "#fffaf3",
|
||||
"elevated_surface_background": "#f2e9e1",
|
||||
"panel_background": "#faf4ed",
|
||||
"overlay": "#5752791a",
|
||||
"title_bar": "#fffaf3",
|
||||
"title_bar_inactive": "#f2e9e1",
|
||||
"window_border": "#cecacd",
|
||||
"border": "#dfdad9",
|
||||
"border_variant": "#f4ede8",
|
||||
"border_focused": "#907aa9",
|
||||
"border_selected": "#907aa9",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#f2e9e1",
|
||||
"ring": "#907aa9",
|
||||
"text": "#575279",
|
||||
"text_muted": "#797593",
|
||||
"text_placeholder": "#9893a5",
|
||||
"text_accent": "#907aa9",
|
||||
"icon": "#797593",
|
||||
"icon_muted": "#9893a5",
|
||||
"icon_accent": "#907aa9",
|
||||
"element_foreground": "#faf4ed",
|
||||
"element_background": "#907aa9",
|
||||
"element_hover": "#907aa9e6",
|
||||
"element_active": "#826b95",
|
||||
"element_selected": "#745c81",
|
||||
"element_disabled": "#907aa94d",
|
||||
"secondary_foreground": "#745c81",
|
||||
"secondary_background": "#fffaf3",
|
||||
"secondary_hover": "#907aa91a",
|
||||
"secondary_active": "#f2e9e1",
|
||||
"secondary_selected": "#f2e9e1",
|
||||
"secondary_disabled": "#907aa94d",
|
||||
"danger_foreground": "#faf4ed",
|
||||
"danger_background": "#b4637a",
|
||||
"danger_hover": "#a7586e",
|
||||
"danger_active": "#9a4d62",
|
||||
"danger_selected": "#8d4256",
|
||||
"danger_disabled": "#b4637a4d",
|
||||
"warning_foreground": "#575279",
|
||||
"warning_background": "#ea9d34",
|
||||
"warning_hover": "#d98e2f",
|
||||
"warning_active": "#c87f2a",
|
||||
"warning_selected": "#b77025",
|
||||
"warning_disabled": "#ea9d344d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#f2e9e1",
|
||||
"ghost_element_hover": "#5752791a",
|
||||
"ghost_element_active": "#dfdad9",
|
||||
"ghost_element_selected": "#dfdad9",
|
||||
"ghost_element_disabled": "#5752790d",
|
||||
"tab_inactive_background": "#fffaf3",
|
||||
"tab_inactive_foreground": "#797593",
|
||||
"tab_active_background": "#faf4ed",
|
||||
"tab_active_foreground": "#575279",
|
||||
"tab_hover_foreground": "#907aa9",
|
||||
"scrollbar_thumb_background": "#57527933",
|
||||
"scrollbar_thumb_hover_background": "#5752794d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#dfdad9",
|
||||
"drop_target_background": "#907aa91a",
|
||||
"cursor": "#907aa9",
|
||||
"selection": "#907aa940"
|
||||
}
|
||||
}
|
||||
@@ -1,140 +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": "#232136",
|
||||
"overlay": "#e0def41a",
|
||||
"title_bar": "#2a273f",
|
||||
"title_bar_inactive": "#393552",
|
||||
"window_border": "#56526e",
|
||||
"border": "#44415a",
|
||||
"border_variant": "#393552",
|
||||
"border_focused": "#c4a7e7",
|
||||
"border_selected": "#c4a7e7",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#393552",
|
||||
"ring": "#c4a7e7",
|
||||
"text": "#e0def4",
|
||||
"text_muted": "#908caa",
|
||||
"text_placeholder": "#6e6a86",
|
||||
"text_accent": "#c4a7e7",
|
||||
"icon": "#908caa",
|
||||
"icon_muted": "#6e6a86",
|
||||
"icon_accent": "#c4a7e7",
|
||||
"element_foreground": "#232136",
|
||||
"element_background": "#c4a7e7",
|
||||
"element_hover": "#c4a7e7e6",
|
||||
"element_active": "#b296d6",
|
||||
"element_selected": "#a085c5",
|
||||
"element_disabled": "#c4a7e74d",
|
||||
"secondary_foreground": "#a085c5",
|
||||
"secondary_background": "#393552",
|
||||
"secondary_hover": "#c4a7e71a",
|
||||
"secondary_active": "#44415a",
|
||||
"secondary_selected": "#44415a",
|
||||
"secondary_disabled": "#c4a7e74d",
|
||||
"danger_foreground": "#232136",
|
||||
"danger_background": "#eb6f92",
|
||||
"danger_hover": "#e55a82",
|
||||
"danger_active": "#df4572",
|
||||
"danger_selected": "#d93062",
|
||||
"danger_disabled": "#eb6f924d",
|
||||
"warning_foreground": "#232136",
|
||||
"warning_background": "#f6c177",
|
||||
"warning_hover": "#f4b35e",
|
||||
"warning_active": "#f2a545",
|
||||
"warning_selected": "#f0972c",
|
||||
"warning_disabled": "#f6c1774d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#393552",
|
||||
"ghost_element_hover": "#e0def41a",
|
||||
"ghost_element_active": "#44415a",
|
||||
"ghost_element_selected": "#44415a",
|
||||
"ghost_element_disabled": "#e0def40d",
|
||||
"tab_inactive_background": "#2a273f",
|
||||
"tab_inactive_foreground": "#908caa",
|
||||
"tab_active_background": "#232136",
|
||||
"tab_active_foreground": "#e0def4",
|
||||
"tab_hover_foreground": "#c4a7e7",
|
||||
"scrollbar_thumb_background": "#e0def433",
|
||||
"scrollbar_thumb_hover_background": "#e0def44d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#44415a",
|
||||
"drop_target_background": "#c4a7e71a",
|
||||
"cursor": "#c4a7e7",
|
||||
"selection": "#c4a7e740"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#232136",
|
||||
"surface_background": "#2a273f",
|
||||
"elevated_surface_background": "#393552",
|
||||
"panel_background": "#232136",
|
||||
"overlay": "#e0def41a",
|
||||
"title_bar": "#2a273f",
|
||||
"title_bar_inactive": "#393552",
|
||||
"window_border": "#56526e",
|
||||
"border": "#44415a",
|
||||
"border_variant": "#393552",
|
||||
"border_focused": "#c4a7e7",
|
||||
"border_selected": "#c4a7e7",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#393552",
|
||||
"ring": "#c4a7e7",
|
||||
"text": "#e0def4",
|
||||
"text_muted": "#908caa",
|
||||
"text_placeholder": "#6e6a86",
|
||||
"text_accent": "#c4a7e7",
|
||||
"icon": "#908caa",
|
||||
"icon_muted": "#6e6a86",
|
||||
"icon_accent": "#c4a7e7",
|
||||
"element_foreground": "#232136",
|
||||
"element_background": "#c4a7e7",
|
||||
"element_hover": "#c4a7e7e6",
|
||||
"element_active": "#b296d6",
|
||||
"element_selected": "#a085c5",
|
||||
"element_disabled": "#c4a7e74d",
|
||||
"secondary_foreground": "#a085c5",
|
||||
"secondary_background": "#393552",
|
||||
"secondary_hover": "#c4a7e71a",
|
||||
"secondary_active": "#44415a",
|
||||
"secondary_selected": "#44415a",
|
||||
"secondary_disabled": "#c4a7e74d",
|
||||
"danger_foreground": "#232136",
|
||||
"danger_background": "#eb6f92",
|
||||
"danger_hover": "#e55a82",
|
||||
"danger_active": "#df4572",
|
||||
"danger_selected": "#d93062",
|
||||
"danger_disabled": "#eb6f924d",
|
||||
"warning_foreground": "#232136",
|
||||
"warning_background": "#f6c177",
|
||||
"warning_hover": "#f4b35e",
|
||||
"warning_active": "#f2a545",
|
||||
"warning_selected": "#f0972c",
|
||||
"warning_disabled": "#f6c1774d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#393552",
|
||||
"ghost_element_hover": "#e0def41a",
|
||||
"ghost_element_active": "#44415a",
|
||||
"ghost_element_selected": "#44415a",
|
||||
"ghost_element_disabled": "#e0def40d",
|
||||
"tab_inactive_background": "#2a273f",
|
||||
"tab_inactive_foreground": "#908caa",
|
||||
"tab_active_background": "#232136",
|
||||
"tab_active_foreground": "#e0def4",
|
||||
"tab_hover_foreground": "#c4a7e7",
|
||||
"scrollbar_thumb_background": "#e0def433",
|
||||
"scrollbar_thumb_hover_background": "#e0def44d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#44415a",
|
||||
"drop_target_background": "#c4a7e71a",
|
||||
"cursor": "#c4a7e7",
|
||||
"selection": "#c4a7e740"
|
||||
}
|
||||
}
|
||||
@@ -1,140 +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": "#191724",
|
||||
"overlay": "#e0def41a",
|
||||
"title_bar": "#1f1d2e",
|
||||
"title_bar_inactive": "#26233a",
|
||||
"window_border": "#524f67",
|
||||
"border": "#403d52",
|
||||
"border_variant": "#26233a",
|
||||
"border_focused": "#c4a7e7",
|
||||
"border_selected": "#c4a7e7",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#26233a",
|
||||
"ring": "#c4a7e7",
|
||||
"text": "#e0def4",
|
||||
"text_muted": "#908caa",
|
||||
"text_placeholder": "#6e6a86",
|
||||
"text_accent": "#c4a7e7",
|
||||
"icon": "#908caa",
|
||||
"icon_muted": "#6e6a86",
|
||||
"icon_accent": "#c4a7e7",
|
||||
"element_foreground": "#191724",
|
||||
"element_background": "#c4a7e7",
|
||||
"element_hover": "#c4a7e7e6",
|
||||
"element_active": "#b296d6",
|
||||
"element_selected": "#a085c5",
|
||||
"element_disabled": "#c4a7e74d",
|
||||
"secondary_foreground": "#a085c5",
|
||||
"secondary_background": "#26233a",
|
||||
"secondary_hover": "#c4a7e71a",
|
||||
"secondary_active": "#403d52",
|
||||
"secondary_selected": "#403d52",
|
||||
"secondary_disabled": "#c4a7e74d",
|
||||
"danger_foreground": "#191724",
|
||||
"danger_background": "#eb6f92",
|
||||
"danger_hover": "#e55a82",
|
||||
"danger_active": "#df4572",
|
||||
"danger_selected": "#d93062",
|
||||
"danger_disabled": "#eb6f924d",
|
||||
"warning_foreground": "#191724",
|
||||
"warning_background": "#f6c177",
|
||||
"warning_hover": "#f4b35e",
|
||||
"warning_active": "#f2a545",
|
||||
"warning_selected": "#f0972c",
|
||||
"warning_disabled": "#f6c1774d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#26233a",
|
||||
"ghost_element_hover": "#e0def41a",
|
||||
"ghost_element_active": "#403d52",
|
||||
"ghost_element_selected": "#403d52",
|
||||
"ghost_element_disabled": "#e0def40d",
|
||||
"tab_inactive_background": "#1f1d2e",
|
||||
"tab_inactive_foreground": "#908caa",
|
||||
"tab_active_background": "#191724",
|
||||
"tab_active_foreground": "#e0def4",
|
||||
"tab_hover_foreground": "#c4a7e7",
|
||||
"scrollbar_thumb_background": "#e0def433",
|
||||
"scrollbar_thumb_hover_background": "#e0def44d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#403d52",
|
||||
"drop_target_background": "#c4a7e71a",
|
||||
"cursor": "#c4a7e7",
|
||||
"selection": "#c4a7e740"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#191724",
|
||||
"surface_background": "#1f1d2e",
|
||||
"elevated_surface_background": "#26233a",
|
||||
"panel_background": "#191724",
|
||||
"overlay": "#e0def41a",
|
||||
"title_bar": "#1f1d2e",
|
||||
"title_bar_inactive": "#26233a",
|
||||
"window_border": "#524f67",
|
||||
"border": "#403d52",
|
||||
"border_variant": "#26233a",
|
||||
"border_focused": "#c4a7e7",
|
||||
"border_selected": "#c4a7e7",
|
||||
"border_transparent": "#00000000",
|
||||
"border_disabled": "#26233a",
|
||||
"ring": "#c4a7e7",
|
||||
"text": "#e0def4",
|
||||
"text_muted": "#908caa",
|
||||
"text_placeholder": "#6e6a86",
|
||||
"text_accent": "#c4a7e7",
|
||||
"icon": "#908caa",
|
||||
"icon_muted": "#6e6a86",
|
||||
"icon_accent": "#c4a7e7",
|
||||
"element_foreground": "#191724",
|
||||
"element_background": "#c4a7e7",
|
||||
"element_hover": "#c4a7e7e6",
|
||||
"element_active": "#b296d6",
|
||||
"element_selected": "#a085c5",
|
||||
"element_disabled": "#c4a7e74d",
|
||||
"secondary_foreground": "#a085c5",
|
||||
"secondary_background": "#26233a",
|
||||
"secondary_hover": "#c4a7e71a",
|
||||
"secondary_active": "#403d52",
|
||||
"secondary_selected": "#403d52",
|
||||
"secondary_disabled": "#c4a7e74d",
|
||||
"danger_foreground": "#191724",
|
||||
"danger_background": "#eb6f92",
|
||||
"danger_hover": "#e55a82",
|
||||
"danger_active": "#df4572",
|
||||
"danger_selected": "#d93062",
|
||||
"danger_disabled": "#eb6f924d",
|
||||
"warning_foreground": "#191724",
|
||||
"warning_background": "#f6c177",
|
||||
"warning_hover": "#f4b35e",
|
||||
"warning_active": "#f2a545",
|
||||
"warning_selected": "#f0972c",
|
||||
"warning_disabled": "#f6c1774d",
|
||||
"ghost_element_background": "#00000000",
|
||||
"ghost_element_background_alt": "#26233a",
|
||||
"ghost_element_hover": "#e0def41a",
|
||||
"ghost_element_active": "#403d52",
|
||||
"ghost_element_selected": "#403d52",
|
||||
"ghost_element_disabled": "#e0def40d",
|
||||
"tab_inactive_background": "#1f1d2e",
|
||||
"tab_inactive_foreground": "#908caa",
|
||||
"tab_active_background": "#191724",
|
||||
"tab_active_foreground": "#e0def4",
|
||||
"tab_hover_foreground": "#c4a7e7",
|
||||
"scrollbar_thumb_background": "#e0def433",
|
||||
"scrollbar_thumb_hover_background": "#e0def44d",
|
||||
"scrollbar_thumb_border": "#00000000",
|
||||
"scrollbar_track_background": "#00000000",
|
||||
"scrollbar_track_border": "#403d52",
|
||||
"drop_target_background": "#c4a7e71a",
|
||||
"cursor": "#c4a7e7",
|
||||
"selection": "#c4a7e740"
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,12 @@ use common::EventUtils;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window,
|
||||
App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Subscription, Task,
|
||||
WeakEntity, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{DEVICE_GIFTWRAP, NostrRegistry, RelayState, TIMEOUT, USER_GIFTWRAP};
|
||||
use state::{DEVICE_GIFTWRAP, NostrRegistry, StateEvent, TIMEOUT, USER_GIFTWRAP};
|
||||
|
||||
mod message;
|
||||
mod room;
|
||||
@@ -39,6 +40,10 @@ pub enum ChatEvent {
|
||||
CloseRoom(u64),
|
||||
/// An event to notify UI about a new chat request
|
||||
Ping,
|
||||
/// An event to notify UI that the chat registry has subscribed to messaging relays
|
||||
Subscribed,
|
||||
/// An error occurred
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
/// Channel signal.
|
||||
@@ -48,41 +53,25 @@ enum Signal {
|
||||
Message(NewMessage),
|
||||
/// Eose received from relay pool
|
||||
Eose,
|
||||
}
|
||||
|
||||
/// Inbox state.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum InboxState {
|
||||
#[default]
|
||||
Idle,
|
||||
Checking,
|
||||
RelayNotAvailable,
|
||||
RelayConfigured(Box<Event>),
|
||||
Subscribing,
|
||||
}
|
||||
|
||||
impl InboxState {
|
||||
pub fn not_configured(&self) -> bool {
|
||||
matches!(self, InboxState::RelayNotAvailable)
|
||||
}
|
||||
|
||||
pub fn subscribing(&self) -> bool {
|
||||
matches!(self, InboxState::Subscribing)
|
||||
}
|
||||
/// An error occurred
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
/// Chat Registry
|
||||
#[derive(Debug)]
|
||||
pub struct ChatRegistry {
|
||||
/// Relay state for messaging relay list
|
||||
state: Entity<InboxState>,
|
||||
|
||||
/// Collection of all chat rooms
|
||||
rooms: Vec<Entity<Room>>,
|
||||
|
||||
/// Tracking the status of unwrapping gift wrap events.
|
||||
tracking_flag: Arc<AtomicBool>,
|
||||
|
||||
/// Channel for sending signals to the UI.
|
||||
signal_tx: flume::Sender<Signal>,
|
||||
|
||||
/// Channel for receiving signals from the UI.
|
||||
signal_rx: flume::Receiver<Signal>,
|
||||
|
||||
/// Async tasks
|
||||
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
|
||||
|
||||
@@ -105,36 +94,18 @@ impl ChatRegistry {
|
||||
|
||||
/// Create a new chat registry instance
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let state = cx.new(|_| InboxState::default());
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
|
||||
let (tx, rx) = flume::unbounded::<Signal>();
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
// Observe the nip65 state and load chat rooms on every state change
|
||||
cx.observe(&nostr, |this, state, cx| {
|
||||
match state.read(cx).relay_list_state {
|
||||
RelayState::Idle => {
|
||||
// Subscribe to the signer event
|
||||
cx.subscribe(&nostr, |this, _state, event, cx| {
|
||||
if let StateEvent::SignerSet = event {
|
||||
this.reset(cx);
|
||||
}
|
||||
RelayState::Configured => {
|
||||
this.get_contact_list(cx);
|
||||
this.ensure_messaging_relays(cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Load rooms on every state change
|
||||
this.get_rooms(cx);
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Observe the nip17 state and load chat rooms on every state change
|
||||
cx.observe(&state, |this, state, cx| {
|
||||
if let InboxState::RelayConfigured(event) = state.read(cx) {
|
||||
let relay_urls: Vec<_> = nip17::extract_relay_list(event).cloned().collect();
|
||||
this.get_messages(relay_urls, cx);
|
||||
this.get_contact_list(cx);
|
||||
this.get_messages(cx)
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -147,9 +118,10 @@ impl ChatRegistry {
|
||||
});
|
||||
|
||||
Self {
|
||||
state,
|
||||
rooms: vec![],
|
||||
tracking_flag: Arc::new(AtomicBool::new(false)),
|
||||
signal_rx: rx,
|
||||
signal_tx: tx,
|
||||
tasks: smallvec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
@@ -167,7 +139,8 @@ impl ChatRegistry {
|
||||
let sub_id2 = SubscriptionId::new(USER_GIFTWRAP);
|
||||
|
||||
// Channel for communication between nostr and gpui
|
||||
let (tx, rx) = flume::bounded::<Signal>(1024);
|
||||
let tx = self.signal_tx.clone();
|
||||
let rx = self.signal_rx.clone();
|
||||
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
let device_signer = signer.get_encryption_signer().await;
|
||||
@@ -194,7 +167,14 @@ impl ChatRegistry {
|
||||
|
||||
// Extract the rumor from the gift wrap event
|
||||
match extract_rumor(&client, &device_signer, event.as_ref()).await {
|
||||
Ok(rumor) => match rumor.created_at >= initialized_at {
|
||||
Ok(rumor) => {
|
||||
if rumor.tags.is_empty() {
|
||||
let error: SharedString =
|
||||
"Message doesn't belong to any rooms".into();
|
||||
tx.send_async(Signal::Error(error)).await?;
|
||||
}
|
||||
|
||||
match rumor.created_at >= initialized_at {
|
||||
true => {
|
||||
let new_message = NewMessage::new(event.id, rumor);
|
||||
let signal = Signal::Message(new_message);
|
||||
@@ -204,9 +184,12 @@ impl ChatRegistry {
|
||||
false => {
|
||||
status.store(true, Ordering::Release);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to unwrap the gift wrap event: {e}");
|
||||
let error: SharedString =
|
||||
format!("Failed to unwrap the gift wrap event: {e}").into();
|
||||
tx.send_async(Signal::Error(error)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,6 +218,11 @@ impl ChatRegistry {
|
||||
this.get_rooms(cx);
|
||||
})?;
|
||||
}
|
||||
Signal::Error(error) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(ChatEvent::Error(error));
|
||||
})?;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -245,6 +233,7 @@ impl ChatRegistry {
|
||||
/// Tracking the status of unwrapping gift wrap events.
|
||||
fn tracking(&mut self, cx: &mut Context<Self>) {
|
||||
let status = self.tracking_flag.clone();
|
||||
let tx = self.signal_tx.clone();
|
||||
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
let loop_duration = Duration::from_secs(15);
|
||||
@@ -252,6 +241,9 @@ impl ChatRegistry {
|
||||
loop {
|
||||
if status.load(Ordering::Acquire) {
|
||||
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
|
||||
_ = tx.send_async(Signal::Eose).await;
|
||||
} else {
|
||||
_ = tx.send_async(Signal::Eose).await;
|
||||
}
|
||||
smol::Timer::after(loop_duration).await;
|
||||
}
|
||||
@@ -268,29 +260,20 @@ impl ChatRegistry {
|
||||
return;
|
||||
};
|
||||
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let id = SubscriptionId::new("contact-list");
|
||||
let opts = SubscribeAutoCloseOptions::default()
|
||||
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
||||
.timeout(Some(Duration::from_secs(TIMEOUT)));
|
||||
|
||||
// Get user's write relays
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct filter for inbox relays
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ContactList)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&RelayUrl, Filter> =
|
||||
urls.iter().map(|relay| (relay, filter.clone())).collect();
|
||||
|
||||
// Subscribe
|
||||
client.subscribe(target).close_on(opts).with_id(id).await?;
|
||||
client.subscribe(filter).close_on(opts).with_id(id).await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -298,39 +281,35 @@ impl ChatRegistry {
|
||||
self.tasks.push(task);
|
||||
}
|
||||
|
||||
/// Ensure messaging relays are set up for the current user.
|
||||
pub fn ensure_messaging_relays(&mut self, cx: &mut Context<Self>) {
|
||||
let task = self.verify_relays(cx);
|
||||
|
||||
// Set state to checking
|
||||
self.set_state(InboxState::Checking, cx);
|
||||
/// Get all messages for current user
|
||||
pub fn get_messages(&mut self, cx: &mut Context<Self>) {
|
||||
let task = self.subscribe(cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let result = task.await?;
|
||||
|
||||
// Update state
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_state(result, cx);
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(ChatEvent::Subscribed);
|
||||
})?;
|
||||
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(ChatEvent::Error(SharedString::from(e.to_string())));
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
// Verify messaging relay list for current user
|
||||
fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<InboxState, Error>> {
|
||||
// Get messaging relay list for current user
|
||||
fn get_messaging_relays(&self, cx: &App) -> Task<Result<Vec<RelayUrl>, Error>> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return Task::ready(Err(anyhow!("User not found")));
|
||||
};
|
||||
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
// Construct filter for inbox relays
|
||||
let filter = Filter::new()
|
||||
@@ -338,61 +317,32 @@ impl ChatRegistry {
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&RelayUrl, Filter> =
|
||||
urls.iter().map(|relay| (relay, filter.clone())).collect();
|
||||
|
||||
// Stream events from user's write relays
|
||||
let mut stream = client
|
||||
.stream_events(target)
|
||||
.stream_events(filter)
|
||||
.timeout(Duration::from_secs(TIMEOUT))
|
||||
.await?;
|
||||
|
||||
while let Some((_url, res)) = stream.next().await {
|
||||
match res {
|
||||
Ok(event) => {
|
||||
return Ok(InboxState::RelayConfigured(Box::new(event)));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to receive relay list event: {e}");
|
||||
}
|
||||
if let Ok(event) = res {
|
||||
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
|
||||
return Ok(urls);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(InboxState::RelayNotAvailable)
|
||||
Err(anyhow!("Messaging Relays not found"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all messages for current user
|
||||
fn get_messages<I>(&mut self, relay_urls: I, cx: &mut Context<Self>)
|
||||
where
|
||||
I: IntoIterator<Item = RelayUrl>,
|
||||
{
|
||||
let task = self.subscribe(relay_urls, cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
task.await?;
|
||||
|
||||
// Update state
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_state(InboxState::Subscribing, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Continuously get gift wrap events for the current user in their messaging relays
|
||||
fn subscribe<I>(&mut self, urls: I, cx: &mut Context<Self>) -> Task<Result<(), Error>>
|
||||
where
|
||||
I: IntoIterator<Item = RelayUrl>,
|
||||
{
|
||||
fn subscribe(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
let urls = urls.into_iter().collect::<Vec<_>>();
|
||||
let urls = self.get_messaging_relays(cx);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let urls = urls.await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||
let id = SubscriptionId::new(USER_GIFTWRAP);
|
||||
@@ -419,19 +369,6 @@ impl ChatRegistry {
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the state of the inbox
|
||||
fn set_state(&mut self, state: InboxState, cx: &mut Context<Self>) {
|
||||
self.state.update(cx, |this, cx| {
|
||||
*this = state;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the relay state
|
||||
pub fn state(&self, cx: &App) -> InboxState {
|
||||
self.state.read(cx).clone()
|
||||
}
|
||||
|
||||
/// Get the loading status of the chat registry
|
||||
pub fn loading(&self) -> bool {
|
||||
self.tracking_flag.load(Ordering::Acquire)
|
||||
@@ -633,12 +570,12 @@ impl ChatRegistry {
|
||||
|
||||
// Process each event and group by room hash
|
||||
for raw in events.into_iter() {
|
||||
if let Ok(rumor) = UnsignedEvent::from_json(&raw.content) {
|
||||
if rumor.tags.public_keys().peekable().peek().is_some() {
|
||||
if let Ok(rumor) = UnsignedEvent::from_json(&raw.content)
|
||||
&& rumor.tags.public_keys().peekable().peek().is_some()
|
||||
{
|
||||
grouped.entry(rumor.uniq_id()).or_default().push(rumor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (_id, mut messages) in grouped.into_iter() {
|
||||
messages.sort_by_key(|m| Reverse(m.created_at));
|
||||
@@ -676,9 +613,18 @@ impl ChatRegistry {
|
||||
/// If the room doesn't exist, it will be created.
|
||||
/// Updates room ordering based on the most recent messages.
|
||||
pub fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
match self.rooms.iter().find(|e| e.read(cx).id == message.room) {
|
||||
Some(room) => {
|
||||
room.update(cx, |this, cx| {
|
||||
if this.kind == RoomKind::Request
|
||||
&& let Some(public_key) = signer.public_key()
|
||||
&& message.rumor.pubkey == public_key
|
||||
{
|
||||
this.set_ongoing(cx);
|
||||
}
|
||||
this.push_message(message, cx);
|
||||
});
|
||||
self.sort(cx);
|
||||
@@ -735,11 +681,11 @@ async fn try_unwrap(
|
||||
gift_wrap: &Event,
|
||||
) -> Result<UnwrappedGift, Error> {
|
||||
// Try with the device signer first
|
||||
if let Some(signer) = device_signer {
|
||||
if let Ok(unwrapped) = try_unwrap_with(gift_wrap, signer).await {
|
||||
if let Some(signer) = device_signer
|
||||
&& let Ok(unwrapped) = try_unwrap_with(gift_wrap, signer).await
|
||||
{
|
||||
return Ok(unwrapped);
|
||||
};
|
||||
};
|
||||
|
||||
// Try with the user's signer
|
||||
let user_signer = client.signer().context("Signer not found")?;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -10,7 +9,7 @@ use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{Person, PersonRegistry};
|
||||
use settings::{RoomConfig, SignerKind};
|
||||
use state::{NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
|
||||
use state::{NostrRegistry, TIMEOUT};
|
||||
|
||||
use crate::NewMessage;
|
||||
|
||||
@@ -58,16 +57,44 @@ impl SendReport {
|
||||
|
||||
/// Returns true if the send is pending.
|
||||
pub fn pending(&self) -> bool {
|
||||
self.output.is_none() && self.error.is_none()
|
||||
self.error.is_none()
|
||||
&& self
|
||||
.output
|
||||
.as_ref()
|
||||
.is_some_and(|o| o.success.is_empty() && o.failed.is_empty())
|
||||
}
|
||||
|
||||
/// Returns true if the send was successful.
|
||||
pub fn success(&self) -> bool {
|
||||
if let Some(output) = self.output.as_ref() {
|
||||
!output.failed.is_empty()
|
||||
} else {
|
||||
false
|
||||
self.error.is_none() && self.output.as_ref().is_some_and(|o| !o.success.is_empty())
|
||||
}
|
||||
|
||||
/// Returns true if the send failed.
|
||||
pub fn failed(&self) -> bool {
|
||||
self.error.is_some() && self.output.as_ref().is_some_and(|o| !o.failed.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SendStatus {
|
||||
Ok {
|
||||
id: EventId,
|
||||
relay: RelayUrl,
|
||||
},
|
||||
Failed {
|
||||
id: EventId,
|
||||
relay: RelayUrl,
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl SendStatus {
|
||||
pub fn ok(id: EventId, relay: RelayUrl) -> Self {
|
||||
Self::Ok { id, relay }
|
||||
}
|
||||
|
||||
pub fn failed(id: EventId, relay: RelayUrl, message: String) -> Self {
|
||||
Self::Failed { id, relay, message }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,11 +232,9 @@ impl Room {
|
||||
|
||||
/// Sets this room is ongoing conversation
|
||||
pub fn set_ongoing(&mut self, cx: &mut Context<Self>) {
|
||||
if self.kind != RoomKind::Ongoing {
|
||||
self.kind = RoomKind::Ongoing;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the creation timestamp of the room
|
||||
pub fn set_created_at(&mut self, created_at: impl Into<Timestamp>, cx: &mut Context<Self>) {
|
||||
@@ -333,9 +358,6 @@ impl Room {
|
||||
let signer = nostr.read(cx).signer();
|
||||
let sender = signer.public_key();
|
||||
|
||||
// Get room's id
|
||||
let id = self.id;
|
||||
|
||||
// Get all members, excluding the sender
|
||||
let members: Vec<PublicKey> = self
|
||||
.members
|
||||
@@ -345,30 +367,27 @@ impl Room {
|
||||
.collect();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let id = SubscriptionId::new(format!("room-{id}"));
|
||||
let opts = SubscribeAutoCloseOptions::default()
|
||||
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
||||
.timeout(Some(Duration::from_secs(TIMEOUT)));
|
||||
|
||||
// Construct filters for each member
|
||||
let filters: Vec<Filter> = members
|
||||
.into_iter()
|
||||
.map(|public_key| {
|
||||
Filter::new()
|
||||
for public_key in members.into_iter() {
|
||||
let inbox = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::RelayList)
|
||||
.limit(1)
|
||||
})
|
||||
.collect();
|
||||
.kind(Kind::InboxRelays)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
||||
.into_iter()
|
||||
.map(|relay| (relay, filters.clone()))
|
||||
.collect();
|
||||
let announcement = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::Custom(10044))
|
||||
.limit(1);
|
||||
|
||||
// Subscribe to the target
|
||||
client.subscribe(target).close_on(opts).with_id(id).await?;
|
||||
client
|
||||
.subscribe(vec![inbox, announcement])
|
||||
.close_on(opts)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -491,15 +510,9 @@ impl Room {
|
||||
|
||||
// Process each member
|
||||
for member in members {
|
||||
let relays = member.messaging_relays();
|
||||
let announcement = member.announcement();
|
||||
let public_key = member.public_key();
|
||||
|
||||
if relays.is_empty() {
|
||||
reports.push(SendReport::new(public_key).error("No messaging relays"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle encryption signer requirements
|
||||
if signer_kind.encryption() {
|
||||
if announcement.is_none() {
|
||||
@@ -535,8 +548,7 @@ impl Room {
|
||||
SignerKind::User => (member.public_key(), user_signer.clone()),
|
||||
};
|
||||
|
||||
match send_gift_wrap(&client, &signer, &receiver, &rumor, relays, public_key).await
|
||||
{
|
||||
match send_gift_wrap(&client, &signer, &receiver, &rumor, public_key).await {
|
||||
Ok((report, _)) => {
|
||||
reports.push(report);
|
||||
sents += 1;
|
||||
@@ -549,12 +561,10 @@ impl Room {
|
||||
|
||||
// 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
|
||||
{
|
||||
match send_gift_wrap(&client, signer, &public_key, &rumor, public_key).await {
|
||||
Ok((report, _)) => reports.push(report),
|
||||
Err(report) => reports.push(report),
|
||||
}
|
||||
@@ -571,22 +581,16 @@ async fn send_gift_wrap<T>(
|
||||
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)
|
||||
.to_nip17()
|
||||
.ack_policy(AckPolicy::none())
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -7,21 +7,10 @@ use settings::SignerKind;
|
||||
#[action(namespace = chat, no_json)]
|
||||
pub enum Command {
|
||||
Insert(&'static str),
|
||||
ChangeSubject(&'static str),
|
||||
ChangeSubject(String),
|
||||
ChangeSigner(SignerKind),
|
||||
ToggleBackup,
|
||||
Copy(PublicKey),
|
||||
Relays(PublicKey),
|
||||
Njump(PublicKey),
|
||||
}
|
||||
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[action(namespace = chat, no_json)]
|
||||
pub struct SeenOn(pub EventId);
|
||||
|
||||
/// Define a open public key action
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)]
|
||||
#[action(namespace = pubkey, no_json)]
|
||||
pub struct OpenPublicKey(pub PublicKey);
|
||||
|
||||
/// Define a copy inline public key action
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)]
|
||||
#[action(namespace = pubkey, no_json)]
|
||||
pub struct CopyPublicKey(pub PublicKey);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
pub use actions::*;
|
||||
use anyhow::{Context as AnyhowContext, Error};
|
||||
use chat::{Message, RenderedMessage, Room, RoomEvent, SendReport};
|
||||
use chat::{Message, RenderedMessage, Room, RoomEvent, SendReport, SendStatus};
|
||||
use common::RenderedTimestamp;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
@@ -24,10 +23,9 @@ use state::{NostrRegistry, upload};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::menu::{ContextMenuExt, DropdownMenu};
|
||||
use ui::menu::DropdownMenu;
|
||||
use ui::notification::Notification;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::{
|
||||
@@ -42,11 +40,6 @@ mod text;
|
||||
|
||||
const ANNOUNCEMENT: &str =
|
||||
"This conversation is private. Only members can see each other's messages.";
|
||||
const NO_INBOX: &str = "has not set up messaging relays. \
|
||||
They will not receive messages you send.";
|
||||
const NO_ANNOUNCEMENT: &str = "has not set up an encryption key. \
|
||||
You cannot send messages encrypted with an encryption key to them yet. \
|
||||
Coop automatically uses your identity to encrypt messages.";
|
||||
|
||||
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
|
||||
cx.new(|cx| ChatPanel::new(room, window, cx))
|
||||
@@ -72,9 +65,15 @@ pub struct ChatPanel {
|
||||
/// Mapping message (rumor event) ids to their reports
|
||||
reports_by_id: Entity<BTreeMap<EventId, Vec<SendReport>>>,
|
||||
|
||||
/// Input state
|
||||
/// Chat input state
|
||||
input: Entity<InputState>,
|
||||
|
||||
/// Subject input state
|
||||
subject_input: Entity<InputState>,
|
||||
|
||||
/// Subject bar visibility
|
||||
subject_bar: Entity<bool>,
|
||||
|
||||
/// Sent message ids
|
||||
sent_ids: Arc<RwLock<Vec<EventId>>>,
|
||||
|
||||
@@ -91,7 +90,7 @@ pub struct ChatPanel {
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
|
||||
/// Event subscriptions
|
||||
subscriptions: SmallVec<[Subscription; 2]>,
|
||||
subscriptions: SmallVec<[Subscription; 3]>,
|
||||
}
|
||||
|
||||
impl ChatPanel {
|
||||
@@ -124,19 +123,38 @@ impl ChatPanel {
|
||||
.clean_on_escape()
|
||||
});
|
||||
|
||||
// Define subject input state
|
||||
let subject_input = cx.new(|cx| InputState::new(window, cx).placeholder("New subject..."));
|
||||
let subject_bar = cx.new(|_cx| false);
|
||||
|
||||
// Define subscriptions
|
||||
let subscriptions =
|
||||
smallvec![
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe the chat input event
|
||||
cx.subscribe_in(&input, window, move |this, _input, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.send_text_message(window, cx);
|
||||
};
|
||||
})
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe the subject input event
|
||||
cx.subscribe_in(
|
||||
&subject_input,
|
||||
window,
|
||||
move |this, _input, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.change_subject(window, cx);
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Define all functions that will run after the current cycle
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.connect(window, cx);
|
||||
this.connect(cx);
|
||||
this.handle_notifications(cx);
|
||||
this.subscribe_room_events(window, cx);
|
||||
this.get_messages(window, cx);
|
||||
@@ -149,6 +167,8 @@ impl ChatPanel {
|
||||
room,
|
||||
list_state,
|
||||
input,
|
||||
subject_input,
|
||||
subject_bar,
|
||||
replies_to,
|
||||
attachments,
|
||||
rendered_texts_by_id: BTreeMap::new(),
|
||||
@@ -160,47 +180,12 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all necessary data for each member
|
||||
fn connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Ok((members, connect)) = self
|
||||
.room
|
||||
.read_with(cx, |this, cx| (this.members(), this.connect(cx)))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Run the connect task in background
|
||||
self.tasks.push(connect);
|
||||
|
||||
// Spawn another task to verify after 3 seconds
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||
|
||||
// Verify the connection
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
|
||||
for member in members.into_iter() {
|
||||
let profile = persons.read(cx).get(&member, cx);
|
||||
|
||||
if profile.announcement().is_none() {
|
||||
let content = format!("{} {}", profile.name(), NO_ANNOUNCEMENT);
|
||||
let message = Message::warning(content);
|
||||
|
||||
this.insert_message(message, true, cx);
|
||||
/// Get messaging relays and announcement for each member
|
||||
fn connect(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(room) = self.room.upgrade() {
|
||||
let task = room.read(cx).connect(cx);
|
||||
self.tasks.push(task);
|
||||
}
|
||||
|
||||
if profile.messaging_relays().is_empty() {
|
||||
let content = format!("{} {}", profile.name(), NO_INBOX);
|
||||
let message = Message::warning(content);
|
||||
|
||||
this.insert_message(message, true, cx);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Handle nostr notifications
|
||||
@@ -208,22 +193,33 @@ impl ChatPanel {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let sent_ids = self.sent_ids.clone();
|
||||
let reports = self.reports_by_id.downgrade();
|
||||
|
||||
let (tx, rx) = flume::bounded::<(EventId, RelayUrl)>(256);
|
||||
let (tx, rx) = flume::bounded::<Arc<SendStatus>>(256);
|
||||
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
let mut notifications = client.notifications();
|
||||
|
||||
while let Some(notification) = notifications.next().await {
|
||||
if let ClientNotification::Message {
|
||||
message: RelayMessage::Ok { event_id, .. },
|
||||
message:
|
||||
RelayMessage::Ok {
|
||||
event_id,
|
||||
status,
|
||||
message,
|
||||
},
|
||||
relay_url,
|
||||
} = notification
|
||||
{
|
||||
let sent_ids = sent_ids.read().await;
|
||||
|
||||
if sent_ids.contains(&event_id) {
|
||||
tx.send_async((event_id, relay_url)).await.ok();
|
||||
let status = if status {
|
||||
SendStatus::ok(event_id, relay_url)
|
||||
} else {
|
||||
SendStatus::failed(event_id, relay_url, message.into())
|
||||
};
|
||||
tx.send_async(Arc::new(status)).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,36 +227,42 @@ impl ChatPanel {
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
while let Ok((event_id, relay_url)) = rx.recv_async().await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.reports_by_id.update(cx, |this, cx| {
|
||||
self.tasks.push(cx.spawn(async move |_this, cx| {
|
||||
while let Ok(status) = rx.recv_async().await {
|
||||
reports.update(cx, |this, cx| {
|
||||
for reports in this.values_mut() {
|
||||
for report in reports.iter_mut() {
|
||||
if let Some(output) = report.output.as_mut() {
|
||||
if output.id() == &event_id {
|
||||
output.success.insert(relay_url.clone());
|
||||
let Some(output) = report.output.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
match &*status {
|
||||
SendStatus::Ok { id, relay } => {
|
||||
if output.id() == id {
|
||||
output.success.insert(relay.clone());
|
||||
}
|
||||
}
|
||||
SendStatus::Failed { id, relay, message } => {
|
||||
if output.id() == id {
|
||||
output.failed.insert(relay.clone(), message.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Subscribe to room events
|
||||
fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(room) = self.room.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.subscriptions.push(
|
||||
// Subscribe to room events
|
||||
cx.subscribe_in(&room, window, move |this, _room, event, window, cx| {
|
||||
if let Some(room) = self.room.upgrade() {
|
||||
self.subscriptions.push(cx.subscribe_in(
|
||||
&room,
|
||||
window,
|
||||
move |this, _room, event, window, cx| {
|
||||
match event {
|
||||
RoomEvent::Incoming(message) => {
|
||||
this.insert_message(message, false, cx);
|
||||
@@ -269,8 +271,9 @@ impl ChatPanel {
|
||||
this.get_messages(window, cx);
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all messages belonging to this room
|
||||
@@ -316,6 +319,16 @@ impl ChatPanel {
|
||||
content
|
||||
}
|
||||
|
||||
fn change_subject(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let subject = self.subject_input.read(cx).value();
|
||||
|
||||
self.room
|
||||
.update(cx, |this, cx| {
|
||||
this.set_subject(subject, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn send_text_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Get the message which includes all attachments
|
||||
let content = self.get_input_value(cx);
|
||||
@@ -457,23 +470,12 @@ impl ChatPanel {
|
||||
self.reports_by_id
|
||||
.read(cx)
|
||||
.get(id)
|
||||
.is_some_and(|reports| reports.iter().any(|r| r.pending()))
|
||||
.is_some_and(|reports| reports.iter().all(|r| r.pending()))
|
||||
}
|
||||
|
||||
/// Check if a message was sent successfully by its ID
|
||||
fn sent_success(&self, id: &EventId, cx: &App) -> bool {
|
||||
self.reports_by_id
|
||||
.read(cx)
|
||||
.get(id)
|
||||
.is_some_and(|reports| reports.iter().any(|r| r.success()))
|
||||
}
|
||||
|
||||
/// Check if a message failed to send by its ID
|
||||
fn sent_failed(&self, id: &EventId, cx: &App) -> Option<bool> {
|
||||
self.reports_by_id
|
||||
.read(cx)
|
||||
.get(id)
|
||||
.map(|reports| reports.iter().all(|r| !r.success()))
|
||||
/// Check if a message has any reports
|
||||
fn has_reports(&self, id: &EventId, cx: &App) -> bool {
|
||||
self.reports_by_id.read(cx).get(id).is_some()
|
||||
}
|
||||
|
||||
/// Get all sent reports for a message by its ID
|
||||
@@ -484,11 +486,11 @@ impl ChatPanel {
|
||||
/// Get a message by its ID
|
||||
fn message(&self, id: &EventId) -> Option<&RenderedMessage> {
|
||||
self.messages.iter().find_map(|msg| {
|
||||
if let Message::User(rendered) = msg {
|
||||
if &rendered.id == id {
|
||||
if let Message::User(rendered) = msg
|
||||
&& &rendered.id == id
|
||||
{
|
||||
return Some(rendered);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
@@ -505,10 +507,21 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_message(&self, id: &EventId, cx: &Context<Self>) {
|
||||
if let Some(message) = self.message(id) {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(message.content.to_string()));
|
||||
fn copy_author(&self, public_key: &PublicKey, cx: &App) {
|
||||
let content = public_key.to_bech32().unwrap();
|
||||
let item = ClipboardItem::new_string(content);
|
||||
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
|
||||
fn copy_message(&self, id: &EventId, cx: &App) {
|
||||
let Some(message) = self.message(id) else {
|
||||
return;
|
||||
};
|
||||
let content = message.content.to_string();
|
||||
let item = ClipboardItem::new_string(content);
|
||||
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
|
||||
fn reply_to(&mut self, id: &EventId, cx: &mut Context<Self>) {
|
||||
@@ -558,7 +571,10 @@ impl ChatPanel {
|
||||
Err(e) => {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_uploading(false, cx);
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@@ -588,7 +604,7 @@ impl ChatPanel {
|
||||
});
|
||||
}
|
||||
|
||||
fn profile(&self, public_key: &PublicKey, cx: &Context<Self>) -> Person {
|
||||
fn profile(&self, public_key: &PublicKey, cx: &App) -> Person {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
persons.read(cx).get(public_key, cx)
|
||||
}
|
||||
@@ -602,11 +618,14 @@ impl ChatPanel {
|
||||
if self
|
||||
.room
|
||||
.update(cx, |this, cx| {
|
||||
this.set_subject(*subject, cx);
|
||||
this.set_subject(subject, cx);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
window.push_notification(Notification::error("Failed to change subject"), cx);
|
||||
window.push_notification(
|
||||
Notification::error("Failed to change subject").autohide(false),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
Command::ChangeSigner(kind) => {
|
||||
@@ -617,7 +636,10 @@ impl ChatPanel {
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
window.push_notification(Notification::error("Failed to change signer"), cx);
|
||||
window.push_notification(
|
||||
Notification::error("Failed to change signer").autohide(false),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
Command::ToggleBackup => {
|
||||
@@ -628,10 +650,57 @@ impl ChatPanel {
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
window.push_notification(Notification::error("Failed to toggle backup"), cx);
|
||||
window.push_notification(
|
||||
Notification::error("Failed to toggle backup").autohide(false),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
Command::Copy(public_key) => {
|
||||
self.copy_author(public_key, cx);
|
||||
}
|
||||
Command::Relays(public_key) => {
|
||||
self.open_relays(public_key, window, cx);
|
||||
}
|
||||
Command::Njump(public_key) => {
|
||||
self.open_njump(public_key, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_relays(&mut self, public_key: &PublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let profile = self.profile(public_key, cx);
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
let relays = profile.messaging_relays();
|
||||
|
||||
this.title("Messaging Relays")
|
||||
.show_close(true)
|
||||
.child(v_flex().gap_1().children({
|
||||
let mut items = vec![];
|
||||
|
||||
for url in relays.iter() {
|
||||
items.push(
|
||||
h_flex()
|
||||
.h_7()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.rounded(cx.theme().radius)
|
||||
.text_sm()
|
||||
.child(div().size_1p5().rounded_full().bg(gpui::green()))
|
||||
.child(SharedString::from(url.to_string())),
|
||||
);
|
||||
}
|
||||
|
||||
items
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
fn open_njump(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
||||
let content = format!("https://njump.me/{}", public_key.to_bech32().unwrap());
|
||||
cx.open_url(&content);
|
||||
}
|
||||
|
||||
fn render_announcement(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||
@@ -729,19 +798,13 @@ impl ChatPanel {
|
||||
) -> AnyElement {
|
||||
let id = message.id;
|
||||
let author = self.profile(&message.author, cx);
|
||||
let public_key = author.public_key();
|
||||
let pk = author.public_key();
|
||||
|
||||
let replies = message.replies_to.as_slice();
|
||||
let has_replies = !replies.is_empty();
|
||||
|
||||
// Check if message is sent failed
|
||||
let sent_pending = self.sent_pending(&id, cx);
|
||||
|
||||
// Check if message is sent successfully
|
||||
let sent_success = self.sent_success(&id, cx);
|
||||
|
||||
// Check if message is sent failed
|
||||
let sent_failed = self.sent_failed(&id, cx);
|
||||
let has_reports = self.has_reports(&id, cx);
|
||||
|
||||
// Hide avatar setting
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
@@ -759,15 +822,14 @@ impl ChatPanel {
|
||||
.gap_3()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("{ix}-avatar")))
|
||||
.child(Avatar::new(author.avatar()))
|
||||
.context_menu(move |this, _window, _cx| {
|
||||
let view = Box::new(OpenPublicKey(public_key));
|
||||
let copy = Box::new(CopyPublicKey(public_key));
|
||||
|
||||
this.menu("View Profile", view)
|
||||
.menu("Copy Public Key", copy)
|
||||
Avatar::new(author.avatar())
|
||||
.flex_shrink_0()
|
||||
.relative()
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
this.menu("Public Key", Box::new(Command::Copy(pk)))
|
||||
.menu("View Relays", Box::new(Command::Relays(pk)))
|
||||
.separator()
|
||||
.menu("View on njump.me", Box::new(Command::Njump(pk)))
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -790,25 +852,29 @@ impl ChatPanel {
|
||||
)
|
||||
.child(message.created_at.to_human_time())
|
||||
.when(sent_pending, |this| {
|
||||
this.child(deferred(Indicator::new().small()))
|
||||
this.child(SharedString::from("• Sending..."))
|
||||
})
|
||||
.when(sent_success, |this| {
|
||||
this.child(deferred(self.render_sent_indicator(&id, cx)))
|
||||
.when(has_reports, |this| {
|
||||
this.child(deferred(self.render_sent_reports(&id, cx)))
|
||||
}),
|
||||
)
|
||||
.when(has_replies, |this| {
|
||||
this.children(self.render_message_replies(replies, cx))
|
||||
})
|
||||
.child(rendered_text)
|
||||
.when_some(sent_failed, |this, failed| {
|
||||
this.when(failed, |this| {
|
||||
this.child(deferred(self.render_message_reports(&id, cx)))
|
||||
})
|
||||
}),
|
||||
.child(rendered_text),
|
||||
),
|
||||
)
|
||||
.child(self.render_border(cx))
|
||||
.child(self.render_actions(&id, cx))
|
||||
.child(
|
||||
div()
|
||||
.group_hover("", |this| this.bg(cx.theme().element_active))
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top_0()
|
||||
.w(px(2.))
|
||||
.h_full()
|
||||
.bg(cx.theme().border_transparent),
|
||||
)
|
||||
.child(self.render_actions(&id, &pk, cx))
|
||||
.on_mouse_down(
|
||||
MouseButton::Middle,
|
||||
cx.listener(move |this, _, _window, cx| {
|
||||
@@ -868,50 +934,37 @@ impl ChatPanel {
|
||||
items
|
||||
}
|
||||
|
||||
fn render_sent_indicator(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
|
||||
fn render_sent_reports(&self, id: &EventId, cx: &App) -> impl IntoElement {
|
||||
let reports = self.sent_reports(id, cx);
|
||||
|
||||
let success = reports
|
||||
.as_ref()
|
||||
.is_some_and(|reports| reports.iter().any(|r| r.success()));
|
||||
|
||||
let failed = reports
|
||||
.as_ref()
|
||||
.is_some_and(|reports| reports.iter().all(|r| r.failed()));
|
||||
|
||||
let label = if success {
|
||||
SharedString::from("• Sent")
|
||||
} else if failed {
|
||||
SharedString::from("• Failed")
|
||||
} else {
|
||||
SharedString::from("• Sending...")
|
||||
};
|
||||
|
||||
div()
|
||||
.id(SharedString::from(id.to_hex()))
|
||||
.child(SharedString::from("• Sent"))
|
||||
.when_some(self.sent_reports(id, cx), |this, reports| {
|
||||
.child(label)
|
||||
.when(failed, |this| this.text_color(cx.theme().text_danger))
|
||||
.when_some(reports, |this, reports| {
|
||||
this.on_click(move |_e, window, cx| {
|
||||
let reports = reports.clone();
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.show_close(true)
|
||||
.title(SharedString::from("Sent Reports"))
|
||||
.child(v_flex().pb_2().gap_4().children({
|
||||
let mut items = Vec::with_capacity(reports.len());
|
||||
|
||||
for report in reports.iter() {
|
||||
items.push(Self::render_report(report, cx))
|
||||
}
|
||||
|
||||
items
|
||||
}))
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn render_message_reports(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id(SharedString::from(id.to_hex()))
|
||||
.gap_0p5()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_xs()
|
||||
.italic()
|
||||
.child(Icon::new(IconName::Info).xsmall())
|
||||
.child(SharedString::from(
|
||||
"Failed to send message. Click to see details.",
|
||||
))
|
||||
.when_some(self.sent_reports(id, cx), |this, reports| {
|
||||
this.on_click(move |_e, window, cx| {
|
||||
let reports = reports.clone();
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.show_close(true)
|
||||
.title(SharedString::from("Sent Reports"))
|
||||
.child(v_flex().gap_4().pb_4().w_full().children({
|
||||
this.title(SharedString::from("Sent Reports"))
|
||||
.show_close(true)
|
||||
.child(v_flex().gap_4().children({
|
||||
let mut items = Vec::with_capacity(reports.len());
|
||||
|
||||
for report in reports.iter() {
|
||||
@@ -1030,18 +1083,12 @@ impl ChatPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_border(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.group_hover("", |this| this.bg(cx.theme().element_active))
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top_0()
|
||||
.w(px(2.))
|
||||
.h_full()
|
||||
.bg(cx.theme().border_transparent)
|
||||
}
|
||||
|
||||
fn render_actions(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
|
||||
fn render_actions(
|
||||
&self,
|
||||
id: &EventId,
|
||||
public_key: &PublicKey,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
h_flex()
|
||||
.p_0p5()
|
||||
.gap_1()
|
||||
@@ -1082,13 +1129,22 @@ impl ChatPanel {
|
||||
)
|
||||
.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
|
||||
.child(
|
||||
Button::new("seen-on")
|
||||
Button::new("advance")
|
||||
.icon(IconName::Ellipsis)
|
||||
.small()
|
||||
.ghost()
|
||||
.dropdown_menu({
|
||||
let id = id.to_owned();
|
||||
move |this, _window, _cx| this.menu("Seen on", Box::new(SeenOn(id)))
|
||||
let public_key = *public_key;
|
||||
let _id = *id;
|
||||
move |this, _window, _cx| {
|
||||
this.menu("Copy author", Box::new(Command::Copy(public_key)))
|
||||
/*
|
||||
.menu(
|
||||
"Trace",
|
||||
Box::new(Command::Trace(id)),
|
||||
)
|
||||
*/
|
||||
}
|
||||
}),
|
||||
)
|
||||
.group_hover("", |this| this.visible())
|
||||
@@ -1286,12 +1342,30 @@ impl Panel for ChatPanel {
|
||||
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Avatar::new(url).small())
|
||||
.child(Avatar::new(url).xsmall())
|
||||
.child(label)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap_or(div().child("Unknown").into_any_element())
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
let subject_bar = self.subject_bar.clone();
|
||||
|
||||
vec![
|
||||
Button::new("subject")
|
||||
.icon(IconName::Input)
|
||||
.tooltip("Change subject")
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(move |_ev, _window, cx| {
|
||||
subject_bar.update(cx, |this, cx| {
|
||||
*this = !*this;
|
||||
cx.notify();
|
||||
});
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for ChatPanel {}
|
||||
@@ -1307,6 +1381,33 @@ impl Render for ChatPanel {
|
||||
v_flex()
|
||||
.on_action(cx.listener(Self::on_command))
|
||||
.size_full()
|
||||
.when(*self.subject_bar.read(cx), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.h_12()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
TextInput::new(&self.subject_input)
|
||||
.text_sm()
|
||||
.small()
|
||||
.bordered(false),
|
||||
)
|
||||
.child(
|
||||
Button::new("change")
|
||||
.icon(IconName::CheckCircle)
|
||||
.label("Change")
|
||||
.secondary()
|
||||
.disabled(self.uploading)
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.change_subject(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
use gpui::{
|
||||
div, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
|
||||
Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{v_flex, Sizable};
|
||||
|
||||
pub fn init(subject: Option<String>, window: &mut Window, cx: &mut App) -> Entity<Subject> {
|
||||
cx.new(|cx| Subject::new(subject, window, cx))
|
||||
}
|
||||
|
||||
pub struct Subject {
|
||||
input: Entity<InputState>,
|
||||
}
|
||||
|
||||
impl Subject {
|
||||
pub fn new(subject: Option<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("Plan for holiday"));
|
||||
|
||||
if let Some(value) = subject {
|
||||
input.update(cx, |this, cx| {
|
||||
this.set_value(value, window, cx);
|
||||
});
|
||||
};
|
||||
|
||||
Self { input }
|
||||
}
|
||||
|
||||
pub fn new_subject(&self, cx: &App) -> SharedString {
|
||||
self.input.read(cx).value()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Subject {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::from("Subject:")),
|
||||
)
|
||||
.child(TextInput::new(&self.input).small()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.italic()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(SharedString::from(
|
||||
"Subject will be updated when you send a new message.",
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use anyhow::{Error, anyhow};
|
||||
use chrono::{Local, TimeZone};
|
||||
use gpui::{Image, ImageFormat, SharedString};
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
@@ -14,7 +14,7 @@ product-name = "Coop"
|
||||
description = "Chat Freely, Stay Private on Nostr"
|
||||
identifier = "su.reya.coop"
|
||||
category = "SocialNetworking"
|
||||
version = "1.0.0-beta"
|
||||
version = "1.0.0-beta1"
|
||||
out-dir = "../../dist"
|
||||
before-packaging-command = "cargo build --release"
|
||||
resources = ["Cargo.toml", "src"]
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use anyhow::Error;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
|
||||
App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, div, px,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use state::{NostrRegistry, SignerEvent};
|
||||
use state::{NostrRegistry, StateEvent};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, WindowExtension};
|
||||
use ui::{Disableable, Icon, IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
use crate::dialogs::connect::ConnectSigner;
|
||||
use crate::dialogs::import::ImportKey;
|
||||
@@ -44,13 +44,14 @@ impl AccountSelector {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| {
|
||||
match event {
|
||||
SignerEvent::Set => {
|
||||
StateEvent::SignerSet => {
|
||||
window.close_all_modals(cx);
|
||||
window.refresh();
|
||||
}
|
||||
SignerEvent::Error(e) => {
|
||||
StateEvent::Error(e) => {
|
||||
this.set_error(e.to_string(), cx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -161,7 +162,7 @@ impl Render for AccountSelector {
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_color(cx.theme().text_danger)
|
||||
.child(error.clone()),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -4,13 +4,13 @@ use std::time::Duration;
|
||||
use common::TextUtils;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, px, AppContext, Context, Entity, Image, IntoElement, ParentElement, Render,
|
||||
SharedString, Styled, Subscription, Window,
|
||||
AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Subscription, Window, div, img, px,
|
||||
};
|
||||
use nostr_connect::prelude::*;
|
||||
use state::{
|
||||
CoopAuthUrlHandler, NostrRegistry, SignerEvent, CLIENT_NAME, NOSTR_CONNECT_RELAY,
|
||||
NOSTR_CONNECT_TIMEOUT,
|
||||
CLIENT_NAME, CoopAuthUrlHandler, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT, NostrRegistry,
|
||||
StateEvent,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::v_flex;
|
||||
@@ -31,7 +31,7 @@ impl ConnectSigner {
|
||||
let error = cx.new(|_| None);
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let app_keys = nostr.read(cx).app_keys.clone();
|
||||
let app_keys = nostr.read(cx).keys();
|
||||
|
||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||
@@ -55,7 +55,7 @@ impl ConnectSigner {
|
||||
|
||||
// Subscribe to the signer event
|
||||
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
||||
if let SignerEvent::Error(e) = event {
|
||||
if let StateEvent::Error(e) = event {
|
||||
this.set_error(e, cx);
|
||||
}
|
||||
});
|
||||
@@ -101,7 +101,7 @@ impl Render for ConnectSigner {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_color(cx.theme().text_danger)
|
||||
.child(error.clone()),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use anyhow::{Error, anyhow};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Subscription, Task, Window,
|
||||
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Subscription, Task, Window, div,
|
||||
};
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use state::{CoopAuthUrlHandler, NostrRegistry, SignerEvent};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{CoopAuthUrlHandler, NostrRegistry, StateEvent};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::{v_flex, Disableable};
|
||||
use ui::{Disableable, v_flex};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ImportKey {
|
||||
@@ -60,7 +60,7 @@ impl ImportKey {
|
||||
subscriptions.push(
|
||||
// Subscribe to the nostr signer event
|
||||
cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
||||
if let SignerEvent::Error(e) = event {
|
||||
if let StateEvent::Error(e) = event {
|
||||
this.set_error(e, cx);
|
||||
}
|
||||
}),
|
||||
@@ -117,7 +117,7 @@ impl ImportKey {
|
||||
};
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let app_keys = nostr.read(cx).app_keys.clone();
|
||||
let app_keys = nostr.read(cx).keys();
|
||||
let timeout = Duration::from_secs(30);
|
||||
|
||||
// Construct the nostr connect signer
|
||||
@@ -293,7 +293,7 @@ impl Render for ImportKey {
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_color(cx.theme().text_danger)
|
||||
.child(error.clone()),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::http_client::Url;
|
||||
use gpui::{
|
||||
div, px, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
|
||||
Styled, Window,
|
||||
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Window, div, px,
|
||||
};
|
||||
use settings::{AppSettings, AuthMode};
|
||||
use theme::{ActiveTheme, ThemeMode};
|
||||
@@ -11,7 +11,7 @@ use ui::input::{InputState, TextInput};
|
||||
use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||
use ui::notification::Notification;
|
||||
use ui::switch::Switch;
|
||||
use ui::{h_flex, v_flex, IconName, Sizable, WindowExtension};
|
||||
use ui::{IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
||||
cx.new(|cx| Preferences::new(window, cx))
|
||||
@@ -41,7 +41,7 @@ impl Preferences {
|
||||
AppSettings::update_file_server(url, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(Notification::error(e.to_string()).autohide(false), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use assets::Assets;
|
||||
use gpui::{
|
||||
actions, point, px, size, App, AppContext, Bounds, KeyBinding, Menu, MenuItem, SharedString,
|
||||
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
|
||||
WindowOptions,
|
||||
App, AppContext, Bounds, KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
||||
actions, point, px, size,
|
||||
};
|
||||
use gpui_platform::application;
|
||||
use state::{APP_ID, CLIENT_NAME};
|
||||
@@ -86,7 +86,7 @@ fn main() {
|
||||
state::init(window, cx);
|
||||
|
||||
// Initialize person registry
|
||||
person::init(cx);
|
||||
person::init(window, cx);
|
||||
|
||||
// Initialize relay auth registry
|
||||
relay_auth::init(window, cx);
|
||||
|
||||
@@ -2,16 +2,16 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use gpui::{
|
||||
div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window,
|
||||
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use state::KEYRING;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::{divider, v_flex, IconName, Sizable, StyledExt};
|
||||
use ui::{IconName, Sizable, StyledExt, divider, v_flex};
|
||||
|
||||
const MSG: &str = "Store your account keys in a safe location. \
|
||||
You can restore your account or move to another client anytime you want.";
|
||||
|
||||
@@ -4,20 +4,20 @@ use std::time::Duration;
|
||||
use anyhow::{Context as AnyhowContext, Error};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, rems, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
Task, TextAlign, Window,
|
||||
Task, TextAlign, Window, div, rems,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::{h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
|
||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> {
|
||||
cx.new(|cx| ContactListPanel::new(window, cx))
|
||||
@@ -156,15 +156,6 @@ impl ContactListPanel {
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
window.push_notification("Public Key not found", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
// Get user's write relays
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
// Get contacts
|
||||
let contacts: Vec<Contact> = self
|
||||
@@ -177,14 +168,12 @@ impl ContactListPanel {
|
||||
self.set_updating(true, cx);
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct contact list event builder
|
||||
let builder = EventBuilder::contact_list(contacts);
|
||||
let event = client.sign_event_builder(builder).await?;
|
||||
|
||||
// Set contact list
|
||||
client.send_event(&event).to(urls).await?;
|
||||
client.send_event(&event).to_nip65().await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -333,7 +322,7 @@ impl Render for ContactListPanel {
|
||||
div()
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_color(cx.theme().text_danger)
|
||||
.child(error.clone()),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
use chat::{ChatRegistry, InboxState};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg,
|
||||
};
|
||||
use state::{NostrRegistry, RelayState};
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::dock::DockPlacement;
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
|
||||
use ui::dock::{DockPlacement, Panel, PanelEvent};
|
||||
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
||||
|
||||
use crate::panels::{messaging_relays, profile, relay_list};
|
||||
use crate::panels::profile;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
||||
@@ -82,15 +79,6 @@ impl Render for GreeterPanel {
|
||||
const TITLE: &str = "Welcome to Coop!";
|
||||
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
|
||||
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let nip17 = chat.read(cx).state(cx);
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let nip65 = nostr.read(cx).relay_list_state.clone();
|
||||
|
||||
let required_actions =
|
||||
nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable;
|
||||
|
||||
h_flex()
|
||||
.size_full()
|
||||
.items_center()
|
||||
@@ -130,64 +118,6 @@ impl Render for GreeterPanel {
|
||||
),
|
||||
),
|
||||
)
|
||||
.when(required_actions, |this| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::from("Required Actions"))
|
||||
.child(div().flex_1().h_px().bg(cx.theme().border)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.when(nip65.not_configured(), |this| {
|
||||
this.child(
|
||||
Button::new("relaylist")
|
||||
.icon(Icon::new(IconName::Relay))
|
||||
.label("Set up relay list")
|
||||
.ghost()
|
||||
.small()
|
||||
.justify_start()
|
||||
.on_click(move |_ev, window, cx| {
|
||||
Workspace::add_panel(
|
||||
relay_list::init(window, cx),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(nip17.not_configured(), |this| {
|
||||
this.child(
|
||||
Button::new("import")
|
||||
.icon(Icon::new(IconName::Relay))
|
||||
.label("Set up messaging relays")
|
||||
.ghost()
|
||||
.small()
|
||||
.justify_start()
|
||||
.on_click(move |_ev, window, cx| {
|
||||
Workspace::add_panel(
|
||||
messaging_relays::init(window, cx),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, rems, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
Task, TextAlign, Window,
|
||||
Task, TextAlign, Window, div, rems,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
|
||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
|
||||
|
||||
const MSG: &str = "Messaging Relays are relays that hosted all your messages. \
|
||||
Other users will find your relays and send messages to it.";
|
||||
@@ -170,15 +170,6 @@ impl MessagingRelayPanel {
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
window.push_notification("Public Key not found", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
// Get user's write relays
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
// Construct event tags
|
||||
let tags: Vec<Tag> = self
|
||||
@@ -191,14 +182,12 @@ impl MessagingRelayPanel {
|
||||
self.set_updating(true, cx);
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct nip17 event builder
|
||||
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
|
||||
let event = client.sign_event_builder(builder).await?;
|
||||
|
||||
// Set messaging relays
|
||||
client.send_event(&event).to(urls).await?;
|
||||
client.send_event(&event).to_nip65().await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -349,7 +338,7 @@ impl Render for MessagingRelayPanel {
|
||||
div()
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_color(cx.theme().text_danger)
|
||||
.child(error.clone()),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -3,21 +3,21 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as AnyhowContext, Error};
|
||||
use gpui::{
|
||||
div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
|
||||
Window,
|
||||
Window, div,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{shorten_pubkey, Person, PersonRegistry};
|
||||
use person::{Person, PersonRegistry, shorten_pubkey};
|
||||
use settings::AppSettings;
|
||||
use state::{upload, NostrRegistry};
|
||||
use state::{NostrRegistry, upload};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::{h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
|
||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> {
|
||||
cx.new(|cx| ProfilePanel::new(public_key, window, cx))
|
||||
@@ -186,7 +186,10 @@ impl ProfilePanel {
|
||||
Err(e) => {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_uploading(false, cx);
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@@ -269,7 +272,10 @@ impl ProfilePanel {
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, rems, Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
Subscription, Task, TextAlign, Window,
|
||||
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
Task, TextAlign, Window, div, px, rems,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::menu::DropdownMenu;
|
||||
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
|
||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
|
||||
|
||||
const MSG: &str = "Relay List (or Gossip Relays) are a set of relays \
|
||||
where you will publish all your events. Others also publish events \
|
||||
@@ -408,7 +408,7 @@ impl Render for RelayListPanel {
|
||||
div()
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger_active)
|
||||
.text_color(cx.theme().text_danger)
|
||||
.child(error.clone()),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -3,16 +3,16 @@ use std::rc::Rc;
|
||||
use chat::RoomKind;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window, div,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::dock_area::ClosePanel;
|
||||
use ui::dock::ClosePanel;
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::{h_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension};
|
||||
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex};
|
||||
|
||||
use crate::dialogs::screening;
|
||||
|
||||
@@ -148,8 +148,10 @@ impl RenderOnce for RoomEntry {
|
||||
this.on_click(move |event, window, cx| {
|
||||
handler(event, window, cx);
|
||||
|
||||
if let Some(public_key) = public_key {
|
||||
if self.kind != Some(RoomKind::Ongoing) && screening {
|
||||
if let Some(public_key) = public_key
|
||||
&& self.kind != Some(RoomKind::Ongoing)
|
||||
&& screening
|
||||
{
|
||||
let screening = screening::init(public_key, window, cx);
|
||||
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
@@ -168,7 +170,6 @@ impl RenderOnce for RoomEntry {
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use smallvec::{SmallVec, smallvec};
|
||||
use state::{FIND_DELAY, NostrRegistry};
|
||||
use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
@@ -180,7 +180,10 @@ impl Sidebar {
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
};
|
||||
@@ -363,7 +366,11 @@ impl Sidebar {
|
||||
self.new_requests = false;
|
||||
}
|
||||
|
||||
fn render_list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
||||
fn render_list_items(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
cx: &Context<Self>,
|
||||
) -> Vec<impl IntoElement + use<>> {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let rooms = chat.read(cx).rooms(self.filter.read(cx), cx);
|
||||
|
||||
@@ -395,7 +402,11 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
/// Render the contact list
|
||||
fn render_results(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
||||
fn render_results(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
cx: &Context<Self>,
|
||||
) -> Vec<impl IntoElement + use<>> {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
|
||||
// Get the contact list
|
||||
@@ -428,7 +439,11 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
/// Render the contact list
|
||||
fn render_contacts(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
|
||||
fn render_contacts(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
cx: &Context<Self>,
|
||||
) -> Vec<impl IntoElement + use<>> {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
|
||||
// Get the contact list
|
||||
@@ -500,7 +515,7 @@ impl Render for Sidebar {
|
||||
.h(TABBAR_HEIGHT)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().elevated_surface_background)
|
||||
.bg(cx.theme().tab_background)
|
||||
.child(
|
||||
TextInput::new(&self.find_input)
|
||||
.appearance(false)
|
||||
@@ -638,7 +653,7 @@ impl Render for Sidebar {
|
||||
uniform_list(
|
||||
"rooms",
|
||||
results.len(),
|
||||
cx.processor(|this, range, _window, cx| {
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_results(range, cx)
|
||||
}),
|
||||
)
|
||||
@@ -665,7 +680,7 @@ impl Render for Sidebar {
|
||||
uniform_list(
|
||||
"contacts",
|
||||
contacts.len(),
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
cx.processor(|this, range, _window, cx| {
|
||||
this.render_contacts(range, cx)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::settings::AppSettings;
|
||||
use chat::{ChatEvent, ChatRegistry, InboxState};
|
||||
use device::DeviceRegistry;
|
||||
use chat::{ChatEvent, ChatRegistry};
|
||||
use device::{DeviceEvent, DeviceRegistry};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||
@@ -11,17 +13,15 @@ use gpui::{
|
||||
use person::PersonRegistry;
|
||||
use serde::Deserialize;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{NostrRegistry, RelayState, SignerEvent};
|
||||
use state::{NostrRegistry, StateEvent};
|
||||
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
|
||||
use title_bar::TitleBar;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::dock::DockPlacement;
|
||||
use ui::dock_area::panel::PanelView;
|
||||
use ui::dock_area::{ClosePanel, DockArea, DockItem};
|
||||
use ui::dock::{ClosePanel, DockArea, DockItem, DockPlacement, PanelView};
|
||||
use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||
use ui::notification::Notification;
|
||||
use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
||||
use ui::notification::{Notification, NotificationKind};
|
||||
use ui::{Disableable, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
use crate::dialogs::{accounts, settings};
|
||||
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
|
||||
@@ -37,6 +37,8 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
||||
cx.new(|cx| Workspace::new(window, cx))
|
||||
}
|
||||
|
||||
struct RelayNotifcation;
|
||||
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[action(namespace = workspace, no_json)]
|
||||
enum Command {
|
||||
@@ -63,15 +65,23 @@ pub struct Workspace {
|
||||
/// App's Dock Area
|
||||
dock: Entity<DockArea>,
|
||||
|
||||
/// Whether a user's relay list is connected
|
||||
relay_connected: bool,
|
||||
|
||||
/// Whether the inbox is connected
|
||||
inbox_connected: bool,
|
||||
|
||||
/// Event subscriptions
|
||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||
_subscriptions: SmallVec<[Subscription; 6]>,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let device = DeviceRegistry::global(cx);
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let npubs = nostr.read(cx).npubs();
|
||||
let chat = ChatRegistry::global(cx);
|
||||
|
||||
let titlebar = cx.new(|_| TitleBar::new());
|
||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
||||
|
||||
@@ -96,9 +106,56 @@ impl Workspace {
|
||||
subscriptions.push(
|
||||
// Subscribe to the signer events
|
||||
cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| {
|
||||
if let SignerEvent::Set = event {
|
||||
this.set_center_layout(window, cx);
|
||||
match event {
|
||||
StateEvent::Connecting => {
|
||||
let note = Notification::new()
|
||||
.id::<RelayNotifcation>()
|
||||
.message("Connecting to the bootstrap relay...")
|
||||
.with_kind(NotificationKind::Info)
|
||||
.icon(IconName::Relay);
|
||||
|
||||
window.push_notification(note, cx);
|
||||
}
|
||||
StateEvent::Connected => {
|
||||
let note = Notification::new()
|
||||
.id::<RelayNotifcation>()
|
||||
.message("Connected to the bootstrap relay")
|
||||
.with_kind(NotificationKind::Success)
|
||||
.icon(IconName::Relay);
|
||||
|
||||
window.push_notification(note, cx);
|
||||
}
|
||||
StateEvent::RelayNotConfigured => {
|
||||
this.relay_notification(window, cx);
|
||||
}
|
||||
StateEvent::RelayConnected => {
|
||||
window.clear_notification::<RelayNotifcation>(cx);
|
||||
this.set_relay_connected(true, cx);
|
||||
}
|
||||
StateEvent::SignerSet => {
|
||||
this.set_center_layout(window, cx);
|
||||
this.set_relay_connected(false, cx);
|
||||
this.set_inbox_connected(false, cx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Observe all events emitted by the device registry
|
||||
cx.subscribe_in(&device, window, |_this, _device, ev, window, cx| {
|
||||
match ev {
|
||||
DeviceEvent::Set => {
|
||||
window.push_notification(
|
||||
Notification::success("Encryption Key has been set"),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
DeviceEvent::Error(error) => {
|
||||
window.push_notification(Notification::error(error).autohide(false), cx);
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -130,6 +187,12 @@ impl Workspace {
|
||||
});
|
||||
});
|
||||
}
|
||||
ChatEvent::Subscribed => {
|
||||
this.set_inbox_connected(true, cx);
|
||||
}
|
||||
ChatEvent::Error(error) => {
|
||||
window.push_notification(Notification::error(error).autohide(false), cx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}),
|
||||
@@ -154,6 +217,8 @@ impl Workspace {
|
||||
Self {
|
||||
titlebar,
|
||||
dock,
|
||||
relay_connected: false,
|
||||
inbox_connected: false,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -163,8 +228,9 @@ impl Workspace {
|
||||
where
|
||||
P: PanelView,
|
||||
{
|
||||
if let Some(root) = window.root::<Root>().flatten() {
|
||||
if let Ok(workspace) = root.read(cx).view().clone().downcast::<Self>() {
|
||||
if let Some(root) = window.root::<Root>().flatten()
|
||||
&& let Ok(workspace) = root.read(cx).view().clone().downcast::<Self>()
|
||||
{
|
||||
workspace.update(cx, |this, cx| {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
this.add_panel(Arc::new(panel), placement, window, cx);
|
||||
@@ -172,7 +238,6 @@ impl Workspace {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all panel ids
|
||||
fn panel_ids(&self, cx: &App) -> Vec<u64> {
|
||||
@@ -185,6 +250,18 @@ impl Workspace {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Set whether the relay list is connected
|
||||
fn set_relay_connected(&mut self, connected: bool, cx: &mut Context<Self>) {
|
||||
self.relay_connected = connected;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Set whether the inbox is connected
|
||||
fn set_inbox_connected(&mut self, connected: bool, cx: &mut Context<Self>) {
|
||||
self.inbox_connected = connected;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Set the dock layout
|
||||
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
||||
@@ -198,8 +275,8 @@ impl Workspace {
|
||||
/// Set the center dock layout
|
||||
fn set_center_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let dock = self.dock.downgrade();
|
||||
let greeeter = Arc::new(greeter::init(window, cx));
|
||||
let tabs = DockItem::tabs(vec![greeeter], None, &dock, window, cx);
|
||||
let greeter = Arc::new(greeter::init(window, cx));
|
||||
let tabs = DockItem::tabs(vec![greeter], None, &dock, window, cx);
|
||||
let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx);
|
||||
|
||||
// Update the layout with center dock
|
||||
@@ -267,6 +344,12 @@ impl Workspace {
|
||||
);
|
||||
});
|
||||
}
|
||||
Command::RefreshMessagingRelays => {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
chat.update(cx, |this, cx| {
|
||||
this.get_messages(cx);
|
||||
});
|
||||
}
|
||||
Command::ShowRelayList => {
|
||||
self.dock.update(cx, |this, cx| {
|
||||
this.add_panel(
|
||||
@@ -277,27 +360,25 @@ impl Workspace {
|
||||
);
|
||||
});
|
||||
}
|
||||
Command::RefreshRelayList => {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
if let Some(public_key) = signer.public_key() {
|
||||
nostr.update(cx, |this, cx| {
|
||||
this.ensure_relay_list(&public_key, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
Command::RefreshEncryption => {
|
||||
let device = DeviceRegistry::global(cx);
|
||||
device.update(cx, |this, cx| {
|
||||
this.get_announcement(cx);
|
||||
});
|
||||
}
|
||||
Command::RefreshRelayList => {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
nostr.update(cx, |this, cx| {
|
||||
this.ensure_relay_list(cx);
|
||||
});
|
||||
}
|
||||
Command::ResetEncryption => {
|
||||
self.confirm_reset_encryption(window, cx);
|
||||
}
|
||||
Command::RefreshMessagingRelays => {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
chat.update(cx, |this, cx| {
|
||||
this.ensure_messaging_relays(cx);
|
||||
});
|
||||
}
|
||||
Command::ToggleTheme => {
|
||||
self.theme_selector(window, cx);
|
||||
}
|
||||
@@ -341,8 +422,10 @@ impl Workspace {
|
||||
window.close_modal(cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window
|
||||
.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
@@ -450,7 +533,56 @@ impl Workspace {
|
||||
});
|
||||
}
|
||||
|
||||
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
||||
fn relay_notification(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
const BODY: &str = "Coop cannot found your gossip relay list. \
|
||||
Maybe you haven't set it yet or relay not responsed";
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let entity = nostr.downgrade();
|
||||
let loading = Rc::new(Cell::new(false));
|
||||
|
||||
let note = Notification::new()
|
||||
.autohide(false)
|
||||
.id::<RelayNotifcation>()
|
||||
.icon(IconName::Relay)
|
||||
.title("Gossip Relays are required")
|
||||
.message(BODY)
|
||||
.action(move |_this, _window, _cx| {
|
||||
let entity = entity.clone();
|
||||
let public_key = public_key.to_owned();
|
||||
|
||||
Button::new("retry")
|
||||
.label("Retry")
|
||||
.small()
|
||||
.primary()
|
||||
.loading(loading.get())
|
||||
.disabled(loading.get())
|
||||
.on_click({
|
||||
let loading = Rc::clone(&loading);
|
||||
|
||||
move |_ev, _window, cx| {
|
||||
// Set loading state to true
|
||||
loading.set(true);
|
||||
// Retry
|
||||
entity
|
||||
.update(cx, |this, cx| {
|
||||
this.ensure_relay_list(&public_key, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
window.push_notification(note, cx);
|
||||
}
|
||||
|
||||
fn titlebar_left(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
let current_user = signer.public_key();
|
||||
@@ -529,14 +661,14 @@ impl Workspace {
|
||||
})
|
||||
}
|
||||
|
||||
fn titlebar_right(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
||||
fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let relay_connected = self.relay_connected;
|
||||
let inbox_connected = self.inbox_connected;
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let inbox_state = chat.read(cx).state(cx);
|
||||
|
||||
let Some(pkey) = signer.public_key() else {
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return div();
|
||||
};
|
||||
|
||||
@@ -554,7 +686,7 @@ impl Workspace {
|
||||
let state = device.read(cx).state();
|
||||
|
||||
this.min_w(px(260.))
|
||||
.item(PopupMenuItem::element(move |_window, _cx| {
|
||||
.item(PopupMenuItem::element(move |_window, cx| {
|
||||
h_flex()
|
||||
.px_1()
|
||||
.w_full()
|
||||
@@ -566,7 +698,7 @@ impl Workspace {
|
||||
.rounded_full()
|
||||
.when(state.set(), |this| this.bg(gpui::green()))
|
||||
.when(state.requesting(), |this| {
|
||||
this.bg(gpui::yellow())
|
||||
this.bg(cx.theme().icon_accent)
|
||||
}),
|
||||
)
|
||||
.child(SharedString::from(state.to_string()))
|
||||
@@ -584,37 +716,21 @@ impl Workspace {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.map(|this| match inbox_state {
|
||||
InboxState::Checking => this.child(div().child(
|
||||
SharedString::from("Fetching user's messaging relay list..."),
|
||||
)),
|
||||
InboxState::RelayNotAvailable => {
|
||||
this.child(div().text_color(cx.theme().warning_active).child(
|
||||
SharedString::from(
|
||||
"User hasn't configured a messaging relay list",
|
||||
),
|
||||
))
|
||||
}
|
||||
_ => this,
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("inbox")
|
||||
.icon(IconName::Inbox)
|
||||
.tooltip("Inbox")
|
||||
.small()
|
||||
.ghost()
|
||||
.when(inbox_state.subscribing(), |this| this.indicator())
|
||||
.loading(!inbox_connected)
|
||||
.disabled(!inbox_connected)
|
||||
.when(!inbox_connected, |this| {
|
||||
this.tooltip("Connecting to user's messaging relays...")
|
||||
})
|
||||
.when(inbox_connected, |this| this.indicator())
|
||||
.dropdown_menu(move |this, _window, cx| {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let profile = persons.read(cx).get(&pkey, cx);
|
||||
let profile = persons.read(cx).get(&public_key, cx);
|
||||
|
||||
let urls: Vec<SharedString> = profile
|
||||
.messaging_relays()
|
||||
.iter()
|
||||
@@ -632,9 +748,7 @@ impl Workspace {
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(
|
||||
div().size_1p5().rounded_full().bg(gpui::green()),
|
||||
)
|
||||
.child(div().size_1p5().rounded_full().bg(gpui::green()))
|
||||
.child(url.clone())
|
||||
}))
|
||||
});
|
||||
@@ -652,73 +766,32 @@ impl Workspace {
|
||||
Box::new(Command::ShowMessaging),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.map(|this| match nostr.read(cx).relay_list_state {
|
||||
RelayState::Checking => this
|
||||
.child(div().child(SharedString::from(
|
||||
"Fetching user's relay list...",
|
||||
))),
|
||||
RelayState::NotConfigured => {
|
||||
this.child(div().text_color(cx.theme().warning_active).child(
|
||||
SharedString::from("User hasn't configured a relay list"),
|
||||
))
|
||||
}
|
||||
_ => this,
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("relay-list")
|
||||
.icon(IconName::Relay)
|
||||
.tooltip("User's relay list")
|
||||
.small()
|
||||
.ghost()
|
||||
.when(nostr.read(cx).relay_list_state.configured(), |this| {
|
||||
this.indicator()
|
||||
.loading(!relay_connected)
|
||||
.disabled(!relay_connected)
|
||||
.when(!relay_connected, |this| {
|
||||
this.tooltip("Connecting to user's relay list...")
|
||||
})
|
||||
.dropdown_menu(move |this, _window, cx| {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let urls = nostr.read(cx).read_only_relays(&pkey, cx);
|
||||
|
||||
// Header
|
||||
let menu = this.min_w(px(260.)).label("Relays");
|
||||
|
||||
// Content
|
||||
let menu = urls.into_iter().fold(menu, |this, url| {
|
||||
this.item(PopupMenuItem::element(move |_window, _cx| {
|
||||
h_flex()
|
||||
.px_1()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(
|
||||
div().size_1p5().rounded_full().bg(gpui::green()),
|
||||
)
|
||||
.child(url.clone())
|
||||
}))
|
||||
});
|
||||
|
||||
// Footer
|
||||
menu.separator()
|
||||
.when(relay_connected, |this| this.indicator())
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
this.label("User's Relay List")
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"Reload",
|
||||
IconName::Refresh,
|
||||
Box::new(Command::RefreshRelayList),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Update relay list",
|
||||
"Update",
|
||||
IconName::Settings,
|
||||
Box::new(Command::ShowRelayList),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -729,8 +802,8 @@ impl Render for Workspace {
|
||||
let notification_layer = Root::render_notification_layer(window, cx);
|
||||
|
||||
// Titlebar elements
|
||||
let left = self.titlebar_left(window, cx).into_any_element();
|
||||
let right = self.titlebar_right(window, cx).into_any_element();
|
||||
let left = self.titlebar_left(cx).into_any_element();
|
||||
let right = self.titlebar_right(cx).into_any_element();
|
||||
|
||||
// Update title bar children
|
||||
self.titlebar.update(cx, |this, _cx| {
|
||||
|
||||
@@ -3,22 +3,19 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||
use gpui::{
|
||||
div, App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString,
|
||||
Styled, Subscription, Task, Window,
|
||||
App, AppContext, Context, Entity, EventEmitter, Global, IntoElement, ParentElement,
|
||||
SharedString, Styled, Task, Window, div, relative,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use state::{
|
||||
app_name, Announcement, DeviceState, NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT,
|
||||
};
|
||||
use state::{Announcement, DEVICE_GIFTWRAP, DeviceState, NostrRegistry, TIMEOUT, app_name};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::notification::Notification;
|
||||
use ui::{h_flex, v_flex, Disableable, IconName, Sizable, WindowExtension};
|
||||
use ui::{Disableable, IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||
|
||||
const IDENTIFIER: &str = "coop:device";
|
||||
const MSG: &str = "You've requested an encryption key from another device. \
|
||||
@@ -32,6 +29,15 @@ struct GlobalDeviceRegistry(Entity<DeviceRegistry>);
|
||||
|
||||
impl Global for GlobalDeviceRegistry {}
|
||||
|
||||
/// Device event.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum DeviceEvent {
|
||||
/// A new encryption signer has been set
|
||||
Set,
|
||||
/// An error occurred
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
/// Device Registry
|
||||
///
|
||||
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||
@@ -42,11 +48,10 @@ pub struct DeviceRegistry {
|
||||
|
||||
/// Async tasks
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
|
||||
/// Subscriptions
|
||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||
}
|
||||
|
||||
impl EventEmitter<DeviceEvent> for DeviceRegistry {}
|
||||
|
||||
impl DeviceRegistry {
|
||||
/// Retrieve the global device registry state
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
@@ -60,27 +65,16 @@ impl DeviceRegistry {
|
||||
|
||||
/// Create a new device registry instance
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let mut subscriptions = smallvec![];
|
||||
let state = DeviceState::default();
|
||||
|
||||
subscriptions.push(
|
||||
// Observe the NIP-65 state
|
||||
cx.observe(&nostr, |this, state, cx| {
|
||||
if state.read(cx).relay_list_state == RelayState::Configured {
|
||||
this.get_announcement(cx);
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// Run at the end of current cycle
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.handle_notifications(window, cx);
|
||||
this.get_announcement(cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
state: DeviceState::default(),
|
||||
state,
|
||||
tasks: vec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,9 +117,7 @@ impl DeviceRegistry {
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
self.tasks.push(
|
||||
// Update GPUI states
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
while let Ok(event) = rx.recv_async().await {
|
||||
match event.kind {
|
||||
// New request event
|
||||
@@ -145,8 +137,7 @@ impl DeviceRegistry {
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}),
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Get the device state
|
||||
@@ -191,45 +182,68 @@ impl DeviceRegistry {
|
||||
fn get_messages(&mut self, cx: &mut Context<Self>) {
|
||||
let task = self.subscribe_to_giftwrap_events(cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, _cx| {
|
||||
task.await?;
|
||||
|
||||
// Update state
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
if let Err(e) = task.await {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(DeviceEvent::Error(SharedString::from(e.to_string())));
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Continuously get gift wrap events for the current user in their messaging relays
|
||||
fn subscribe_to_giftwrap_events(&mut self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
|
||||
/// Get the messaging relays for the current user
|
||||
fn get_user_messaging_relays(&self, cx: &App) -> Task<Result<Vec<RelayUrl>, Error>> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return Task::ready(Err(anyhow!("User not found")));
|
||||
};
|
||||
cx.background_spawn(async move {
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let profile = persons.read(cx).get(&public_key, cx);
|
||||
let relay_urls = profile.messaging_relays().clone();
|
||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||
// Extract relay URLs from the event
|
||||
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
|
||||
|
||||
// Ensure all relays are connected
|
||||
for url in urls.iter() {
|
||||
client.add_relay(url).and_connect().await?;
|
||||
}
|
||||
|
||||
Ok(urls)
|
||||
} else {
|
||||
Err(anyhow!("Relays not found"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Continuously get gift wrap events for the current user in their messaging relays
|
||||
fn subscribe_to_giftwrap_events(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
let urls = self.get_user_messaging_relays(cx);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let urls = urls.await?;
|
||||
let encryption = signer.get_encryption_signer().await.context("not found")?;
|
||||
let public_key = encryption.get_public_key().await?;
|
||||
|
||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||
let id = SubscriptionId::new(DEVICE_GIFTWRAP);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<RelayUrl, Filter> = relay_urls
|
||||
let target: HashMap<RelayUrl, Filter> = urls
|
||||
.into_iter()
|
||||
.map(|relay| (relay, filter.clone()))
|
||||
.collect();
|
||||
|
||||
let output = client.subscribe(target).with_id(id).await?;
|
||||
|
||||
log::info!(
|
||||
"Successfully subscribed to encryption gift-wrap messages on: {:?}",
|
||||
output.success
|
||||
);
|
||||
// Subscribe
|
||||
client.subscribe(target).with_id(id).await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -239,20 +253,13 @@ impl DeviceRegistry {
|
||||
pub fn get_announcement(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Reset state before fetching announcement
|
||||
self.reset(cx);
|
||||
|
||||
// Get user's write relays
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
let task: Task<Result<Event, Error>> = cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
let signer = client.signer().context("Signer not found")?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
// Construct the filter for the device announcement event
|
||||
let filter = Filter::new()
|
||||
@@ -260,29 +267,19 @@ impl DeviceRegistry {
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&RelayUrl, Filter> =
|
||||
urls.iter().map(|relay| (relay, filter.clone())).collect();
|
||||
|
||||
// Stream events from user's write relays
|
||||
let mut stream = client
|
||||
.stream_events(target)
|
||||
.stream_events(filter)
|
||||
.timeout(Duration::from_secs(TIMEOUT))
|
||||
.await?;
|
||||
|
||||
while let Some((_url, res)) = stream.next().await {
|
||||
match res {
|
||||
Ok(event) => {
|
||||
log::info!("Received device announcement event: {event:?}");
|
||||
if let Ok(event) = res {
|
||||
return Ok(event);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to receive device announcement event: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("Device announcement not found"))
|
||||
Err(anyhow!("Announcement not found"))
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
@@ -307,22 +304,12 @@ impl DeviceRegistry {
|
||||
pub fn create_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return Task::ready(Err(anyhow!("User not found")));
|
||||
};
|
||||
|
||||
// Get user's write relays
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
let keys = Keys::generate();
|
||||
let secret = keys.secret_key().to_secret_hex();
|
||||
let n = keys.public_key();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct an announcement event
|
||||
let event = client
|
||||
.sign_event_builder(EventBuilder::new(Kind::Custom(10044), "").tags(vec![
|
||||
@@ -332,7 +319,7 @@ impl DeviceRegistry {
|
||||
.await?;
|
||||
|
||||
// Publish announcement
|
||||
client.send_event(&event).to(urls).await?;
|
||||
client.send_event(&event).to_nip65().await?;
|
||||
|
||||
// Save device keys to the database
|
||||
set_keys(&client, &secret).await?;
|
||||
@@ -409,23 +396,15 @@ impl DeviceRegistry {
|
||||
return;
|
||||
};
|
||||
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct a filter for device key requests
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(4454))
|
||||
.author(public_key)
|
||||
.since(Timestamp::now());
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&RelayUrl, Filter> =
|
||||
urls.iter().map(|relay| (relay, filter.clone())).collect();
|
||||
|
||||
// Subscribe to the device key requests on user's write relays
|
||||
client.subscribe(target).await?;
|
||||
client.subscribe(filter).await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -443,23 +422,15 @@ impl DeviceRegistry {
|
||||
return;
|
||||
};
|
||||
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct a filter for device key requests
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(4455))
|
||||
.author(public_key)
|
||||
.since(Timestamp::now());
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&RelayUrl, Filter> =
|
||||
urls.iter().map(|relay| (relay, filter.clone())).collect();
|
||||
|
||||
// Subscribe to the device key requests on user's write relays
|
||||
client.subscribe(target).await?;
|
||||
client.subscribe(filter).await?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
@@ -471,13 +442,7 @@ impl DeviceRegistry {
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
|
||||
let app_keys = nostr.read(cx).app_keys.clone();
|
||||
let app_keys = nostr.read(cx).keys();
|
||||
let app_pubkey = app_keys.public_key();
|
||||
|
||||
let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move {
|
||||
@@ -507,8 +472,6 @@ impl DeviceRegistry {
|
||||
Ok(Some(keys))
|
||||
}
|
||||
None => {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Construct an event for device key request
|
||||
let event = client
|
||||
.sign_event_builder(EventBuilder::new(Kind::Custom(4454), "").tags(vec![
|
||||
@@ -518,7 +481,7 @@ impl DeviceRegistry {
|
||||
.await?;
|
||||
|
||||
// Send the event to write relays
|
||||
client.send_event(&event).to(urls).await?;
|
||||
client.send_event(&event).to_nip65().await?;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
@@ -549,7 +512,7 @@ impl DeviceRegistry {
|
||||
/// Parse the response event for device keys from other devices
|
||||
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let app_keys = nostr.read(cx).app_keys.clone();
|
||||
let app_keys = nostr.read(cx).keys();
|
||||
|
||||
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
|
||||
let root_device = event
|
||||
@@ -586,18 +549,11 @@ impl DeviceRegistry {
|
||||
let client = nostr.read(cx).client();
|
||||
let signer = nostr.read(cx).signer();
|
||||
|
||||
let Some(public_key) = signer.public_key() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Get user's write relays
|
||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||
let event = event.clone();
|
||||
let id: SharedString = event.id.to_hex().into();
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let urls = write_relays.await;
|
||||
|
||||
// Get device keys
|
||||
let keys = get_keys(&client).await?;
|
||||
let secret = keys.secret_key().to_secret_hex();
|
||||
@@ -626,7 +582,7 @@ impl DeviceRegistry {
|
||||
let event = client.sign_event_builder(builder).await?;
|
||||
|
||||
// Send the response event to the user's relay list
|
||||
client.send_event(&event).to(urls).await?;
|
||||
client.send_event(&event).to_nip65().await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -635,13 +591,16 @@ impl DeviceRegistry {
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
cx.update(|window, cx| {
|
||||
window.clear_notification(id, cx);
|
||||
window.clear_notification_by_id::<DeviceNotification>(id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -671,17 +630,23 @@ impl DeviceRegistry {
|
||||
|
||||
let entity = cx.entity().downgrade();
|
||||
let loading = Rc::new(Cell::new(false));
|
||||
let key = SharedString::from(event.id.to_hex());
|
||||
|
||||
Notification::new()
|
||||
.custom_id(SharedString::from(event.id.to_hex()))
|
||||
.type_id::<DeviceNotification>(key)
|
||||
.autohide(false)
|
||||
.icon(IconName::UserKey)
|
||||
.title(SharedString::from("New request"))
|
||||
.content(move |_window, cx| {
|
||||
.content(move |_this, _window, cx| {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(SharedString::from(MSG))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.line_height(relative(1.25))
|
||||
.child(SharedString::from(MSG)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
@@ -733,7 +698,7 @@ impl DeviceRegistry {
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.action(move |_window, _cx| {
|
||||
.action(move |_this, _window, _cx| {
|
||||
let view = entity.clone();
|
||||
let event = event.clone();
|
||||
|
||||
@@ -759,13 +724,15 @@ impl DeviceRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
struct DeviceNotification;
|
||||
|
||||
/// Verify the author of an event
|
||||
async fn verify_author(client: &Client, event: &Event) -> bool {
|
||||
if let Some(signer) = client.signer() {
|
||||
if let Ok(public_key) = signer.get_public_key().await {
|
||||
if let Some(signer) = client.signer()
|
||||
&& let Ok(public_key) = signer.get_public_key().await
|
||||
{
|
||||
return public_key == event.pubkey;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
@@ -15,3 +15,4 @@ smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
flume.workspace = true
|
||||
log.workspace = true
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
@@ -3,19 +3,19 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use anyhow::{Error, anyhow};
|
||||
use common::EventUtils;
|
||||
use gpui::{App, AppContext, Context, Entity, Global, Task};
|
||||
use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT};
|
||||
|
||||
mod person;
|
||||
|
||||
pub use person::*;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
|
||||
pub fn init(window: &mut Window, cx: &mut App) {
|
||||
PersonRegistry::set_global(cx.new(|cx| PersonRegistry::new(window, cx)), cx);
|
||||
}
|
||||
|
||||
struct GlobalPersonRegistry(Entity<PersonRegistry>);
|
||||
@@ -36,13 +36,13 @@ pub struct PersonRegistry {
|
||||
persons: HashMap<PublicKey, Entity<Person>>,
|
||||
|
||||
/// Set of public keys that have been seen
|
||||
seen: Rc<RefCell<HashSet<PublicKey>>>,
|
||||
seens: Rc<RefCell<HashSet<PublicKey>>>,
|
||||
|
||||
/// Sender for requesting metadata
|
||||
sender: flume::Sender<PublicKey>,
|
||||
|
||||
/// Tasks for asynchronous operations
|
||||
_tasks: SmallVec<[Task<()>; 4]>,
|
||||
tasks: SmallVec<[Task<()>; 4]>,
|
||||
}
|
||||
|
||||
impl PersonRegistry {
|
||||
@@ -57,13 +57,13 @@ impl PersonRegistry {
|
||||
}
|
||||
|
||||
/// Create a new person registry instance
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
// Channel for communication between nostr and gpui
|
||||
let (tx, rx) = flume::bounded::<Dispatch>(100);
|
||||
let (mta_tx, mta_rx) = flume::bounded::<PublicKey>(100);
|
||||
let (mta_tx, mta_rx) = flume::unbounded::<PublicKey>();
|
||||
|
||||
let mut tasks = smallvec![];
|
||||
|
||||
@@ -111,33 +111,16 @@ impl PersonRegistry {
|
||||
}),
|
||||
);
|
||||
|
||||
tasks.push(
|
||||
// Load all user profiles from the database
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = cx
|
||||
.background_executor()
|
||||
.await_on_background(async move { load_persons(&client).await })
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(persons) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.bulk_inserts(persons, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to load all persons from the database: {e}");
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
cx.defer_in(window, |this, _window, cx| {
|
||||
this.load(cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
persons: HashMap::new(),
|
||||
seen: Rc::new(RefCell::new(HashSet::new())),
|
||||
seens: Rc::new(RefCell::new(HashSet::new())),
|
||||
sender: mta_tx,
|
||||
_tasks: tasks,
|
||||
tasks,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,25 +146,21 @@ impl PersonRegistry {
|
||||
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
||||
let person = Person::new(event.pubkey, metadata);
|
||||
let val = Box::new(person);
|
||||
|
||||
// Send
|
||||
tx.send_async(Dispatch::Person(val)).await.ok();
|
||||
}
|
||||
Kind::ContactList => {
|
||||
let public_keys = event.extract_public_keys();
|
||||
|
||||
// Get metadata for all public keys
|
||||
get_metadata(client, public_keys).await.ok();
|
||||
}
|
||||
Kind::InboxRelays => {
|
||||
let val = Box::new(event.into_owned());
|
||||
|
||||
// Send
|
||||
tx.send_async(Dispatch::Relays(val)).await.ok();
|
||||
}
|
||||
Kind::Custom(10044) => {
|
||||
let val = Box::new(event.into_owned());
|
||||
|
||||
// Send
|
||||
tx.send_async(Dispatch::Announcement(val)).await.ok();
|
||||
}
|
||||
@@ -198,7 +177,7 @@ impl PersonRegistry {
|
||||
loop {
|
||||
match flume::Selector::new()
|
||||
.recv(rx, |result| result.ok())
|
||||
.wait_timeout(Duration::from_secs(2))
|
||||
.wait_timeout(Duration::from_secs(TIMEOUT))
|
||||
{
|
||||
Ok(Some(public_key)) => {
|
||||
batch.insert(public_key);
|
||||
@@ -208,40 +187,81 @@ impl PersonRegistry {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if !batch.is_empty() {
|
||||
get_metadata(client, std::mem::take(&mut batch)).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all user profiles from the database
|
||||
fn load(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
let task: Task<Result<Vec<Person>, Error>> = cx.background_spawn(async move {
|
||||
let filter = Filter::new().kind(Kind::Metadata).limit(200);
|
||||
let events = client.database().query(filter).await?;
|
||||
let persons = events
|
||||
.into_iter()
|
||||
.map(|event| {
|
||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||
Person::new(event.pubkey, metadata)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(persons)
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
if let Ok(persons) = task.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.bulk_inserts(persons, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Set profile encryption keys announcement
|
||||
fn set_announcement(&mut self, event: &Event, cx: &mut App) {
|
||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||
let announcement = Announcement::from(event);
|
||||
|
||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||
person.update(cx, |person, cx| {
|
||||
person.set_announcement(announcement);
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
let person =
|
||||
Person::new(event.pubkey, Metadata::default()).with_announcement(announcement);
|
||||
self.insert(person, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set messaging relays for a person
|
||||
fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) {
|
||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||
let urls: Vec<RelayUrl> = nip17::extract_relay_list(event).cloned().collect();
|
||||
|
||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||
person.update(cx, |person, cx| {
|
||||
person.set_messaging_relays(urls);
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
let person = Person::new(event.pubkey, Metadata::default()).with_messaging_relays(urls);
|
||||
self.insert(person, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert batch of persons
|
||||
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
|
||||
for person in persons.into_iter() {
|
||||
self.persons.insert(person.public_key(), cx.new(|_| person));
|
||||
let public_key = person.public_key();
|
||||
self.persons
|
||||
.entry(public_key)
|
||||
.or_insert_with(|| cx.new(|_| person));
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -270,7 +290,7 @@ impl PersonRegistry {
|
||||
}
|
||||
|
||||
let public_key = *public_key;
|
||||
let mut seen = self.seen.borrow_mut();
|
||||
let mut seen = self.seens.borrow_mut();
|
||||
|
||||
if seen.insert(public_key) {
|
||||
let sender = self.sender.clone();
|
||||
@@ -322,19 +342,3 @@ where
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load all user profiles from the database
|
||||
async fn load_persons(client: &Client) -> Result<Vec<Person>, Error> {
|
||||
let filter = Filter::new().kind(Kind::Metadata).limit(200);
|
||||
let events = client.database().query(filter).await?;
|
||||
|
||||
let mut persons = vec![];
|
||||
|
||||
for event in events.into_iter() {
|
||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||
let person = Person::new(event.pubkey, metadata);
|
||||
persons.push(person);
|
||||
}
|
||||
|
||||
Ok(persons)
|
||||
}
|
||||
|
||||
@@ -65,6 +65,21 @@ impl Person {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build profile encryption keys announcement
|
||||
pub fn with_announcement(mut self, announcement: Announcement) -> Self {
|
||||
self.announcement = Some(announcement);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build profile messaging relays
|
||||
pub fn with_messaging_relays<I>(mut self, relays: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = RelayUrl>,
|
||||
{
|
||||
self.messaging_relays = relays.into_iter().collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Get profile public key
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
self.public_key
|
||||
@@ -75,21 +90,11 @@ impl Person {
|
||||
self.metadata.clone()
|
||||
}
|
||||
|
||||
/// Set profile metadata
|
||||
pub fn set_metadata(&mut self, metadata: Metadata) {
|
||||
self.metadata = metadata;
|
||||
}
|
||||
|
||||
/// Get profile encryption keys announcement
|
||||
pub fn announcement(&self) -> Option<Announcement> {
|
||||
self.announcement.clone()
|
||||
}
|
||||
|
||||
/// Set profile encryption keys announcement
|
||||
pub fn set_announcement(&mut self, announcement: Announcement) {
|
||||
self.announcement = Some(announcement);
|
||||
}
|
||||
|
||||
/// Get profile messaging relays
|
||||
pub fn messaging_relays(&self) -> &Vec<RelayUrl> {
|
||||
&self.messaging_relays
|
||||
@@ -100,14 +105,6 @@ impl Person {
|
||||
self.messaging_relays.first().cloned()
|
||||
}
|
||||
|
||||
/// Set profile messaging relays
|
||||
pub fn set_messaging_relays<I>(&mut self, relays: I)
|
||||
where
|
||||
I: IntoIterator<Item = RelayUrl>,
|
||||
{
|
||||
self.messaging_relays = relays.into_iter().collect();
|
||||
}
|
||||
|
||||
/// Get profile avatar
|
||||
pub fn avatar(&self) -> SharedString {
|
||||
self.metadata()
|
||||
@@ -115,8 +112,9 @@ impl Person {
|
||||
.as_ref()
|
||||
.filter(|picture| !picture.is_empty())
|
||||
.map(|picture| {
|
||||
let encoded_picture = urlencoding::encode(picture);
|
||||
let url = format!(
|
||||
"{IMAGE_RESIZER}/?url={picture}&w=100&h=100&fit=cover&mask=circle&n=-1"
|
||||
"{IMAGE_RESIZER}/?url={encoded_picture}&w=100&h=100&fit=cover&mask=circle&n=-1"
|
||||
);
|
||||
url.into()
|
||||
})
|
||||
@@ -125,20 +123,38 @@ impl Person {
|
||||
|
||||
/// Get profile name
|
||||
pub fn name(&self) -> SharedString {
|
||||
if let Some(display_name) = self.metadata().display_name.as_ref() {
|
||||
if !display_name.is_empty() {
|
||||
if let Some(display_name) = self.metadata().display_name.as_ref()
|
||||
&& !display_name.is_empty()
|
||||
{
|
||||
return SharedString::from(display_name);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = self.metadata().name.as_ref() {
|
||||
if !name.is_empty() {
|
||||
if let Some(name) = self.metadata().name.as_ref()
|
||||
&& !name.is_empty()
|
||||
{
|
||||
return SharedString::from(name);
|
||||
}
|
||||
}
|
||||
|
||||
SharedString::from(shorten_pubkey(self.public_key(), 4))
|
||||
}
|
||||
|
||||
/// Set profile metadata
|
||||
pub fn set_metadata(&mut self, metadata: Metadata) {
|
||||
self.metadata = metadata;
|
||||
}
|
||||
|
||||
/// Set profile encryption keys announcement
|
||||
pub fn set_announcement(&mut self, announcement: Announcement) {
|
||||
self.announcement = Some(announcement);
|
||||
}
|
||||
|
||||
/// Set profile messaging relays
|
||||
pub fn set_messaging_relays<I>(&mut self, relays: I)
|
||||
where
|
||||
I: IntoIterator<Item = RelayUrl>,
|
||||
{
|
||||
self.messaging_relays = relays.into_iter().collect();
|
||||
}
|
||||
}
|
||||
|
||||
/// Shorten a [`PublicKey`] to a string with the first and last `len` characters
|
||||
@@ -148,7 +164,7 @@ pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String {
|
||||
let Ok(pubkey) = public_key.to_bech32();
|
||||
|
||||
format!(
|
||||
"{}:{}",
|
||||
"{}...{}",
|
||||
&pubkey[0..(len + 1)],
|
||||
&pubkey[pubkey.len() - len..]
|
||||
)
|
||||
|
||||
@@ -5,19 +5,19 @@ use std::hash::Hash;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
|
||||
Task, Window,
|
||||
Task, Window, div, relative,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::{AppSettings, AuthMode};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::notification::Notification;
|
||||
use ui::{v_flex, Disableable, IconName, Sizable, WindowExtension};
|
||||
use ui::{Disableable, IconName, Sizable, WindowExtension, v_flex};
|
||||
|
||||
const AUTH_MESSAGE: &str =
|
||||
"Approve the authentication request to allow Coop to continue sending or receiving events.";
|
||||
@@ -34,7 +34,10 @@ struct AuthRequest {
|
||||
}
|
||||
|
||||
impl AuthRequest {
|
||||
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self {
|
||||
pub fn new<S>(challenge: S, url: RelayUrl) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
Self {
|
||||
challenge: challenge.into(),
|
||||
url,
|
||||
@@ -106,22 +109,6 @@ impl RelayAuth {
|
||||
tx.send_async(signal).await.ok();
|
||||
}
|
||||
}
|
||||
RelayMessage::Closed {
|
||||
subscription_id,
|
||||
message,
|
||||
} => {
|
||||
let msg = MachineReadablePrefix::parse(&message);
|
||||
|
||||
if let Some(MachineReadablePrefix::AuthRequired) = msg {
|
||||
if let Ok(Some(relay)) = client.relay(&relay_url).await {
|
||||
// Send close message to relay
|
||||
relay
|
||||
.send_msg(ClientMessage::Close(subscription_id))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
RelayMessage::Ok {
|
||||
event_id, message, ..
|
||||
} => {
|
||||
@@ -273,7 +260,7 @@ impl RelayAuth {
|
||||
fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) {
|
||||
let settings = AppSettings::global(cx);
|
||||
let req = req.clone();
|
||||
let challenge = req.challenge().to_string();
|
||||
let challenge = SharedString::from(req.challenge().to_string());
|
||||
|
||||
// Create a task for authentication
|
||||
let task = self.auth(&req, cx);
|
||||
@@ -283,7 +270,7 @@ impl RelayAuth {
|
||||
let url = req.url();
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
window.clear_notification(challenge, cx);
|
||||
window.clear_notification_by_id::<AuthNotification>(challenge, cx);
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
@@ -295,10 +282,19 @@ impl RelayAuth {
|
||||
this.add_trusted_relay(url, cx);
|
||||
});
|
||||
|
||||
window.push_notification(format!("{} has been authenticated", url), cx);
|
||||
window.push_notification(
|
||||
Notification::success(format!(
|
||||
"Relay {} has been authenticated",
|
||||
url.domain().unwrap_or_default()
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -323,20 +319,25 @@ impl RelayAuth {
|
||||
/// Build a notification for the authentication request.
|
||||
fn notification(&self, req: &Arc<AuthRequest>, cx: &Context<Self>) -> Notification {
|
||||
let req = req.clone();
|
||||
let challenge = SharedString::from(req.challenge.clone());
|
||||
let url = SharedString::from(req.url().to_string());
|
||||
let entity = cx.entity().downgrade();
|
||||
let loading = Rc::new(Cell::new(false));
|
||||
|
||||
Notification::new()
|
||||
.custom_id(SharedString::from(&req.challenge))
|
||||
.type_id::<AuthNotification>(challenge)
|
||||
.autohide(false)
|
||||
.icon(IconName::Info)
|
||||
.icon(IconName::Warning)
|
||||
.title(SharedString::from("Authentication Required"))
|
||||
.content(move |_window, cx| {
|
||||
.content(move |_this, _window, cx| {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.child(SharedString::from(AUTH_MESSAGE))
|
||||
.line_height(relative(1.25))
|
||||
.child(SharedString::from(AUTH_MESSAGE)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.py_1()
|
||||
@@ -349,7 +350,7 @@ impl RelayAuth {
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.action(move |_window, _cx| {
|
||||
.action(move |_this, _window, _cx| {
|
||||
let view = entity.clone();
|
||||
let req = req.clone();
|
||||
|
||||
@@ -374,3 +375,5 @@ impl RelayAuth {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthNotification;
|
||||
|
||||
@@ -10,6 +10,8 @@ common = { path = "../common" }
|
||||
nostr.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
nostr-lmdb.workspace = true
|
||||
nostr-memory.workspace = true
|
||||
nostr-gossip-sqlite.workspace = true
|
||||
nostr-connect.workspace = true
|
||||
nostr-blossom.workspace = true
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
|
||||
pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
|
||||
|
||||
/// Default bootstrap relays
|
||||
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
||||
pub const BOOTSTRAP_RELAYS: [&str; 4] = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://relay.primal.net",
|
||||
"wss://indexer.coracle.social",
|
||||
"wss://user.kindpag.es",
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use gpui::SharedString;
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
/// Gossip
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Gossip {
|
||||
relays: HashMap<PublicKey, HashSet<(RelayUrl, Option<RelayMetadata>)>>,
|
||||
}
|
||||
|
||||
impl Gossip {
|
||||
pub fn read_only_relays(&self, public_key: &PublicKey) -> Vec<SharedString> {
|
||||
self.relays
|
||||
.get(public_key)
|
||||
.map(|relays| {
|
||||
relays
|
||||
.iter()
|
||||
.map(|(url, _)| url.to_string().into())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get read relays for a given public key
|
||||
pub fn read_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
|
||||
self.relays
|
||||
.get(public_key)
|
||||
.map(|relays| {
|
||||
relays
|
||||
.iter()
|
||||
.filter_map(|(url, metadata)| {
|
||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Read) {
|
||||
Some(url.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get write relays for a given public key
|
||||
pub fn write_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
|
||||
self.relays
|
||||
.get(public_key)
|
||||
.map(|relays| {
|
||||
relays
|
||||
.iter()
|
||||
.filter_map(|(url, metadata)| {
|
||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
|
||||
Some(url.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Insert gossip relays for a public key
|
||||
pub fn insert_relays(&mut self, event: &Event) {
|
||||
self.relays.entry(event.pubkey).or_default().extend(
|
||||
event
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|tag| {
|
||||
if let Some(TagStandard::RelayMetadata {
|
||||
relay_url,
|
||||
metadata,
|
||||
}) = tag.clone().to_standardized()
|
||||
{
|
||||
Some((relay_url, metadata))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.take(3),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -6,20 +6,20 @@ use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||
use common::config_dir;
|
||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window};
|
||||
use nostr_connect::prelude::*;
|
||||
use nostr_gossip_sqlite::prelude::*;
|
||||
use nostr_lmdb::prelude::*;
|
||||
use nostr_memory::prelude::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
mod blossom;
|
||||
mod constants;
|
||||
mod device;
|
||||
mod gossip;
|
||||
mod nip05;
|
||||
mod signer;
|
||||
|
||||
pub use blossom::*;
|
||||
pub use constants::*;
|
||||
pub use device::*;
|
||||
pub use gossip::*;
|
||||
pub use nip05::*;
|
||||
pub use signer::*;
|
||||
|
||||
@@ -41,6 +41,23 @@ struct GlobalNostrRegistry(Entity<NostrRegistry>);
|
||||
|
||||
impl Global for GlobalNostrRegistry {}
|
||||
|
||||
/// Signer event.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum StateEvent {
|
||||
/// Connecting to the bootstrapping relay
|
||||
Connecting,
|
||||
/// Connected to the bootstrapping relay
|
||||
Connected,
|
||||
/// User has not set up NIP-65 relays
|
||||
RelayNotConfigured,
|
||||
/// Connected to NIP-65 relays
|
||||
RelayConnected,
|
||||
/// A new signer has been set
|
||||
SignerSet,
|
||||
/// An error occurred
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
/// Nostr Registry
|
||||
#[derive(Debug)]
|
||||
pub struct NostrRegistry {
|
||||
@@ -53,21 +70,17 @@ pub struct NostrRegistry {
|
||||
/// Local public keys
|
||||
npubs: Entity<Vec<PublicKey>>,
|
||||
|
||||
/// Custom gossip implementation
|
||||
gossip: Entity<Gossip>,
|
||||
|
||||
/// App keys
|
||||
///
|
||||
/// Used for Nostr Connect and NIP-4e operations
|
||||
pub app_keys: Keys,
|
||||
|
||||
/// Relay list state
|
||||
pub relay_list_state: RelayState,
|
||||
app_keys: Keys,
|
||||
|
||||
/// Tasks for asynchronous operations
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
tasks: Vec<Task<()>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<StateEvent> for NostrRegistry {}
|
||||
|
||||
impl NostrRegistry {
|
||||
/// Retrieve the global nostr state
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
@@ -88,32 +101,43 @@ impl NostrRegistry {
|
||||
// Construct the nostr npubs entity
|
||||
let npubs = cx.new(|_| vec![]);
|
||||
|
||||
// Construct the gossip entity
|
||||
let gossip = cx.new(|_| Gossip::default());
|
||||
// Construct the nostr gossip instance
|
||||
let gossip = cx.foreground_executor().block_on(async move {
|
||||
NostrGossipSqlite::open(config_dir().join("gossip"))
|
||||
.await
|
||||
.expect("Failed to initialize gossip instance")
|
||||
});
|
||||
|
||||
// Construct the nostr client builder
|
||||
let mut builder = ClientBuilder::default()
|
||||
.signer(signer.clone())
|
||||
.gossip(gossip)
|
||||
.automatic_authentication(false)
|
||||
.verify_subscriptions(false)
|
||||
.connect_timeout(Duration::from_secs(TIMEOUT))
|
||||
.sleep_when_idle(SleepWhenIdle::Enabled {
|
||||
timeout: Duration::from_secs(600),
|
||||
});
|
||||
|
||||
// Add database if not in debug mode
|
||||
if !cfg!(debug_assertions) {
|
||||
// Construct the nostr lmdb instance
|
||||
let lmdb = cx.foreground_executor().block_on(async move {
|
||||
NostrLmdb::open(config_dir().join("nostr"))
|
||||
.await
|
||||
.expect("Failed to initialize database")
|
||||
});
|
||||
builder = builder.database(lmdb);
|
||||
} else {
|
||||
builder = builder.database(MemoryDatabase::unbounded())
|
||||
}
|
||||
|
||||
// Construct the nostr client
|
||||
let client = ClientBuilder::default()
|
||||
.signer(signer.clone())
|
||||
.database(lmdb)
|
||||
.automatic_authentication(false)
|
||||
.verify_subscriptions(false)
|
||||
.connect_timeout(Duration::from_secs(TIMEOUT))
|
||||
.sleep_when_idle(SleepWhenIdle::Enabled {
|
||||
timeout: Duration::from_secs(600),
|
||||
})
|
||||
.build();
|
||||
// Build the nostr client
|
||||
let client = builder.build();
|
||||
|
||||
// Run at the end of current cycle
|
||||
cx.defer_in(window, |this, _window, cx| {
|
||||
this.connect(cx);
|
||||
this.handle_notifications(cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -121,8 +145,6 @@ impl NostrRegistry {
|
||||
signer,
|
||||
npubs,
|
||||
app_keys,
|
||||
gossip,
|
||||
relay_list_state: RelayState::Idle,
|
||||
tasks: vec![],
|
||||
}
|
||||
}
|
||||
@@ -142,94 +164,57 @@ impl NostrRegistry {
|
||||
self.npubs.clone()
|
||||
}
|
||||
|
||||
/// Get the app keys
|
||||
pub fn keys(&self) -> Keys {
|
||||
self.app_keys.clone()
|
||||
}
|
||||
|
||||
/// Connect to the bootstrapping relays
|
||||
fn connect(&mut self, cx: &mut Context<Self>) {
|
||||
let client = self.client();
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.await_on_background(async move {
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
// Add search relay to the relay pool
|
||||
for url in SEARCH_RELAYS.into_iter() {
|
||||
client.add_relay(url).await.ok();
|
||||
client.add_relay(url).await?;
|
||||
}
|
||||
|
||||
// Add bootstrap relay to the relay pool
|
||||
for url in BOOTSTRAP_RELAYS.into_iter() {
|
||||
client.add_relay(url).await.ok();
|
||||
client.add_relay(url).await?;
|
||||
}
|
||||
|
||||
// Connect to all added relays
|
||||
client.connect().and_wait(Duration::from_secs(2)).await;
|
||||
})
|
||||
.await;
|
||||
client.connect().await;
|
||||
|
||||
// Update the state
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Emit connecting event
|
||||
cx.emit(StateEvent::Connecting);
|
||||
|
||||
self.tasks
|
||||
.push(cx.spawn(async move |this, cx| match task.await {
|
||||
Ok(_) => {
|
||||
this.update(cx, |this, cx| {
|
||||
cx.emit(StateEvent::Connected);
|
||||
this.get_npubs(cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// Handle nostr notifications
|
||||
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
|
||||
let client = self.client();
|
||||
let gossip = self.gossip.downgrade();
|
||||
|
||||
// Channel for communication between nostr and gpui
|
||||
let (tx, rx) = flume::bounded::<Event>(2048);
|
||||
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
// Handle nostr notifications
|
||||
let mut notifications = client.notifications();
|
||||
let mut processed_events = HashSet::new();
|
||||
|
||||
while let Some(notification) = notifications.next().await {
|
||||
if let ClientNotification::Message {
|
||||
message:
|
||||
RelayMessage::Event {
|
||||
event,
|
||||
subscription_id,
|
||||
},
|
||||
..
|
||||
} = notification
|
||||
{
|
||||
if !processed_events.insert(event.id) {
|
||||
// Skip if the event has already been processed
|
||||
continue;
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
if let Kind::RelayList = event.kind {
|
||||
if subscription_id.as_str().contains("room-") {
|
||||
get_events_for_room(&client, &event).await.ok();
|
||||
}
|
||||
tx.send_async(event.into_owned()).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, cx| {
|
||||
while let Ok(event) = rx.recv_async().await {
|
||||
if let Kind::RelayList = event.kind {
|
||||
gossip.update(cx, |this, cx| {
|
||||
this.insert_relays(&event);
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Get all used npubs
|
||||
fn get_npubs(&mut self, cx: &mut Context<Self>) {
|
||||
let npubs = self.npubs.downgrade();
|
||||
|
||||
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
|
||||
let dir = config_dir().join("keys");
|
||||
// Ensure keys directory exists
|
||||
@@ -269,25 +254,26 @@ impl NostrRegistry {
|
||||
true => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.create_identity(cx);
|
||||
})?;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
false => {
|
||||
// TODO: auto login
|
||||
npubs.update(cx, |this, cx| {
|
||||
npubs
|
||||
.update(cx, |this, cx| {
|
||||
this.extend(public_keys);
|
||||
cx.notify();
|
||||
})?;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Failed to get npubs: {e}");
|
||||
this.update(cx, |this, cx| {
|
||||
this.create_identity(cx);
|
||||
})?;
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -307,74 +293,49 @@ impl NostrRegistry {
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let signer = async_keys.into_nostr_signer();
|
||||
|
||||
// Get default relay list
|
||||
// Construct relay list event
|
||||
let relay_list = default_relay_list();
|
||||
|
||||
// Extract write relays
|
||||
let write_urls: Vec<RelayUrl> = relay_list
|
||||
.iter()
|
||||
.filter_map(|(url, metadata)| {
|
||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Ensure connected to all relays
|
||||
for (url, _metadata) in relay_list.iter() {
|
||||
client.add_relay(url).and_connect().await?;
|
||||
}
|
||||
|
||||
// Publish relay list event
|
||||
let event = EventBuilder::relay_list(relay_list).sign(&signer).await?;
|
||||
let output = client
|
||||
|
||||
// Publish relay list
|
||||
client
|
||||
.send_event(&event)
|
||||
.to(BOOTSTRAP_RELAYS)
|
||||
.ok_timeout(Duration::from_secs(TIMEOUT))
|
||||
.await?;
|
||||
|
||||
log::info!("Sent gossip relay list: {output:?}");
|
||||
|
||||
// Construct the default metadata
|
||||
let name = petname::petname(2, "-").unwrap_or("Cooper".to_string());
|
||||
let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap();
|
||||
let metadata = Metadata::new().display_name(&name).picture(avatar);
|
||||
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
|
||||
|
||||
// Publish metadata event
|
||||
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
|
||||
client
|
||||
.send_event(&event)
|
||||
.to(&write_urls)
|
||||
.to_nip65()
|
||||
.ack_policy(AckPolicy::none())
|
||||
.await?;
|
||||
|
||||
// Construct the default contact list
|
||||
let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())];
|
||||
let event = EventBuilder::contact_list(contacts).sign(&signer).await?;
|
||||
|
||||
// Publish contact list event
|
||||
let event = EventBuilder::contact_list(contacts).sign(&signer).await?;
|
||||
client
|
||||
.send_event(&event)
|
||||
.to(&write_urls)
|
||||
.to_nip65()
|
||||
.ack_policy(AckPolicy::none())
|
||||
.await?;
|
||||
|
||||
// Construct the default messaging relay list
|
||||
let relays = default_messaging_relays();
|
||||
|
||||
// Ensure connected to all relays
|
||||
for url in relays.iter() {
|
||||
client.add_relay(url).and_connect().await?;
|
||||
}
|
||||
let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?;
|
||||
|
||||
// Publish messaging relay list event
|
||||
let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?;
|
||||
client
|
||||
.send_event(&event)
|
||||
.to(&write_urls)
|
||||
.to_nip65()
|
||||
.ack_policy(AckPolicy::none())
|
||||
.await?;
|
||||
|
||||
@@ -385,15 +346,20 @@ impl NostrRegistry {
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
// Wait for the task to complete
|
||||
task.await?;
|
||||
|
||||
// Set signer
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_signer(keys, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -472,6 +438,7 @@ impl NostrRegistry {
|
||||
Ok(public_key) => {
|
||||
// Update states
|
||||
this.update(cx, |this, cx| {
|
||||
this.ensure_relay_list(&public_key, cx);
|
||||
// Add public key to npubs if not already present
|
||||
this.npubs.update(cx, |this, cx| {
|
||||
if !this.contains(&public_key) {
|
||||
@@ -479,22 +446,18 @@ impl NostrRegistry {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure relay list for the user
|
||||
this.ensure_relay_list(cx);
|
||||
|
||||
// Emit signer changed event
|
||||
cx.emit(SignerEvent::Set);
|
||||
})?;
|
||||
cx.emit(StateEvent::SignerSet);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(SignerEvent::Error(e.to_string()));
|
||||
})?;
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -506,16 +469,15 @@ impl NostrRegistry {
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let key_path = keys_dir.join(format!("{}.npub", npub));
|
||||
smol::fs::remove_file(key_path).await?;
|
||||
smol::fs::remove_file(key_path).await.ok();
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.npubs().update(cx, |this, cx| {
|
||||
this.retain(|k| k != &public_key);
|
||||
cx.notify();
|
||||
});
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -533,16 +495,16 @@ impl NostrRegistry {
|
||||
Ok(_) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_signer(keys, cx);
|
||||
})?;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(SignerEvent::Error(e.to_string()));
|
||||
})?;
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -564,192 +526,90 @@ impl NostrRegistry {
|
||||
match task.await {
|
||||
Ok((public_key, uri)) => {
|
||||
let username = public_key.to_bech32().unwrap();
|
||||
let write_credential = this.read_with(cx, |_this, cx| {
|
||||
cx.write_credentials(&username, "nostrconnect", uri.to_string().as_bytes())
|
||||
})?;
|
||||
let write_credential = this
|
||||
.read_with(cx, |_this, cx| {
|
||||
cx.write_credentials(
|
||||
&username,
|
||||
"nostrconnect",
|
||||
uri.to_string().as_bytes(),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
match write_credential.await {
|
||||
Ok(_) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_signer(nip46, cx);
|
||||
})?;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(SignerEvent::Error(e.to_string()));
|
||||
})?;
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(SignerEvent::Error(e.to_string()));
|
||||
})?;
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
/// Set the state of the relay list
|
||||
fn set_relay_state(&mut self, state: RelayState, cx: &mut Context<Self>) {
|
||||
self.relay_list_state = state;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn ensure_relay_list(&mut self, cx: &mut Context<Self>) {
|
||||
let task = self.verify_relay_list(cx);
|
||||
|
||||
// Set the state to idle before starting the task
|
||||
self.set_relay_state(RelayState::default(), cx);
|
||||
pub fn ensure_relay_list(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
||||
let task = self.get_event(public_key, Kind::RelayList, cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let result = task.await?;
|
||||
|
||||
// Update state
|
||||
this.update(cx, |this, cx| {
|
||||
this.relay_list_state = result;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::RelayConnected);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::RelayNotConfigured);
|
||||
cx.emit(StateEvent::Error(SharedString::from(e.to_string())));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
// Verify relay list for current user
|
||||
fn verify_relay_list(&mut self, cx: &mut Context<Self>) -> Task<Result<RelayState, Error>> {
|
||||
/// Get an event with the given author and kind.
|
||||
pub fn get_event(
|
||||
&self,
|
||||
author: &PublicKey,
|
||||
kind: Kind,
|
||||
cx: &App,
|
||||
) -> Task<Result<Event, Error>> {
|
||||
let client = self.client();
|
||||
let public_key = *author;
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let signer = client.signer().context("Signer not found")?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::RelayList)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
||||
.into_iter()
|
||||
.map(|relay| (relay, vec![filter.clone()]))
|
||||
.collect();
|
||||
|
||||
// Stream events from the bootstrap relays
|
||||
let filter = Filter::new().kind(kind).author(public_key).limit(1);
|
||||
let mut stream = client
|
||||
.stream_events(target)
|
||||
.timeout(Duration::from_secs(TIMEOUT))
|
||||
.stream_events(filter)
|
||||
.timeout(Duration::from_millis(800))
|
||||
.await?;
|
||||
|
||||
while let Some((_url, res)) = stream.next().await {
|
||||
match res {
|
||||
Ok(event) => {
|
||||
log::info!("Received relay list event: {event:?}");
|
||||
return Ok(RelayState::Configured);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to receive relay list event: {e}");
|
||||
}
|
||||
if let Ok(event) = res {
|
||||
return Ok(event);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RelayState::NotConfigured)
|
||||
Err(anyhow!("No event found"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Ensure write relays for a given public key
|
||||
pub fn ensure_write_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
|
||||
let client = self.client();
|
||||
let public_key = *public_key;
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut relays = vec![];
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::RelayList)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
||||
.into_iter()
|
||||
.map(|relay| (relay, vec![filter.clone()]))
|
||||
.collect();
|
||||
|
||||
if let Ok(mut stream) = client
|
||||
.stream_events(target)
|
||||
.timeout(Duration::from_secs(TIMEOUT))
|
||||
.await
|
||||
{
|
||||
while let Some((_url, res)) = stream.next().await {
|
||||
match res {
|
||||
Ok(event) => {
|
||||
// Extract relay urls
|
||||
relays.extend(nip65::extract_owned_relay_list(event).filter_map(
|
||||
|(url, metadata)| {
|
||||
if metadata.is_none() || metadata == Some(RelayMetadata::Write)
|
||||
{
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
// Ensure connections
|
||||
for url in relays.iter() {
|
||||
client.add_relay(url).and_connect().await.ok();
|
||||
}
|
||||
|
||||
return relays;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to receive relay list event: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
relays
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a list of write relays for a given public key
|
||||
pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
|
||||
let client = self.client();
|
||||
let relays = self.gossip.read(cx).write_relays(public_key);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
// Ensure relay connections
|
||||
for url in relays.iter() {
|
||||
client.add_relay(url).and_connect().await.ok();
|
||||
}
|
||||
|
||||
relays
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a list of read relays for a given public key
|
||||
pub fn read_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
|
||||
let client = self.client();
|
||||
let relays = self.gossip.read(cx).read_relays(public_key);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
// Ensure relay connections
|
||||
for url in relays.iter() {
|
||||
client.add_relay(url).and_connect().await.ok();
|
||||
}
|
||||
|
||||
relays
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all relays for a given public key without ensuring connections
|
||||
pub fn read_only_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<SharedString> {
|
||||
self.gossip.read(cx).read_only_relays(public_key)
|
||||
}
|
||||
|
||||
/// Get the public key of a NIP-05 address
|
||||
pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
|
||||
let client = self.client();
|
||||
@@ -797,12 +657,12 @@ impl NostrRegistry {
|
||||
let mut results: Vec<PublicKey> = Vec::with_capacity(FIND_LIMIT);
|
||||
|
||||
// Return early if the query is a valid NIP-05 address
|
||||
if let Some(task) = address_task {
|
||||
if let Ok(public_key) = task.await {
|
||||
if let Some(task) = address_task
|
||||
&& let Ok(public_key) = task.await
|
||||
{
|
||||
results.push(public_key);
|
||||
return Ok(results);
|
||||
}
|
||||
}
|
||||
|
||||
// Return early if the query is a valid public key
|
||||
if let Ok(public_key) = PublicKey::parse(&query) {
|
||||
@@ -905,8 +765,6 @@ impl NostrRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<SignerEvent> for NostrRegistry {}
|
||||
|
||||
/// Get or create a new app keys
|
||||
fn get_or_init_app_keys() -> Result<Keys, Error> {
|
||||
let dir = config_dir().join(".app_keys");
|
||||
@@ -932,52 +790,6 @@ fn get_or_init_app_keys() -> Result<Keys, Error> {
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
async fn get_events_for_room(client: &Client, nip65: &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(nip65)
|
||||
.filter_map(|(url, metadata)| {
|
||||
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Ensure relay connections
|
||||
for url in write_relays.iter() {
|
||||
client.add_relay(*url).and_connect().await.ok();
|
||||
}
|
||||
|
||||
// Construct filter for inbox relays
|
||||
let inbox = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(nip65.pubkey)
|
||||
.limit(1);
|
||||
|
||||
// Construct filter for encryption announcement
|
||||
let announcement = Filter::new()
|
||||
.kind(Kind::Custom(10044))
|
||||
.author(nip65.pubkey)
|
||||
.limit(1);
|
||||
|
||||
// Construct target for subscription
|
||||
let target: HashMap<&RelayUrl, Vec<Filter>> = write_relays
|
||||
.into_iter()
|
||||
.map(|relay| (relay, vec![inbox.clone(), announcement.clone()]))
|
||||
.collect();
|
||||
|
||||
// Subscribe to inbox relays and encryption announcements
|
||||
client.subscribe(target).close_on(opts).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn default_relay_list() -> Vec<(RelayUrl, Option<RelayMetadata>)> {
|
||||
vec![
|
||||
(
|
||||
@@ -1011,43 +823,6 @@ fn default_messaging_relays() -> Vec<RelayUrl> {
|
||||
]
|
||||
}
|
||||
|
||||
/// Signer event.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum SignerEvent {
|
||||
/// A new signer has been set
|
||||
Set,
|
||||
|
||||
/// An error occurred
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum RelayState {
|
||||
#[default]
|
||||
Idle,
|
||||
Checking,
|
||||
NotConfigured,
|
||||
Configured,
|
||||
}
|
||||
|
||||
impl RelayState {
|
||||
pub fn idle(&self) -> bool {
|
||||
matches!(self, RelayState::Idle)
|
||||
}
|
||||
|
||||
pub fn checking(&self) -> bool {
|
||||
matches!(self, RelayState::Checking)
|
||||
}
|
||||
|
||||
pub fn not_configured(&self) -> bool {
|
||||
matches!(self, RelayState::NotConfigured)
|
||||
}
|
||||
|
||||
pub fn configured(&self) -> bool {
|
||||
matches!(self, RelayState::Configured)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CoopAuthUrlHandler;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use gpui::{hsla, Hsla, Rgba};
|
||||
use gpui::{Hsla, Rgba, hsla};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -30,6 +30,8 @@ pub struct ThemeColors {
|
||||
pub text_muted: Hsla,
|
||||
pub text_placeholder: Hsla,
|
||||
pub text_accent: Hsla,
|
||||
pub text_danger: Hsla,
|
||||
pub text_warning: Hsla,
|
||||
|
||||
// Icon colors
|
||||
pub icon: Hsla,
|
||||
@@ -77,11 +79,11 @@ pub struct ThemeColors {
|
||||
pub ghost_element_disabled: Hsla,
|
||||
|
||||
// Tab colors
|
||||
pub tab_inactive_background: Hsla,
|
||||
pub tab_inactive_foreground: Hsla,
|
||||
pub tab_background: Hsla,
|
||||
pub tab_foreground: Hsla,
|
||||
pub tab_hover_background: Hsla,
|
||||
pub tab_active_background: Hsla,
|
||||
pub tab_active_foreground: Hsla,
|
||||
pub tab_hover_foreground: Hsla,
|
||||
|
||||
// Scrollbar colors
|
||||
pub scrollbar_thumb_background: Hsla,
|
||||
@@ -110,8 +112,8 @@ impl ThemeColors {
|
||||
elevated_surface_background: neutral().light().step_3(),
|
||||
panel_background: neutral().light().step_1(),
|
||||
overlay: neutral().light_alpha().step_3(),
|
||||
title_bar: neutral().light().step_2(),
|
||||
title_bar_inactive: neutral().light().step_3(),
|
||||
title_bar: neutral().light().step_3(),
|
||||
title_bar_inactive: neutral().light().step_1(),
|
||||
window_border: hsl(240.0, 5.9, 78.0),
|
||||
|
||||
border: neutral().light().step_6(),
|
||||
@@ -125,7 +127,9 @@ impl ThemeColors {
|
||||
text: neutral().light().step_12(),
|
||||
text_muted: neutral().light().step_11(),
|
||||
text_placeholder: neutral().light().step_10(),
|
||||
text_accent: brand().light().step_11(),
|
||||
text_accent: brand().light().step_9(),
|
||||
text_danger: danger().light().step_9(),
|
||||
text_warning: warning().light().step_9(),
|
||||
|
||||
icon: neutral().light().step_11(),
|
||||
icon_muted: neutral().light().step_10(),
|
||||
@@ -166,17 +170,17 @@ impl ThemeColors {
|
||||
ghost_element_selected: neutral().light().step_5(),
|
||||
ghost_element_disabled: neutral().light_alpha().step_2(),
|
||||
|
||||
tab_inactive_background: neutral().light().step_2(),
|
||||
tab_inactive_foreground: neutral().light().step_11(),
|
||||
tab_background: neutral().light().step_3(),
|
||||
tab_foreground: neutral().light().step_11(),
|
||||
tab_hover_background: neutral().light_alpha().step_4(),
|
||||
tab_active_background: neutral().light().step_1(),
|
||||
tab_active_foreground: neutral().light().step_12(),
|
||||
tab_hover_foreground: brand().light().step_9(),
|
||||
|
||||
scrollbar_thumb_background: neutral().light_alpha().step_3(),
|
||||
scrollbar_thumb_hover_background: neutral().light_alpha().step_4(),
|
||||
scrollbar_thumb_border: gpui::transparent_black(),
|
||||
scrollbar_track_background: gpui::transparent_black(),
|
||||
scrollbar_track_border: neutral().light().step_5(),
|
||||
scrollbar_track_border: gpui::transparent_black(),
|
||||
|
||||
drop_target_background: brand().light_alpha().step_2(),
|
||||
cursor: hsl(200., 100., 50.),
|
||||
@@ -192,9 +196,9 @@ impl ThemeColors {
|
||||
background: neutral().dark().step_1(),
|
||||
surface_background: neutral().dark().step_2(),
|
||||
elevated_surface_background: neutral().dark().step_3(),
|
||||
panel_background: gpui::black(),
|
||||
panel_background: neutral().dark().step_1(),
|
||||
overlay: neutral().dark_alpha().step_3(),
|
||||
title_bar: gpui::transparent_black(),
|
||||
title_bar: neutral().dark().step_3(),
|
||||
title_bar_inactive: neutral().dark().step_1(),
|
||||
window_border: hsl(240.0, 3.7, 28.0),
|
||||
|
||||
@@ -209,7 +213,9 @@ impl ThemeColors {
|
||||
text: neutral().dark().step_12(),
|
||||
text_muted: neutral().dark().step_11(),
|
||||
text_placeholder: neutral().dark().step_10(),
|
||||
text_accent: brand().dark().step_11(),
|
||||
text_accent: brand().dark().step_9(),
|
||||
text_danger: danger().dark().step_9(),
|
||||
text_warning: warning().dark().step_9(),
|
||||
|
||||
icon: neutral().dark().step_11(),
|
||||
icon_muted: neutral().dark().step_10(),
|
||||
@@ -250,17 +256,17 @@ impl ThemeColors {
|
||||
ghost_element_selected: neutral().dark().step_5(),
|
||||
ghost_element_disabled: neutral().dark_alpha().step_2(),
|
||||
|
||||
tab_inactive_background: neutral().dark().step_2(),
|
||||
tab_inactive_foreground: neutral().dark().step_11(),
|
||||
tab_active_background: neutral().dark().step_3(),
|
||||
tab_background: neutral().dark().step_3(),
|
||||
tab_foreground: neutral().dark().step_11(),
|
||||
tab_hover_background: neutral().dark_alpha().step_4(),
|
||||
tab_active_background: neutral().dark().step_1(),
|
||||
tab_active_foreground: neutral().dark().step_12(),
|
||||
tab_hover_foreground: brand().dark().step_9(),
|
||||
|
||||
scrollbar_thumb_background: neutral().dark_alpha().step_3(),
|
||||
scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),
|
||||
scrollbar_thumb_border: gpui::transparent_black(),
|
||||
scrollbar_track_background: gpui::transparent_black(),
|
||||
scrollbar_track_border: neutral().dark().step_5(),
|
||||
scrollbar_track_border: gpui::transparent_black(),
|
||||
|
||||
drop_target_background: brand().dark_alpha().step_2(),
|
||||
cursor: hsl(200., 100., 50.),
|
||||
|
||||
@@ -138,7 +138,7 @@ impl Anchor {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn other_side_corner_along(&self, axis: Axis) -> Anchor {
|
||||
pub fn other_side_corner_along(&self, axis: Axis) -> Anchor {
|
||||
match axis {
|
||||
Axis::Vertical => match self {
|
||||
Self::TopLeft => Self::BottomLeft,
|
||||
@@ -4,6 +4,8 @@ use std::rc::Rc;
|
||||
use gpui::{App, Global, Pixels, SharedString, Window, px};
|
||||
|
||||
mod colors;
|
||||
mod geometry;
|
||||
mod notification;
|
||||
mod platform_kind;
|
||||
mod registry;
|
||||
mod scale;
|
||||
@@ -11,6 +13,8 @@ mod scrollbar_mode;
|
||||
mod theme;
|
||||
|
||||
pub use colors::*;
|
||||
pub use geometry::*;
|
||||
pub use notification::*;
|
||||
pub use platform_kind::PlatformKind;
|
||||
pub use registry::*;
|
||||
pub use scale::*;
|
||||
@@ -82,6 +86,9 @@ pub struct Theme {
|
||||
/// Show the scrollbar mode, default: scrolling
|
||||
pub scrollbar_mode: ScrollbarMode,
|
||||
|
||||
/// Notification settings
|
||||
pub notification: NotificationSettings,
|
||||
|
||||
/// Platform kind
|
||||
pub platform: PlatformKind,
|
||||
}
|
||||
@@ -200,10 +207,11 @@ impl From<ThemeFamily> for Theme {
|
||||
Theme {
|
||||
font_size: px(15.),
|
||||
font_family: font_family.into(),
|
||||
radius: px(5.),
|
||||
radius: px(6.),
|
||||
radius_lg: px(10.),
|
||||
shadow: true,
|
||||
scrollbar_mode: ScrollbarMode::default(),
|
||||
notification: NotificationSettings::default(),
|
||||
mode,
|
||||
colors: *colors,
|
||||
theme: Rc::new(family),
|
||||
|
||||
31
crates/theme/src/notification.rs
Normal file
31
crates/theme/src/notification.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use gpui::{Pixels, px};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Anchor, Edges, TITLEBAR_HEIGHT};
|
||||
|
||||
/// The settings for notifications.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NotificationSettings {
|
||||
/// The placement of the notification, default: [`Anchor::TopRight`]
|
||||
pub placement: Anchor,
|
||||
/// The margins of the notification with respect to the window edges.
|
||||
pub margins: Edges<Pixels>,
|
||||
/// The maximum number of notifications to show at once, default: 10
|
||||
pub max_items: usize,
|
||||
}
|
||||
|
||||
impl Default for NotificationSettings {
|
||||
fn default() -> Self {
|
||||
let offset = px(16.);
|
||||
Self {
|
||||
placement: Anchor::TopRight,
|
||||
margins: Edges {
|
||||
top: TITLEBAR_HEIGHT + offset, // avoid overlap with title bar
|
||||
right: offset,
|
||||
bottom: offset,
|
||||
left: offset,
|
||||
},
|
||||
max_items: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
//! This is a fork of gpui's anchored element that adds support for offsetting
|
||||
//! https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/elements/anchored.rs
|
||||
use gpui::{
|
||||
point, px, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, Half,
|
||||
AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, Half,
|
||||
InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
|
||||
Window,
|
||||
Window, point, px,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::Anchor;
|
||||
use theme::Anchor;
|
||||
|
||||
/// The state that the anchored element element uses to track its children.
|
||||
pub struct AnchoredState {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, px, AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement,
|
||||
Interactivity, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage,
|
||||
Window,
|
||||
AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement, Interactivity,
|
||||
IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage, Window, div, img,
|
||||
px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{Sizable, Size};
|
||||
use crate::{Selectable, Sizable, Size};
|
||||
|
||||
/// Returns the size of the avatar based on the given [`Size`].
|
||||
pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
|
||||
@@ -37,6 +37,7 @@ pub struct Avatar {
|
||||
style: StyleRefinement,
|
||||
size: Size,
|
||||
border_color: Option<Hsla>,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl Avatar {
|
||||
@@ -48,6 +49,7 @@ impl Avatar {
|
||||
style: StyleRefinement::default(),
|
||||
size: Size::Medium,
|
||||
border_color: None,
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +91,17 @@ impl Styled for Avatar {
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for Avatar {
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
|
||||
fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Avatar {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
self.base.interactivity()
|
||||
|
||||
@@ -3,20 +3,26 @@ use std::sync::Arc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, px, App, AppContext, Axis, Context, Element, Entity, IntoElement, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, Styled as _, WeakEntity,
|
||||
Window,
|
||||
App, AppContext, Axis, Context, Element, Empty, Entity, IntoElement, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, StyleRefinement, Styled as _,
|
||||
WeakEntity, Window, div, px,
|
||||
};
|
||||
|
||||
use super::{DockArea, DockItem};
|
||||
use crate::dock_area::panel::PanelView;
|
||||
use crate::dock_area::tab_panel::TabPanel;
|
||||
use crate::resizable::{resize_handle, PANEL_MIN_SIZE};
|
||||
use crate::StyledExt;
|
||||
use crate::dock::panel::PanelView;
|
||||
use crate::dock::tab_panel::TabPanel;
|
||||
use crate::resizable::{PANEL_MIN_SIZE, resize_handle};
|
||||
|
||||
#[derive(Clone, Render)]
|
||||
#[derive(Clone)]
|
||||
struct ResizePanel;
|
||||
|
||||
impl Render for ResizePanel {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
Empty
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DockPlacement {
|
||||
Center,
|
||||
@@ -265,24 +271,24 @@ impl Dock {
|
||||
let mut right_dock_size = px(0.0);
|
||||
|
||||
// Get the size of the left dock if it's open and not the current dock
|
||||
if let Some(left_dock) = &dock_area.left_dock {
|
||||
if left_dock.entity_id() != cx.entity().entity_id() {
|
||||
if let Some(left_dock) = &dock_area.left_dock
|
||||
&& left_dock.entity_id() != cx.entity().entity_id()
|
||||
{
|
||||
let left_dock_read = left_dock.read(cx);
|
||||
if left_dock_read.is_open() {
|
||||
left_dock_size = left_dock_read.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the size of the right dock if it's open and not the current dock
|
||||
if let Some(right_dock) = &dock_area.right_dock {
|
||||
if right_dock.entity_id() != cx.entity().entity_id() {
|
||||
if let Some(right_dock) = &dock_area.right_dock
|
||||
&& right_dock.entity_id() != cx.entity().entity_id()
|
||||
{
|
||||
let right_dock_read = right_dock.read(cx);
|
||||
if right_dock_read.is_open() {
|
||||
right_dock_size = right_dock_read.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let size = match self.placement {
|
||||
DockPlacement::Left => mouse_position.x - area_bounds.left(),
|
||||
@@ -321,6 +327,8 @@ impl Render for Dock {
|
||||
return div();
|
||||
}
|
||||
|
||||
let cache_style = StyleRefinement::default().absolute().size_full();
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
@@ -336,7 +344,7 @@ impl Render for Dock {
|
||||
.map(|this| match &self.panel {
|
||||
DockItem::Split { view, .. } => this.child(view.clone()),
|
||||
DockItem::Tabs { view, .. } => this.child(view.clone()),
|
||||
DockItem::Panel { view, .. } => this.child(view.clone().view()),
|
||||
DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
|
||||
})
|
||||
.child(self.render_resize_handle(window, cx))
|
||||
.child(DockElement {
|
||||
@@ -2,22 +2,24 @@ use std::sync::Arc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations,
|
||||
Edges, Entity, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
||||
AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations, Edges, Entity,
|
||||
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||
Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px,
|
||||
};
|
||||
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
|
||||
|
||||
use crate::dock_area::dock::{Dock, DockPlacement};
|
||||
use crate::dock_area::panel::{Panel, PanelEvent, PanelStyle, PanelView};
|
||||
use crate::dock_area::stack_panel::StackPanel;
|
||||
use crate::dock_area::tab_panel::TabPanel;
|
||||
use crate::ElementExt;
|
||||
|
||||
pub mod dock;
|
||||
pub mod panel;
|
||||
pub mod stack_panel;
|
||||
pub mod tab_panel;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod dock;
|
||||
mod panel;
|
||||
mod stack_panel;
|
||||
mod tab_panel;
|
||||
|
||||
pub use dock::*;
|
||||
pub use panel::*;
|
||||
pub use stack_panel::*;
|
||||
pub use tab_panel::*;
|
||||
|
||||
actions!(dock, [ToggleZoom, ClosePanel]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Render,
|
||||
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Render,
|
||||
SharedString, Window,
|
||||
};
|
||||
|
||||
@@ -21,12 +21,6 @@ pub enum PanelStyle {
|
||||
TabBar,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TitleStyle {
|
||||
pub background: Hsla,
|
||||
pub foreground: Hsla,
|
||||
}
|
||||
|
||||
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
|
||||
/// The name of the panel used to serialize, deserialize and identify the panel.
|
||||
///
|
||||
@@ -7,16 +7,16 @@ use gpui::{
|
||||
Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
|
||||
use theme::{ActiveTheme, AxisExt as _, CLIENT_SIDE_DECORATION_ROUNDING, Placement};
|
||||
|
||||
use super::{DockArea, PanelEvent};
|
||||
use crate::dock_area::panel::{Panel, PanelView};
|
||||
use crate::dock_area::tab_panel::TabPanel;
|
||||
use crate::dock::panel::{Panel, PanelView};
|
||||
use crate::dock::tab_panel::TabPanel;
|
||||
use crate::h_flex;
|
||||
use crate::resizable::{
|
||||
resizable_panel, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
|
||||
PANEL_MIN_SIZE,
|
||||
PANEL_MIN_SIZE, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
|
||||
resizable_panel,
|
||||
};
|
||||
use crate::{h_flex, AxisExt as _, Placement};
|
||||
|
||||
pub struct StackPanel {
|
||||
pub(super) parent: Option<WeakEntity<StackPanel>>,
|
||||
@@ -70,11 +70,11 @@ impl StackPanel {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(parent) = &self.parent {
|
||||
if let Some(parent) = parent.upgrade() {
|
||||
if let Some(parent) = &self.parent
|
||||
&& let Some(parent) = parent.upgrade()
|
||||
{
|
||||
return parent.read(cx).is_last_panel(cx);
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
@@ -297,13 +297,12 @@ impl StackPanel {
|
||||
|
||||
/// Find the first top left in the stack.
|
||||
pub fn left_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
||||
if check_parent {
|
||||
if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) {
|
||||
if let Some(panel) = parent.read(cx).left_top_tab_panel(true, cx) {
|
||||
if check_parent
|
||||
&& let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade())
|
||||
&& let Some(panel) = parent.read(cx).left_top_tab_panel(true, cx)
|
||||
{
|
||||
return Some(panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let first_panel = self.panels.first();
|
||||
if let Some(view) = first_panel {
|
||||
@@ -321,13 +320,12 @@ impl StackPanel {
|
||||
|
||||
/// Find the first top right in the stack.
|
||||
pub fn right_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
||||
if check_parent {
|
||||
if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) {
|
||||
if let Some(panel) = parent.read(cx).right_top_tab_panel(true, cx) {
|
||||
if check_parent
|
||||
&& let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade())
|
||||
&& let Some(panel) = parent.read(cx).right_top_tab_panel(true, cx)
|
||||
{
|
||||
return Some(panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let panel = if self.axis.is_vertical() {
|
||||
self.panels.first()
|
||||
@@ -2,22 +2,22 @@ use std::sync::Arc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent,
|
||||
Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
|
||||
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
|
||||
StatefulInteractiveElement, Styled, WeakEntity, Window,
|
||||
App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
|
||||
WeakEntity, Window, div, px, rems,
|
||||
};
|
||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TABBAR_HEIGHT};
|
||||
use theme::{ActiveTheme, AxisExt, CLIENT_SIDE_DECORATION_ROUNDING, Placement, TABBAR_HEIGHT};
|
||||
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::dock_area::dock::DockPlacement;
|
||||
use crate::dock_area::panel::{Panel, PanelView};
|
||||
use crate::dock_area::stack_panel::StackPanel;
|
||||
use crate::dock_area::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
||||
use crate::dock::dock::DockPlacement;
|
||||
use crate::dock::panel::{Panel, PanelView};
|
||||
use crate::dock::stack_panel::StackPanel;
|
||||
use crate::dock::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
||||
use crate::menu::{DropdownMenu, PopupMenu};
|
||||
use crate::tab::tab_bar::TabBar;
|
||||
use crate::tab::Tab;
|
||||
use crate::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
|
||||
use crate::tab::tab_bar::TabBar;
|
||||
use crate::{IconName, Selectable, Sizable, StyledExt, h_flex, v_flex};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TabState {
|
||||
@@ -42,22 +42,20 @@ impl DragPanel {
|
||||
|
||||
impl Render for DragPanel {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
h_flex()
|
||||
.id("drag-panel")
|
||||
.cursor_grab()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.w_24()
|
||||
.flex()
|
||||
.items_center()
|
||||
.p_2()
|
||||
.min_w_24()
|
||||
.justify_center()
|
||||
.overflow_hidden()
|
||||
.whitespace_nowrap()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.rounded(cx.theme().radius)
|
||||
.text_xs()
|
||||
.when(cx.theme().shadow, |this| this.shadow_lg())
|
||||
.text_color(cx.theme().text)
|
||||
.text_ellipsis()
|
||||
.when(cx.theme().shadow, |this| this.shadow_xs())
|
||||
.bg(cx.theme().background)
|
||||
.text_color(cx.theme().text_accent)
|
||||
.child(self.panel.title(cx))
|
||||
}
|
||||
}
|
||||
@@ -234,15 +232,14 @@ impl TabPanel {
|
||||
.any(|p| p.panel_id(cx) == panel.panel_id(cx))
|
||||
{
|
||||
// Set the active panel to the matched panel
|
||||
if active {
|
||||
if let Some(ix) = self
|
||||
if active
|
||||
&& let Some(ix) = self
|
||||
.panels
|
||||
.iter()
|
||||
.position(|p| p.panel_id(cx) == panel.panel_id(cx))
|
||||
{
|
||||
self.set_active_ix(ix, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -374,13 +371,12 @@ impl TabPanel {
|
||||
|
||||
/// Return true if self or parent only have last panel.
|
||||
fn is_last_panel(&self, cx: &App) -> bool {
|
||||
if let Some(parent) = &self.stack_panel {
|
||||
if let Some(stack_panel) = parent.upgrade() {
|
||||
if !stack_panel.read(cx).is_last_panel(cx) {
|
||||
if let Some(parent) = &self.stack_panel
|
||||
&& let Some(stack_panel) = parent.upgrade()
|
||||
&& !stack_panel.read(cx).is_last_panel(cx)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.panels.len() <= 1
|
||||
}
|
||||
@@ -425,14 +421,13 @@ impl TabPanel {
|
||||
let view = cx.entity().clone();
|
||||
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
|
||||
let toolbar = self.toolbar_buttons(window, cx);
|
||||
let has_toolbar = !toolbar.is_empty();
|
||||
|
||||
h_flex()
|
||||
.p_0p5()
|
||||
.gap_1()
|
||||
.gap_1p5()
|
||||
.occlude()
|
||||
.rounded_full()
|
||||
.children(toolbar.into_iter().map(|btn| btn.small().ghost().rounded()))
|
||||
.children(toolbar.into_iter().map(|btn| btn.small().ghost()))
|
||||
.when(self.zoomed, |this| {
|
||||
this.child(
|
||||
Button::new("zoom")
|
||||
@@ -445,15 +440,11 @@ impl TabPanel {
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(has_toolbar, |this| {
|
||||
this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
|
||||
})
|
||||
.child(
|
||||
Button::new("menu")
|
||||
.icon(IconName::Ellipsis)
|
||||
.small()
|
||||
.ghost()
|
||||
.rounded()
|
||||
.dropdown_menu({
|
||||
let zoomable = state.zoomable;
|
||||
let closable = state.closable;
|
||||
@@ -578,6 +569,7 @@ impl TabPanel {
|
||||
let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx);
|
||||
let has_extend_dock_button = left_dock_button.is_some() || bottom_dock_button.is_some();
|
||||
let tabs_count = self.panels.len();
|
||||
let is_bottom_dock = bottom_dock_button.is_some();
|
||||
|
||||
if tabs_count == 1 && dock_area.read(cx).panel_style == PanelStyle::Default {
|
||||
let panel = self.panels.first().unwrap();
|
||||
@@ -646,7 +638,7 @@ impl TabPanel {
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
TabBar::new()
|
||||
TabBar::new("tab-bar")
|
||||
.track_scroll(&self.tab_bar_scroll_handle)
|
||||
.h(TABBAR_HEIGHT)
|
||||
.when(has_extend_dock_button, |this| {
|
||||
@@ -659,8 +651,9 @@ impl TabPanel {
|
||||
.border_b_1()
|
||||
.h_full()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().surface_background)
|
||||
.px_2()
|
||||
.bg(cx.theme().tab_background)
|
||||
.pl_0p5()
|
||||
.pr_1()
|
||||
.children(left_dock_button)
|
||||
.children(bottom_dock_button),
|
||||
)
|
||||
@@ -682,16 +675,43 @@ impl TabPanel {
|
||||
Some(
|
||||
Tab::new()
|
||||
.ix(ix)
|
||||
.label(panel.title(cx))
|
||||
.py_2()
|
||||
.tab_bar_prefix(has_extend_dock_button)
|
||||
.child(panel.title(cx))
|
||||
.selected(active)
|
||||
.disabled(disabled)
|
||||
.suffix(
|
||||
Button::new("close-{ix}")
|
||||
.icon(IconName::Close)
|
||||
.tooltip("Close panel")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener({
|
||||
let panel = panel.clone();
|
||||
move |view, _ev, window, cx| {
|
||||
view.remove_panel(&panel, window, cx);
|
||||
}
|
||||
})),
|
||||
)
|
||||
.on_click(cx.listener({
|
||||
let is_collapsed = self.collapsed;
|
||||
let dock_area = self.dock_area.clone();
|
||||
move |view, _, window, cx| {
|
||||
view.set_active_ix(ix, window, cx);
|
||||
|
||||
// Open dock if clicked on the collapsed bottom dock
|
||||
if is_bottom_dock && is_collapsed {
|
||||
_ = dock_area.update(cx, |dock_area, cx| {
|
||||
dock_area.toggle_dock(DockPlacement::Bottom, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}))
|
||||
.when(!disabled, |this| {
|
||||
this.on_mouse_down(
|
||||
MouseButton::Middle,
|
||||
cx.listener({
|
||||
let panel = panel.clone();
|
||||
move |view, _, window, cx| {
|
||||
move |view, _ev, window, cx| {
|
||||
view.remove_panel(&panel, window, cx);
|
||||
}
|
||||
}),
|
||||
@@ -757,14 +777,15 @@ impl TabPanel {
|
||||
this.suffix(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.h_full()
|
||||
.border_color(cx.theme().border)
|
||||
.border_l_1()
|
||||
.border_b_1()
|
||||
.px_0p5()
|
||||
.gap_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().tab_background)
|
||||
.child(self.render_toolbar(state, window, cx))
|
||||
.when_some(right_dock_button, |this, btn| this.child(btn)),
|
||||
)
|
||||
@@ -1080,13 +1101,13 @@ impl TabPanel {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.panels.len() > 1 {
|
||||
if let Some(panel) = self.active_panel(cx) {
|
||||
if self.panels.len() > 1
|
||||
&& let Some(panel) = self.active_panel(cx)
|
||||
{
|
||||
self.remove_panel(&panel, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for TabPanel {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
@@ -1099,6 +1120,7 @@ impl Focusable for TabPanel {
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for TabPanel {}
|
||||
|
||||
impl EventEmitter<PanelEvent> for TabPanel {}
|
||||
|
||||
impl Render for TabPanel {
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
svg, AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render,
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
||||
AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
|
||||
SharedString, StyleRefinement, Styled, Svg, Transformation, Window, svg,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -39,6 +39,7 @@ pub enum IconName {
|
||||
Ellipsis,
|
||||
Emoji,
|
||||
Eye,
|
||||
Input,
|
||||
Info,
|
||||
Invite,
|
||||
Inbox,
|
||||
@@ -110,6 +111,7 @@ impl IconNamed for IconName {
|
||||
Self::Ellipsis => "icons/ellipsis.svg",
|
||||
Self::Emoji => "icons/emoji.svg",
|
||||
Self::Eye => "icons/eye.svg",
|
||||
Self::Input => "icons/input.svg",
|
||||
Self::Info => "icons/info.svg",
|
||||
Self::Invite => "icons/invite.svg",
|
||||
Self::Inbox => "icons/inbox.svg",
|
||||
|
||||
@@ -2,10 +2,10 @@ use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
|
||||
Entity, GlobalElementId, Hitbox, IntoElement, LayoutId, MouseButton, MouseMoveEvent, Path,
|
||||
Pixels, Point, ShapedLine, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle,
|
||||
Window,
|
||||
App, Bounds, Corners, Element, ElementId, ElementInputHandler, Entity, GlobalElementId, Hitbox,
|
||||
IntoElement, LayoutId, MouseButton, MouseMoveEvent, Path, Pixels, Point, ShapedLine,
|
||||
SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window, fill, point, px,
|
||||
relative, size,
|
||||
};
|
||||
use rope::Rope;
|
||||
use smallvec::SmallVec;
|
||||
@@ -348,12 +348,12 @@ impl TextElement {
|
||||
let mut rev_line_corners = line_corners.iter().rev().peekable();
|
||||
while let Some(corners) = rev_line_corners.next() {
|
||||
points.push(corners.top_left);
|
||||
if let Some(next) = rev_line_corners.peek() {
|
||||
if next.top_left.x > corners.top_left.x {
|
||||
if let Some(next) = rev_line_corners.peek()
|
||||
&& next.top_left.x > corners.top_left.x
|
||||
{
|
||||
points.push(point(next.top_left.x, corners.top_left.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// print_points_as_svg_path(&line_corners, &points);
|
||||
|
||||
@@ -376,11 +376,11 @@ impl TextElement {
|
||||
) -> Option<Path<Pixels>> {
|
||||
let state = self.state.read(cx);
|
||||
let mut selected_range = state.selected_range;
|
||||
if let Some(ime_marked_range) = &state.ime_marked_range {
|
||||
if !ime_marked_range.is_empty() {
|
||||
if let Some(ime_marked_range) = &state.ime_marked_range
|
||||
&& !ime_marked_range.is_empty()
|
||||
{
|
||||
selected_range = (ime_marked_range.end..ime_marked_range.end).into();
|
||||
}
|
||||
}
|
||||
if selected_range.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -830,12 +830,13 @@ impl Element for TextElement {
|
||||
}
|
||||
|
||||
// Paint blinking cursor
|
||||
if focused && show_cursor {
|
||||
if let Some(mut cursor_bounds) = prepaint.cursor_bounds.take() {
|
||||
if focused
|
||||
&& show_cursor
|
||||
&& let Some(mut cursor_bounds) = prepaint.cursor_bounds.take()
|
||||
{
|
||||
cursor_bounds.origin.y += prepaint.cursor_scroll_offset.y;
|
||||
window.paint_quad(fill(cursor_bounds, cx.theme().cursor));
|
||||
}
|
||||
}
|
||||
|
||||
// Paint line numbers
|
||||
let mut offset_y = px(0.);
|
||||
|
||||
@@ -225,14 +225,13 @@ impl MaskPattern {
|
||||
}
|
||||
|
||||
// check if the fraction part is valid
|
||||
if let Some(frac) = frac_part {
|
||||
if !frac
|
||||
if let Some(frac) = frac_part
|
||||
&& !frac
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
@@ -255,13 +254,13 @@ impl MaskPattern {
|
||||
|
||||
if token.is_sep() {
|
||||
// If next token is match, it's valid
|
||||
if let Some(next_token) = tokens.get(pos + 1) {
|
||||
if next_token.is_match(ch) {
|
||||
if let Some(next_token) = tokens.get(pos + 1)
|
||||
&& next_token.is_match(ch)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
actions, div, point, px, Action, App, AppContext, Bounds, ClipboardItem, Context, Entity,
|
||||
EntityInputHandler, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
|
||||
KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
ParentElement as _, Pixels, Point, Render, ScrollHandle, ScrollWheelEvent, SharedString,
|
||||
Styled as _, Subscription, UTF16Selection, Window, WrappedLine,
|
||||
Action, App, AppContext, Bounds, ClipboardItem, Context, Entity, EntityInputHandler,
|
||||
EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding,
|
||||
KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _,
|
||||
Pixels, Point, Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription,
|
||||
UTF16Selection, Window, WrappedLine, actions, div, point, px,
|
||||
};
|
||||
use lsp_types::Position;
|
||||
use rope::{OffsetUtf16, Rope};
|
||||
@@ -25,9 +25,9 @@ use super::mask_pattern::MaskPattern;
|
||||
use super::mode::{InputMode, TabSize};
|
||||
use super::rope_ext::RopeExt;
|
||||
use super::text_wrapper::{LineItem, TextWrapper};
|
||||
use crate::Root;
|
||||
use crate::history::History;
|
||||
use crate::input::element::RIGHT_MARGIN;
|
||||
use crate::Root;
|
||||
|
||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[action(namespace = input, no_json)]
|
||||
@@ -521,19 +521,19 @@ impl InputState {
|
||||
let new_row = new_row as usize;
|
||||
if new_row >= last_layout.visible_range.start {
|
||||
let visible_row = new_row.saturating_sub(last_layout.visible_range.start);
|
||||
if let Some(line) = last_layout.lines.get(visible_row) {
|
||||
if let Ok(x) = line.closest_index_for_position(
|
||||
if let Some(line) = last_layout.lines.get(visible_row)
|
||||
&& let Ok(x) = line.closest_index_for_position(
|
||||
Point {
|
||||
x: preferred_x,
|
||||
y: px(0.),
|
||||
},
|
||||
last_layout.line_height,
|
||||
) {
|
||||
)
|
||||
{
|
||||
new_offset = line_start_offset + x;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.pause_blink_cursor(cx);
|
||||
self.move_to(new_offset, cx);
|
||||
// Set back the preferred_column
|
||||
@@ -1355,11 +1355,11 @@ impl InputState {
|
||||
) {
|
||||
// If there have IME marked range and is empty (Means pressed Esc to abort IME typing)
|
||||
// Clear the marked range.
|
||||
if let Some(ime_marked_range) = &self.ime_marked_range {
|
||||
if ime_marked_range.is_empty() {
|
||||
if let Some(ime_marked_range) = &self.ime_marked_range
|
||||
&& ime_marked_range.is_empty()
|
||||
{
|
||||
self.ime_marked_range = None;
|
||||
}
|
||||
}
|
||||
|
||||
self.selecting = true;
|
||||
let offset = self.index_for_mouse_position(event.position, window, cx);
|
||||
@@ -1842,23 +1842,21 @@ impl InputState {
|
||||
|
||||
fn previous_boundary(&self, offset: usize) -> usize {
|
||||
let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left);
|
||||
if let Some(ch) = self.text.char_at(offset) {
|
||||
if ch == '\r' {
|
||||
if let Some(ch) = self.text.char_at(offset)
|
||||
&& ch == '\r'
|
||||
{
|
||||
offset -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
offset
|
||||
}
|
||||
|
||||
fn next_boundary(&self, offset: usize) -> usize {
|
||||
let mut offset = self.text.clip_offset(offset + 1, Bias::Right);
|
||||
if let Some(ch) = self.text.char_at(offset) {
|
||||
if ch == '\r' {
|
||||
if let Some(ch) = self.text.char_at(offset)
|
||||
&& ch == '\r'
|
||||
{
|
||||
offset += 1;
|
||||
}
|
||||
}
|
||||
|
||||
offset
|
||||
}
|
||||
|
||||
@@ -1927,11 +1925,11 @@ impl InputState {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(validate) = &self.validate {
|
||||
if !validate(new_text, cx) {
|
||||
if let Some(validate) = &self.validate
|
||||
&& !validate(new_text, cx)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.mask_pattern.is_valid(new_text) {
|
||||
return false;
|
||||
@@ -1979,8 +1977,9 @@ impl InputState {
|
||||
self.input_bounds = new_bounds;
|
||||
|
||||
// Update text_wrapper wrap_width if changed.
|
||||
if let Some(last_layout) = self.last_layout.as_ref() {
|
||||
if wrap_width_changed {
|
||||
if let Some(last_layout) = self.last_layout.as_ref()
|
||||
&& wrap_width_changed
|
||||
{
|
||||
let wrap_width = if !self.soft_wrap {
|
||||
// None to disable wrapping (will use Pixels::MAX)
|
||||
None
|
||||
@@ -1993,7 +1992,6 @@ impl InputState {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace text by [`lsp_types::Range`].
|
||||
///
|
||||
@@ -2209,21 +2207,19 @@ impl EntityInputHandler for InputState {
|
||||
break;
|
||||
}
|
||||
|
||||
if start_origin.is_none() {
|
||||
if let Some(p) =
|
||||
if start_origin.is_none()
|
||||
&& let Some(p) =
|
||||
line.position_for_index(range.start.saturating_sub(index_offset), line_height)
|
||||
{
|
||||
start_origin = Some(p + point(px(0.), y_offset));
|
||||
}
|
||||
}
|
||||
|
||||
if end_origin.is_none() {
|
||||
if let Some(p) =
|
||||
if end_origin.is_none()
|
||||
&& let Some(p) =
|
||||
line.position_for_index(range.end.saturating_sub(index_offset), line_height)
|
||||
{
|
||||
end_origin = Some(p + point(px(0.), y_offset));
|
||||
}
|
||||
}
|
||||
|
||||
index_offset += line.len() + 1;
|
||||
y_offset += line.size(line_height).height;
|
||||
|
||||
@@ -2,11 +2,10 @@ pub use anchored::*;
|
||||
pub use element_ext::ElementExt;
|
||||
pub use event::InteractiveElementExt;
|
||||
pub use focusable::FocusableCycle;
|
||||
pub use geometry::*;
|
||||
pub use icon::*;
|
||||
pub use index_path::IndexPath;
|
||||
pub use kbd::*;
|
||||
pub use root::{window_paddings, Root};
|
||||
pub use root::{Root, window_paddings};
|
||||
pub use styled::*;
|
||||
pub use window_ext::*;
|
||||
|
||||
@@ -18,7 +17,7 @@ pub mod avatar;
|
||||
pub mod button;
|
||||
pub mod checkbox;
|
||||
pub mod divider;
|
||||
pub mod dock_area;
|
||||
pub mod dock;
|
||||
pub mod group_box;
|
||||
pub mod history;
|
||||
pub mod indicator;
|
||||
@@ -39,7 +38,6 @@ mod anchored;
|
||||
mod element_ext;
|
||||
mod event;
|
||||
mod focusable;
|
||||
mod geometry;
|
||||
mod icon;
|
||||
mod index_path;
|
||||
mod kbd;
|
||||
|
||||
@@ -3,21 +3,21 @@ use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, size, uniform_list, App, AppContext, AvailableSpace, ClickEvent, Context,
|
||||
DefiniteLength, EdgesRefinement, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, KeyBinding, Length, ListSizingBehavior, MouseButton,
|
||||
ParentElement, Render, RenderOnce, ScrollStrategy, SharedString, StatefulInteractiveElement,
|
||||
StyleRefinement, Styled, Subscription, Task, UniformListScrollHandle, Window,
|
||||
App, AppContext, AvailableSpace, ClickEvent, Context, DefiniteLength, EdgesRefinement, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, Length,
|
||||
ListSizingBehavior, MouseButton, ParentElement, Render, RenderOnce, ScrollStrategy,
|
||||
SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task,
|
||||
UniformListScrollHandle, Window, div, px, size, uniform_list,
|
||||
};
|
||||
use smol::Timer;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
|
||||
use crate::input::{InputEvent, InputState, TextInput};
|
||||
use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache};
|
||||
use crate::list::ListDelegate;
|
||||
use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache};
|
||||
use crate::scroll::{Scrollbar, ScrollbarHandle};
|
||||
use crate::{v_flex, Icon, IconName, IndexPath, Selectable, Sizable, Size, StyledExt};
|
||||
use crate::{Icon, IconName, IndexPath, Selectable, Sizable, Size, StyledExt, v_flex};
|
||||
|
||||
pub(crate) fn init(cx: &mut App) {
|
||||
let context: Option<&str> = Some("List");
|
||||
@@ -578,11 +578,11 @@ where
|
||||
self.prepare_items_if_needed(window, cx);
|
||||
|
||||
// Scroll to the selected item if it is set.
|
||||
if let Some((ix, strategy)) = self.deferred_scroll_to_index.take() {
|
||||
if let Some(item_ix) = self.rows_cache.position_of(&ix) {
|
||||
if let Some((ix, strategy)) = self.deferred_scroll_to_index.take()
|
||||
&& let Some(item_ix) = self.rows_cache.position_of(&ix)
|
||||
{
|
||||
self.scroll_handle.scroll_to_item(item_ix, strategy);
|
||||
}
|
||||
}
|
||||
|
||||
let loading = self.delegate().loading(cx);
|
||||
let query_input = if self.searchable {
|
||||
|
||||
@@ -5,10 +5,11 @@ use gpui::{
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, Window,
|
||||
};
|
||||
|
||||
use crate::Selectable;
|
||||
use crate::avatar::Avatar;
|
||||
use crate::button::Button;
|
||||
use crate::menu::PopupMenu;
|
||||
use crate::popover::Popover;
|
||||
use crate::Selectable;
|
||||
|
||||
/// A dropdown menu trait for buttons and other interactive elements
|
||||
pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static {
|
||||
@@ -35,6 +36,8 @@ pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement +
|
||||
|
||||
impl DropdownMenu for Button {}
|
||||
|
||||
impl DropdownMenu for Avatar {}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
|
||||
id: ElementId,
|
||||
|
||||
@@ -2,19 +2,19 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
anchored, div, px, rems, Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent,
|
||||
Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, Half,
|
||||
InteractiveElement, IntoElement, KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement,
|
||||
Pixels, Point, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
|
||||
Subscription, WeakEntity, Window,
|
||||
Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent, Context, Corner, DismissEvent,
|
||||
Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, InteractiveElement, IntoElement,
|
||||
KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement, Pixels, Point, Render, ScrollHandle,
|
||||
SharedString, StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, anchored,
|
||||
div, px, rems,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, Side};
|
||||
|
||||
use crate::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp};
|
||||
use crate::kbd::Kbd;
|
||||
use crate::menu::menu_item::MenuItemElement;
|
||||
use crate::scroll::ScrollableElement;
|
||||
use crate::{h_flex, v_flex, ElementExt, Icon, IconName, Side, Sizable as _, Size, StyledExt};
|
||||
use crate::{ElementExt, Icon, IconName, Sizable as _, Size, StyledExt, h_flex, v_flex};
|
||||
|
||||
const CONTEXT: &str = "PopupMenu";
|
||||
|
||||
@@ -719,14 +719,14 @@ impl PopupMenu {
|
||||
}
|
||||
|
||||
pub(crate) fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
|
||||
if let Some(ix) = self.selected_index {
|
||||
if let Some(item) = self.menu_items.get(ix) {
|
||||
if let Some(ix) = self.selected_index
|
||||
&& let Some(item) = self.menu_items.get(ix)
|
||||
{
|
||||
return match item {
|
||||
PopupMenuItem::Submenu { menu, .. } => Some(menu.clone()),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -965,13 +965,12 @@ impl PopupMenu {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Do not dismiss, if click inside the parent menu
|
||||
if let Some(parent) = self.parent_menu.as_ref() {
|
||||
if let Some(parent) = parent.upgrade() {
|
||||
if parent.read(cx).bounds.contains(position) {
|
||||
if let Some(parent) = self.parent_menu.as_ref()
|
||||
&& let Some(parent) = parent.upgrade()
|
||||
&& parent.read(cx).bounds.contains(position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.dismiss(&Cancel, window, cx);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
anchored, div, hsla, point, px, Animation, AnimationExt as _, AnyElement, App, Bounds,
|
||||
BoxShadow, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
||||
MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, StyleRefinement, Styled,
|
||||
Window,
|
||||
Animation, AnimationExt as _, AnyElement, App, Bounds, BoxShadow, ClickEvent, Div, FocusHandle,
|
||||
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, Window, anchored, div, hsla, point, px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -14,7 +13,7 @@ use crate::actions::{Cancel, Confirm};
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
|
||||
use crate::scroll::ScrollableElement;
|
||||
use crate::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension};
|
||||
use crate::{IconName, Root, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
const CONTEXT: &str = "Modal";
|
||||
|
||||
@@ -297,11 +296,11 @@ impl RenderOnce for Modal {
|
||||
let on_close = on_close.clone();
|
||||
|
||||
move |_, window, cx| {
|
||||
if let Some(on_ok) = &on_ok {
|
||||
if !on_ok(&ClickEvent::default(), window, cx) {
|
||||
if let Some(on_ok) = &on_ok
|
||||
&& !on_ok(&ClickEvent::default(), window, cx)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
on_close(&ClickEvent::default(), window, cx);
|
||||
window.close_modal(cx);
|
||||
@@ -500,6 +499,7 @@ impl RenderOnce for Modal {
|
||||
.child(self.content),
|
||||
),
|
||||
)
|
||||
.when_none(&self.footer, |this| this.child(div().pt(padding_left)))
|
||||
.when_some(self.footer, |this, footer| {
|
||||
this.child(
|
||||
h_flex()
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
use std::any::TypeId;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context,
|
||||
DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
|
||||
Subscription, Window,
|
||||
Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context, DismissEvent,
|
||||
ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||
Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription,
|
||||
Window, div, px, relative,
|
||||
};
|
||||
use smol::Timer;
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, Anchor};
|
||||
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt};
|
||||
use crate::{Icon, IconName, Sizable as _, StyledExt, h_flex, v_flex};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub enum NotificationType {
|
||||
pub enum NotificationKind {
|
||||
#[default]
|
||||
Info,
|
||||
Success,
|
||||
@@ -27,13 +25,15 @@ pub enum NotificationType {
|
||||
Error,
|
||||
}
|
||||
|
||||
impl NotificationType {
|
||||
impl NotificationKind {
|
||||
fn icon(&self, cx: &App) -> Icon {
|
||||
match self {
|
||||
Self::Info => Icon::new(IconName::Info).text_color(cx.theme().element_foreground),
|
||||
Self::Success => Icon::new(IconName::Info).text_color(cx.theme().secondary_foreground),
|
||||
Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().warning_foreground),
|
||||
Self::Error => Icon::new(IconName::Warning).text_color(cx.theme().danger_foreground),
|
||||
Self::Info => Icon::new(IconName::Info).text_color(cx.theme().icon),
|
||||
Self::Success => Icon::new(IconName::CheckCircle).text_color(cx.theme().icon_accent),
|
||||
Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().text_warning),
|
||||
Self::Error => {
|
||||
Icon::new(IconName::CloseCircle).text_color(cx.theme().danger_foreground)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ impl From<(TypeId, ElementId)> for NotificationId {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// A notification element.
|
||||
pub struct Notification {
|
||||
/// The id is used make the notification unique.
|
||||
@@ -64,16 +65,13 @@ pub struct Notification {
|
||||
/// None means the notification will be added to the end of the list.
|
||||
id: NotificationId,
|
||||
style: StyleRefinement,
|
||||
type_: Option<NotificationType>,
|
||||
kind: Option<NotificationKind>,
|
||||
title: Option<SharedString>,
|
||||
message: Option<SharedString>,
|
||||
icon: Option<Icon>,
|
||||
autohide: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
action_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> Button>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
content_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
action_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button>>,
|
||||
content_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement>>,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
closing: bool,
|
||||
}
|
||||
@@ -84,12 +82,6 @@ impl From<String> for Notification {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cow<'static, str>> for Notification {
|
||||
fn from(s: Cow<'static, str>) -> Self {
|
||||
Self::new().message(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for Notification {
|
||||
fn from(s: SharedString) -> Self {
|
||||
Self::new().message(s)
|
||||
@@ -102,24 +94,24 @@ impl From<&'static str> for Notification {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(NotificationType, &'static str)> for Notification {
|
||||
fn from((type_, content): (NotificationType, &'static str)) -> Self {
|
||||
Self::new().message(content).with_type(type_)
|
||||
impl From<(NotificationKind, &'static str)> for Notification {
|
||||
fn from((kind, content): (NotificationKind, &'static str)) -> Self {
|
||||
Self::new().message(content).with_kind(kind)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(NotificationType, SharedString)> for Notification {
|
||||
fn from((type_, content): (NotificationType, SharedString)) -> Self {
|
||||
Self::new().message(content).with_type(type_)
|
||||
impl From<(NotificationKind, SharedString)> for Notification {
|
||||
fn from((kind, content): (NotificationKind, SharedString)) -> Self {
|
||||
Self::new().message(content).with_kind(kind)
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultIdType;
|
||||
|
||||
impl Notification {
|
||||
/// Create a new notification with the given content.
|
||||
/// Create a new notification.
|
||||
///
|
||||
/// default width is 320px.
|
||||
/// The default id is a random UUID.
|
||||
pub fn new() -> Self {
|
||||
let id: SharedString = uuid::Uuid::new_v4().to_string().into();
|
||||
let id = (TypeId::of::<DefaultIdType>(), id.into());
|
||||
@@ -129,7 +121,7 @@ impl Notification {
|
||||
style: StyleRefinement::default(),
|
||||
title: None,
|
||||
message: None,
|
||||
type_: None,
|
||||
kind: None,
|
||||
icon: None,
|
||||
autohide: true,
|
||||
action_builder: None,
|
||||
@@ -139,33 +131,38 @@ impl Notification {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the message of the notification, default is None.
|
||||
pub fn message(mut self, message: impl Into<SharedString>) -> Self {
|
||||
self.message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Create an info notification with the given message.
|
||||
pub fn info(message: impl Into<SharedString>) -> Self {
|
||||
Self::new()
|
||||
.message(message)
|
||||
.with_type(NotificationType::Info)
|
||||
.with_kind(NotificationKind::Info)
|
||||
}
|
||||
|
||||
/// Create a success notification with the given message.
|
||||
pub fn success(message: impl Into<SharedString>) -> Self {
|
||||
Self::new()
|
||||
.message(message)
|
||||
.with_type(NotificationType::Success)
|
||||
.with_kind(NotificationKind::Success)
|
||||
}
|
||||
|
||||
/// Create a warning notification with the given message.
|
||||
pub fn warning(message: impl Into<SharedString>) -> Self {
|
||||
Self::new()
|
||||
.message(message)
|
||||
.with_type(NotificationType::Warning)
|
||||
.with_kind(NotificationKind::Warning)
|
||||
}
|
||||
|
||||
/// Create an error notification with the given message.
|
||||
pub fn error(message: impl Into<SharedString>) -> Self {
|
||||
Self::new()
|
||||
.message(message)
|
||||
.with_type(NotificationType::Error)
|
||||
.with_kind(NotificationKind::Error)
|
||||
}
|
||||
|
||||
/// Set the type for unique identification of the notification.
|
||||
@@ -180,8 +177,8 @@ impl Notification {
|
||||
}
|
||||
|
||||
/// Set the type and id of the notification, used to uniquely identify the notification.
|
||||
pub fn custom_id(mut self, key: impl Into<ElementId>) -> Self {
|
||||
self.id = (TypeId::of::<DefaultIdType>(), key.into()).into();
|
||||
pub fn type_id<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
|
||||
self.id = (TypeId::of::<T>(), key.into()).into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -202,8 +199,8 @@ impl Notification {
|
||||
}
|
||||
|
||||
/// Set the type of the notification, default is NotificationType::Info.
|
||||
pub fn with_type(mut self, type_: NotificationType) -> Self {
|
||||
self.type_ = Some(type_);
|
||||
pub fn with_kind(mut self, kind: NotificationKind) -> Self {
|
||||
self.kind = Some(kind);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -223,22 +220,31 @@ impl Notification {
|
||||
}
|
||||
|
||||
/// Set the action button of the notification.
|
||||
///
|
||||
/// When an action is set, the notification will not autohide.
|
||||
pub fn action<F>(mut self, action: F) -> Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut Context<Self>) -> Button + 'static,
|
||||
F: Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button + 'static,
|
||||
{
|
||||
self.action_builder = Some(Rc::new(action));
|
||||
self.autohide = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Dismiss the notification.
|
||||
pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.closing {
|
||||
return;
|
||||
}
|
||||
self.closing = true;
|
||||
cx.notify();
|
||||
|
||||
// Dismiss the notification after 0.15s to show the animation.
|
||||
cx.spawn(async move |view, cx| {
|
||||
Timer::after(Duration::from_secs_f32(0.15)).await;
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_secs_f32(0.15))
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
if let Some(view) = view.upgrade() {
|
||||
view.update(cx, |view, cx| {
|
||||
@@ -248,13 +254,13 @@ impl Notification {
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach()
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Set the content of the notification.
|
||||
pub fn content(
|
||||
mut self,
|
||||
content: impl Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static,
|
||||
content: impl Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement + 'static,
|
||||
) -> Self {
|
||||
self.content_builder = Some(Rc::new(content));
|
||||
self
|
||||
@@ -276,57 +282,76 @@ impl Styled for Notification {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Notification {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let closing = self.closing;
|
||||
let icon = match self.type_ {
|
||||
let placement = cx.theme().notification.placement;
|
||||
|
||||
let content = self
|
||||
.content_builder
|
||||
.clone()
|
||||
.map(|builder| builder(self, window, cx));
|
||||
|
||||
let action = self
|
||||
.action_builder
|
||||
.clone()
|
||||
.map(|builder| builder(self, window, cx).small().mr_3p5());
|
||||
|
||||
let icon = match self.kind {
|
||||
None => self.icon.clone(),
|
||||
Some(type_) => Some(type_.icon(cx)),
|
||||
Some(kind) => Some(kind.icon(cx)),
|
||||
};
|
||||
|
||||
let background = match self.kind {
|
||||
Some(NotificationKind::Error) => cx.theme().danger_background,
|
||||
_ => cx.theme().surface_background,
|
||||
};
|
||||
|
||||
let text_color = match self.kind {
|
||||
Some(NotificationKind::Error) => cx.theme().danger_foreground,
|
||||
_ => cx.theme().text,
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id("notification")
|
||||
.refine_style(&self.style)
|
||||
.group("")
|
||||
.occlude()
|
||||
.relative()
|
||||
.w_96()
|
||||
.w_112()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().surface_background)
|
||||
.bg(background)
|
||||
.text_color(text_color)
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||
.p_2()
|
||||
.gap_3()
|
||||
.gap_2()
|
||||
.justify_start()
|
||||
.items_start()
|
||||
.refine_style(&self.style)
|
||||
.when_some(icon, |this, icon| {
|
||||
this.child(div().flex_shrink_0().pt_1().child(icon))
|
||||
this.child(div().flex_shrink_0().child(icon))
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.gap_1()
|
||||
.overflow_hidden()
|
||||
.when_some(self.title.clone(), |this, title| {
|
||||
this.child(div().text_sm().font_semibold().child(title))
|
||||
})
|
||||
.when_some(self.message.clone(), |this, message| {
|
||||
this.child(div().text_sm().child(message))
|
||||
this.child(div().text_sm().line_height(relative(1.25)).child(message))
|
||||
})
|
||||
.when_some(self.content_builder.clone(), |this, child_builder| {
|
||||
this.child(child_builder(window, cx))
|
||||
})
|
||||
.when_some(self.action_builder.clone(), |this, action_builder| {
|
||||
this.child(action_builder(window, cx).small().w_full().my_2())
|
||||
.when_some(content, |this, content| this.child(content))
|
||||
.when_some(action, |this, action| {
|
||||
this.child(h_flex().flex_1().gap_1().justify_end().child(action))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_2p5()
|
||||
.right_2p5()
|
||||
.top(px(6.5))
|
||||
.right(px(6.5))
|
||||
.invisible()
|
||||
.group_hover("", |this| this.visible())
|
||||
.child(
|
||||
@@ -334,7 +359,7 @@ impl Render for Notification {
|
||||
.icon(IconName::Close)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||
this.dismiss(window, cx);
|
||||
})),
|
||||
),
|
||||
@@ -345,21 +370,47 @@ impl Render for Notification {
|
||||
on_click(event, window, cx);
|
||||
}))
|
||||
})
|
||||
.on_aux_click(cx.listener(move |view, event: &ClickEvent, window, cx| {
|
||||
if event.is_middle_click() {
|
||||
view.dismiss(window, cx);
|
||||
}
|
||||
}))
|
||||
.with_animation(
|
||||
ElementId::NamedInteger("slide-down".into(), closing as u64),
|
||||
Animation::new(Duration::from_secs_f64(0.25))
|
||||
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
|
||||
move |this, delta| {
|
||||
if closing {
|
||||
let x_offset = px(0.) + delta * px(45.);
|
||||
let opacity = 1. - delta;
|
||||
this.left(px(0.) + x_offset)
|
||||
let that = this
|
||||
.shadow_none()
|
||||
.opacity(opacity)
|
||||
.when(opacity < 0.85, |this| this.shadow_none())
|
||||
.when(opacity < 0.85, |this| this.shadow_none());
|
||||
match placement {
|
||||
Anchor::TopRight | Anchor::BottomRight => {
|
||||
let x_offset = px(0.) + delta * px(45.);
|
||||
that.left(px(0.) + x_offset)
|
||||
}
|
||||
Anchor::TopLeft | Anchor::BottomLeft => {
|
||||
let x_offset = px(0.) - delta * px(45.);
|
||||
that.left(px(0.) + x_offset)
|
||||
}
|
||||
Anchor::TopCenter => {
|
||||
let y_offset = px(0.) - delta * px(45.);
|
||||
that.top(px(0.) + y_offset)
|
||||
}
|
||||
Anchor::BottomCenter => {
|
||||
let y_offset = px(0.) + delta * px(45.);
|
||||
that.top(px(0.) + y_offset)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let y_offset = px(-45.) + delta * px(45.);
|
||||
let opacity = delta;
|
||||
let y_offset = match placement {
|
||||
placement if placement.is_top() => px(-45.) + delta * px(45.),
|
||||
placement if placement.is_bottom() => px(45.) - delta * px(45.),
|
||||
_ => px(0.),
|
||||
};
|
||||
this.top(px(0.) + y_offset)
|
||||
.opacity(opacity)
|
||||
.when(opacity < 0.85, |this| this.shadow_none())
|
||||
@@ -373,7 +424,11 @@ impl Render for Notification {
|
||||
pub struct NotificationList {
|
||||
/// Notifications that will be auto hidden.
|
||||
pub(crate) notifications: VecDeque<Entity<Notification>>,
|
||||
|
||||
/// Whether the notification list is expanded.
|
||||
expanded: bool,
|
||||
|
||||
/// Subscriptions
|
||||
_subscriptions: HashMap<NotificationId, Subscription>,
|
||||
}
|
||||
|
||||
@@ -386,10 +441,12 @@ impl NotificationList {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push<T>(&mut self, notification: T, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
T: Into<Notification>,
|
||||
{
|
||||
pub fn push(
|
||||
&mut self,
|
||||
notification: impl Into<Notification>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let notification = notification.into();
|
||||
let id = notification.id.clone();
|
||||
let autohide = notification.autohide;
|
||||
@@ -411,36 +468,35 @@ impl NotificationList {
|
||||
|
||||
if autohide {
|
||||
// Sleep for 5 seconds to autohide the notification
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
Timer::after(Duration::from_secs(5)).await;
|
||||
cx.spawn_in(window, async move |_this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(5)).await;
|
||||
|
||||
if let Err(error) =
|
||||
if let Err(err) =
|
||||
notification.update_in(cx, |note, window, cx| note.dismiss(window, cx))
|
||||
{
|
||||
log::error!("Failed to auto hide notification: {error}");
|
||||
log::error!("failed to auto hide notification: {:?}", err);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn close<T>(&mut self, key: T, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
T: Into<ElementId>,
|
||||
{
|
||||
let id = (TypeId::of::<DefaultIdType>(), key.into()).into();
|
||||
|
||||
pub(crate) fn close(
|
||||
&mut self,
|
||||
id: impl Into<NotificationId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let id: NotificationId = id.into();
|
||||
if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) {
|
||||
n.update(cx, |note, cx| {
|
||||
note.dismiss(window, cx);
|
||||
});
|
||||
n.update(cx, |note, cx| note.dismiss(window, cx))
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub fn clear(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.notifications.clear();
|
||||
cx.notify();
|
||||
}
|
||||
@@ -451,25 +507,46 @@ impl NotificationList {
|
||||
}
|
||||
|
||||
impl Render for NotificationList {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let size = window.viewport_size();
|
||||
let items = self.notifications.iter().rev().take(10).rev().cloned();
|
||||
|
||||
div()
|
||||
.id("notification-wrapper")
|
||||
.absolute()
|
||||
.top_4()
|
||||
.right_4()
|
||||
.child(
|
||||
let placement = cx.theme().notification.placement;
|
||||
let margins = &cx.theme().notification.margins;
|
||||
|
||||
v_flex()
|
||||
.id("notification-list")
|
||||
.h(size.height - px(8.))
|
||||
.max_h(size.height)
|
||||
.pt(margins.top)
|
||||
.pb(margins.bottom)
|
||||
.gap_3()
|
||||
.children(items)
|
||||
.when(
|
||||
matches!(placement, Anchor::TopRight),
|
||||
|this| this.pr(margins.right), // ignore left
|
||||
)
|
||||
.when(
|
||||
matches!(placement, Anchor::TopLeft),
|
||||
|this| this.pl(margins.left), // ignore right
|
||||
)
|
||||
.when(
|
||||
matches!(placement, Anchor::BottomLeft),
|
||||
|this| this.flex_col_reverse().pl(margins.left), // ignore right
|
||||
)
|
||||
.when(
|
||||
matches!(placement, Anchor::BottomRight),
|
||||
|this| this.flex_col_reverse().pr(margins.right), // ignore left
|
||||
)
|
||||
.when(matches!(placement, Anchor::BottomCenter), |this| {
|
||||
this.flex_col_reverse()
|
||||
})
|
||||
.on_hover(cx.listener(|view, hovered, _, cx| {
|
||||
view.expanded = *hovered;
|
||||
cx.notify()
|
||||
})),
|
||||
)
|
||||
}))
|
||||
.children(items)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
deferred, div, px, AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId,
|
||||
EventEmitter, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding,
|
||||
MouseButton, ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement,
|
||||
Styled, Subscription, Window,
|
||||
AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId, EventEmitter,
|
||||
FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
|
||||
ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement, Styled,
|
||||
Subscription, Window, deferred, div, px,
|
||||
};
|
||||
use theme::Anchor;
|
||||
|
||||
use crate::actions::Cancel;
|
||||
use crate::{anchored, v_flex, Anchor, ElementExt, Selectable, StyledExt as _};
|
||||
use crate::{ElementExt, Selectable, StyledExt as _, anchored, v_flex};
|
||||
|
||||
const CONTEXT: &str = "Popover";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{
|
||||
px, Along, App, Axis, Bounds, Context, ElementId, EventEmitter, IsZero, Pixels, Window,
|
||||
Along, App, Axis, Bounds, Context, ElementId, EventEmitter, IsZero, Pixels, Window, px,
|
||||
};
|
||||
|
||||
mod panel;
|
||||
@@ -142,11 +142,11 @@ impl ResizableState {
|
||||
pub(crate) fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
|
||||
self.panels.remove(panel_ix);
|
||||
self.sizes.remove(panel_ix);
|
||||
if let Some(resizing_panel_ix) = self.resizing_panel_ix {
|
||||
if resizing_panel_ix > panel_ix {
|
||||
if let Some(resizing_panel_ix) = self.resizing_panel_ix
|
||||
&& resizing_panel_ix > panel_ix
|
||||
{
|
||||
self.resizing_panel_ix = Some(resizing_panel_ix - 1);
|
||||
}
|
||||
}
|
||||
self.adjust_to_container_size(cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,15 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty,
|
||||
Entity, EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window,
|
||||
Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, Entity,
|
||||
EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, MouseUpEvent,
|
||||
ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window, div,
|
||||
};
|
||||
use theme::AxisExt;
|
||||
|
||||
use super::{resizable_panel, resize_handle, ResizableState};
|
||||
use super::{ResizableState, resizable_panel, resize_handle};
|
||||
use crate::resizable::PANEL_MIN_SIZE;
|
||||
use crate::{h_flex, v_flex, AxisExt, ElementExt};
|
||||
use crate::{ElementExt, h_flex, v_flex};
|
||||
|
||||
pub enum ResizablePanelEvent {
|
||||
Resized,
|
||||
|
||||
@@ -3,14 +3,13 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, px, AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId,
|
||||
InteractiveElement, IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels,
|
||||
Point, Render, StatefulInteractiveElement, Styled as _, Window,
|
||||
AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, InteractiveElement,
|
||||
IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
|
||||
StatefulInteractiveElement, Styled as _, Window, div, px,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, AxisExt};
|
||||
|
||||
use crate::dock_area::dock::DockPlacement;
|
||||
use crate::AxisExt;
|
||||
use crate::dock::DockPlacement;
|
||||
|
||||
pub(crate) const HANDLE_PADDING: Pixels = px(4.);
|
||||
pub(crate) const HANDLE_SIZE: Pixels = px(1.);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::any::TypeId;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, Edges, Entity,
|
||||
AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, Edges, ElementId, Entity,
|
||||
FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement as _, Pixels, Point, Render, ResizeEdge, SharedString, Size, Styled, Tiling,
|
||||
WeakFocusHandle, Window, canvas, div, point, px, size,
|
||||
ParentElement as _, Pixels, Point, Render, ResizeEdge, Size, Styled, Tiling, WeakFocusHandle,
|
||||
Window, canvas, div, point, px, size,
|
||||
};
|
||||
use theme::{
|
||||
ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
|
||||
@@ -91,7 +92,7 @@ impl Root {
|
||||
pub fn render_notification_layer(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement> {
|
||||
) -> Option<impl IntoElement + use<>> {
|
||||
let root = window.root::<Root>()??;
|
||||
|
||||
Some(
|
||||
@@ -104,7 +105,10 @@ impl Root {
|
||||
}
|
||||
|
||||
/// Render the modal layer.
|
||||
pub fn render_modal_layer(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
|
||||
pub fn render_modal_layer(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement + use<>> {
|
||||
let root = window.root::<Root>()??;
|
||||
let active_modals = root.read(cx).active_modals.clone();
|
||||
|
||||
@@ -138,11 +142,11 @@ impl Root {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(ix) = show_overlay_ix {
|
||||
if let Some(modal) = modals.get_mut(ix) {
|
||||
if let Some(ix) = show_overlay_ix
|
||||
&& let Some(modal) = modals.get_mut(ix)
|
||||
{
|
||||
modal.overlay_visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
Some(div().children(modals))
|
||||
}
|
||||
@@ -213,13 +217,30 @@ impl Root {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Clear a notification by its ID.
|
||||
pub fn clear_notification<T>(&mut self, id: T, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
T: Into<SharedString>,
|
||||
{
|
||||
self.notification
|
||||
.update(cx, |view, cx| view.close(id.into(), window, cx));
|
||||
/// Clear a notification by its type.
|
||||
pub fn clear_notification<T: Sized + 'static>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Root>,
|
||||
) {
|
||||
self.notification.update(cx, |view, cx| {
|
||||
let id = TypeId::of::<T>();
|
||||
view.close(id, window, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Clear a notification by its type.
|
||||
pub fn clear_notification_by_id<T: Sized + 'static>(
|
||||
&mut self,
|
||||
key: impl Into<ElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Root>,
|
||||
) {
|
||||
self.notification.update(cx, |view, cx| {
|
||||
let id = (TypeId::of::<T>(), key.into());
|
||||
view.close(id, window, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use gpui::{
|
||||
px, relative, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId,
|
||||
EntityId, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels,
|
||||
Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window,
|
||||
App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId, EntityId,
|
||||
GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point,
|
||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window, px, relative,
|
||||
};
|
||||
|
||||
use crate::AxisExt;
|
||||
use theme::AxisExt;
|
||||
|
||||
/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening.
|
||||
///
|
||||
|
||||
@@ -11,9 +11,7 @@ use gpui::{
|
||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill,
|
||||
point, px, relative, size,
|
||||
};
|
||||
use theme::{ActiveTheme, ScrollbarMode};
|
||||
|
||||
use crate::AxisExt;
|
||||
use theme::{ActiveTheme, AxisExt, ScrollbarMode};
|
||||
|
||||
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
|
||||
const WIDTH: Pixels = px(1. * 2. + 8.);
|
||||
@@ -54,7 +52,7 @@ impl ScrollbarHandle for ScrollHandle {
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.max_offset() + self.bounds().size
|
||||
Size::from(self.max_offset()) + self.bounds().size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +67,7 @@ impl ScrollbarHandle for UniformListScrollHandle {
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
let base_handle = &self.0.borrow().base_handle;
|
||||
base_handle.max_offset() + base_handle.bounds().size
|
||||
Size::from(base_handle.max_offset()) + base_handle.bounds().size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +81,7 @@ impl ScrollbarHandle for ListState {
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.viewport_bounds().size + self.max_offset_for_scrollbar()
|
||||
Size::from(self.max_offset_for_scrollbar()) + self.viewport_bounds().size
|
||||
}
|
||||
|
||||
fn start_drag(&self) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use gpui::{div, px, App, Div, Pixels, Refineable, StyleRefinement, Styled};
|
||||
use gpui::{App, DefiniteLength, Div, Edges, Pixels, Refineable, StyleRefinement, Styled, div, px};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
@@ -46,6 +46,30 @@ pub trait StyledExt: Styled + Sized {
|
||||
self.flex().flex_col()
|
||||
}
|
||||
|
||||
/// Apply paddings to the element.
|
||||
fn paddings<L>(self, paddings: impl Into<Edges<L>>) -> Self
|
||||
where
|
||||
L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
|
||||
{
|
||||
let paddings = paddings.into();
|
||||
self.pt(paddings.top.into())
|
||||
.pb(paddings.bottom.into())
|
||||
.pl(paddings.left.into())
|
||||
.pr(paddings.right.into())
|
||||
}
|
||||
|
||||
/// Apply margins to the element.
|
||||
fn margins<L>(self, margins: impl Into<Edges<L>>) -> Self
|
||||
where
|
||||
L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
|
||||
{
|
||||
let margins = margins.into();
|
||||
self.mt(margins.top.into())
|
||||
.mb(margins.bottom.into())
|
||||
.ml(margins.left.into())
|
||||
.mr(margins.right.into())
|
||||
}
|
||||
|
||||
font_weight!(font_thin, THIN);
|
||||
font_weight!(font_extralight, EXTRA_LIGHT);
|
||||
font_weight!(font_light, LIGHT);
|
||||
|
||||
@@ -4,13 +4,13 @@ use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, px, white, Animation, AnimationExt as _, AnyElement, App, Element, ElementId,
|
||||
GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString,
|
||||
Styled as _, Window,
|
||||
Animation, AnimationExt as _, AnyElement, App, Element, ElementId, GlobalElementId,
|
||||
InteractiveElement, IntoElement, LayoutId, ParentElement as _, SharedString, Styled as _,
|
||||
Window, div, px, white,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, Side};
|
||||
|
||||
use crate::{Disableable, Side, Sizable, Size};
|
||||
use crate::{Disableable, Sizable, Size};
|
||||
|
||||
type OnClick = Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>;
|
||||
|
||||
|
||||
@@ -1,74 +1,557 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
|
||||
RenderOnce, StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Selectable, Sizable, Size};
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, Div, Edges, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
div, px, relative,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{Icon, IconName, Selectable, Sizable, Size, StyledExt, h_flex};
|
||||
|
||||
pub mod tab_bar;
|
||||
|
||||
/// Tab variants.
|
||||
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TabVariant {
|
||||
#[default]
|
||||
Tab,
|
||||
Outline,
|
||||
Pill,
|
||||
Segmented,
|
||||
Underline,
|
||||
}
|
||||
|
||||
impl TabVariant {
|
||||
fn height(&self, size: Size) -> Pixels {
|
||||
match size {
|
||||
Size::XSmall => match self {
|
||||
TabVariant::Underline => px(26.),
|
||||
_ => px(20.),
|
||||
},
|
||||
Size::Small => match self {
|
||||
TabVariant::Underline => px(30.),
|
||||
_ => px(24.),
|
||||
},
|
||||
Size::Large => match self {
|
||||
TabVariant::Underline => px(44.),
|
||||
_ => px(36.),
|
||||
},
|
||||
_ => match self {
|
||||
TabVariant::Underline => px(36.),
|
||||
_ => px(32.),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_height(&self, size: Size) -> Pixels {
|
||||
match size {
|
||||
Size::XSmall => match self {
|
||||
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(18.),
|
||||
TabVariant::Segmented => px(16.),
|
||||
TabVariant::Underline => px(20.),
|
||||
},
|
||||
Size::Small => match self {
|
||||
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(22.),
|
||||
TabVariant::Segmented => px(18.),
|
||||
TabVariant::Underline => px(22.),
|
||||
},
|
||||
Size::Large => match self {
|
||||
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(36.),
|
||||
TabVariant::Segmented => px(28.),
|
||||
TabVariant::Underline => px(32.),
|
||||
},
|
||||
_ => match self {
|
||||
TabVariant::Tab => px(30.),
|
||||
TabVariant::Outline | TabVariant::Pill => px(26.),
|
||||
TabVariant::Segmented => px(24.),
|
||||
TabVariant::Underline => px(26.),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Default px(12) to match panel px_3, See [`crate::dock::TabPanel`]
|
||||
fn inner_paddings(&self, size: Size) -> Edges<Pixels> {
|
||||
let mut padding_x = match size {
|
||||
Size::XSmall => px(8.),
|
||||
Size::Small => px(10.),
|
||||
Size::Large => px(16.),
|
||||
_ => px(12.),
|
||||
};
|
||||
|
||||
if matches!(self, TabVariant::Underline) {
|
||||
padding_x = px(0.);
|
||||
}
|
||||
|
||||
Edges {
|
||||
left: padding_x,
|
||||
right: padding_x,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_margins(&self, size: Size) -> Edges<Pixels> {
|
||||
match size {
|
||||
Size::XSmall => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
top: px(1.),
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
Size::Small => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
top: px(2.),
|
||||
bottom: px(3.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
Size::Large => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
top: px(5.),
|
||||
bottom: px(6.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
_ => match self {
|
||||
TabVariant::Underline => Edges {
|
||||
top: px(3.),
|
||||
bottom: px(4.),
|
||||
..Default::default()
|
||||
},
|
||||
_ => Edges::all(px(0.)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn normal(&self, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Outline => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: cx.theme().border,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().text,
|
||||
bg: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
inner_bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hovered(&self, selected: bool, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Outline => TabStyle {
|
||||
fg: cx.theme().secondary_foreground,
|
||||
bg: cx.theme().secondary_hover,
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: cx.theme().border,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().secondary_foreground,
|
||||
bg: cx.theme().secondary_background,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
inner_bg: if selected {
|
||||
cx.theme().background
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().tab_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
inner_bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: gpui::transparent_black(),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn selected(&self, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().tab_active_foreground,
|
||||
bg: cx.theme().tab_active_background,
|
||||
borders: Edges {
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: cx.theme().border,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Outline => TabStyle {
|
||||
fg: cx.theme().text_accent,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: cx.theme().element_active,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: cx.theme().element_foreground,
|
||||
bg: cx.theme().element_background,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().tab_active_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
inner_bg: cx.theme().background,
|
||||
shadow: true,
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().tab_active_foreground,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
border_color: cx.theme().element_active,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled(&self, selected: bool, cx: &App) -> TabStyle {
|
||||
match self {
|
||||
TabVariant::Tab => TabStyle {
|
||||
fg: cx.theme().text_muted,
|
||||
bg: gpui::transparent_black(),
|
||||
border_color: if selected {
|
||||
cx.theme().border
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
borders: Edges {
|
||||
left: px(1.),
|
||||
right: px(1.),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Outline => TabStyle {
|
||||
fg: cx.theme().text_muted,
|
||||
bg: gpui::transparent_black(),
|
||||
borders: Edges::all(px(1.)),
|
||||
border_color: if selected {
|
||||
cx.theme().element_active
|
||||
} else {
|
||||
cx.theme().border
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Pill => TabStyle {
|
||||
fg: if selected {
|
||||
cx.theme().element_foreground.opacity(0.5)
|
||||
} else {
|
||||
cx.theme().text_muted
|
||||
},
|
||||
bg: if selected {
|
||||
cx.theme().element_background.opacity(0.5)
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Segmented => TabStyle {
|
||||
fg: cx.theme().text_muted,
|
||||
bg: cx.theme().tab_background,
|
||||
inner_bg: if selected {
|
||||
cx.theme().background
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
TabVariant::Underline => TabStyle {
|
||||
fg: cx.theme().text_muted,
|
||||
bg: gpui::transparent_black(),
|
||||
border_color: if selected {
|
||||
cx.theme().border
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
},
|
||||
borders: Edges {
|
||||
bottom: px(2.),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn tab_bar_radius(&self, size: Size, cx: &App) -> Pixels {
|
||||
if *self != TabVariant::Segmented {
|
||||
return px(0.);
|
||||
}
|
||||
|
||||
match size {
|
||||
Size::XSmall | Size::Small => cx.theme().radius,
|
||||
Size::Large => cx.theme().radius_lg,
|
||||
_ => cx.theme().radius_lg,
|
||||
}
|
||||
}
|
||||
|
||||
fn radius(&self, size: Size, cx: &App) -> Pixels {
|
||||
match self {
|
||||
TabVariant::Outline | TabVariant::Pill => px(99.),
|
||||
TabVariant::Segmented => match size {
|
||||
Size::XSmall | Size::Small => cx.theme().radius,
|
||||
Size::Large => cx.theme().radius_lg,
|
||||
_ => cx.theme().radius_lg,
|
||||
},
|
||||
_ => px(0.),
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_radius(&self, size: Size, cx: &App) -> Pixels {
|
||||
match self {
|
||||
TabVariant::Segmented => match size {
|
||||
Size::Large => self.tab_bar_radius(size, cx) - px(3.),
|
||||
_ => self.tab_bar_radius(size, cx) - px(2.),
|
||||
},
|
||||
_ => px(0.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct TabStyle {
|
||||
borders: Edges<Pixels>,
|
||||
border_color: Hsla,
|
||||
bg: Hsla,
|
||||
fg: Hsla,
|
||||
shadow: bool,
|
||||
inner_bg: Hsla,
|
||||
}
|
||||
|
||||
impl Default for TabStyle {
|
||||
fn default() -> Self {
|
||||
TabStyle {
|
||||
borders: Edges::all(px(0.)),
|
||||
border_color: gpui::transparent_white(),
|
||||
bg: gpui::transparent_white(),
|
||||
fg: gpui::transparent_white(),
|
||||
shadow: false,
|
||||
inner_bg: gpui::transparent_white(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// A Tab element for the [`super::TabBar`].
|
||||
#[derive(IntoElement)]
|
||||
pub struct Tab {
|
||||
ix: usize,
|
||||
base: Div,
|
||||
label: Option<AnyElement>,
|
||||
pub(super) label: Option<SharedString>,
|
||||
icon: Option<Icon>,
|
||||
prefix: Option<AnyElement>,
|
||||
pub(super) tab_bar_prefix: Option<bool>,
|
||||
suffix: Option<AnyElement>,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
children: Vec<AnyElement>,
|
||||
variant: TabVariant,
|
||||
size: Size,
|
||||
pub(super) disabled: bool,
|
||||
pub(super) selected: bool,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ix: 0,
|
||||
base: div(),
|
||||
label: None,
|
||||
disabled: false,
|
||||
selected: false,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
size: Size::default(),
|
||||
impl From<&'static str> for Tab {
|
||||
fn from(label: &'static str) -> Self {
|
||||
Self::new().label(label)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set label for the tab.
|
||||
pub fn label(mut self, label: impl Into<AnyElement>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
impl From<String> for Tab {
|
||||
fn from(label: String) -> Self {
|
||||
Self::new().label(label)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the left side of the tab
|
||||
pub fn prefix(mut self, prefix: impl Into<AnyElement>) -> Self {
|
||||
self.prefix = Some(prefix.into());
|
||||
self
|
||||
impl From<SharedString> for Tab {
|
||||
fn from(label: SharedString) -> Self {
|
||||
Self::new().label(label)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the right side of the tab
|
||||
pub fn suffix(mut self, suffix: impl Into<AnyElement>) -> Self {
|
||||
self.suffix = Some(suffix.into());
|
||||
self
|
||||
impl From<Icon> for Tab {
|
||||
fn from(icon: Icon) -> Self {
|
||||
Self::default().icon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set disabled state to the tab
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set index to the tab.
|
||||
pub fn ix(mut self, ix: usize) -> Self {
|
||||
self.ix = ix;
|
||||
self
|
||||
impl From<IconName> for Tab {
|
||||
fn from(icon_name: IconName) -> Self {
|
||||
Self::default().icon(Icon::new(icon_name))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Tab {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
Self {
|
||||
ix: 0,
|
||||
base: div(),
|
||||
label: None,
|
||||
icon: None,
|
||||
tab_bar_prefix: None,
|
||||
children: Vec::new(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
variant: TabVariant::default(),
|
||||
size: Size::default(),
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
/// Create a new tab with a label.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set label for the tab.
|
||||
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set icon for the tab.
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set Tab Variant.
|
||||
pub fn with_variant(mut self, variant: TabVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use Pill variant.
|
||||
pub fn pill(mut self) -> Self {
|
||||
self.variant = TabVariant::Pill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use outline variant.
|
||||
pub fn outline(mut self) -> Self {
|
||||
self.variant = TabVariant::Outline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use Segmented variant.
|
||||
pub fn segmented(mut self) -> Self {
|
||||
self.variant = TabVariant::Segmented;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use Underline variant.
|
||||
pub fn underline(mut self) -> Self {
|
||||
self.variant = TabVariant::Underline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the left side of the tab
|
||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||
self.prefix = Some(prefix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the right side of the tab
|
||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||
self.suffix = Some(suffix.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set disabled state to the tab, default false.
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the click handler for the tab.
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Rc::new(on_click));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set index to the tab.
|
||||
pub(crate) fn ix(mut self, ix: usize) -> Self {
|
||||
self.ix = ix;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set if the tab bar has a prefix.
|
||||
pub(crate) fn tab_bar_prefix(mut self, tab_bar_prefix: bool) -> Self {
|
||||
self.tab_bar_prefix = Some(tab_bar_prefix);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Tab {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,62 +588,115 @@ impl Sizable for Tab {
|
||||
}
|
||||
|
||||
impl RenderOnce for Tab {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let (text_color, hover_text_color, bg_color, border_color) =
|
||||
match (self.selected, self.disabled) {
|
||||
(true, false) => (
|
||||
cx.theme().tab_active_foreground,
|
||||
cx.theme().tab_hover_foreground,
|
||||
cx.theme().tab_active_background,
|
||||
cx.theme().border,
|
||||
),
|
||||
(false, false) => (
|
||||
cx.theme().tab_inactive_foreground,
|
||||
cx.theme().tab_hover_foreground,
|
||||
cx.theme().ghost_element_background,
|
||||
cx.theme().border_transparent,
|
||||
),
|
||||
(true, true) => (
|
||||
cx.theme().tab_inactive_foreground,
|
||||
cx.theme().tab_hover_foreground,
|
||||
cx.theme().ghost_element_background,
|
||||
cx.theme().border_disabled,
|
||||
),
|
||||
(false, true) => (
|
||||
cx.theme().tab_inactive_foreground,
|
||||
cx.theme().tab_hover_foreground,
|
||||
cx.theme().ghost_element_background,
|
||||
cx.theme().border_disabled,
|
||||
),
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let mut tab_style = if self.selected {
|
||||
self.variant.selected(cx)
|
||||
} else {
|
||||
self.variant.normal(cx)
|
||||
};
|
||||
|
||||
let mut hover_style = self.variant.hovered(self.selected, cx);
|
||||
|
||||
if self.disabled {
|
||||
tab_style = self.variant.disabled(self.selected, cx);
|
||||
hover_style = self.variant.disabled(self.selected, cx);
|
||||
}
|
||||
|
||||
let tab_bar_prefix = self.tab_bar_prefix.unwrap_or_default();
|
||||
|
||||
if !tab_bar_prefix && self.ix == 0 && self.variant == TabVariant::Tab {
|
||||
tab_style.borders.left = px(0.);
|
||||
hover_style.borders.left = px(0.);
|
||||
}
|
||||
|
||||
let radius = self.variant.radius(self.size, cx);
|
||||
let inner_radius = self.variant.inner_radius(self.size, cx);
|
||||
let inner_paddings = self.variant.inner_paddings(self.size);
|
||||
let inner_margins = self.variant.inner_margins(self.size);
|
||||
let inner_height = self.variant.inner_height(self.size);
|
||||
let height = self.variant.height(self.size);
|
||||
|
||||
self.base
|
||||
.id(self.ix)
|
||||
.h(TABBAR_HEIGHT)
|
||||
.px_4()
|
||||
.relative()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.flex_shrink_0()
|
||||
.cursor_pointer()
|
||||
.h(height)
|
||||
.overflow_hidden()
|
||||
.text_xs()
|
||||
.text_ellipsis()
|
||||
.text_color(text_color)
|
||||
.bg(bg_color)
|
||||
.border_l(px(1.))
|
||||
.border_r(px(1.))
|
||||
.border_color(border_color)
|
||||
.text_color(tab_style.fg)
|
||||
.map(|this| match self.size {
|
||||
Size::XSmall => this.text_xs(),
|
||||
Size::Large => this.text_base(),
|
||||
_ => this.text_sm(),
|
||||
})
|
||||
.bg(tab_style.bg)
|
||||
.border_l(tab_style.borders.left)
|
||||
.border_r(tab_style.borders.right)
|
||||
.border_t(tab_style.borders.top)
|
||||
.border_b(tab_style.borders.bottom)
|
||||
.border_color(tab_style.border_color)
|
||||
.rounded(radius)
|
||||
.when(!self.selected && !self.disabled, |this| {
|
||||
this.hover(|this| this.text_color(hover_text_color))
|
||||
this.hover(|this| {
|
||||
this.text_color(hover_style.fg)
|
||||
.bg(hover_style.bg)
|
||||
.border_l(hover_style.borders.left)
|
||||
.border_r(hover_style.borders.right)
|
||||
.border_t(hover_style.borders.top)
|
||||
.border_b(hover_style.borders.bottom)
|
||||
.border_color(hover_style.border_color)
|
||||
.rounded(radius)
|
||||
})
|
||||
.when_some(self.prefix, |this, prefix| {
|
||||
this.child(prefix).text_color(text_color)
|
||||
})
|
||||
.when_some(self.label, |this, label| this.child(label))
|
||||
.when_some(self.suffix, |this, suffix| this.child(suffix))
|
||||
.on_mouse_down(MouseButton::Left, |_ev, _window, cx| {
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.h(inner_height)
|
||||
.line_height(relative(1.))
|
||||
.whitespace_nowrap()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.overflow_hidden()
|
||||
.margins(inner_margins)
|
||||
.flex_shrink_0()
|
||||
.map(|this| match self.icon {
|
||||
Some(icon) => {
|
||||
this.w(inner_height * 1.25)
|
||||
.child(icon.map(|this| match self.size {
|
||||
Size::XSmall => this.size_2p5(),
|
||||
Size::Small => this.size_3p5(),
|
||||
Size::Large => this.size_4(),
|
||||
_ => this.size_4(),
|
||||
}))
|
||||
}
|
||||
None => this
|
||||
.paddings(inner_paddings)
|
||||
.map(|this| match self.label {
|
||||
Some(label) => this.child(label),
|
||||
None => this,
|
||||
})
|
||||
.children(self.children),
|
||||
})
|
||||
.bg(tab_style.inner_bg)
|
||||
.rounded(inner_radius)
|
||||
.when(tab_style.shadow, |this| this.shadow_xs())
|
||||
.hover(|this| this.bg(hover_style.inner_bg).rounded(inner_radius)),
|
||||
)
|
||||
.when_some(self.suffix, |this, suffix| {
|
||||
this.child(div().pr_2().child(suffix))
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| {
|
||||
// Stop propagation behavior, for works on TitleBar.
|
||||
// https://github.com/longbridge/gpui-component/issues/1836
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.when(!self.disabled, |this| {
|
||||
this.when_some(self.on_click.clone(), |this, on_click| {
|
||||
this.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,92 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use gpui::Pixels;
|
||||
use gpui::{
|
||||
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
ScrollHandle, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
|
||||
AnyElement, App, Corner, Div, Edges, ElementId, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, ScrollHandle, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled,
|
||||
Window, div, px,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{h_flex, Sizable, Size, StyledExt};
|
||||
use super::{Tab, TabVariant};
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::menu::{DropdownMenu as _, PopupMenuItem};
|
||||
use crate::{IconName, Selectable, Sizable, Size, StyledExt, h_flex};
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// A TabBar element that contains multiple [`Tab`] items.
|
||||
#[derive(IntoElement)]
|
||||
pub struct TabBar {
|
||||
base: Div,
|
||||
base: Stateful<Div>,
|
||||
style: StyleRefinement,
|
||||
scroll_handle: Option<ScrollHandle>,
|
||||
prefix: Option<AnyElement>,
|
||||
suffix: Option<AnyElement>,
|
||||
children: SmallVec<[Tab; 2]>,
|
||||
last_empty_space: AnyElement,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
selected_index: Option<usize>,
|
||||
variant: TabVariant,
|
||||
size: Size,
|
||||
menu: bool,
|
||||
on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl TabBar {
|
||||
pub fn new() -> Self {
|
||||
/// Create a new TabBar.
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
base: h_flex().px(px(-1.)),
|
||||
base: div().id(id).px(px(-1.)),
|
||||
style: StyleRefinement::default(),
|
||||
scroll_handle: None,
|
||||
children: SmallVec::new(),
|
||||
scroll_handle: None,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
variant: TabVariant::default(),
|
||||
size: Size::default(),
|
||||
last_empty_space: div().w_3().into_any_element(),
|
||||
selected_index: None,
|
||||
on_click: None,
|
||||
menu: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the Tab variant, all children will inherit the variant.
|
||||
pub fn with_variant(mut self, variant: TabVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Pill, all children will inherit the variant.
|
||||
pub fn pill(mut self) -> Self {
|
||||
self.variant = TabVariant::Pill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Outline, all children will inherit the variant.
|
||||
pub fn outline(mut self) -> Self {
|
||||
self.variant = TabVariant::Outline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Segmented, all children will inherit the variant.
|
||||
pub fn segmented(mut self) -> Self {
|
||||
self.variant = TabVariant::Segmented;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Tab variant to Underline, all children will inherit the variant.
|
||||
pub fn underline(mut self) -> Self {
|
||||
self.variant = TabVariant::Underline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to show the menu button when tabs overflow, default is false.
|
||||
pub fn menu(mut self, menu: bool) -> Self {
|
||||
self.menu = menu;
|
||||
self
|
||||
}
|
||||
|
||||
/// Track the scroll of the TabBar.
|
||||
pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
|
||||
self.scroll_handle = Some(scroll_handle.clone());
|
||||
@@ -54,27 +105,39 @@ impl TabBar {
|
||||
self
|
||||
}
|
||||
|
||||
/// Add children of the TabBar, all children will inherit the variant.
|
||||
pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Tab>>) -> Self {
|
||||
self.children.extend(children.into_iter().map(Into::into));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add child of the TabBar, tab will inherit the variant.
|
||||
pub fn child(mut self, child: impl Into<Tab>) -> Self {
|
||||
self.children.push(child.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the selected index of the TabBar.
|
||||
pub fn selected_index(mut self, index: usize) -> Self {
|
||||
self.selected_index = Some(index);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the last empty space element of the TabBar.
|
||||
pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
|
||||
self.last_empty_space = last_empty_space.into_any_element();
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn height(window: &mut Window) -> Pixels {
|
||||
(1.75 * window.rem_size()).max(px(36.))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TabBar {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for TabBar {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
/// Set the on_click callback of the TabBar, the first parameter is the index of the clicked tab.
|
||||
///
|
||||
/// When this is set, the children's on_click will be ignored.
|
||||
pub fn on_click<F>(mut self, on_click: F) -> Self
|
||||
where
|
||||
F: Fn(&usize, &mut Window, &mut App) + 'static,
|
||||
{
|
||||
self.on_click = Some(Rc::new(on_click));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,15 +155,69 @@ impl Sizable for TabBar {
|
||||
}
|
||||
|
||||
impl RenderOnce for TabBar {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let default_gap = match self.size {
|
||||
Size::Small | Size::XSmall => px(8.),
|
||||
Size::Large => px(16.),
|
||||
_ => px(12.),
|
||||
};
|
||||
let (bg, paddings, gap) = match self.variant {
|
||||
TabVariant::Tab => {
|
||||
let padding = Edges::all(px(0.));
|
||||
(cx.theme().tab_background, padding, px(0.))
|
||||
}
|
||||
TabVariant::Outline => {
|
||||
let padding = Edges::all(px(0.));
|
||||
(gpui::transparent_black(), padding, default_gap)
|
||||
}
|
||||
TabVariant::Pill => {
|
||||
let padding = Edges::all(px(0.));
|
||||
(gpui::transparent_black(), padding, px(4.))
|
||||
}
|
||||
TabVariant::Segmented => {
|
||||
let padding_x = match self.size {
|
||||
Size::XSmall => px(2.),
|
||||
Size::Small => px(3.),
|
||||
_ => px(4.),
|
||||
};
|
||||
let padding = Edges {
|
||||
left: padding_x,
|
||||
right: padding_x,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
(cx.theme().tab_background, padding, px(2.))
|
||||
}
|
||||
TabVariant::Underline => {
|
||||
// This gap is same as the tab inner_paddings
|
||||
let gap = match self.size {
|
||||
Size::XSmall => px(10.),
|
||||
Size::Small => px(12.),
|
||||
Size::Large => px(20.),
|
||||
_ => px(16.),
|
||||
};
|
||||
|
||||
(gpui::transparent_black(), Edges::all(px(0.)), gap)
|
||||
}
|
||||
};
|
||||
|
||||
let mut item_labels = Vec::new();
|
||||
let selected_index = self.selected_index;
|
||||
let on_click = self.on_click.clone();
|
||||
|
||||
self.base
|
||||
.group("tab-bar")
|
||||
.relative()
|
||||
.refine_style(&self.style)
|
||||
.bg(cx.theme().surface_background)
|
||||
.child(
|
||||
.flex()
|
||||
.items_center()
|
||||
.bg(bg)
|
||||
.text_color(cx.theme().tab_foreground)
|
||||
.when(
|
||||
self.variant == TabVariant::Underline || self.variant == TabVariant::Tab,
|
||||
|this| {
|
||||
this.child(
|
||||
div()
|
||||
.id("border-bottom")
|
||||
.id("border-b")
|
||||
.absolute()
|
||||
.left_0()
|
||||
.bottom_0()
|
||||
@@ -108,21 +225,66 @@ impl RenderOnce for TabBar {
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border),
|
||||
)
|
||||
.text_color(cx.theme().text)
|
||||
},
|
||||
)
|
||||
.rounded(self.variant.tab_bar_radius(self.size, cx))
|
||||
.paddings(paddings)
|
||||
.refine_style(&self.style)
|
||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||
.child(
|
||||
h_flex()
|
||||
.id("tabs")
|
||||
.flex_grow()
|
||||
.flex_1()
|
||||
.overflow_x_scroll()
|
||||
.when_some(self.scroll_handle, |this, scroll_handle| {
|
||||
this.track_scroll(&scroll_handle)
|
||||
})
|
||||
.children(self.children)
|
||||
.when(self.suffix.is_some(), |this| {
|
||||
.gap(gap)
|
||||
.children(self.children.into_iter().enumerate().map(|(ix, child)| {
|
||||
item_labels.push((child.label.clone(), child.disabled));
|
||||
let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true);
|
||||
child
|
||||
.ix(ix)
|
||||
.tab_bar_prefix(tab_bar_prefix)
|
||||
.with_variant(self.variant)
|
||||
.with_size(self.size)
|
||||
.when_some(self.selected_index, |this, selected_ix| {
|
||||
this.selected(selected_ix == ix)
|
||||
})
|
||||
.when_some(self.on_click.clone(), move |this, on_click| {
|
||||
this.on_click(move |_, window, cx| on_click(&ix, window, cx))
|
||||
})
|
||||
}))
|
||||
.when(self.suffix.is_some() || self.menu, |this| {
|
||||
this.child(self.last_empty_space)
|
||||
}),
|
||||
)
|
||||
.when(self.menu, |this| {
|
||||
this.child(
|
||||
Button::new("more")
|
||||
.xsmall()
|
||||
.ghost()
|
||||
.icon(IconName::ChevronDown)
|
||||
.dropdown_menu(move |mut this, _, _| {
|
||||
this = this.scrollable(true);
|
||||
for (ix, (label, disabled)) in item_labels.iter().enumerate() {
|
||||
this = this.item(
|
||||
PopupMenuItem::new(label.clone().unwrap_or_default())
|
||||
.checked(selected_index == Some(ix))
|
||||
.disabled(*disabled)
|
||||
.when_some(on_click.clone(), |this, on_click| {
|
||||
this.on_click(move |_, window, cx| {
|
||||
on_click(&ix, window, cx)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
this
|
||||
})
|
||||
.anchor(Corner::TopRight),
|
||||
)
|
||||
})
|
||||
.when_some(self.suffix, |this, suffix| this.child(suffix))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{App, Entity, SharedString, Window};
|
||||
use gpui::{App, ElementId, Entity, Window};
|
||||
|
||||
use crate::Root;
|
||||
use crate::input::InputState;
|
||||
use crate::modal::Modal;
|
||||
use crate::notification::Notification;
|
||||
use crate::Root;
|
||||
|
||||
/// Extension trait for [`Window`] to add modal, notification .. functionality.
|
||||
pub trait WindowExtension: Sized {
|
||||
@@ -31,10 +31,15 @@ pub trait WindowExtension: Sized {
|
||||
where
|
||||
T: Into<Notification>;
|
||||
|
||||
/// Clears a notification by its ID.
|
||||
fn clear_notification<T>(&mut self, id: T, cx: &mut App)
|
||||
where
|
||||
T: Into<SharedString>;
|
||||
/// Clear the unique notification.
|
||||
fn clear_notification<T: Sized + 'static>(&mut self, cx: &mut App);
|
||||
|
||||
/// Clear the unique notification with the given id.
|
||||
fn clear_notification_by_id<T: Sized + 'static>(
|
||||
&mut self,
|
||||
key: impl Into<ElementId>,
|
||||
cx: &mut App,
|
||||
);
|
||||
|
||||
/// Clear all notifications
|
||||
fn clear_notifications(&mut self, cx: &mut App);
|
||||
@@ -88,13 +93,21 @@ impl WindowExtension for Window {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clear_notification<T>(&mut self, id: T, cx: &mut App)
|
||||
where
|
||||
T: Into<SharedString>,
|
||||
{
|
||||
let id = id.into();
|
||||
Root::update(self, cx, move |root, window, cx| {
|
||||
root.clear_notification(id, window, cx);
|
||||
fn clear_notification<T: Sized + 'static>(&mut self, cx: &mut App) {
|
||||
Root::update(self, cx, |root, window, cx| {
|
||||
root.clear_notification::<T>(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clear_notification_by_id<T: Sized + 'static>(
|
||||
&mut self,
|
||||
key: impl Into<ElementId>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let key: ElementId = key.into();
|
||||
Root::update(self, cx, |root, window, cx| {
|
||||
root.clear_notification_by_id::<T>(key, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -34,13 +34,9 @@ update_version() {
|
||||
# Backup the original file
|
||||
cp "$file" "$backup"
|
||||
|
||||
# Replace the version in Cargo.toml
|
||||
if sed -i.bak -E "s/^version = \"[0-9]+\.[0-9]+\.[0-9]+\"/version = \"$NEW_VERSION\"/" "$file"; then
|
||||
# More flexible regex that handles various version formats and whitespace
|
||||
if sed -i -E "s/^[[:space:]]*version[[:space:]]*=[[:space:]]*\"[^\"]+\"/version = \"$NEW_VERSION\"/" "$file"; then
|
||||
echo "✓ Updated version to $NEW_VERSION in $file"
|
||||
# Remove backup created by sed
|
||||
if [ -f "${file}.bak" ]; then
|
||||
rm "${file}.bak"
|
||||
fi
|
||||
else
|
||||
echo "Error: Failed to update version in $file"
|
||||
# Restore original backup
|
||||
@@ -48,7 +44,7 @@ update_version() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove the initial backup file
|
||||
# Remove the backup file
|
||||
rm -f "$backup"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user