Compare commits
35 Commits
b7ffdc8431
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b41de00c95 | |||
| 94cbb4aa0e | |||
| 40e7ca368b | |||
| b91697defc | |||
| 1d57a2deab | |||
| aa26c9ccba | |||
| ff61c28a76 | |||
| 069eae8145 | |||
| ccbcc644db | |||
| 15c5ce7677 | |||
| 40d726c986 | |||
| fe4eb7df74 | |||
| b5d6d91851 | |||
| d475d03d0c | |||
| 0f00fed122 | |||
| ef73b3c629 | |||
| bbf31baee5 | |||
| 80227b3ed3 | |||
| d00c5a1982 | |||
| c054017d7e | |||
| d065e70cd1 | |||
| 7a6b6feacc | |||
| 55c5ebbf17 | |||
| 3fecda175b | |||
| 2423cdca19 | |||
| 4b021bef01 | |||
| dcf28e2b60 | |||
| 624140c061 | |||
| fcb2b671e7 | |||
| a86219dcb0 | |||
| c22a7291c7 | |||
| d7996bf32e | |||
| 2dcf825105 | |||
| 3debfa81d7 | |||
| 4ba2049756 |
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
os: windows-11-arm
|
os: windows-11-arm
|
||||||
target: aarch64-pc-windows-msvc
|
target: aarch64-pc-windows-msvc
|
||||||
- platform: macos-x64
|
- platform: macos-x64
|
||||||
os: macos-13
|
os: macos-15-intel
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
- platform: macos-arm64
|
- platform: macos-arm64
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
@@ -130,7 +130,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Make get-crate-version executable
|
- name: Make get-crate-version executable
|
||||||
run: chmod +x script/get-crate-version
|
run: chmod +x script/get-crate-version
|
||||||
@@ -163,8 +163,6 @@ jobs:
|
|||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: |
|
files: |
|
||||||
artifacts/**/*
|
artifacts/**/*
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Output release info
|
- name: Output release info
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
975
Cargo.lock
generated
975
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@@ -4,15 +4,14 @@ members = ["crates/*"]
|
|||||||
default-members = ["crates/coop"]
|
default-members = ["crates/coop"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.0"
|
version = "1.0.0-beta2"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
||||||
# GPUI
|
# GPUI
|
||||||
gpui = { git = "https://github.com/zed-industries/zed" }
|
gpui = { git = "https://github.com/zed-industries/zed" }
|
||||||
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "screen-capture", "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_linux = { git = "https://github.com/zed-industries/zed" }
|
||||||
gpui_windows = { 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" }
|
gpui_macos = { git = "https://github.com/zed-industries/zed" }
|
||||||
@@ -21,9 +20,10 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
|||||||
|
|
||||||
# Nostr
|
# Nostr
|
||||||
nostr-lmdb = { git = "https://github.com/rust-nostr/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-connect = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-blossom = { 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-gossip-memory = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
nostr-sdk = { 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" ] }
|
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] }
|
||||||
|
|
||||||
|
|||||||
3
assets/icons/book.svg
Normal file
3
assets/icons/book.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M4.75 20V4.75C4.75 3.64543 5.64543 2.75 6.75 2.75H19.25V18.75H6C5.30964 18.75 4.75 19.3096 4.75 20ZM4.75 20C4.75 20.6904 5.30964 21.25 6 21.25H19.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 12.25C9.75 12.25 9.75 13.75 9.75 13.75H14.25C14.25 13.75 14.25 12.25 12 12.25Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 9.25C13 9.80228 12.5523 10.25 12 10.25C11.4477 10.25 11 9.80228 11 9.25C11 8.69772 11.4477 8.25 12 8.25C12.5523 8.25 13 8.69772 13 9.25Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M11.5 9.25H12.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 866 B |
3
assets/icons/device.svg
Normal file
3
assets/icons/device.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M14.25 10.75C14.25 9.64543 15.1454 8.75 16.25 8.75H20.25C21.3546 8.75 22.25 9.64543 22.25 10.75V19.25C22.25 20.3546 21.3546 21.25 20.25 21.25H16.25C15.1454 21.25 14.25 20.3546 14.25 19.25V10.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M17.25 18.25H19.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M20.25 8.75V5.75C20.25 4.64543 19.3546 3.75 18.25 3.75H5.75C4.64543 3.75 3.75 4.64543 3.75 5.75V14.75C3.75 15.8546 2.85457 16.75 1.75 16.75V18.25C1.75 19.3546 2.64543 20.25 3.75 20.25H14.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.75 16.75H14.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 898 B |
3
assets/icons/group.svg
Normal file
3
assets/icons/group.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="8.75" r="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="4" cy="9.80005" r="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="20" cy="9.80005" r="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.25 16.625V16.5C7.25 13.8766 9.37665 11.75 12 11.75C14.6234 11.75 16.75 13.8766 16.75 16.5V16.625C16.75 17.5225 16.0225 18.25 15.125 18.25H8.875C7.97754 18.25 7.25 17.5225 7.25 16.625Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M4.25 17.2602H2.75C1.64543 17.2602 0.706551 16.3538 0.919944 15.2701C1.25877 13.5493 2.15049 12.3257 4 11.8301" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M19.75 17.2601H21.25C22.3546 17.2601 23.2935 16.3538 23.08 15.27C22.7412 13.5493 21.8495 12.3257 20 11.8301" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
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 |
3
assets/icons/scan.svg
Normal file
3
assets/icons/scan.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M7.25 4.75H4.75C3.64543 4.75 2.75 5.64543 2.75 6.75V9.25M16.75 4.75H19.25C20.3546 4.75 21.25 5.64543 21.25 6.75V9.25M21.25 14.75V17.25C21.25 18.3546 20.3546 19.25 19.25 19.25H16.75M7.25 19.25H4.75C3.64543 19.25 2.75 18.3546 2.75 17.25V14.75M7.75 9.75V14.25M16.25 9.75V14.25M12 9.75V12.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 468 B |
3
assets/icons/settings2.svg
Normal file
3
assets/icons/settings2.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M13 19.25H5.95C4.82989 19.25 4.26984 19.25 3.84202 19.032C3.46569 18.8403 3.15973 18.5343 2.96799 18.158C2.75 17.7302 2.75 17.1701 2.75 16.05V7.95C2.75 6.82989 2.75 6.26984 2.96799 5.84202C3.15973 5.46569 3.46569 5.15973 3.84202 4.96799C4.26984 4.75 4.8299 4.75 5.95 4.75H18.05C19.1701 4.75 19.7302 4.75 20.158 4.96799C20.5343 5.15973 20.8403 5.46569 21.032 5.84202C21.25 6.26984 21.25 6.8299 21.25 7.95V11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M3 5.63635L10.9761 10.3898C11.6069 10.7657 12.3931 10.7657 13.0239 10.3898L21 5.63635" stroke="currentColor" stroke-width="1.5"/><path d="M21.8148 15.375L21.1669 15.7491M16.1856 18.625L16.8335 18.2509M21.8147 18.625L21.1669 18.251M16.1855 15.375L16.8335 15.7491M19.0002 20.25L19.0002 19.4375M19.0002 13.75V14.5625M21.1669 17C21.1669 16.6053 21.0613 16.2352 20.8769 15.9165C20.5022 15.269 19.8021 14.8333 19.0002 14.8333C18.1983 14.8333 17.4981 15.269 17.1235 15.9165C16.9391 16.2352 16.8335 16.6053 16.8335 17C16.8335 17.3947 16.9391 17.7648 17.1235 18.0835C17.4982 18.731 18.1983 19.1667 19.0002 19.1667C19.8021 19.1667 20.5022 18.731 20.8769 18.0835C21.0613 17.7648 21.1669 17.3947 21.1669 17Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
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",
|
"id": "catppuccin-frappe",
|
||||||
"name": "Catppuccin Frappé",
|
"name": "Catppuccin Frappé",
|
||||||
"author": "Catppuccin",
|
"author": "Catppuccin Org (ported by Coop)",
|
||||||
"url": "https://github.com/catppuccin/catppuccin",
|
"url": "https://catppuccin.com",
|
||||||
"light": {
|
"light": {
|
||||||
"background": "#303446",
|
"background": "#303446",
|
||||||
"surface_background": "#292c3c",
|
"surface_background": "#292c3c",
|
||||||
"elevated_surface_background": "#232634",
|
"elevated_surface_background": "#232634",
|
||||||
"panel_background": "#303446",
|
"panel_background": "#303446",
|
||||||
"overlay": "#c6d0f51a",
|
"overlay": "#c6d0f51a",
|
||||||
"title_bar": "#292c3c",
|
"title_bar": "#232634",
|
||||||
"title_bar_inactive": "#232634",
|
"title_bar_inactive": "#303446",
|
||||||
"window_border": "#737994",
|
"window_border": "#51576d",
|
||||||
"border": "#626880",
|
"border": "#51576d",
|
||||||
"border_variant": "#51576d",
|
"border_variant": "#414559",
|
||||||
"border_focused": "#8caaee",
|
"border_focused": "#8caaee",
|
||||||
"border_selected": "#8caaee",
|
"border_selected": "#8caaee",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#c6d0f500",
|
||||||
"border_disabled": "#414559",
|
"border_disabled": "#292c3c",
|
||||||
"ring": "#8caaee",
|
"ring": "#babbf1",
|
||||||
"text": "#c6d0f5",
|
"text": "#c6d0f5",
|
||||||
"text_muted": "#b5bfe2",
|
"text_muted": "#a5adce",
|
||||||
"text_placeholder": "#a5adce",
|
"text_placeholder": "#838ba7",
|
||||||
"text_accent": "#8caaee",
|
"text_accent": "#8caaee",
|
||||||
"icon": "#b5bfe2",
|
"text_danger": "#e78284",
|
||||||
"icon_muted": "#a5adce",
|
"text_warning": "#ef9f76",
|
||||||
"icon_accent": "#8caaee",
|
"icon": "#a5adce",
|
||||||
"element_foreground": "#232634",
|
"icon_muted": "#838ba7",
|
||||||
|
"icon_accent": "#babbf1",
|
||||||
|
"element_foreground": "#303446",
|
||||||
"element_background": "#8caaee",
|
"element_background": "#8caaee",
|
||||||
"element_hover": "#babbf1",
|
"element_hover": "#babbf1",
|
||||||
"element_active": "#7e99d6",
|
"element_active": "#99d1db",
|
||||||
"element_selected": "#7088bf",
|
"element_selected": "#85c1dc",
|
||||||
"element_disabled": "#8caaee4d",
|
"element_disabled": "#8caaee4d",
|
||||||
"secondary_foreground": "#7088bf",
|
"secondary_foreground": "#c6d0f5",
|
||||||
"secondary_background": "#292c3c",
|
"secondary_background": "#414559",
|
||||||
"secondary_hover": "#8caaee33",
|
"secondary_hover": "#51576d",
|
||||||
"secondary_active": "#232634",
|
"secondary_active": "#626880",
|
||||||
"secondary_selected": "#232634",
|
"secondary_selected": "#626880",
|
||||||
"secondary_disabled": "#8caaee4d",
|
"secondary_disabled": "#8caaee4d",
|
||||||
"danger_foreground": "#232634",
|
"danger_foreground": "#303446",
|
||||||
"danger_background": "#e78284",
|
"danger_background": "#e78284",
|
||||||
"danger_hover": "#ea999c",
|
"danger_hover": "#ea999c",
|
||||||
"danger_active": "#d07576",
|
"danger_active": "#ef9f76",
|
||||||
"danger_selected": "#b96869",
|
"danger_selected": "#e5c890",
|
||||||
"danger_disabled": "#e782844d",
|
"danger_disabled": "#e782844d",
|
||||||
"warning_foreground": "#232634",
|
"warning_foreground": "#303446",
|
||||||
"warning_background": "#e5c890",
|
"warning_background": "#ef9f76",
|
||||||
"warning_hover": "#ef9f76",
|
"warning_hover": "#e5c890",
|
||||||
"warning_active": "#ceb482",
|
"warning_active": "#a6d189",
|
||||||
"warning_selected": "#b7a074",
|
"warning_selected": "#81c8be",
|
||||||
"warning_disabled": "#e5c8904d",
|
"warning_disabled": "#ef9f764d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#c6d0f500",
|
||||||
"ghost_element_background_alt": "#414559",
|
"ghost_element_background_alt": "#292c3c",
|
||||||
"ghost_element_hover": "#c6d0f533",
|
"ghost_element_hover": "#c6d0f50d",
|
||||||
"ghost_element_active": "#51576d",
|
"ghost_element_active": "#c6d0f51a",
|
||||||
"ghost_element_selected": "#51576d",
|
"ghost_element_selected": "#c6d0f51a",
|
||||||
"ghost_element_disabled": "#c6d0f50d",
|
"ghost_element_disabled": "#c6d0f505",
|
||||||
"tab_inactive_background": "#292c3c",
|
"tab_background": "#232634",
|
||||||
"tab_inactive_foreground": "#b5bfe2",
|
"tab_foreground": "#a5adce",
|
||||||
|
"tab_hover_background": "#c6d0f50d",
|
||||||
"tab_active_background": "#303446",
|
"tab_active_background": "#303446",
|
||||||
"tab_active_foreground": "#c6d0f5",
|
"tab_active_foreground": "#c6d0f5",
|
||||||
"tab_hover_foreground": "#babbf1",
|
"scrollbar_thumb_background": "#c6d0f51a",
|
||||||
"scrollbar_thumb_background": "#c6d0f533",
|
"scrollbar_thumb_hover_background": "#c6d0f526",
|
||||||
"scrollbar_thumb_hover_background": "#c6d0f580",
|
"scrollbar_thumb_border": "#c6d0f500",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#c6d0f500",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#c6d0f500",
|
||||||
"scrollbar_track_border": "#51576d",
|
|
||||||
"drop_target_background": "#8caaee1a",
|
"drop_target_background": "#8caaee1a",
|
||||||
"cursor": "#f2d5cf",
|
"cursor": "#8caaee",
|
||||||
"selection": "#949cbb40"
|
"selection": "#8caaee40"
|
||||||
},
|
},
|
||||||
"dark": {
|
"dark": {
|
||||||
"background": "#303446",
|
"background": "#303446",
|
||||||
@@ -76,65 +78,67 @@
|
|||||||
"elevated_surface_background": "#232634",
|
"elevated_surface_background": "#232634",
|
||||||
"panel_background": "#303446",
|
"panel_background": "#303446",
|
||||||
"overlay": "#c6d0f51a",
|
"overlay": "#c6d0f51a",
|
||||||
"title_bar": "#292c3c",
|
"title_bar": "#232634",
|
||||||
"title_bar_inactive": "#232634",
|
"title_bar_inactive": "#303446",
|
||||||
"window_border": "#737994",
|
"window_border": "#51576d",
|
||||||
"border": "#626880",
|
"border": "#51576d",
|
||||||
"border_variant": "#51576d",
|
"border_variant": "#414559",
|
||||||
"border_focused": "#8caaee",
|
"border_focused": "#8caaee",
|
||||||
"border_selected": "#8caaee",
|
"border_selected": "#8caaee",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#c6d0f500",
|
||||||
"border_disabled": "#414559",
|
"border_disabled": "#292c3c",
|
||||||
"ring": "#8caaee",
|
"ring": "#babbf1",
|
||||||
"text": "#c6d0f5",
|
"text": "#c6d0f5",
|
||||||
"text_muted": "#b5bfe2",
|
"text_muted": "#a5adce",
|
||||||
"text_placeholder": "#a5adce",
|
"text_placeholder": "#838ba7",
|
||||||
"text_accent": "#8caaee",
|
"text_accent": "#8caaee",
|
||||||
"icon": "#b5bfe2",
|
"text_danger": "#e78284",
|
||||||
"icon_muted": "#a5adce",
|
"text_warning": "#ef9f76",
|
||||||
"icon_accent": "#8caaee",
|
"icon": "#a5adce",
|
||||||
"element_foreground": "#232634",
|
"icon_muted": "#838ba7",
|
||||||
|
"icon_accent": "#babbf1",
|
||||||
|
"element_foreground": "#303446",
|
||||||
"element_background": "#8caaee",
|
"element_background": "#8caaee",
|
||||||
"element_hover": "#babbf1",
|
"element_hover": "#babbf1",
|
||||||
"element_active": "#7e99d6",
|
"element_active": "#99d1db",
|
||||||
"element_selected": "#7088bf",
|
"element_selected": "#85c1dc",
|
||||||
"element_disabled": "#8caaee4d",
|
"element_disabled": "#8caaee4d",
|
||||||
"secondary_foreground": "#7088bf",
|
"secondary_foreground": "#c6d0f5",
|
||||||
"secondary_background": "#292c3c",
|
"secondary_background": "#414559",
|
||||||
"secondary_hover": "#8caaee33",
|
"secondary_hover": "#51576d",
|
||||||
"secondary_active": "#232634",
|
"secondary_active": "#626880",
|
||||||
"secondary_selected": "#232634",
|
"secondary_selected": "#626880",
|
||||||
"secondary_disabled": "#8caaee4d",
|
"secondary_disabled": "#8caaee4d",
|
||||||
"danger_foreground": "#232634",
|
"danger_foreground": "#303446",
|
||||||
"danger_background": "#e78284",
|
"danger_background": "#e78284",
|
||||||
"danger_hover": "#ea999c",
|
"danger_hover": "#ea999c",
|
||||||
"danger_active": "#d07576",
|
"danger_active": "#ef9f76",
|
||||||
"danger_selected": "#b96869",
|
"danger_selected": "#e5c890",
|
||||||
"danger_disabled": "#e782844d",
|
"danger_disabled": "#e782844d",
|
||||||
"warning_foreground": "#232634",
|
"warning_foreground": "#303446",
|
||||||
"warning_background": "#e5c890",
|
"warning_background": "#ef9f76",
|
||||||
"warning_hover": "#ef9f76",
|
"warning_hover": "#e5c890",
|
||||||
"warning_active": "#ceb482",
|
"warning_active": "#a6d189",
|
||||||
"warning_selected": "#b7a074",
|
"warning_selected": "#81c8be",
|
||||||
"warning_disabled": "#e5c8904d",
|
"warning_disabled": "#ef9f764d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#c6d0f500",
|
||||||
"ghost_element_background_alt": "#414559",
|
"ghost_element_background_alt": "#292c3c",
|
||||||
"ghost_element_hover": "#c6d0f533",
|
"ghost_element_hover": "#c6d0f50d",
|
||||||
"ghost_element_active": "#51576d",
|
"ghost_element_active": "#c6d0f51a",
|
||||||
"ghost_element_selected": "#51576d",
|
"ghost_element_selected": "#c6d0f51a",
|
||||||
"ghost_element_disabled": "#c6d0f50d",
|
"ghost_element_disabled": "#c6d0f505",
|
||||||
"tab_inactive_background": "#292c3c",
|
"tab_background": "#232634",
|
||||||
"tab_inactive_foreground": "#b5bfe2",
|
"tab_foreground": "#a5adce",
|
||||||
|
"tab_hover_background": "#c6d0f50d",
|
||||||
"tab_active_background": "#303446",
|
"tab_active_background": "#303446",
|
||||||
"tab_active_foreground": "#c6d0f5",
|
"tab_active_foreground": "#c6d0f5",
|
||||||
"tab_hover_foreground": "#babbf1",
|
"scrollbar_thumb_background": "#c6d0f51a",
|
||||||
"scrollbar_thumb_background": "#c6d0f533",
|
"scrollbar_thumb_hover_background": "#c6d0f526",
|
||||||
"scrollbar_thumb_hover_background": "#c6d0f580",
|
"scrollbar_thumb_border": "#c6d0f500",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#c6d0f500",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#c6d0f500",
|
||||||
"scrollbar_track_border": "#51576d",
|
|
||||||
"drop_target_background": "#8caaee1a",
|
"drop_target_background": "#8caaee1a",
|
||||||
"cursor": "#f2d5cf",
|
"cursor": "#8caaee",
|
||||||
"selection": "#949cbb40"
|
"selection": "#8caaee40"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +1,76 @@
|
|||||||
{
|
{
|
||||||
"id": "catppuccin-latte",
|
"id": "catppuccin-latte",
|
||||||
"name": "Catppuccin Latte",
|
"name": "Catppuccin Latte",
|
||||||
"author": "Catppuccin",
|
"author": "Catppuccin Org (ported by Coop)",
|
||||||
"url": "https://github.com/catppuccin/catppuccin",
|
"url": "https://catppuccin.com",
|
||||||
"light": {
|
"light": {
|
||||||
"background": "#eff1f5",
|
"background": "#eff1f5",
|
||||||
"surface_background": "#e6e9ef",
|
"surface_background": "#e6e9ef",
|
||||||
"elevated_surface_background": "#dce0e8",
|
"elevated_surface_background": "#dce0e8",
|
||||||
"panel_background": "#eff1f5",
|
"panel_background": "#eff1f5",
|
||||||
"overlay": "#4c4f691a",
|
"overlay": "#4c4f691a",
|
||||||
"title_bar": "#e6e9ef",
|
"title_bar": "#dce0e8",
|
||||||
"title_bar_inactive": "#dce0e8",
|
"title_bar_inactive": "#eff1f5",
|
||||||
"window_border": "#9ca0b0",
|
"window_border": "#bcc0cc",
|
||||||
"border": "#acb0be",
|
"border": "#bcc0cc",
|
||||||
"border_variant": "#bcc0cc",
|
"border_variant": "#ccd0da",
|
||||||
"border_focused": "#1e66f5",
|
"border_focused": "#1e66f5",
|
||||||
"border_selected": "#1e66f5",
|
"border_selected": "#1e66f5",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#4c4f6900",
|
||||||
"border_disabled": "#ccd0da",
|
"border_disabled": "#e6e9ef",
|
||||||
"ring": "#1e66f5",
|
"ring": "#7287fd",
|
||||||
"text": "#4c4f69",
|
"text": "#4c4f69",
|
||||||
"text_muted": "#5c5f77",
|
"text_muted": "#6c6f85",
|
||||||
"text_placeholder": "#6c6f85",
|
"text_placeholder": "#8c8fa1",
|
||||||
"text_accent": "#1e66f5",
|
"text_accent": "#1e66f5",
|
||||||
"icon": "#5c5f77",
|
"text_danger": "#d20f39",
|
||||||
"icon_muted": "#6c6f85",
|
"text_warning": "#fe640b",
|
||||||
"icon_accent": "#1e66f5",
|
"icon": "#6c6f85",
|
||||||
|
"icon_muted": "#8c8fa1",
|
||||||
|
"icon_accent": "#7287fd",
|
||||||
"element_foreground": "#eff1f5",
|
"element_foreground": "#eff1f5",
|
||||||
"element_background": "#1e66f5",
|
"element_background": "#1e66f5",
|
||||||
"element_hover": "#8839ef",
|
"element_hover": "#7287fd",
|
||||||
"element_active": "#1c5ce0",
|
"element_active": "#04a5e5",
|
||||||
"element_selected": "#1a52cc",
|
"element_selected": "#209fb5",
|
||||||
"element_disabled": "#1e66f54d",
|
"element_disabled": "#1e66f54d",
|
||||||
"secondary_foreground": "#1a52cc",
|
"secondary_foreground": "#4c4f69",
|
||||||
"secondary_background": "#e6e9ef",
|
"secondary_background": "#ccd0da",
|
||||||
"secondary_hover": "#8839ef33",
|
"secondary_hover": "#bcc0cc",
|
||||||
"secondary_active": "#dce0e8",
|
"secondary_active": "#acb0be",
|
||||||
"secondary_selected": "#dce0e8",
|
"secondary_selected": "#acb0be",
|
||||||
"secondary_disabled": "#1e66f54d",
|
"secondary_disabled": "#1e66f54d",
|
||||||
"danger_foreground": "#eff1f5",
|
"danger_foreground": "#eff1f5",
|
||||||
"danger_background": "#d20f39",
|
"danger_background": "#d20f39",
|
||||||
"danger_hover": "#e64553",
|
"danger_hover": "#e64553",
|
||||||
"danger_active": "#bd0d33",
|
"danger_active": "#fe640b",
|
||||||
"danger_selected": "#a80b2d",
|
"danger_selected": "#df8e1d",
|
||||||
"danger_disabled": "#d20f394d",
|
"danger_disabled": "#d20f394d",
|
||||||
"warning_foreground": "#4c4f69",
|
"warning_foreground": "#eff1f5",
|
||||||
"warning_background": "#df8e1d",
|
"warning_background": "#fe640b",
|
||||||
"warning_hover": "#fe640b",
|
"warning_hover": "#df8e1d",
|
||||||
"warning_active": "#c9801a",
|
"warning_active": "#40a02b",
|
||||||
"warning_selected": "#b47217",
|
"warning_selected": "#179299",
|
||||||
"warning_disabled": "#df8e1d4d",
|
"warning_disabled": "#fe640b4d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#4c4f6900",
|
||||||
"ghost_element_background_alt": "#ccd0da",
|
"ghost_element_background_alt": "#e6e9ef",
|
||||||
"ghost_element_hover": "#4c4f6933",
|
"ghost_element_hover": "#4c4f690d",
|
||||||
"ghost_element_active": "#bcc0cc",
|
"ghost_element_active": "#4c4f691a",
|
||||||
"ghost_element_selected": "#bcc0cc",
|
"ghost_element_selected": "#4c4f691a",
|
||||||
"ghost_element_disabled": "#4c4f690d",
|
"ghost_element_disabled": "#4c4f6905",
|
||||||
"tab_inactive_background": "#e6e9ef",
|
"tab_background": "#e6e9ef",
|
||||||
"tab_inactive_foreground": "#5c5f77",
|
"tab_foreground": "#6c6f85",
|
||||||
|
"tab_hover_background": "#4c4f690d",
|
||||||
"tab_active_background": "#eff1f5",
|
"tab_active_background": "#eff1f5",
|
||||||
"tab_active_foreground": "#4c4f69",
|
"tab_active_foreground": "#4c4f69",
|
||||||
"tab_hover_foreground": "#8839ef",
|
"scrollbar_thumb_background": "#4c4f691a",
|
||||||
"scrollbar_thumb_background": "#4c4f6933",
|
"scrollbar_thumb_hover_background": "#4c4f6926",
|
||||||
"scrollbar_thumb_hover_background": "#4c4f6980",
|
"scrollbar_thumb_border": "#4c4f6900",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#4c4f6900",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#4c4f6900",
|
||||||
"scrollbar_track_border": "#bcc0cc",
|
|
||||||
"drop_target_background": "#1e66f51a",
|
"drop_target_background": "#1e66f51a",
|
||||||
"cursor": "#dc8a78",
|
"cursor": "#1e66f5",
|
||||||
"selection": "#7c7f9340"
|
"selection": "#1e66f540"
|
||||||
},
|
},
|
||||||
"dark": {
|
"dark": {
|
||||||
"background": "#eff1f5",
|
"background": "#eff1f5",
|
||||||
@@ -76,65 +78,67 @@
|
|||||||
"elevated_surface_background": "#dce0e8",
|
"elevated_surface_background": "#dce0e8",
|
||||||
"panel_background": "#eff1f5",
|
"panel_background": "#eff1f5",
|
||||||
"overlay": "#4c4f691a",
|
"overlay": "#4c4f691a",
|
||||||
"title_bar": "#e6e9ef",
|
"title_bar": "#dce0e8",
|
||||||
"title_bar_inactive": "#dce0e8",
|
"title_bar_inactive": "#eff1f5",
|
||||||
"window_border": "#9ca0b0",
|
"window_border": "#bcc0cc",
|
||||||
"border": "#acb0be",
|
"border": "#bcc0cc",
|
||||||
"border_variant": "#bcc0cc",
|
"border_variant": "#ccd0da",
|
||||||
"border_focused": "#1e66f5",
|
"border_focused": "#1e66f5",
|
||||||
"border_selected": "#1e66f5",
|
"border_selected": "#1e66f5",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#4c4f6900",
|
||||||
"border_disabled": "#ccd0da",
|
"border_disabled": "#e6e9ef",
|
||||||
"ring": "#1e66f5",
|
"ring": "#7287fd",
|
||||||
"text": "#4c4f69",
|
"text": "#4c4f69",
|
||||||
"text_muted": "#5c5f77",
|
"text_muted": "#6c6f85",
|
||||||
"text_placeholder": "#6c6f85",
|
"text_placeholder": "#8c8fa1",
|
||||||
"text_accent": "#1e66f5",
|
"text_accent": "#1e66f5",
|
||||||
"icon": "#5c5f77",
|
"text_danger": "#d20f39",
|
||||||
"icon_muted": "#6c6f85",
|
"text_warning": "#fe640b",
|
||||||
"icon_accent": "#1e66f5",
|
"icon": "#6c6f85",
|
||||||
|
"icon_muted": "#8c8fa1",
|
||||||
|
"icon_accent": "#7287fd",
|
||||||
"element_foreground": "#eff1f5",
|
"element_foreground": "#eff1f5",
|
||||||
"element_background": "#1e66f5",
|
"element_background": "#1e66f5",
|
||||||
"element_hover": "#8839ef",
|
"element_hover": "#7287fd",
|
||||||
"element_active": "#1c5ce0",
|
"element_active": "#04a5e5",
|
||||||
"element_selected": "#1a52cc",
|
"element_selected": "#209fb5",
|
||||||
"element_disabled": "#1e66f54d",
|
"element_disabled": "#1e66f54d",
|
||||||
"secondary_foreground": "#1a52cc",
|
"secondary_foreground": "#4c4f69",
|
||||||
"secondary_background": "#e6e9ef",
|
"secondary_background": "#ccd0da",
|
||||||
"secondary_hover": "#8839ef33",
|
"secondary_hover": "#bcc0cc",
|
||||||
"secondary_active": "#dce0e8",
|
"secondary_active": "#acb0be",
|
||||||
"secondary_selected": "#dce0e8",
|
"secondary_selected": "#acb0be",
|
||||||
"secondary_disabled": "#1e66f54d",
|
"secondary_disabled": "#1e66f54d",
|
||||||
"danger_foreground": "#eff1f5",
|
"danger_foreground": "#eff1f5",
|
||||||
"danger_background": "#d20f39",
|
"danger_background": "#d20f39",
|
||||||
"danger_hover": "#e64553",
|
"danger_hover": "#e64553",
|
||||||
"danger_active": "#bd0d33",
|
"danger_active": "#fe640b",
|
||||||
"danger_selected": "#a80b2d",
|
"danger_selected": "#df8e1d",
|
||||||
"danger_disabled": "#d20f394d",
|
"danger_disabled": "#d20f394d",
|
||||||
"warning_foreground": "#4c4f69",
|
"warning_foreground": "#eff1f5",
|
||||||
"warning_background": "#df8e1d",
|
"warning_background": "#fe640b",
|
||||||
"warning_hover": "#fe640b",
|
"warning_hover": "#df8e1d",
|
||||||
"warning_active": "#c9801a",
|
"warning_active": "#40a02b",
|
||||||
"warning_selected": "#b47217",
|
"warning_selected": "#179299",
|
||||||
"warning_disabled": "#df8e1d4d",
|
"warning_disabled": "#fe640b4d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#4c4f6900",
|
||||||
"ghost_element_background_alt": "#ccd0da",
|
"ghost_element_background_alt": "#e6e9ef",
|
||||||
"ghost_element_hover": "#4c4f6933",
|
"ghost_element_hover": "#4c4f690d",
|
||||||
"ghost_element_active": "#bcc0cc",
|
"ghost_element_active": "#4c4f691a",
|
||||||
"ghost_element_selected": "#bcc0cc",
|
"ghost_element_selected": "#4c4f691a",
|
||||||
"ghost_element_disabled": "#4c4f690d",
|
"ghost_element_disabled": "#4c4f6905",
|
||||||
"tab_inactive_background": "#e6e9ef",
|
"tab_background": "#e6e9ef",
|
||||||
"tab_inactive_foreground": "#5c5f77",
|
"tab_foreground": "#6c6f85",
|
||||||
|
"tab_hover_background": "#4c4f690d",
|
||||||
"tab_active_background": "#eff1f5",
|
"tab_active_background": "#eff1f5",
|
||||||
"tab_active_foreground": "#4c4f69",
|
"tab_active_foreground": "#4c4f69",
|
||||||
"tab_hover_foreground": "#8839ef",
|
"scrollbar_thumb_background": "#4c4f691a",
|
||||||
"scrollbar_thumb_background": "#4c4f6933",
|
"scrollbar_thumb_hover_background": "#4c4f6926",
|
||||||
"scrollbar_thumb_hover_background": "#4c4f6980",
|
"scrollbar_thumb_border": "#4c4f6900",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#4c4f6900",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#4c4f6900",
|
||||||
"scrollbar_track_border": "#bcc0cc",
|
|
||||||
"drop_target_background": "#1e66f51a",
|
"drop_target_background": "#1e66f51a",
|
||||||
"cursor": "#dc8a78",
|
"cursor": "#1e66f5",
|
||||||
"selection": "#7c7f9340"
|
"selection": "#1e66f540"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +1,76 @@
|
|||||||
{
|
{
|
||||||
"id": "catppuccin-macchiato",
|
"id": "catppuccin-macchiato",
|
||||||
"name": "Catppuccin Macchiato",
|
"name": "Catppuccin Macchiato",
|
||||||
"author": "Catppuccin",
|
"author": "Catppuccin Org (ported by Coop)",
|
||||||
"url": "https://github.com/catppuccin/catppuccin",
|
"url": "https://catppuccin.com",
|
||||||
"light": {
|
"light": {
|
||||||
"background": "#24273a",
|
"background": "#24273a",
|
||||||
"surface_background": "#1e2030",
|
"surface_background": "#1e2030",
|
||||||
"elevated_surface_background": "#181926",
|
"elevated_surface_background": "#181926",
|
||||||
"panel_background": "#24273a",
|
"panel_background": "#24273a",
|
||||||
"overlay": "#cad3f51a",
|
"overlay": "#cad3f51a",
|
||||||
"title_bar": "#1e2030",
|
"title_bar": "#181926",
|
||||||
"title_bar_inactive": "#181926",
|
"title_bar_inactive": "#24273a",
|
||||||
"window_border": "#6e738d",
|
"window_border": "#494d64",
|
||||||
"border": "#5b6078",
|
"border": "#494d64",
|
||||||
"border_variant": "#494d64",
|
"border_variant": "#363a4f",
|
||||||
"border_focused": "#8aadf4",
|
"border_focused": "#8aadf4",
|
||||||
"border_selected": "#8aadf4",
|
"border_selected": "#8aadf4",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#cad3f500",
|
||||||
"border_disabled": "#363a4f",
|
"border_disabled": "#1e2030",
|
||||||
"ring": "#8aadf4",
|
"ring": "#b7bdf8",
|
||||||
"text": "#cad3f5",
|
"text": "#cad3f5",
|
||||||
"text_muted": "#b8c0e0",
|
"text_muted": "#a5adcb",
|
||||||
"text_placeholder": "#a5adcb",
|
"text_placeholder": "#8087a2",
|
||||||
"text_accent": "#8aadf4",
|
"text_accent": "#8aadf4",
|
||||||
"icon": "#b8c0e0",
|
"text_danger": "#ed8796",
|
||||||
"icon_muted": "#a5adcb",
|
"text_warning": "#f5a97f",
|
||||||
"icon_accent": "#8aadf4",
|
"icon": "#a5adcb",
|
||||||
"element_foreground": "#181926",
|
"icon_muted": "#8087a2",
|
||||||
|
"icon_accent": "#b7bdf8",
|
||||||
|
"element_foreground": "#24273a",
|
||||||
"element_background": "#8aadf4",
|
"element_background": "#8aadf4",
|
||||||
"element_hover": "#b7bdf8",
|
"element_hover": "#b7bdf8",
|
||||||
"element_active": "#7c9cdc",
|
"element_active": "#91d7e3",
|
||||||
"element_selected": "#6e8bc5",
|
"element_selected": "#7dc4e4",
|
||||||
"element_disabled": "#8aadf44d",
|
"element_disabled": "#8aadf44d",
|
||||||
"secondary_foreground": "#6e8bc5",
|
"secondary_foreground": "#cad3f5",
|
||||||
"secondary_background": "#1e2030",
|
"secondary_background": "#363a4f",
|
||||||
"secondary_hover": "#8aadf433",
|
"secondary_hover": "#494d64",
|
||||||
"secondary_active": "#181926",
|
"secondary_active": "#5b6078",
|
||||||
"secondary_selected": "#181926",
|
"secondary_selected": "#5b6078",
|
||||||
"secondary_disabled": "#8aadf44d",
|
"secondary_disabled": "#8aadf44d",
|
||||||
"danger_foreground": "#181926",
|
"danger_foreground": "#24273a",
|
||||||
"danger_background": "#ed8796",
|
"danger_background": "#ed8796",
|
||||||
"danger_hover": "#ee99a0",
|
"danger_hover": "#ee99a0",
|
||||||
"danger_active": "#d57a87",
|
"danger_active": "#f5a97f",
|
||||||
"danger_selected": "#be6d78",
|
"danger_selected": "#eed49f",
|
||||||
"danger_disabled": "#ed87964d",
|
"danger_disabled": "#ed87964d",
|
||||||
"warning_foreground": "#181926",
|
"warning_foreground": "#24273a",
|
||||||
"warning_background": "#eed49f",
|
"warning_background": "#f5a97f",
|
||||||
"warning_hover": "#f5a97f",
|
"warning_hover": "#eed49f",
|
||||||
"warning_active": "#d6bf8f",
|
"warning_active": "#a6da95",
|
||||||
"warning_selected": "#beaa7f",
|
"warning_selected": "#8bd5ca",
|
||||||
"warning_disabled": "#eed49f4d",
|
"warning_disabled": "#f5a97f4d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#cad3f500",
|
||||||
"ghost_element_background_alt": "#363a4f",
|
"ghost_element_background_alt": "#1e2030",
|
||||||
"ghost_element_hover": "#cad3f533",
|
"ghost_element_hover": "#cad3f50d",
|
||||||
"ghost_element_active": "#494d64",
|
"ghost_element_active": "#cad3f51a",
|
||||||
"ghost_element_selected": "#494d64",
|
"ghost_element_selected": "#cad3f51a",
|
||||||
"ghost_element_disabled": "#cad3f50d",
|
"ghost_element_disabled": "#cad3f505",
|
||||||
"tab_inactive_background": "#1e2030",
|
"tab_background": "#181926",
|
||||||
"tab_inactive_foreground": "#b8c0e0",
|
"tab_foreground": "#a5adcb",
|
||||||
|
"tab_hover_background": "#cad3f50d",
|
||||||
"tab_active_background": "#24273a",
|
"tab_active_background": "#24273a",
|
||||||
"tab_active_foreground": "#cad3f5",
|
"tab_active_foreground": "#cad3f5",
|
||||||
"tab_hover_foreground": "#b7bdf8",
|
"scrollbar_thumb_background": "#cad3f51a",
|
||||||
"scrollbar_thumb_background": "#cad3f533",
|
"scrollbar_thumb_hover_background": "#cad3f526",
|
||||||
"scrollbar_thumb_hover_background": "#cad3f580",
|
"scrollbar_thumb_border": "#cad3f500",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#cad3f500",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#cad3f500",
|
||||||
"scrollbar_track_border": "#494d64",
|
|
||||||
"drop_target_background": "#8aadf41a",
|
"drop_target_background": "#8aadf41a",
|
||||||
"cursor": "#f4dbd6",
|
"cursor": "#8aadf4",
|
||||||
"selection": "#939ab740"
|
"selection": "#8aadf440"
|
||||||
},
|
},
|
||||||
"dark": {
|
"dark": {
|
||||||
"background": "#24273a",
|
"background": "#24273a",
|
||||||
@@ -76,65 +78,67 @@
|
|||||||
"elevated_surface_background": "#181926",
|
"elevated_surface_background": "#181926",
|
||||||
"panel_background": "#24273a",
|
"panel_background": "#24273a",
|
||||||
"overlay": "#cad3f51a",
|
"overlay": "#cad3f51a",
|
||||||
"title_bar": "#1e2030",
|
"title_bar": "#181926",
|
||||||
"title_bar_inactive": "#181926",
|
"title_bar_inactive": "#24273a",
|
||||||
"window_border": "#6e738d",
|
"window_border": "#494d64",
|
||||||
"border": "#5b6078",
|
"border": "#494d64",
|
||||||
"border_variant": "#494d64",
|
"border_variant": "#363a4f",
|
||||||
"border_focused": "#8aadf4",
|
"border_focused": "#8aadf4",
|
||||||
"border_selected": "#8aadf4",
|
"border_selected": "#8aadf4",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#cad3f500",
|
||||||
"border_disabled": "#363a4f",
|
"border_disabled": "#1e2030",
|
||||||
"ring": "#8aadf4",
|
"ring": "#b7bdf8",
|
||||||
"text": "#cad3f5",
|
"text": "#cad3f5",
|
||||||
"text_muted": "#b8c0e0",
|
"text_muted": "#a5adcb",
|
||||||
"text_placeholder": "#a5adcb",
|
"text_placeholder": "#8087a2",
|
||||||
"text_accent": "#8aadf4",
|
"text_accent": "#8aadf4",
|
||||||
"icon": "#b8c0e0",
|
"text_danger": "#ed8796",
|
||||||
"icon_muted": "#a5adcb",
|
"text_warning": "#f5a97f",
|
||||||
"icon_accent": "#8aadf4",
|
"icon": "#a5adcb",
|
||||||
"element_foreground": "#181926",
|
"icon_muted": "#8087a2",
|
||||||
|
"icon_accent": "#b7bdf8",
|
||||||
|
"element_foreground": "#24273a",
|
||||||
"element_background": "#8aadf4",
|
"element_background": "#8aadf4",
|
||||||
"element_hover": "#b7bdf8",
|
"element_hover": "#b7bdf8",
|
||||||
"element_active": "#7c9cdc",
|
"element_active": "#91d7e3",
|
||||||
"element_selected": "#6e8bc5",
|
"element_selected": "#7dc4e4",
|
||||||
"element_disabled": "#8aadf44d",
|
"element_disabled": "#8aadf44d",
|
||||||
"secondary_foreground": "#6e8bc5",
|
"secondary_foreground": "#cad3f5",
|
||||||
"secondary_background": "#1e2030",
|
"secondary_background": "#363a4f",
|
||||||
"secondary_hover": "#8aadf433",
|
"secondary_hover": "#494d64",
|
||||||
"secondary_active": "#181926",
|
"secondary_active": "#5b6078",
|
||||||
"secondary_selected": "#181926",
|
"secondary_selected": "#5b6078",
|
||||||
"secondary_disabled": "#8aadf44d",
|
"secondary_disabled": "#8aadf44d",
|
||||||
"danger_foreground": "#181926",
|
"danger_foreground": "#24273a",
|
||||||
"danger_background": "#ed8796",
|
"danger_background": "#ed8796",
|
||||||
"danger_hover": "#ee99a0",
|
"danger_hover": "#ee99a0",
|
||||||
"danger_active": "#d57a87",
|
"danger_active": "#f5a97f",
|
||||||
"danger_selected": "#be6d78",
|
"danger_selected": "#eed49f",
|
||||||
"danger_disabled": "#ed87964d",
|
"danger_disabled": "#ed87964d",
|
||||||
"warning_foreground": "#181926",
|
"warning_foreground": "#24273a",
|
||||||
"warning_background": "#eed49f",
|
"warning_background": "#f5a97f",
|
||||||
"warning_hover": "#f5a97f",
|
"warning_hover": "#eed49f",
|
||||||
"warning_active": "#d6bf8f",
|
"warning_active": "#a6da95",
|
||||||
"warning_selected": "#beaa7f",
|
"warning_selected": "#8bd5ca",
|
||||||
"warning_disabled": "#eed49f4d",
|
"warning_disabled": "#f5a97f4d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#cad3f500",
|
||||||
"ghost_element_background_alt": "#363a4f",
|
"ghost_element_background_alt": "#1e2030",
|
||||||
"ghost_element_hover": "#cad3f533",
|
"ghost_element_hover": "#cad3f50d",
|
||||||
"ghost_element_active": "#494d64",
|
"ghost_element_active": "#cad3f51a",
|
||||||
"ghost_element_selected": "#494d64",
|
"ghost_element_selected": "#cad3f51a",
|
||||||
"ghost_element_disabled": "#cad3f50d",
|
"ghost_element_disabled": "#cad3f505",
|
||||||
"tab_inactive_background": "#1e2030",
|
"tab_background": "#181926",
|
||||||
"tab_inactive_foreground": "#b8c0e0",
|
"tab_foreground": "#a5adcb",
|
||||||
|
"tab_hover_background": "#cad3f50d",
|
||||||
"tab_active_background": "#24273a",
|
"tab_active_background": "#24273a",
|
||||||
"tab_active_foreground": "#cad3f5",
|
"tab_active_foreground": "#cad3f5",
|
||||||
"tab_hover_foreground": "#b7bdf8",
|
"scrollbar_thumb_background": "#cad3f51a",
|
||||||
"scrollbar_thumb_background": "#cad3f533",
|
"scrollbar_thumb_hover_background": "#cad3f526",
|
||||||
"scrollbar_thumb_hover_background": "#cad3f580",
|
"scrollbar_thumb_border": "#cad3f500",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#cad3f500",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#cad3f500",
|
||||||
"scrollbar_track_border": "#494d64",
|
|
||||||
"drop_target_background": "#8aadf41a",
|
"drop_target_background": "#8aadf41a",
|
||||||
"cursor": "#f4dbd6",
|
"cursor": "#8aadf4",
|
||||||
"selection": "#939ab740"
|
"selection": "#8aadf440"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +1,76 @@
|
|||||||
{
|
{
|
||||||
"id": "catppuccin-mocha",
|
"id": "catppuccin-mocha",
|
||||||
"name": "Catppuccin Mocha",
|
"name": "Catppuccin Mocha",
|
||||||
"author": "Catppuccin",
|
"author": "Catppuccin Org (ported by Coop)",
|
||||||
"url": "https://github.com/catppuccin/catppuccin",
|
"url": "https://catppuccin.com",
|
||||||
"light": {
|
"light": {
|
||||||
"background": "#1e1e2e",
|
"background": "#1e1e2e",
|
||||||
"surface_background": "#181825",
|
"surface_background": "#181825",
|
||||||
"elevated_surface_background": "#11111b",
|
"elevated_surface_background": "#11111b",
|
||||||
"panel_background": "#1e1e2e",
|
"panel_background": "#1e1e2e",
|
||||||
"overlay": "#cdd6f41a",
|
"overlay": "#cdd6f41a",
|
||||||
"title_bar": "#181825",
|
"title_bar": "#11111b",
|
||||||
"title_bar_inactive": "#11111b",
|
"title_bar_inactive": "#1e1e2e",
|
||||||
"window_border": "#6c7086",
|
"window_border": "#45475a",
|
||||||
"border": "#585b70",
|
"border": "#45475a",
|
||||||
"border_variant": "#45475a",
|
"border_variant": "#313244",
|
||||||
"border_focused": "#89b4fa",
|
"border_focused": "#89b4fa",
|
||||||
"border_selected": "#89b4fa",
|
"border_selected": "#89b4fa",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#cdd6f400",
|
||||||
"border_disabled": "#313244",
|
"border_disabled": "#181825",
|
||||||
"ring": "#89b4fa",
|
"ring": "#b4befe",
|
||||||
"text": "#cdd6f4",
|
"text": "#cdd6f4",
|
||||||
"text_muted": "#bac2de",
|
"text_muted": "#a6adc8",
|
||||||
"text_placeholder": "#a6adc8",
|
"text_placeholder": "#7f849c",
|
||||||
"text_accent": "#89b4fa",
|
"text_accent": "#89b4fa",
|
||||||
"icon": "#bac2de",
|
"text_danger": "#f38ba8",
|
||||||
"icon_muted": "#a6adc8",
|
"text_warning": "#fab387",
|
||||||
"icon_accent": "#89b4fa",
|
"icon": "#a6adc8",
|
||||||
"element_foreground": "#11111b",
|
"icon_muted": "#7f849c",
|
||||||
|
"icon_accent": "#b4befe",
|
||||||
|
"element_foreground": "#1e1e2e",
|
||||||
"element_background": "#89b4fa",
|
"element_background": "#89b4fa",
|
||||||
"element_hover": "#b4befe",
|
"element_hover": "#b4befe",
|
||||||
"element_active": "#7ba2e1",
|
"element_active": "#89dceb",
|
||||||
"element_selected": "#6d90c9",
|
"element_selected": "#74c7ec",
|
||||||
"element_disabled": "#89b4fa4d",
|
"element_disabled": "#89b4fa4d",
|
||||||
"secondary_foreground": "#6d90c9",
|
"secondary_foreground": "#cdd6f4",
|
||||||
"secondary_background": "#181825",
|
"secondary_background": "#313244",
|
||||||
"secondary_hover": "#89b4fa33",
|
"secondary_hover": "#45475a",
|
||||||
"secondary_active": "#11111b",
|
"secondary_active": "#585b70",
|
||||||
"secondary_selected": "#11111b",
|
"secondary_selected": "#585b70",
|
||||||
"secondary_disabled": "#89b4fa4d",
|
"secondary_disabled": "#89b4fa4d",
|
||||||
"danger_foreground": "#11111b",
|
"danger_foreground": "#1e1e2e",
|
||||||
"danger_background": "#f38ba8",
|
"danger_background": "#f38ba8",
|
||||||
"danger_hover": "#eba0ac",
|
"danger_hover": "#eba0ac",
|
||||||
"danger_active": "#db7d98",
|
"danger_active": "#fab387",
|
||||||
"danger_selected": "#c46f88",
|
"danger_selected": "#f9e2af",
|
||||||
"danger_disabled": "#f38ba84d",
|
"danger_disabled": "#f38ba84d",
|
||||||
"warning_foreground": "#11111b",
|
"warning_foreground": "#1e1e2e",
|
||||||
"warning_background": "#f9e2af",
|
"warning_background": "#fab387",
|
||||||
"warning_hover": "#fab387",
|
"warning_hover": "#f9e2af",
|
||||||
"warning_active": "#e0cb9e",
|
"warning_active": "#a6e3a1",
|
||||||
"warning_selected": "#c8b48d",
|
"warning_selected": "#94e2d5",
|
||||||
"warning_disabled": "#f9e2af4d",
|
"warning_disabled": "#fab3874d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#cdd6f400",
|
||||||
"ghost_element_background_alt": "#313244",
|
"ghost_element_background_alt": "#181825",
|
||||||
"ghost_element_hover": "#cdd6f433",
|
"ghost_element_hover": "#cdd6f40d",
|
||||||
"ghost_element_active": "#45475a",
|
"ghost_element_active": "#cdd6f41a",
|
||||||
"ghost_element_selected": "#45475a",
|
"ghost_element_selected": "#cdd6f41a",
|
||||||
"ghost_element_disabled": "#cdd6f40d",
|
"ghost_element_disabled": "#cdd6f405",
|
||||||
"tab_inactive_background": "#181825",
|
"tab_background": "#11111b",
|
||||||
"tab_inactive_foreground": "#bac2de",
|
"tab_foreground": "#a6adc8",
|
||||||
|
"tab_hover_background": "#cdd6f40d",
|
||||||
"tab_active_background": "#1e1e2e",
|
"tab_active_background": "#1e1e2e",
|
||||||
"tab_active_foreground": "#cdd6f4",
|
"tab_active_foreground": "#cdd6f4",
|
||||||
"tab_hover_foreground": "#b4befe",
|
"scrollbar_thumb_background": "#cdd6f41a",
|
||||||
"scrollbar_thumb_background": "#cdd6f433",
|
"scrollbar_thumb_hover_background": "#cdd6f426",
|
||||||
"scrollbar_thumb_hover_background": "#cdd6f580",
|
"scrollbar_thumb_border": "#cdd6f400",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#cdd6f400",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#cdd6f400",
|
||||||
"scrollbar_track_border": "#45475a",
|
|
||||||
"drop_target_background": "#89b4fa1a",
|
"drop_target_background": "#89b4fa1a",
|
||||||
"cursor": "#f5e0dc",
|
"cursor": "#89b4fa",
|
||||||
"selection": "#9399b240"
|
"selection": "#89b4fa40"
|
||||||
},
|
},
|
||||||
"dark": {
|
"dark": {
|
||||||
"background": "#1e1e2e",
|
"background": "#1e1e2e",
|
||||||
@@ -76,65 +78,67 @@
|
|||||||
"elevated_surface_background": "#11111b",
|
"elevated_surface_background": "#11111b",
|
||||||
"panel_background": "#1e1e2e",
|
"panel_background": "#1e1e2e",
|
||||||
"overlay": "#cdd6f41a",
|
"overlay": "#cdd6f41a",
|
||||||
"title_bar": "#181825",
|
"title_bar": "#11111b",
|
||||||
"title_bar_inactive": "#11111b",
|
"title_bar_inactive": "#1e1e2e",
|
||||||
"window_border": "#6c7086",
|
"window_border": "#45475a",
|
||||||
"border": "#585b70",
|
"border": "#45475a",
|
||||||
"border_variant": "#45475a",
|
"border_variant": "#313244",
|
||||||
"border_focused": "#89b4fa",
|
"border_focused": "#89b4fa",
|
||||||
"border_selected": "#89b4fa",
|
"border_selected": "#89b4fa",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#cdd6f400",
|
||||||
"border_disabled": "#313244",
|
"border_disabled": "#181825",
|
||||||
"ring": "#89b4fa",
|
"ring": "#b4befe",
|
||||||
"text": "#cdd6f4",
|
"text": "#cdd6f4",
|
||||||
"text_muted": "#bac2de",
|
"text_muted": "#a6adc8",
|
||||||
"text_placeholder": "#a6adc8",
|
"text_placeholder": "#7f849c",
|
||||||
"text_accent": "#89b4fa",
|
"text_accent": "#89b4fa",
|
||||||
"icon": "#bac2de",
|
"text_danger": "#f38ba8",
|
||||||
"icon_muted": "#a6adc8",
|
"text_warning": "#fab387",
|
||||||
"icon_accent": "#89b4fa",
|
"icon": "#a6adc8",
|
||||||
"element_foreground": "#11111b",
|
"icon_muted": "#7f849c",
|
||||||
|
"icon_accent": "#b4befe",
|
||||||
|
"element_foreground": "#1e1e2e",
|
||||||
"element_background": "#89b4fa",
|
"element_background": "#89b4fa",
|
||||||
"element_hover": "#b4befe",
|
"element_hover": "#b4befe",
|
||||||
"element_active": "#7ba2e1",
|
"element_active": "#89dceb",
|
||||||
"element_selected": "#6d90c9",
|
"element_selected": "#74c7ec",
|
||||||
"element_disabled": "#89b4fa4d",
|
"element_disabled": "#89b4fa4d",
|
||||||
"secondary_foreground": "#6d90c9",
|
"secondary_foreground": "#cdd6f4",
|
||||||
"secondary_background": "#181825",
|
"secondary_background": "#313244",
|
||||||
"secondary_hover": "#89b4fa33",
|
"secondary_hover": "#45475a",
|
||||||
"secondary_active": "#11111b",
|
"secondary_active": "#585b70",
|
||||||
"secondary_selected": "#11111b",
|
"secondary_selected": "#585b70",
|
||||||
"secondary_disabled": "#89b4fa4d",
|
"secondary_disabled": "#89b4fa4d",
|
||||||
"danger_foreground": "#11111b",
|
"danger_foreground": "#1e1e2e",
|
||||||
"danger_background": "#f38ba8",
|
"danger_background": "#f38ba8",
|
||||||
"danger_hover": "#eba0ac",
|
"danger_hover": "#eba0ac",
|
||||||
"danger_active": "#db7d98",
|
"danger_active": "#fab387",
|
||||||
"danger_selected": "#c46f88",
|
"danger_selected": "#f9e2af",
|
||||||
"danger_disabled": "#f38ba84d",
|
"danger_disabled": "#f38ba84d",
|
||||||
"warning_foreground": "#11111b",
|
"warning_foreground": "#1e1e2e",
|
||||||
"warning_background": "#f9e2af",
|
"warning_background": "#fab387",
|
||||||
"warning_hover": "#fab387",
|
"warning_hover": "#f9e2af",
|
||||||
"warning_active": "#e0cb9e",
|
"warning_active": "#a6e3a1",
|
||||||
"warning_selected": "#c8b48d",
|
"warning_selected": "#94e2d5",
|
||||||
"warning_disabled": "#f9e2af4d",
|
"warning_disabled": "#fab3874d",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#cdd6f400",
|
||||||
"ghost_element_background_alt": "#313244",
|
"ghost_element_background_alt": "#181825",
|
||||||
"ghost_element_hover": "#cdd6f433",
|
"ghost_element_hover": "#cdd6f40d",
|
||||||
"ghost_element_active": "#45475a",
|
"ghost_element_active": "#cdd6f41a",
|
||||||
"ghost_element_selected": "#45475a",
|
"ghost_element_selected": "#cdd6f41a",
|
||||||
"ghost_element_disabled": "#cdd6f40d",
|
"ghost_element_disabled": "#cdd6f405",
|
||||||
"tab_inactive_background": "#181825",
|
"tab_background": "#11111b",
|
||||||
"tab_inactive_foreground": "#bac2de",
|
"tab_foreground": "#a6adc8",
|
||||||
|
"tab_hover_background": "#cdd6f40d",
|
||||||
"tab_active_background": "#1e1e2e",
|
"tab_active_background": "#1e1e2e",
|
||||||
"tab_active_foreground": "#cdd6f4",
|
"tab_active_foreground": "#cdd6f4",
|
||||||
"tab_hover_foreground": "#b4befe",
|
"scrollbar_thumb_background": "#cdd6f41a",
|
||||||
"scrollbar_thumb_background": "#cdd6f433",
|
"scrollbar_thumb_hover_background": "#cdd6f426",
|
||||||
"scrollbar_thumb_hover_background": "#cdd6f580",
|
"scrollbar_thumb_border": "#cdd6f400",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#cdd6f400",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#cdd6f400",
|
||||||
"scrollbar_track_border": "#45475a",
|
|
||||||
"drop_target_background": "#89b4fa1a",
|
"drop_target_background": "#89b4fa1a",
|
||||||
"cursor": "#f5e0dc",
|
"cursor": "#89b4fa",
|
||||||
"selection": "#9399b240"
|
"selection": "#89b4fa40"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,140 +1,144 @@
|
|||||||
{
|
{
|
||||||
"id": "flexoki",
|
"id": "flexoki",
|
||||||
"name": "Flexoki",
|
"name": "Flexoki",
|
||||||
"author": "Stephan Ango",
|
"author": "Steph Ango (ported by Coop)",
|
||||||
"url": "https://stephango.com/flexoki",
|
"url": "https://stephango.com/flexoki",
|
||||||
"light": {
|
"light": {
|
||||||
"background": "#FFFCF0",
|
"background": "#FFFCF0",
|
||||||
"surface_background": "#F2F0E5",
|
"surface_background": "#F2F0E5",
|
||||||
"elevated_surface_background": "#E6E4D9",
|
"elevated_surface_background": "#E6E4D9",
|
||||||
"panel_background": "#FFFCF0",
|
"panel_background": "#FFFCF0",
|
||||||
"overlay": "#100F0F1a",
|
"overlay": "#100F0F1A",
|
||||||
"title_bar": "#F2F0E5",
|
"title_bar": "#E6E4D9",
|
||||||
"title_bar_inactive": "#E6E4D9",
|
"title_bar_inactive": "#FFFCF0",
|
||||||
"window_border": "#B7B5AC",
|
"window_border": "#CECDC3",
|
||||||
"border": "#CECDC3",
|
"border": "#CECDC3",
|
||||||
"border_variant": "#DAD8CE",
|
"border_variant": "#DAD8CE",
|
||||||
"border_focused": "#205EA6",
|
"border_focused": "#24837B",
|
||||||
"border_selected": "#205EA6",
|
"border_selected": "#24837B",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#100F0F00",
|
||||||
"border_disabled": "#E6E4D9",
|
"border_disabled": "#E6E4D9",
|
||||||
"ring": "#205EA6",
|
"ring": "#3AA99F",
|
||||||
"text": "#100F0F",
|
"text": "#100F0F",
|
||||||
"text_muted": "#6F6E69",
|
"text_muted": "#6F6E69",
|
||||||
"text_placeholder": "#9F9D96",
|
"text_placeholder": "#B7B5AC",
|
||||||
"text_accent": "#205EA6",
|
"text_accent": "#24837B",
|
||||||
|
"text_danger": "#AF3029",
|
||||||
|
"text_warning": "#BC5215",
|
||||||
"icon": "#6F6E69",
|
"icon": "#6F6E69",
|
||||||
"icon_muted": "#9F9D96",
|
"icon_muted": "#B7B5AC",
|
||||||
"icon_accent": "#205EA6",
|
"icon_accent": "#3AA99F",
|
||||||
"element_foreground": "#FFFCF0",
|
"element_foreground": "#FFFCF0",
|
||||||
"element_background": "#205EA6",
|
"element_background": "#24837B",
|
||||||
"element_hover": "#1A4F8C",
|
"element_hover": "#3AA99F",
|
||||||
"element_active": "#163B66",
|
"element_active": "#1C1B1A",
|
||||||
"element_selected": "#133051",
|
"element_selected": "#100F0F",
|
||||||
"element_disabled": "#205EA64d",
|
"element_disabled": "#24837B4D",
|
||||||
"secondary_foreground": "#163B66",
|
"secondary_foreground": "#100F0F",
|
||||||
"secondary_background": "#F2F0E5",
|
"secondary_background": "#E6E4D9",
|
||||||
"secondary_hover": "#205EA61a",
|
"secondary_hover": "#DAD8CE",
|
||||||
"secondary_active": "#E6E4D9",
|
"secondary_active": "#CECDC3",
|
||||||
"secondary_selected": "#E6E4D9",
|
"secondary_selected": "#CECDC3",
|
||||||
"secondary_disabled": "#205EA64d",
|
"secondary_disabled": "#24837B4D",
|
||||||
"danger_foreground": "#FFFCF0",
|
"danger_foreground": "#FFFCF0",
|
||||||
"danger_background": "#D14D41",
|
"danger_background": "#AF3029",
|
||||||
"danger_hover": "#C03E35",
|
"danger_hover": "#D14D41",
|
||||||
"danger_active": "#AF3029",
|
"danger_active": "#1C1B1A",
|
||||||
"danger_selected": "#942822",
|
"danger_selected": "#100F0F",
|
||||||
"danger_disabled": "#D14D414d",
|
"danger_disabled": "#AF30294D",
|
||||||
"warning_foreground": "#100F0F",
|
"warning_foreground": "#FFFCF0",
|
||||||
"warning_background": "#D0A215",
|
"warning_background": "#BC5215",
|
||||||
"warning_hover": "#BE9207",
|
"warning_hover": "#DA702C",
|
||||||
"warning_active": "#AD8301",
|
"warning_active": "#1C1B1A",
|
||||||
"warning_selected": "#8E6B01",
|
"warning_selected": "#100F0F",
|
||||||
"warning_disabled": "#D0A2154d",
|
"warning_disabled": "#BC52154D",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#100F0F00",
|
||||||
"ghost_element_background_alt": "#E6E4D9",
|
"ghost_element_background_alt": "#F2F0E5",
|
||||||
"ghost_element_hover": "#100F0F1a",
|
"ghost_element_hover": "#100F0F0D",
|
||||||
"ghost_element_active": "#DAD8CE",
|
"ghost_element_active": "#100F0F1A",
|
||||||
"ghost_element_selected": "#DAD8CE",
|
"ghost_element_selected": "#100F0F1A",
|
||||||
"ghost_element_disabled": "#100F0F0d",
|
"ghost_element_disabled": "#100F0F05",
|
||||||
"tab_inactive_background": "#F2F0E5",
|
"tab_background": "#E6E4D9",
|
||||||
"tab_inactive_foreground": "#6F6E69",
|
"tab_foreground": "#6F6E69",
|
||||||
|
"tab_hover_background": "#100F0F0D",
|
||||||
"tab_active_background": "#FFFCF0",
|
"tab_active_background": "#FFFCF0",
|
||||||
"tab_active_foreground": "#100F0F",
|
"tab_active_foreground": "#100F0F",
|
||||||
"tab_hover_foreground": "#205EA6",
|
"scrollbar_thumb_background": "#100F0F1A",
|
||||||
"scrollbar_thumb_background": "#100F0F33",
|
"scrollbar_thumb_hover_background": "#100F0F26",
|
||||||
"scrollbar_thumb_hover_background": "#100F0F4d",
|
"scrollbar_thumb_border": "#100F0F00",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#100F0F00",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#100F0F00",
|
||||||
"scrollbar_track_border": "#DAD8CE",
|
"drop_target_background": "#24837B1A",
|
||||||
"drop_target_background": "#205EA61a",
|
"cursor": "#24837B",
|
||||||
"cursor": "#205EA6",
|
"selection": "#24837B40"
|
||||||
"selection": "#205EA640"
|
|
||||||
},
|
},
|
||||||
"dark": {
|
"dark": {
|
||||||
"background": "#100F0F",
|
"background": "#100F0F",
|
||||||
"surface_background": "#1C1B1A",
|
"surface_background": "#1C1B1A",
|
||||||
"elevated_surface_background": "#282726",
|
"elevated_surface_background": "#282726",
|
||||||
"panel_background": "#100F0F",
|
"panel_background": "#100F0F",
|
||||||
"overlay": "#FFFCF01a",
|
"overlay": "#FFFCF01A",
|
||||||
"title_bar": "#1C1B1A",
|
"title_bar": "#282726",
|
||||||
"title_bar_inactive": "#282726",
|
"title_bar_inactive": "#100F0F",
|
||||||
"window_border": "#575653",
|
"window_border": "#403E3C",
|
||||||
"border": "#403E3C",
|
"border": "#403E3C",
|
||||||
"border_variant": "#343331",
|
"border_variant": "#343331",
|
||||||
"border_focused": "#4385BE",
|
"border_focused": "#3AA99F",
|
||||||
"border_selected": "#4385BE",
|
"border_selected": "#3AA99F",
|
||||||
"border_transparent": "#00000000",
|
"border_transparent": "#100F0F00",
|
||||||
"border_disabled": "#282726",
|
"border_disabled": "#282726",
|
||||||
"ring": "#4385BE",
|
"ring": "#24837B",
|
||||||
"text": "#FFFCF0",
|
"text": "#CECDC3",
|
||||||
"text_muted": "#878580",
|
"text_muted": "#878580",
|
||||||
"text_placeholder": "#6F6E69",
|
"text_placeholder": "#575653",
|
||||||
"text_accent": "#4385BE",
|
"text_accent": "#3AA99F",
|
||||||
|
"text_danger": "#D14D41",
|
||||||
|
"text_warning": "#DA702C",
|
||||||
"icon": "#878580",
|
"icon": "#878580",
|
||||||
"icon_muted": "#6F6E69",
|
"icon_muted": "#575653",
|
||||||
"icon_accent": "#4385BE",
|
"icon_accent": "#24837B",
|
||||||
"element_foreground": "#100F0F",
|
"element_foreground": "#100F0F",
|
||||||
"element_background": "#4385BE",
|
"element_background": "#3AA99F",
|
||||||
"element_hover": "#3171B2",
|
"element_hover": "#24837B",
|
||||||
"element_active": "#205EA6",
|
"element_active": "#CECDC3",
|
||||||
"element_selected": "#1A4F8C",
|
"element_selected": "#F2F0E5",
|
||||||
"element_disabled": "#4385BE4d",
|
"element_disabled": "#3AA99F4D",
|
||||||
"secondary_foreground": "#205EA6",
|
"secondary_foreground": "#CECDC3",
|
||||||
"secondary_background": "#1C1B1A",
|
"secondary_background": "#1C1B1A",
|
||||||
"secondary_hover": "#4385BE1a",
|
"secondary_hover": "#282726",
|
||||||
"secondary_active": "#282726",
|
"secondary_active": "#343331",
|
||||||
"secondary_selected": "#282726",
|
"secondary_selected": "#343331",
|
||||||
"secondary_disabled": "#4385BE4d",
|
"secondary_disabled": "#3AA99F4D",
|
||||||
"danger_foreground": "#100F0F",
|
"danger_foreground": "#100F0F",
|
||||||
"danger_background": "#E8705F",
|
"danger_background": "#D14D41",
|
||||||
"danger_hover": "#D14D41",
|
"danger_hover": "#AF3029",
|
||||||
"danger_active": "#C03E35",
|
"danger_active": "#CECDC3",
|
||||||
"danger_selected": "#AF3029",
|
"danger_selected": "#F2F0E5",
|
||||||
"danger_disabled": "#E8705F4d",
|
"danger_disabled": "#D14D414D",
|
||||||
"warning_foreground": "#100F0F",
|
"warning_foreground": "#100F0F",
|
||||||
"warning_background": "#DFB431",
|
"warning_background": "#DA702C",
|
||||||
"warning_hover": "#D0A215",
|
"warning_hover": "#BC5215",
|
||||||
"warning_active": "#BE9207",
|
"warning_active": "#CECDC3",
|
||||||
"warning_selected": "#AD8301",
|
"warning_selected": "#F2F0E5",
|
||||||
"warning_disabled": "#DFB4314d",
|
"warning_disabled": "#DA702C4D",
|
||||||
"ghost_element_background": "#00000000",
|
"ghost_element_background": "#100F0F00",
|
||||||
"ghost_element_background_alt": "#282726",
|
"ghost_element_background_alt": "#1C1B1A",
|
||||||
"ghost_element_hover": "#FFFCF01a",
|
"ghost_element_hover": "#FFFCF00D",
|
||||||
"ghost_element_active": "#343331",
|
"ghost_element_active": "#FFFCF01A",
|
||||||
"ghost_element_selected": "#343331",
|
"ghost_element_selected": "#FFFCF01A",
|
||||||
"ghost_element_disabled": "#FFFCF00d",
|
"ghost_element_disabled": "#FFFCF005",
|
||||||
"tab_inactive_background": "#1C1B1A",
|
"tab_background": "#282726",
|
||||||
"tab_inactive_foreground": "#878580",
|
"tab_foreground": "#878580",
|
||||||
|
"tab_hover_background": "#FFFCF00D",
|
||||||
"tab_active_background": "#100F0F",
|
"tab_active_background": "#100F0F",
|
||||||
"tab_active_foreground": "#FFFCF0",
|
"tab_active_foreground": "#CECDC3",
|
||||||
"tab_hover_foreground": "#4385BE",
|
"scrollbar_thumb_background": "#FFFCF01A",
|
||||||
"scrollbar_thumb_background": "#FFFCF033",
|
"scrollbar_thumb_hover_background": "#FFFCF026",
|
||||||
"scrollbar_thumb_hover_background": "#FFFCF04d",
|
"scrollbar_thumb_border": "#100F0F00",
|
||||||
"scrollbar_thumb_border": "#00000000",
|
"scrollbar_track_background": "#100F0F00",
|
||||||
"scrollbar_track_background": "#00000000",
|
"scrollbar_track_border": "#100F0F00",
|
||||||
"scrollbar_track_border": "#343331",
|
"drop_target_background": "#3AA99F1A",
|
||||||
"drop_target_background": "#4385BE1a",
|
"cursor": "#3AA99F",
|
||||||
"cursor": "#4385BE",
|
"selection": "#3AA99F40"
|
||||||
"selection": "#4385BE40"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||||
use gpui::http_client::{AsyncBody, HttpClient};
|
use gpui::http_client::{AsyncBody, HttpClient};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
|
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
|
||||||
@@ -11,7 +11,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use smol::fs::File;
|
use smol::fs::File;
|
||||||
use smol::io::AsyncReadExt;
|
use smol::io::AsyncReadExt;
|
||||||
use smol::process::Command;
|
use smol::process::Command;
|
||||||
@@ -20,11 +20,11 @@ const GITHUB_API_URL: &str = "https://api.github.com";
|
|||||||
const COOP_UPDATE_EXPLANATION: &str = "COOP_UPDATE_EXPLANATION";
|
const COOP_UPDATE_EXPLANATION: &str = "COOP_UPDATE_EXPLANATION";
|
||||||
|
|
||||||
fn get_github_repo_owner() -> String {
|
fn get_github_repo_owner() -> String {
|
||||||
std::env::var("COOP_GITHUB_REPO_OWNER").unwrap_or_else(|_| "your-username".to_string())
|
std::env::var("COOP_GITHUB_REPO_OWNER").unwrap_or_else(|_| "reyakov".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_github_repo_name() -> String {
|
fn get_github_repo_name() -> String {
|
||||||
std::env::var("COOP_GITHUB_REPO_NAME").unwrap_or_else(|_| "your-repo".to_string())
|
std::env::var("COOP_GITHUB_REPO_NAME").unwrap_or_else(|_| "coop".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_flatpak_installation() -> bool {
|
fn is_flatpak_installation() -> bool {
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||||
use common::EventUtils;
|
use common::EventUtils;
|
||||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
|
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||||
use gpui::{
|
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 nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT, USER_GIFTWRAP};
|
use smol::lock::RwLock;
|
||||||
|
use state::{CoopSigner, DEVICE_GIFTWRAP, NostrRegistry, StateEvent, TIMEOUT, USER_GIFTWRAP};
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
mod room;
|
mod room;
|
||||||
@@ -39,6 +41,10 @@ pub enum ChatEvent {
|
|||||||
CloseRoom(u64),
|
CloseRoom(u64),
|
||||||
/// An event to notify UI about a new chat request
|
/// An event to notify UI about a new chat request
|
||||||
Ping,
|
Ping,
|
||||||
|
/// An event to notify UI that the chat registry has subscribed to messaging relays
|
||||||
|
Subscribed,
|
||||||
|
/// An error occurred
|
||||||
|
Error(SharedString),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Channel signal.
|
/// Channel signal.
|
||||||
@@ -48,41 +54,28 @@ enum Signal {
|
|||||||
Message(NewMessage),
|
Message(NewMessage),
|
||||||
/// Eose received from relay pool
|
/// Eose received from relay pool
|
||||||
Eose,
|
Eose,
|
||||||
}
|
/// An error occurred
|
||||||
|
Error(SharedString),
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Chat Registry
|
/// Chat Registry
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ChatRegistry {
|
pub struct ChatRegistry {
|
||||||
/// Relay state for messaging relay list
|
/// Chat rooms
|
||||||
state: Entity<InboxState>,
|
|
||||||
|
|
||||||
/// Collection of all chat rooms
|
|
||||||
rooms: Vec<Entity<Room>>,
|
rooms: Vec<Entity<Room>>,
|
||||||
|
|
||||||
|
/// Tracking events seen on which relays in the current session
|
||||||
|
seens: Arc<RwLock<HashMap<EventId, HashSet<RelayUrl>>>>,
|
||||||
|
|
||||||
/// Tracking the status of unwrapping gift wrap events.
|
/// Tracking the status of unwrapping gift wrap events.
|
||||||
tracking_flag: Arc<AtomicBool>,
|
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
|
/// Async tasks
|
||||||
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
|
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
|
||||||
|
|
||||||
@@ -105,52 +98,40 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
/// Create a new chat registry instance
|
/// Create a new chat registry instance
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let state = cx.new(|_| InboxState::default());
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let (tx, rx) = flume::unbounded::<Signal>();
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe the nip65 state and load chat rooms on every state change
|
// Subscribe to the signer event
|
||||||
cx.observe(&nostr, |this, state, cx| {
|
cx.subscribe(&nostr, |this, _state, event, cx| {
|
||||||
match state.read(cx).relay_list_state() {
|
match event {
|
||||||
RelayState::Idle => {
|
StateEvent::SignerSet => {
|
||||||
this.reset(cx);
|
this.reset(cx);
|
||||||
|
this.get_rooms(cx);
|
||||||
}
|
}
|
||||||
RelayState::Configured => {
|
StateEvent::RelayConnected => {
|
||||||
this.ensure_messaging_relays(cx);
|
this.get_contact_list(cx);
|
||||||
|
this.get_messages(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);
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Run at the end of the current cycle
|
// Run at the end of the current cycle
|
||||||
cx.defer_in(window, |this, _window, cx| {
|
cx.defer_in(window, |this, _window, cx| {
|
||||||
// Load chat rooms
|
|
||||||
this.get_rooms(cx);
|
this.get_rooms(cx);
|
||||||
|
|
||||||
// Handle nostr notifications
|
|
||||||
this.handle_notifications(cx);
|
this.handle_notifications(cx);
|
||||||
|
|
||||||
// Track unwrap gift wrap progress
|
|
||||||
this.tracking(cx);
|
this.tracking(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
state,
|
|
||||||
rooms: vec![],
|
rooms: vec![],
|
||||||
|
seens: Arc::new(RwLock::new(HashMap::default())),
|
||||||
tracking_flag: Arc::new(AtomicBool::new(false)),
|
tracking_flag: Arc::new(AtomicBool::new(false)),
|
||||||
|
signal_rx: rx,
|
||||||
|
signal_tx: tx,
|
||||||
tasks: smallvec![],
|
tasks: smallvec![],
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
@@ -162,52 +143,64 @@ impl ChatRegistry {
|
|||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
let status = self.tracking_flag.clone();
|
let status = self.tracking_flag.clone();
|
||||||
|
let seens = self.seens.clone();
|
||||||
|
|
||||||
let initialized_at = Timestamp::now();
|
let initialized_at = Timestamp::now();
|
||||||
let sub_id1 = SubscriptionId::new(DEVICE_GIFTWRAP);
|
let sub_id1 = SubscriptionId::new(DEVICE_GIFTWRAP);
|
||||||
let sub_id2 = SubscriptionId::new(USER_GIFTWRAP);
|
let sub_id2 = SubscriptionId::new(USER_GIFTWRAP);
|
||||||
|
|
||||||
// Channel for communication between nostr and gpui
|
// 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 {
|
self.tasks.push(cx.background_spawn(async move {
|
||||||
let device_signer = signer.get_encryption_signer().await;
|
|
||||||
let mut notifications = client.notifications();
|
let mut notifications = client.notifications();
|
||||||
let mut processed_events = HashSet::new();
|
let mut processed_events = HashSet::new();
|
||||||
|
|
||||||
while let Some(notification) = notifications.next().await {
|
while let Some(notification) = notifications.next().await {
|
||||||
let ClientNotification::Message { message, .. } = notification else {
|
let ClientNotification::Message { message, relay_url } = notification else {
|
||||||
// Skip non-message notifications
|
// Skip non-message notifications
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
RelayMessage::Event { event, .. } => {
|
RelayMessage::Event { event, .. } => {
|
||||||
|
// Keep track of which relays have seen this event
|
||||||
|
{
|
||||||
|
let mut seens = seens.write().await;
|
||||||
|
seens.entry(event.id).or_default().insert(relay_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// De-duplicate events by their ID
|
||||||
if !processed_events.insert(event.id) {
|
if !processed_events.insert(event.id) {
|
||||||
// Skip if the event has already been processed
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip non-gift wrap events
|
||||||
if event.kind != Kind::GiftWrap {
|
if event.kind != Kind::GiftWrap {
|
||||||
// Skip non-gift wrap events
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the rumor from the gift wrap event
|
// Extract the rumor from the gift wrap event
|
||||||
match extract_rumor(&client, &device_signer, event.as_ref()).await {
|
match extract_rumor(&client, &signer, event.as_ref()).await {
|
||||||
Ok(rumor) => match rumor.created_at >= initialized_at {
|
Ok(rumor) => {
|
||||||
true => {
|
if rumor.tags.is_empty() {
|
||||||
|
let error: SharedString = "No room for message".into();
|
||||||
|
tx.send_async(Signal::Error(error)).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if rumor.created_at >= initialized_at {
|
||||||
let new_message = NewMessage::new(event.id, rumor);
|
let new_message = NewMessage::new(event.id, rumor);
|
||||||
let signal = Signal::Message(new_message);
|
let signal = Signal::Message(new_message);
|
||||||
|
|
||||||
tx.send_async(signal).await?;
|
tx.send_async(signal).await?;
|
||||||
}
|
} else {
|
||||||
false => {
|
|
||||||
status.store(true, Ordering::Release);
|
status.store(true, Ordering::Release);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to unwrap the gift wrap event: {e}");
|
let error: SharedString = format!("Failed to unwrap: {e}").into();
|
||||||
|
tx.send_async(Signal::Error(error)).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,6 +229,11 @@ impl ChatRegistry {
|
|||||||
this.get_rooms(cx);
|
this.get_rooms(cx);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
Signal::Error(error) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(ChatEvent::Error(error));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,47 +244,84 @@ impl ChatRegistry {
|
|||||||
/// Tracking the status of unwrapping gift wrap events.
|
/// Tracking the status of unwrapping gift wrap events.
|
||||||
fn tracking(&mut self, cx: &mut Context<Self>) {
|
fn tracking(&mut self, cx: &mut Context<Self>) {
|
||||||
let status = self.tracking_flag.clone();
|
let status = self.tracking_flag.clone();
|
||||||
|
let tx = self.signal_tx.clone();
|
||||||
|
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
self.tasks.push(cx.background_spawn(async move {
|
||||||
let loop_duration = Duration::from_secs(10);
|
let loop_duration = Duration::from_secs(15);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if status.load(Ordering::Acquire) {
|
if status.load(Ordering::Acquire) {
|
||||||
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
|
_ = 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;
|
smol::Timer::after(loop_duration).await;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure messaging relays are set up for the current user.
|
/// Get contact list from relays
|
||||||
pub fn ensure_messaging_relays(&mut self, cx: &mut Context<Self>) {
|
pub fn get_contact_list(&mut self, cx: &mut Context<Self>) {
|
||||||
let task = self.verify_relays(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
// Set state to checking
|
let Some(public_key) = signer.public_key() else {
|
||||||
self.set_state(InboxState::Checking, cx);
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
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)));
|
||||||
|
|
||||||
|
// Construct filter for inbox relays
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::ContactList)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// Subscribe
|
||||||
|
client.subscribe(filter).close_on(opts).with_id(id).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
let result = task.await?;
|
match task.await {
|
||||||
|
Ok(_) => {
|
||||||
// Update state
|
this.update(cx, |_this, cx| {
|
||||||
this.update(cx, |this, cx| {
|
cx.emit(ChatEvent::Subscribed);
|
||||||
this.set_state(result, cx);
|
})?;
|
||||||
})?;
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(ChatEvent::Error(SharedString::from(e.to_string())));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify messaging relay list for current user
|
// Get messaging relay list for current user
|
||||||
fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<InboxState, Error>> {
|
fn get_messaging_relays(&self, cx: &App) -> Task<Result<Vec<RelayUrl>, Error>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
let public_key = signer.public_key().unwrap();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
// Construct filter for inbox relays
|
// Construct filter for inbox relays
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::InboxRelays)
|
.kind(Kind::InboxRelays)
|
||||||
@@ -300,50 +335,26 @@ impl ChatRegistry {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
while let Some((_url, res)) = stream.next().await {
|
while let Some((_url, res)) = stream.next().await {
|
||||||
match res {
|
if let Ok(event) = res {
|
||||||
Ok(event) => {
|
log::debug!("Got event: {:?}", event);
|
||||||
return Ok(InboxState::RelayConfigured(Box::new(event)));
|
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
|
||||||
}
|
return Ok(urls);
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to receive relay list event: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
/// 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>>
|
fn subscribe(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||||
where
|
|
||||||
I: IntoIterator<Item = RelayUrl>,
|
|
||||||
{
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
let urls = urls.into_iter().collect::<Vec<_>>();
|
let urls = self.get_messaging_relays(cx);
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
|
let urls = urls.await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||||
let id = SubscriptionId::new(USER_GIFTWRAP);
|
let id = SubscriptionId::new(USER_GIFTWRAP);
|
||||||
@@ -370,19 +381,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
|
/// Get the loading status of the chat registry
|
||||||
pub fn loading(&self) -> bool {
|
pub fn loading(&self) -> bool {
|
||||||
self.tracking_flag.load(Ordering::Acquire)
|
self.tracking_flag.load(Ordering::Acquire)
|
||||||
@@ -413,6 +411,24 @@ impl ChatRegistry {
|
|||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Count the number of messages seen by a given relay.
|
||||||
|
pub fn count_messages(&self, relay_url: &RelayUrl) -> usize {
|
||||||
|
self.seens
|
||||||
|
.read_blocking()
|
||||||
|
.values()
|
||||||
|
.filter(|seen| seen.contains(relay_url))
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the relays that have seen a given message.
|
||||||
|
pub fn seen_on(&self, id: &EventId) -> HashSet<RelayUrl> {
|
||||||
|
self.seens
|
||||||
|
.read_blocking()
|
||||||
|
.get(id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a new room to the start of list.
|
/// Add a new room to the start of list.
|
||||||
pub fn add_room<I>(&mut self, room: I, cx: &mut Context<Self>)
|
pub fn add_room<I>(&mut self, room: I, cx: &mut Context<Self>)
|
||||||
where
|
where
|
||||||
@@ -584,10 +600,10 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
// Process each event and group by room hash
|
// Process each event and group by room hash
|
||||||
for raw in events.into_iter() {
|
for raw in events.into_iter() {
|
||||||
if let Ok(rumor) = UnsignedEvent::from_json(&raw.content) {
|
if let Ok(rumor) = UnsignedEvent::from_json(&raw.content)
|
||||||
if rumor.tags.public_keys().peekable().peek().is_some() {
|
&& rumor.tags.public_keys().peekable().peek().is_some()
|
||||||
grouped.entry(rumor.uniq_id()).or_default().push(rumor);
|
{
|
||||||
}
|
grouped.entry(rumor.uniq_id()).or_default().push(rumor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,9 +643,18 @@ impl ChatRegistry {
|
|||||||
/// If the room doesn't exist, it will be created.
|
/// If the room doesn't exist, it will be created.
|
||||||
/// Updates room ordering based on the most recent messages.
|
/// Updates room ordering based on the most recent messages.
|
||||||
pub fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
|
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) {
|
match self.rooms.iter().find(|e| e.read(cx).id == message.room) {
|
||||||
Some(room) => {
|
Some(room) => {
|
||||||
room.update(cx, |this, cx| {
|
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);
|
this.push_message(message, cx);
|
||||||
});
|
});
|
||||||
self.sort(cx);
|
self.sort(cx);
|
||||||
@@ -642,14 +667,12 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Trigger a refresh of the opened chat rooms by their IDs
|
/// Trigger a refresh of the opened chat rooms by their IDs
|
||||||
pub fn refresh_rooms(&mut self, ids: Option<Vec<u64>>, cx: &mut Context<Self>) {
|
pub fn refresh_rooms(&mut self, ids: &[u64], cx: &mut Context<Self>) {
|
||||||
if let Some(ids) = ids {
|
for room in self.rooms.iter() {
|
||||||
for room in self.rooms.iter() {
|
if ids.contains(&room.read(cx).id) {
|
||||||
if ids.contains(&room.read(cx).id) {
|
room.update(cx, |this, cx| {
|
||||||
room.update(cx, |this, cx| {
|
this.emit_refresh(cx);
|
||||||
this.emit_refresh(cx);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -658,7 +681,7 @@ impl ChatRegistry {
|
|||||||
/// Unwraps a gift-wrapped event and processes its contents.
|
/// Unwraps a gift-wrapped event and processes its contents.
|
||||||
async fn extract_rumor(
|
async fn extract_rumor(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
device_signer: &Option<Arc<dyn NostrSigner>>,
|
signer: &Arc<CoopSigner>,
|
||||||
gift_wrap: &Event,
|
gift_wrap: &Event,
|
||||||
) -> Result<UnsignedEvent, Error> {
|
) -> Result<UnsignedEvent, Error> {
|
||||||
// Try to get cached rumor first
|
// Try to get cached rumor first
|
||||||
@@ -667,7 +690,7 @@ async fn extract_rumor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to unwrap with the available signer
|
// Try to unwrap with the available signer
|
||||||
let unwrapped = try_unwrap(client, device_signer, gift_wrap).await?;
|
let unwrapped = try_unwrap(signer, gift_wrap).await?;
|
||||||
let mut rumor = unwrapped.rumor;
|
let mut rumor = unwrapped.rumor;
|
||||||
|
|
||||||
// Generate event id for the rumor if it doesn't have one
|
// Generate event id for the rumor if it doesn't have one
|
||||||
@@ -682,30 +705,27 @@ async fn extract_rumor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper method to try unwrapping with different signers
|
/// Helper method to try unwrapping with different signers
|
||||||
async fn try_unwrap(
|
async fn try_unwrap(signer: &Arc<CoopSigner>, gift_wrap: &Event) -> Result<UnwrappedGift, Error> {
|
||||||
client: &Client,
|
|
||||||
device_signer: &Option<Arc<dyn NostrSigner>>,
|
|
||||||
gift_wrap: &Event,
|
|
||||||
) -> Result<UnwrappedGift, Error> {
|
|
||||||
// Try with the device signer first
|
// Try with the device signer first
|
||||||
if let Some(signer) = device_signer {
|
if let Some(signer) = signer.get_encryption_signer().await {
|
||||||
if let Ok(unwrapped) = try_unwrap_with(gift_wrap, signer).await {
|
log::info!("trying with encryption key");
|
||||||
|
if let Ok(unwrapped) = try_unwrap_with(gift_wrap, &signer).await {
|
||||||
return Ok(unwrapped);
|
return Ok(unwrapped);
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Try with the user's signer
|
// Fallback to the user's signer
|
||||||
let user_signer = client.signer().context("Signer not found")?;
|
let user_signer = signer.get().await;
|
||||||
let unwrapped = UnwrappedGift::from_gift_wrap(user_signer, gift_wrap).await?;
|
let unwrapped = try_unwrap_with(gift_wrap, &user_signer).await?;
|
||||||
|
|
||||||
Ok(unwrapped)
|
Ok(unwrapped)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to unwrap a gift wrap event with a given signer.
|
/// Attempts to unwrap a gift wrap event with a given signer.
|
||||||
async fn try_unwrap_with(
|
async fn try_unwrap_with<T>(gift_wrap: &Event, signer: &T) -> Result<UnwrappedGift, Error>
|
||||||
gift_wrap: &Event,
|
where
|
||||||
signer: &Arc<dyn NostrSigner>,
|
T: NostrSigner + 'static,
|
||||||
) -> Result<UnwrappedGift, Error> {
|
{
|
||||||
// Get the sealed event
|
// Get the sealed event
|
||||||
let seal = signer
|
let seal = signer
|
||||||
.nip44_decrypt(&gift_wrap.pubkey, &gift_wrap.content)
|
.nip44_decrypt(&gift_wrap.pubkey, &gift_wrap.content)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
use common::EventUtils;
|
use common::{EventUtils, NostrParser};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
/// New message.
|
/// New message.
|
||||||
@@ -91,6 +92,18 @@ impl PartialOrd for Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Mention {
|
||||||
|
pub public_key: PublicKey,
|
||||||
|
pub range: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mention {
|
||||||
|
pub fn new(public_key: PublicKey, range: Range<usize>) -> Self {
|
||||||
|
Self { public_key, range }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Rendered message.
|
/// Rendered message.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RenderedMessage {
|
pub struct RenderedMessage {
|
||||||
@@ -102,7 +115,7 @@ pub struct RenderedMessage {
|
|||||||
/// Message created time as unix timestamp
|
/// Message created time as unix timestamp
|
||||||
pub created_at: Timestamp,
|
pub created_at: Timestamp,
|
||||||
/// List of mentioned public keys in the message
|
/// List of mentioned public keys in the message
|
||||||
pub mentions: Vec<PublicKey>,
|
pub mentions: Vec<Mention>,
|
||||||
/// List of event of the message this message is a reply to
|
/// List of event of the message this message is a reply to
|
||||||
pub replies_to: Vec<EventId>,
|
pub replies_to: Vec<EventId>,
|
||||||
}
|
}
|
||||||
@@ -184,20 +197,17 @@ impl Hash for RenderedMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts all mentions (public keys) from a content string.
|
/// Extracts all mentions (public keys) from a content string.
|
||||||
fn extract_mentions(content: &str) -> Vec<PublicKey> {
|
fn extract_mentions(content: &str) -> Vec<Mention> {
|
||||||
let parser = NostrParser::new();
|
let parser = NostrParser::new();
|
||||||
let tokens = parser.parse(content);
|
let tokens = parser.parse(content);
|
||||||
|
|
||||||
tokens
|
tokens
|
||||||
.filter_map(|token| match token {
|
.filter_map(|token| match token.value {
|
||||||
Token::Nostr(nip21) => match nip21 {
|
Nip21::Pubkey(public_key) => Some(Mention::new(public_key, token.range)),
|
||||||
Nip21::Pubkey(pubkey) => Some(pubkey),
|
Nip21::Profile(profile) => Some(Mention::new(profile.public_key, token.range)),
|
||||||
Nip21::Profile(profile) => Some(profile.public_key),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts all reply (ids) from the event tags.
|
/// Extracts all reply (ids) from the event tags.
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::{Error, anyhow};
|
||||||
use common::EventUtils;
|
use common::EventUtils;
|
||||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
|
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@@ -58,16 +57,44 @@ impl SendReport {
|
|||||||
|
|
||||||
/// Returns true if the send is pending.
|
/// Returns true if the send is pending.
|
||||||
pub fn pending(&self) -> bool {
|
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.
|
/// Returns true if the send was successful.
|
||||||
pub fn success(&self) -> bool {
|
pub fn success(&self) -> bool {
|
||||||
if let Some(output) = self.output.as_ref() {
|
self.error.is_none() && self.output.as_ref().is_some_and(|o| !o.success.is_empty())
|
||||||
!output.failed.is_empty()
|
}
|
||||||
} else {
|
|
||||||
false
|
/// 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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +180,7 @@ impl From<&UnsignedEvent> for Room {
|
|||||||
subject,
|
subject,
|
||||||
members,
|
members,
|
||||||
kind: RoomKind::default(),
|
kind: RoomKind::default(),
|
||||||
config: RoomConfig::default(),
|
config: RoomConfig::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,10 +232,8 @@ impl Room {
|
|||||||
|
|
||||||
/// Sets this room is ongoing conversation
|
/// Sets this room is ongoing conversation
|
||||||
pub fn set_ongoing(&mut self, cx: &mut Context<Self>) {
|
pub fn set_ongoing(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.kind != RoomKind::Ongoing {
|
self.kind = RoomKind::Ongoing;
|
||||||
self.kind = RoomKind::Ongoing;
|
cx.notify();
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the creation timestamp of the room
|
/// Updates the creation timestamp of the room
|
||||||
@@ -232,6 +257,12 @@ impl Room {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the backup config for the room
|
||||||
|
pub fn set_backup(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.config.toggle_backup();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the config of the room
|
/// Returns the config of the room
|
||||||
pub fn config(&self) -> &RoomConfig {
|
pub fn config(&self) -> &RoomConfig {
|
||||||
&self.config
|
&self.config
|
||||||
@@ -319,70 +350,37 @@ impl Room {
|
|||||||
cx.emit(RoomEvent::Reload);
|
cx.emit(RoomEvent::Reload);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
/// Get gossip relays for each member
|
/// Get gossip relays for each member
|
||||||
pub fn connect(&self, cx: &App) -> HashMap<PublicKey, Task<Result<(bool, bool), Error>>> {
|
pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
|
||||||
let public_key = signer.public_key().unwrap();
|
|
||||||
|
|
||||||
let members = self.members();
|
let members = self.members();
|
||||||
let mut tasks = HashMap::new();
|
|
||||||
|
|
||||||
for member in members.into_iter() {
|
cx.background_spawn(async move {
|
||||||
let client = nostr.read(cx).client();
|
let opts = SubscribeAutoCloseOptions::default()
|
||||||
|
.exit_policy(ReqExitPolicy::ExitOnEOSE)
|
||||||
|
.timeout(Some(Duration::from_secs(TIMEOUT)));
|
||||||
|
|
||||||
// Skip if member is the current user
|
for public_key in members.into_iter() {
|
||||||
if member == public_key {
|
let inbox = Filter::new()
|
||||||
continue;
|
.author(public_key)
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let announcement = Filter::new()
|
||||||
|
.author(public_key)
|
||||||
|
.kind(Kind::Custom(10044))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// Subscribe to the target
|
||||||
|
client
|
||||||
|
.subscribe(vec![inbox, announcement])
|
||||||
|
.close_on(opts)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.insert(
|
Ok(())
|
||||||
member,
|
})
|
||||||
cx.background_spawn(async move {
|
|
||||||
let mut has_inbox = false;
|
|
||||||
let mut has_announcement = false;
|
|
||||||
|
|
||||||
// Construct filters for inbox
|
|
||||||
let inbox = Filter::new()
|
|
||||||
.kind(Kind::InboxRelays)
|
|
||||||
.author(member)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Construct filters for announcement
|
|
||||||
let announcement = Filter::new()
|
|
||||||
.kind(Kind::Custom(10044))
|
|
||||||
.author(member)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Stream events from user's write relays
|
|
||||||
let mut stream = client
|
|
||||||
.stream_events(vec![inbox.clone(), announcement.clone()])
|
|
||||||
.timeout(Duration::from_secs(TIMEOUT))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
while let Some((_url, res)) = stream.next().await {
|
|
||||||
let event = res?;
|
|
||||||
|
|
||||||
match event.kind {
|
|
||||||
Kind::InboxRelays => has_inbox = true,
|
|
||||||
Kind::Custom(10044) => has_announcement = true,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Early exit if both flags are found
|
|
||||||
if has_inbox && has_announcement {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((has_inbox, has_announcement))
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all messages belonging to the room
|
/// Get all messages belonging to the room
|
||||||
@@ -425,7 +423,7 @@ impl Room {
|
|||||||
// Get current user's public key
|
// Get current user's public key
|
||||||
let sender = nostr.read(cx).signer().public_key()?;
|
let sender = nostr.read(cx).signer().public_key()?;
|
||||||
|
|
||||||
// Get all members
|
// Get all members, excluding the sender
|
||||||
let members: Vec<Person> = self
|
let members: Vec<Person> = self
|
||||||
.members
|
.members
|
||||||
.iter()
|
.iter()
|
||||||
@@ -502,70 +500,85 @@ impl Room {
|
|||||||
|
|
||||||
// Process each member
|
// Process each member
|
||||||
for member in members {
|
for member in members {
|
||||||
let relays = member.messaging_relays();
|
|
||||||
let announcement = member.announcement();
|
let announcement = member.announcement();
|
||||||
let public_key = member.public_key();
|
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
|
// Handle encryption signer requirements
|
||||||
if signer_kind.encryption() {
|
if signer_kind.encryption() {
|
||||||
|
// Receiver didn't set up a decoupled encryption key
|
||||||
if announcement.is_none() {
|
if announcement.is_none() {
|
||||||
reports.push(SendReport::new(public_key).error(NO_DEKEY));
|
reports.push(SendReport::new(public_key).error(NO_DEKEY));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sender didn't set up a decoupled encryption key
|
||||||
if encryption_signer.is_none() {
|
if encryption_signer.is_none() {
|
||||||
reports.push(SendReport::new(sender.public_key()).error(USER_NO_DEKEY));
|
reports.push(SendReport::new(sender.public_key()).error(USER_NO_DEKEY));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine receiver and signer
|
// Determine the signer to use
|
||||||
let (receiver, signer) = match signer_kind {
|
let signer = match signer_kind {
|
||||||
SignerKind::Auto => {
|
SignerKind::Auto => {
|
||||||
if let Some(announcement) = announcement {
|
if announcement.is_some()
|
||||||
if let Some(enc_signer) = encryption_signer.as_ref() {
|
&& let Some(encryption_signer) = encryption_signer.clone()
|
||||||
(announcement.public_key(), enc_signer.clone())
|
{
|
||||||
} else {
|
// Safe to unwrap due to earlier checks
|
||||||
(member.public_key(), user_signer.clone())
|
encryption_signer
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
(member.public_key(), user_signer.clone())
|
user_signer.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SignerKind::Encryption => {
|
SignerKind::Encryption => {
|
||||||
// Safe to unwrap due to earlier checks
|
// Safe to unwrap due to earlier checks
|
||||||
(
|
encryption_signer.as_ref().unwrap().clone()
|
||||||
announcement.unwrap().public_key(),
|
|
||||||
encryption_signer.as_ref().unwrap().clone(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
SignerKind::User => (member.public_key(), user_signer.clone()),
|
SignerKind::User => user_signer.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match send_gift_wrap(&client, &signer, &receiver, &rumor, relays, public_key).await
|
// Send the gift wrap event and collect the report
|
||||||
{
|
match send_gift_wrap(&client, &signer, &member, &rumor, signer_kind).await {
|
||||||
Ok((report, _)) => {
|
Ok(report) => {
|
||||||
reports.push(report);
|
reports.push(report);
|
||||||
sents += 1;
|
sents += 1;
|
||||||
}
|
}
|
||||||
Err(report) => reports.push(report),
|
Err(error) => {
|
||||||
|
let report = SendReport::new(public_key).error(error.to_string());
|
||||||
|
reports.push(report);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send backup to current user if needed
|
// Send backup to current user if needed
|
||||||
if backup && sents >= 1 {
|
if backup && sents >= 1 {
|
||||||
let relays = sender.messaging_relays();
|
|
||||||
let public_key = sender.public_key();
|
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
|
// Determine the signer to use
|
||||||
{
|
let signer = match signer_kind {
|
||||||
Ok((report, _)) => reports.push(report),
|
SignerKind::Auto => {
|
||||||
Err(report) => reports.push(report),
|
if sender.announcement().is_some()
|
||||||
|
&& let Some(encryption_signer) = encryption_signer.clone()
|
||||||
|
{
|
||||||
|
// Safe to unwrap due to earlier checks
|
||||||
|
encryption_signer
|
||||||
|
} else {
|
||||||
|
user_signer.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SignerKind::Encryption => {
|
||||||
|
// Safe to unwrap due to earlier checks
|
||||||
|
encryption_signer.as_ref().unwrap().clone()
|
||||||
|
}
|
||||||
|
SignerKind::User => user_signer.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match send_gift_wrap(&client, &signer, &sender, &rumor, signer_kind).await {
|
||||||
|
Ok(report) => reports.push(report),
|
||||||
|
Err(error) => {
|
||||||
|
let report = SendReport::new(public_key).error(error.to_string());
|
||||||
|
reports.push(report);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,36 +591,50 @@ impl Room {
|
|||||||
async fn send_gift_wrap<T>(
|
async fn send_gift_wrap<T>(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
signer: &T,
|
signer: &T,
|
||||||
receiver: &PublicKey,
|
receiver: &Person,
|
||||||
rumor: &UnsignedEvent,
|
rumor: &UnsignedEvent,
|
||||||
relays: &[RelayUrl],
|
config: &SignerKind,
|
||||||
public_key: PublicKey,
|
) -> Result<SendReport, Error>
|
||||||
) -> Result<(SendReport, bool), SendReport>
|
|
||||||
where
|
where
|
||||||
T: NostrSigner + 'static,
|
T: NostrSigner + 'static,
|
||||||
{
|
{
|
||||||
// Ensure relay connections
|
let mut extra_tags = vec![];
|
||||||
for url in relays {
|
|
||||||
client.add_relay(url).and_connect().await.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
match EventBuilder::gift_wrap(signer, receiver, rumor.clone(), []).await {
|
// Determine the receiver public key based on the config
|
||||||
Ok(event) => {
|
let receiver = match config {
|
||||||
match client
|
SignerKind::Auto => {
|
||||||
.send_event(&event)
|
if let Some(announcement) = receiver.announcement().as_ref() {
|
||||||
.to(relays)
|
extra_tags.push(Tag::public_key(receiver.public_key()));
|
||||||
.ack_policy(AckPolicy::none())
|
announcement.public_key()
|
||||||
.await
|
} else {
|
||||||
{
|
receiver.public_key()
|
||||||
Ok(output) => Ok((
|
|
||||||
SendReport::new(public_key)
|
|
||||||
.gift_wrap_id(event.id)
|
|
||||||
.output(output),
|
|
||||||
true,
|
|
||||||
)),
|
|
||||||
Err(e) => Err(SendReport::new(public_key).error(e.to_string())),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => Err(SendReport::new(public_key).error(e.to_string())),
|
SignerKind::Encryption => {
|
||||||
}
|
if let Some(announcement) = receiver.announcement().as_ref() {
|
||||||
|
extra_tags.push(Tag::public_key(receiver.public_key()));
|
||||||
|
announcement.public_key()
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!("User has no encryption announcement"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SignerKind::User => receiver.public_key(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Construct the gift wrap event
|
||||||
|
let event = EventBuilder::gift_wrap(signer, &receiver, rumor.clone(), extra_tags).await?;
|
||||||
|
|
||||||
|
// Send the gift wrap event and collect the report
|
||||||
|
let report = client
|
||||||
|
.send_event(&event)
|
||||||
|
.to_nip17()
|
||||||
|
.ack_policy(AckPolicy::none())
|
||||||
|
.await
|
||||||
|
.map(|output| {
|
||||||
|
SendReport::new(receiver)
|
||||||
|
.gift_wrap_id(event.id)
|
||||||
|
.output(output)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(report)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,3 +28,5 @@ serde_json.workspace = true
|
|||||||
|
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
linkify = "0.10.0"
|
||||||
|
pulldown-cmark = "0.13.1"
|
||||||
|
|||||||
@@ -7,20 +7,10 @@ use settings::SignerKind;
|
|||||||
#[action(namespace = chat, no_json)]
|
#[action(namespace = chat, no_json)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
Insert(&'static str),
|
Insert(&'static str),
|
||||||
ChangeSubject(&'static str),
|
ChangeSubject(String),
|
||||||
ChangeSigner(SignerKind),
|
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);
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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,29 +1,29 @@
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use chat::Mention;
|
||||||
|
use common::RangeExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ElementId, HighlightStyle, InteractiveText, IntoElement, SharedString,
|
AnyElement, App, ElementId, Entity, FontStyle, FontWeight, HighlightStyle, InteractiveText,
|
||||||
StyledText, UnderlineStyle, Window,
|
IntoElement, SharedString, StrikethroughStyle, StyledText, UnderlineStyle, Window,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use regex::Regex;
|
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::actions::OpenPublicKey;
|
#[allow(clippy::enum_variant_names)]
|
||||||
|
#[allow(dead_code)]
|
||||||
static URL_REGEX: Lazy<Regex> = Lazy::new(|| {
|
|
||||||
Regex::new(r"(?i)(?:^|\s)(?:https?://)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?::\d+)?(?:/[^\s]*)?(?:\s|$)").unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
static NOSTR_URI_REGEX: Lazy<Regex> =
|
|
||||||
Lazy::new(|| Regex::new(r"nostr:(npub|note|nprofile|nevent|naddr)[a-zA-Z0-9]+").unwrap());
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Highlight {
|
pub enum Highlight {
|
||||||
Link,
|
Code,
|
||||||
Nostr,
|
InlineCode(bool),
|
||||||
|
Highlight(HighlightStyle),
|
||||||
|
Mention,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HighlightStyle> for Highlight {
|
||||||
|
fn from(style: HighlightStyle) -> Self {
|
||||||
|
Self::Highlight(style)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -35,7 +35,12 @@ pub struct RenderedText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RenderedText {
|
impl RenderedText {
|
||||||
pub fn new(content: &str, cx: &App) -> Self {
|
pub fn new(
|
||||||
|
content: &str,
|
||||||
|
mentions: &[Mention],
|
||||||
|
persons: &Entity<PersonRegistry>,
|
||||||
|
cx: &App,
|
||||||
|
) -> Self {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let mut highlights = Vec::new();
|
let mut highlights = Vec::new();
|
||||||
let mut link_ranges = Vec::new();
|
let mut link_ranges = Vec::new();
|
||||||
@@ -43,10 +48,12 @@ impl RenderedText {
|
|||||||
|
|
||||||
render_plain_text_mut(
|
render_plain_text_mut(
|
||||||
content,
|
content,
|
||||||
|
mentions,
|
||||||
&mut text,
|
&mut text,
|
||||||
&mut highlights,
|
&mut highlights,
|
||||||
&mut link_ranges,
|
&mut link_ranges,
|
||||||
&mut link_urls,
|
&mut link_urls,
|
||||||
|
persons,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -61,7 +68,7 @@ impl RenderedText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn element(&self, id: ElementId, window: &Window, cx: &App) -> AnyElement {
|
pub fn element(&self, id: ElementId, window: &Window, cx: &App) -> AnyElement {
|
||||||
let link_color = cx.theme().text_accent;
|
let code_background = cx.theme().elevated_surface_background;
|
||||||
|
|
||||||
InteractiveText::new(
|
InteractiveText::new(
|
||||||
id,
|
id,
|
||||||
@@ -71,15 +78,35 @@ impl RenderedText {
|
|||||||
(
|
(
|
||||||
range.clone(),
|
range.clone(),
|
||||||
match highlight {
|
match highlight {
|
||||||
Highlight::Link => HighlightStyle {
|
Highlight::Code => HighlightStyle {
|
||||||
color: Some(link_color),
|
background_color: Some(code_background),
|
||||||
underline: Some(UnderlineStyle::default()),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Highlight::Nostr => HighlightStyle {
|
Highlight::InlineCode(link) => {
|
||||||
color: Some(link_color),
|
if *link {
|
||||||
|
HighlightStyle {
|
||||||
|
background_color: Some(code_background),
|
||||||
|
underline: Some(UnderlineStyle {
|
||||||
|
thickness: 1.0.into(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HighlightStyle {
|
||||||
|
background_color: Some(code_background),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Highlight::Mention => HighlightStyle {
|
||||||
|
underline: Some(UnderlineStyle {
|
||||||
|
thickness: 1.0.into(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
Highlight::Highlight(highlight) => *highlight,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
@@ -87,22 +114,10 @@ impl RenderedText {
|
|||||||
)
|
)
|
||||||
.on_click(self.link_ranges.clone(), {
|
.on_click(self.link_ranges.clone(), {
|
||||||
let link_urls = self.link_urls.clone();
|
let link_urls = self.link_urls.clone();
|
||||||
move |ix, window, cx| {
|
move |ix, _, cx| {
|
||||||
let token = link_urls[ix].as_str();
|
let url = &link_urls[ix];
|
||||||
|
if url.starts_with("http") {
|
||||||
if let Some(clean_url) = token.strip_prefix("nostr:") {
|
cx.open_url(url);
|
||||||
if let Ok(public_key) = PublicKey::parse(clean_url) {
|
|
||||||
window.dispatch_action(Box::new(OpenPublicKey(public_key)), cx);
|
|
||||||
}
|
|
||||||
} else if is_url(token) {
|
|
||||||
let url = if token.starts_with("http") {
|
|
||||||
token.to_string()
|
|
||||||
} else {
|
|
||||||
format!("https://{token}")
|
|
||||||
};
|
|
||||||
cx.open_url(&url);
|
|
||||||
} else {
|
|
||||||
log::warn!("Unrecognized token {token}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -110,214 +125,273 @@ impl RenderedText {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_plain_text_mut(
|
fn render_plain_text_mut(
|
||||||
content: &str,
|
block: &str,
|
||||||
|
mut mentions: &[Mention],
|
||||||
text: &mut String,
|
text: &mut String,
|
||||||
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
||||||
link_ranges: &mut Vec<Range<usize>>,
|
link_ranges: &mut Vec<Range<usize>>,
|
||||||
link_urls: &mut Vec<String>,
|
link_urls: &mut Vec<String>,
|
||||||
|
persons: &Entity<PersonRegistry>,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) {
|
) {
|
||||||
// Copy the content directly
|
use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
|
||||||
text.push_str(content);
|
|
||||||
|
|
||||||
// Collect all URLs
|
let mut bold_depth = 0;
|
||||||
let mut url_matches: Vec<(Range<usize>, String)> = Vec::new();
|
let mut italic_depth = 0;
|
||||||
|
let mut strikethrough_depth = 0;
|
||||||
|
let mut link_url = None;
|
||||||
|
let mut list_stack = Vec::new();
|
||||||
|
|
||||||
for link in URL_REGEX.find_iter(content) {
|
let mut options = Options::all();
|
||||||
let range = link.start()..link.end();
|
options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST);
|
||||||
let url = link.as_str().to_string();
|
|
||||||
|
|
||||||
url_matches.push((range, url));
|
for (event, source_range) in Parser::new_ext(block, options).into_offset_iter() {
|
||||||
}
|
let prev_len = text.len();
|
||||||
|
|
||||||
// Collect all nostr entities with nostr: prefix
|
match event {
|
||||||
let mut nostr_matches: Vec<(Range<usize>, String)> = Vec::new();
|
Event::Text(t) => {
|
||||||
|
// Process text with mention replacements
|
||||||
|
let t_str = t.as_ref();
|
||||||
|
let mut last_processed = 0;
|
||||||
|
|
||||||
for nostr_match in NOSTR_URI_REGEX.find_iter(content) {
|
while let Some(mention) = mentions.first() {
|
||||||
let range = nostr_match.start()..nostr_match.end();
|
if !source_range.contains_inclusive(&mention.range) {
|
||||||
let nostr_uri = nostr_match.as_str().to_string();
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this nostr URI overlaps with any already processed URL
|
// Calculate positions within the current text
|
||||||
if !url_matches
|
let mention_start_in_text = mention.range.start - source_range.start;
|
||||||
.iter()
|
let mention_end_in_text = mention.range.end - source_range.start;
|
||||||
.any(|(url_range, _)| url_range.start < range.end && range.start < url_range.end)
|
|
||||||
{
|
|
||||||
nostr_matches.push((range, nostr_uri));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine all matches for processing from end to start
|
// Add text before this mention
|
||||||
let mut all_matches = Vec::new();
|
if mention_start_in_text > last_processed {
|
||||||
all_matches.extend(url_matches);
|
let before_mention = &t_str[last_processed..mention_start_in_text];
|
||||||
all_matches.extend(nostr_matches);
|
process_text_segment(
|
||||||
|
before_mention,
|
||||||
|
prev_len + last_processed,
|
||||||
|
bold_depth,
|
||||||
|
italic_depth,
|
||||||
|
strikethrough_depth,
|
||||||
|
link_url.clone(),
|
||||||
|
text,
|
||||||
|
highlights,
|
||||||
|
link_ranges,
|
||||||
|
link_urls,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Sort by position (end to start) to avoid changing positions when replacing text
|
// Process the mention replacement
|
||||||
all_matches.sort_by(|(range_a, _), (range_b, _)| range_b.start.cmp(&range_a.start));
|
let profile = persons.read(cx).get(&mention.public_key, cx);
|
||||||
|
let replacement_text = format!("@{}", profile.name());
|
||||||
|
|
||||||
// Process all matches
|
let replacement_start = text.len();
|
||||||
for (range, entity) in all_matches {
|
text.push_str(&replacement_text);
|
||||||
// Handle URL token
|
let replacement_end = text.len();
|
||||||
if is_url(&entity) {
|
|
||||||
highlights.push((range.clone(), Highlight::Link));
|
|
||||||
link_ranges.push(range);
|
|
||||||
link_urls.push(entity);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(nip21) = Nip21::parse(&entity) {
|
highlights.push((replacement_start..replacement_end, Highlight::Mention));
|
||||||
match nip21 {
|
|
||||||
Nip21::Pubkey(public_key) => {
|
last_processed = mention_end_in_text;
|
||||||
render_pubkey(
|
mentions = &mentions[1..];
|
||||||
public_key,
|
|
||||||
text,
|
|
||||||
&range,
|
|
||||||
highlights,
|
|
||||||
link_ranges,
|
|
||||||
link_urls,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Nip21::Profile(nip19_profile) => {
|
|
||||||
render_pubkey(
|
// Add any remaining text after the last mention
|
||||||
nip19_profile.public_key,
|
if last_processed < t_str.len() {
|
||||||
|
let remaining_text = &t_str[last_processed..];
|
||||||
|
process_text_segment(
|
||||||
|
remaining_text,
|
||||||
|
prev_len + last_processed,
|
||||||
|
bold_depth,
|
||||||
|
italic_depth,
|
||||||
|
strikethrough_depth,
|
||||||
|
link_url.clone(),
|
||||||
text,
|
text,
|
||||||
&range,
|
|
||||||
highlights,
|
|
||||||
link_ranges,
|
|
||||||
link_urls,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Nip21::EventId(event_id) => {
|
|
||||||
render_bech32(
|
|
||||||
event_id.to_bech32().unwrap(),
|
|
||||||
text,
|
|
||||||
&range,
|
|
||||||
highlights,
|
|
||||||
link_ranges,
|
|
||||||
link_urls,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Nip21::Event(nip19_event) => {
|
|
||||||
render_bech32(
|
|
||||||
nip19_event.to_bech32().unwrap(),
|
|
||||||
text,
|
|
||||||
&range,
|
|
||||||
highlights,
|
|
||||||
link_ranges,
|
|
||||||
link_urls,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Nip21::Coordinate(nip19_coordinate) => {
|
|
||||||
render_bech32(
|
|
||||||
nip19_coordinate.to_bech32().unwrap(),
|
|
||||||
text,
|
|
||||||
&range,
|
|
||||||
highlights,
|
highlights,
|
||||||
link_ranges,
|
link_ranges,
|
||||||
link_urls,
|
link_urls,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Event::Code(t) => {
|
||||||
|
text.push_str(t.as_ref());
|
||||||
|
let is_link = link_url.is_some();
|
||||||
|
|
||||||
|
if let Some(link_url) = link_url.clone() {
|
||||||
|
link_ranges.push(prev_len..text.len());
|
||||||
|
link_urls.push(link_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
highlights.push((prev_len..text.len(), Highlight::InlineCode(is_link)))
|
||||||
|
}
|
||||||
|
Event::Start(tag) => match tag {
|
||||||
|
Tag::Paragraph => new_paragraph(text, &mut list_stack),
|
||||||
|
Tag::Heading { .. } => {
|
||||||
|
new_paragraph(text, &mut list_stack);
|
||||||
|
bold_depth += 1;
|
||||||
|
}
|
||||||
|
Tag::CodeBlock(_kind) => {
|
||||||
|
new_paragraph(text, &mut list_stack);
|
||||||
|
}
|
||||||
|
Tag::Emphasis => italic_depth += 1,
|
||||||
|
Tag::Strong => bold_depth += 1,
|
||||||
|
Tag::Strikethrough => strikethrough_depth += 1,
|
||||||
|
Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()),
|
||||||
|
Tag::List(number) => {
|
||||||
|
list_stack.push((number, false));
|
||||||
|
}
|
||||||
|
Tag::Item => {
|
||||||
|
let len = list_stack.len();
|
||||||
|
if let Some((list_number, has_content)) = list_stack.last_mut() {
|
||||||
|
*has_content = false;
|
||||||
|
if !text.is_empty() && !text.ends_with('\n') {
|
||||||
|
text.push('\n');
|
||||||
|
}
|
||||||
|
for _ in 0..len - 1 {
|
||||||
|
text.push_str(" ");
|
||||||
|
}
|
||||||
|
if let Some(number) = list_number {
|
||||||
|
text.push_str(&format!("{}. ", number));
|
||||||
|
*number += 1;
|
||||||
|
*has_content = false;
|
||||||
|
} else {
|
||||||
|
text.push_str("- ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Event::End(tag) => match tag {
|
||||||
|
TagEnd::Heading(_) => bold_depth -= 1,
|
||||||
|
TagEnd::Emphasis => italic_depth -= 1,
|
||||||
|
TagEnd::Strong => bold_depth -= 1,
|
||||||
|
TagEnd::Strikethrough => strikethrough_depth -= 1,
|
||||||
|
TagEnd::Link => link_url = None,
|
||||||
|
TagEnd::List(_) => drop(list_stack.pop()),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Event::HardBreak => text.push('\n'),
|
||||||
|
Event::SoftBreak => text.push('\n'),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a string is a URL
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn is_url(s: &str) -> bool {
|
fn process_text_segment(
|
||||||
URL_REGEX.is_match(s)
|
segment: &str,
|
||||||
}
|
segment_start: usize,
|
||||||
|
bold_depth: i32,
|
||||||
|
italic_depth: i32,
|
||||||
|
strikethrough_depth: i32,
|
||||||
|
link_url: Option<String>,
|
||||||
|
text: &mut String,
|
||||||
|
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
||||||
|
link_ranges: &mut Vec<Range<usize>>,
|
||||||
|
link_urls: &mut Vec<String>,
|
||||||
|
) {
|
||||||
|
// Build the style for this segment
|
||||||
|
let mut style = HighlightStyle::default();
|
||||||
|
if bold_depth > 0 {
|
||||||
|
style.font_weight = Some(FontWeight::BOLD);
|
||||||
|
}
|
||||||
|
if italic_depth > 0 {
|
||||||
|
style.font_style = Some(FontStyle::Italic);
|
||||||
|
}
|
||||||
|
if strikethrough_depth > 0 {
|
||||||
|
style.strikethrough = Some(StrikethroughStyle {
|
||||||
|
thickness: 1.0.into(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Format a bech32 entity with ellipsis and last 4 characters
|
// Add the text
|
||||||
fn format_shortened_entity(entity: &str) -> String {
|
text.push_str(segment);
|
||||||
let prefix_end = entity.find('1').unwrap_or(0);
|
let text_end = text.len();
|
||||||
|
|
||||||
if prefix_end > 0 && entity.len() > prefix_end + 5 {
|
if let Some(link_url) = link_url {
|
||||||
let prefix = &entity[0..=prefix_end]; // Include the '1'
|
// Handle as a markdown link
|
||||||
let suffix = &entity[entity.len() - 4..]; // Last 4 chars
|
link_ranges.push(segment_start..text_end);
|
||||||
|
link_urls.push(link_url);
|
||||||
|
style.underline = Some(UnderlineStyle {
|
||||||
|
thickness: 1.0.into(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
format!("{prefix}...{suffix}")
|
// Add highlight for the entire linked segment
|
||||||
|
if style != HighlightStyle::default() {
|
||||||
|
highlights.push((segment_start..text_end, Highlight::Highlight(style)));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
entity.to_string()
|
// Handle link detection within the segment
|
||||||
}
|
let mut finder = linkify::LinkFinder::new();
|
||||||
}
|
finder.kinds(&[linkify::LinkKind::Url]);
|
||||||
|
let mut last_link_pos = 0;
|
||||||
|
|
||||||
fn render_pubkey(
|
for link in finder.links(segment) {
|
||||||
public_key: PublicKey,
|
let start = link.start();
|
||||||
text: &mut String,
|
let end = link.end();
|
||||||
range: &Range<usize>,
|
|
||||||
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
|
||||||
link_ranges: &mut Vec<Range<usize>>,
|
|
||||||
link_urls: &mut Vec<String>,
|
|
||||||
cx: &App,
|
|
||||||
) {
|
|
||||||
let persons = PersonRegistry::global(cx);
|
|
||||||
let profile = persons.read(cx).get(&public_key, cx);
|
|
||||||
let display_name = format!("@{}", profile.name());
|
|
||||||
|
|
||||||
text.replace_range(range.clone(), &display_name);
|
// Add non-link text before this link
|
||||||
|
if start > last_link_pos {
|
||||||
|
let non_link_start = segment_start + last_link_pos;
|
||||||
|
let non_link_end = segment_start + start;
|
||||||
|
|
||||||
let new_length = display_name.len();
|
if style != HighlightStyle::default() {
|
||||||
let length_diff = new_length as isize - (range.end - range.start) as isize;
|
highlights.push((non_link_start..non_link_end, Highlight::Highlight(style)));
|
||||||
let new_range = range.start..(range.start + new_length);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
highlights.push((new_range.clone(), Highlight::Nostr));
|
// Add the link
|
||||||
link_ranges.push(new_range);
|
let range = (segment_start + start)..(segment_start + end);
|
||||||
link_urls.push(format!("nostr:{}", profile.public_key().to_hex()));
|
link_ranges.push(range.clone());
|
||||||
|
link_urls.push(link.as_str().to_string());
|
||||||
|
|
||||||
if length_diff != 0 {
|
// Apply link styling (underline + existing style)
|
||||||
adjust_ranges(highlights, link_ranges, range.end, length_diff);
|
let mut link_style = style;
|
||||||
}
|
link_style.underline = Some(UnderlineStyle {
|
||||||
}
|
thickness: 1.0.into(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
fn render_bech32(
|
highlights.push((range, Highlight::Highlight(link_style)));
|
||||||
bech32: String,
|
|
||||||
text: &mut String,
|
|
||||||
range: &Range<usize>,
|
|
||||||
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
|
||||||
link_ranges: &mut Vec<Range<usize>>,
|
|
||||||
link_urls: &mut Vec<String>,
|
|
||||||
) {
|
|
||||||
let njump_url = format!("https://njump.me/{bech32}");
|
|
||||||
let shortened_entity = format_shortened_entity(&bech32);
|
|
||||||
let display_text = format!("https://njump.me/{shortened_entity}");
|
|
||||||
|
|
||||||
text.replace_range(range.clone(), &display_text);
|
last_link_pos = end;
|
||||||
|
|
||||||
let new_length = display_text.len();
|
|
||||||
let length_diff = new_length as isize - (range.end - range.start) as isize;
|
|
||||||
let new_range = range.start..(range.start + new_length);
|
|
||||||
|
|
||||||
highlights.push((new_range.clone(), Highlight::Link));
|
|
||||||
link_ranges.push(new_range);
|
|
||||||
link_urls.push(njump_url);
|
|
||||||
|
|
||||||
if length_diff != 0 {
|
|
||||||
adjust_ranges(highlights, link_ranges, range.end, length_diff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to adjust ranges when text length changes
|
|
||||||
fn adjust_ranges(
|
|
||||||
highlights: &mut [(Range<usize>, Highlight)],
|
|
||||||
link_ranges: &mut [Range<usize>],
|
|
||||||
position: usize,
|
|
||||||
length_diff: isize,
|
|
||||||
) {
|
|
||||||
// Adjust highlight ranges
|
|
||||||
for (range, _) in highlights.iter_mut() {
|
|
||||||
if range.start > position {
|
|
||||||
range.start = (range.start as isize + length_diff) as usize;
|
|
||||||
range.end = (range.end as isize + length_diff) as usize;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust link ranges
|
// Add any remaining text after the last link
|
||||||
for range in link_ranges.iter_mut() {
|
if last_link_pos < segment.len() {
|
||||||
if range.start > position {
|
let remaining_start = segment_start + last_link_pos;
|
||||||
range.start = (range.start as isize + length_diff) as usize;
|
let remaining_end = segment_start + segment.len();
|
||||||
range.end = (range.end as isize + length_diff) as usize;
|
|
||||||
|
if style != HighlightStyle::default() {
|
||||||
|
highlights.push((remaining_start..remaining_end, Highlight::Highlight(style)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn new_paragraph(text: &mut String, list_stack: &mut [(Option<u64>, bool)]) {
|
||||||
|
let mut is_subsequent_paragraph_of_list = false;
|
||||||
|
if let Some((_, has_content)) = list_stack.last_mut() {
|
||||||
|
if *has_content {
|
||||||
|
is_subsequent_paragraph_of_list = true;
|
||||||
|
} else {
|
||||||
|
*has_content = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !text.is_empty() {
|
||||||
|
if !text.ends_with('\n') {
|
||||||
|
text.push('\n');
|
||||||
|
}
|
||||||
|
text.push('\n');
|
||||||
|
}
|
||||||
|
for _ in 0..list_stack.len().saturating_sub(1) {
|
||||||
|
text.push_str(" ");
|
||||||
|
}
|
||||||
|
if is_subsequent_paragraph_of_list {
|
||||||
|
text.push_str(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,3 +19,4 @@ log.workspace = true
|
|||||||
|
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
qrcode = "0.14.1"
|
qrcode = "0.14.1"
|
||||||
|
bech32 = "0.11.1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{Error, anyhow};
|
||||||
use chrono::{Local, TimeZone};
|
use chrono::{Local, TimeZone};
|
||||||
use gpui::{Image, ImageFormat, SharedString};
|
use gpui::{Image, ImageFormat, SharedString};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
pub use debounced_delay::*;
|
pub use debounced_delay::*;
|
||||||
pub use display::*;
|
pub use display::*;
|
||||||
pub use event::*;
|
pub use event::*;
|
||||||
|
pub use parser::*;
|
||||||
pub use paths::*;
|
pub use paths::*;
|
||||||
|
pub use range::*;
|
||||||
|
|
||||||
mod debounced_delay;
|
mod debounced_delay;
|
||||||
mod display;
|
mod display;
|
||||||
mod event;
|
mod event;
|
||||||
|
mod parser;
|
||||||
mod paths;
|
mod paths;
|
||||||
|
mod range;
|
||||||
|
|||||||
210
crates/common/src/parser.rs
Normal file
210
crates/common/src/parser.rs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use nostr::prelude::*;
|
||||||
|
|
||||||
|
const BECH32_SEPARATOR: u8 = b'1';
|
||||||
|
const SCHEME_WITH_COLON: &str = "nostr:";
|
||||||
|
|
||||||
|
/// Nostr parsed token with its range in the original text
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Token {
|
||||||
|
/// The parsed NIP-21 URI
|
||||||
|
///
|
||||||
|
/// <https://github.com/nostr-protocol/nips/blob/master/21.md>
|
||||||
|
pub value: Nip21,
|
||||||
|
/// The range of this token in the original text
|
||||||
|
pub range: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct Match {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nostr parser
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct NostrParser;
|
||||||
|
|
||||||
|
impl Default for NostrParser {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NostrParser {
|
||||||
|
/// Create new parser
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse text
|
||||||
|
pub fn parse<'a>(&self, text: &'a str) -> NostrParserIter<'a> {
|
||||||
|
NostrParserIter::new(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FindMatches<'a> {
|
||||||
|
bytes: &'a [u8],
|
||||||
|
pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FindMatches<'a> {
|
||||||
|
fn new(text: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
bytes: text.as_bytes(),
|
||||||
|
pos: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_parse_nostr_uri(&mut self) -> Option<Match> {
|
||||||
|
let start = self.pos;
|
||||||
|
let bytes = self.bytes;
|
||||||
|
let len = bytes.len();
|
||||||
|
|
||||||
|
// Check if we have "nostr:" prefix
|
||||||
|
if len - start < SCHEME_WITH_COLON.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for "nostr:" prefix (case-insensitive)
|
||||||
|
let scheme_prefix = &bytes[start..start + SCHEME_WITH_COLON.len()];
|
||||||
|
if !scheme_prefix.eq_ignore_ascii_case(SCHEME_WITH_COLON.as_bytes()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the scheme
|
||||||
|
let pos = start + SCHEME_WITH_COLON.len();
|
||||||
|
|
||||||
|
// Parse bech32 entity
|
||||||
|
let mut has_separator = false;
|
||||||
|
let mut end = pos;
|
||||||
|
|
||||||
|
while end < len {
|
||||||
|
let byte = bytes[end];
|
||||||
|
|
||||||
|
// Check for bech32 separator
|
||||||
|
if byte == BECH32_SEPARATOR && !has_separator {
|
||||||
|
has_separator = true;
|
||||||
|
end += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if character is valid for bech32
|
||||||
|
if !byte.is_ascii_alphanumeric() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
end += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must have at least one character after separator
|
||||||
|
if !has_separator || end <= pos + 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position
|
||||||
|
self.pos = end;
|
||||||
|
|
||||||
|
Some(Match { start, end })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for FindMatches<'_> {
|
||||||
|
type Item = Match;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
while self.pos < self.bytes.len() {
|
||||||
|
// Try to parse nostr URI
|
||||||
|
if let Some(mat) = self.try_parse_nostr_uri() {
|
||||||
|
return Some(mat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip one character if no match found
|
||||||
|
self.pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HandleMatch {
|
||||||
|
Token(Token),
|
||||||
|
Recursion,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NostrParserIter<'a> {
|
||||||
|
/// The original text
|
||||||
|
text: &'a str,
|
||||||
|
/// Matches found
|
||||||
|
matches: FindMatches<'a>,
|
||||||
|
/// A pending match
|
||||||
|
pending_match: Option<Match>,
|
||||||
|
/// Last match end index
|
||||||
|
last_match_end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> NostrParserIter<'a> {
|
||||||
|
fn new(text: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
text,
|
||||||
|
matches: FindMatches::new(text),
|
||||||
|
pending_match: None,
|
||||||
|
last_match_end: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_match(&mut self, mat: Match) -> HandleMatch {
|
||||||
|
// Update last match end
|
||||||
|
self.last_match_end = mat.end;
|
||||||
|
|
||||||
|
// Extract the matched string
|
||||||
|
let data: &str = &self.text[mat.start..mat.end];
|
||||||
|
|
||||||
|
// Parse NIP-21 URI
|
||||||
|
match Nip21::parse(data) {
|
||||||
|
Ok(uri) => HandleMatch::Token(Token {
|
||||||
|
value: uri,
|
||||||
|
range: mat.start..mat.end,
|
||||||
|
}),
|
||||||
|
// If the nostr URI parsing is invalid, skip it
|
||||||
|
Err(_) => HandleMatch::Recursion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for NostrParserIter<'a> {
|
||||||
|
type Item = Token;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
// Handle a pending match
|
||||||
|
if let Some(pending_match) = self.pending_match.take() {
|
||||||
|
return match self.handle_match(pending_match) {
|
||||||
|
HandleMatch::Token(token) => Some(token),
|
||||||
|
HandleMatch::Recursion => self.next(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.matches.next() {
|
||||||
|
Some(mat) => {
|
||||||
|
// Skip any text before this match
|
||||||
|
if mat.start > self.last_match_end {
|
||||||
|
// Update pending match
|
||||||
|
// This will be handled at next iteration, in `handle_match` method.
|
||||||
|
self.pending_match = Some(mat);
|
||||||
|
|
||||||
|
// Skip the text before the match
|
||||||
|
self.last_match_end = mat.start;
|
||||||
|
return self.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle match
|
||||||
|
match self.handle_match(mat) {
|
||||||
|
HandleMatch::Token(token) => Some(token),
|
||||||
|
HandleMatch::Recursion => self.next(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,13 @@ pub fn home_dir() -> &'static PathBuf {
|
|||||||
HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory"))
|
HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the user's download directory.
|
||||||
|
pub fn download_dir() -> &'static PathBuf {
|
||||||
|
static DOWNLOAD_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
DOWNLOAD_DIR
|
||||||
|
.get_or_init(|| dirs::download_dir().expect("failed to determine download directory"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the path to the configuration directory used by Coop.
|
/// Returns the path to the configuration directory used by Coop.
|
||||||
pub fn config_dir() -> &'static PathBuf {
|
pub fn config_dir() -> &'static PathBuf {
|
||||||
static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
|
static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
@@ -56,9 +63,3 @@ pub fn support_dir() -> &'static PathBuf {
|
|||||||
config_dir().clone()
|
config_dir().clone()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to the `nostr` file.
|
|
||||||
pub fn nostr_file() -> &'static PathBuf {
|
|
||||||
static NOSTR_FILE: OnceLock<PathBuf> = OnceLock::new();
|
|
||||||
NOSTR_FILE.get_or_init(|| support_dir().join("nostr-db"))
|
|
||||||
}
|
|
||||||
|
|||||||
45
crates/common/src/range.rs
Normal file
45
crates/common/src/range.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use std::cmp::{self};
|
||||||
|
use std::ops::{Range, RangeInclusive};
|
||||||
|
|
||||||
|
pub trait RangeExt<T> {
|
||||||
|
fn sorted(&self) -> Self;
|
||||||
|
fn to_inclusive(&self) -> RangeInclusive<T>;
|
||||||
|
fn overlaps(&self, other: &Range<T>) -> bool;
|
||||||
|
fn contains_inclusive(&self, other: &Range<T>) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Ord + Clone> RangeExt<T> for Range<T> {
|
||||||
|
fn sorted(&self) -> Self {
|
||||||
|
cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_inclusive(&self) -> RangeInclusive<T> {
|
||||||
|
self.start.clone()..=self.end.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlaps(&self, other: &Range<T>) -> bool {
|
||||||
|
self.start < other.end && other.start < self.end
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_inclusive(&self, other: &Range<T>) -> bool {
|
||||||
|
self.start <= other.start && other.end <= self.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Ord + Clone> RangeExt<T> for RangeInclusive<T> {
|
||||||
|
fn sorted(&self) -> Self {
|
||||||
|
cmp::min(self.start(), self.end()).clone()..=cmp::max(self.start(), self.end()).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_inclusive(&self) -> RangeInclusive<T> {
|
||||||
|
self.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlaps(&self, other: &Range<T>) -> bool {
|
||||||
|
self.start() < &other.end && &other.start <= self.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_inclusive(&self, other: &Range<T>) -> bool {
|
||||||
|
self.start() <= &other.start && &other.end <= self.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ product-name = "Coop"
|
|||||||
description = "Chat Freely, Stay Private on Nostr"
|
description = "Chat Freely, Stay Private on Nostr"
|
||||||
identifier = "su.reya.coop"
|
identifier = "su.reya.coop"
|
||||||
category = "SocialNetworking"
|
category = "SocialNetworking"
|
||||||
version = "0.3.0"
|
version = "1.0.0-beta2"
|
||||||
out-dir = "../../dist"
|
out-dir = "../../dist"
|
||||||
before-packaging-command = "cargo build --release"
|
before-packaging-command = "cargo build --release"
|
||||||
resources = ["Cargo.toml", "src"]
|
resources = ["Cargo.toml", "src"]
|
||||||
@@ -64,4 +64,8 @@ oneshot.workspace = true
|
|||||||
webbrowser.workspace = true
|
webbrowser.workspace = true
|
||||||
|
|
||||||
indexset = "0.12.3"
|
indexset = "0.12.3"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
# Temporary workaround https://github.com/zed-industries/zed/issues/47168
|
||||||
|
core-text = "=21.0.0"
|
||||||
|
|||||||
257
crates/coop/src/dialogs/accounts.rs
Normal file
257
crates/coop/src/dialogs/accounts.rs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
use anyhow::Error;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
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, StateEvent};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::avatar::Avatar;
|
||||||
|
use ui::button::{Button, ButtonVariants};
|
||||||
|
use ui::indicator::Indicator;
|
||||||
|
use ui::{Disableable, Icon, IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
|
use crate::dialogs::connect::ConnectSigner;
|
||||||
|
use crate::dialogs::import::ImportKey;
|
||||||
|
|
||||||
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<AccountSelector> {
|
||||||
|
cx.new(|cx| AccountSelector::new(window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Account selector
|
||||||
|
pub struct AccountSelector {
|
||||||
|
/// Public key currently being chosen for login
|
||||||
|
logging_in: Entity<Option<PublicKey>>,
|
||||||
|
|
||||||
|
/// The error message displayed when an error occurs.
|
||||||
|
error: Entity<Option<SharedString>>,
|
||||||
|
|
||||||
|
/// Async tasks
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
|
/// Subscription to the signer events
|
||||||
|
_subscription: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountSelector {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let logging_in = cx.new(|_| None);
|
||||||
|
let error = cx.new(|_| None);
|
||||||
|
|
||||||
|
// Subscribe to the signer events
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| {
|
||||||
|
match event {
|
||||||
|
StateEvent::SignerSet => {
|
||||||
|
window.close_all_modals(cx);
|
||||||
|
window.refresh();
|
||||||
|
}
|
||||||
|
StateEvent::Error(e) => {
|
||||||
|
this.set_error(e.to_string(), cx);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
logging_in,
|
||||||
|
error,
|
||||||
|
tasks: vec![],
|
||||||
|
_subscription: Some(subscription),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logging_in(&self, public_key: &PublicKey, cx: &App) -> bool {
|
||||||
|
self.logging_in.read(cx) == &Some(*public_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_logging_in(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
|
||||||
|
self.logging_in.update(cx, |this, cx| {
|
||||||
|
*this = Some(public_key);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<T>(&mut self, error: T, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
T: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.error.update(cx, |this, cx| {
|
||||||
|
*this = Some(error.into());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.logging_in.update(cx, |this, cx| {
|
||||||
|
*this = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let task = nostr.read(cx).get_signer(&public_key, cx);
|
||||||
|
|
||||||
|
// Mark the public key as being logged in
|
||||||
|
self.set_logging_in(public_key, cx);
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(signer) => {
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.set_signer(signer, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_error(e.to_string(), cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.remove_signer(&public_key, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_import(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let import = cx.new(|cx| ImportKey::new(window, cx));
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
|
this.width(px(460.))
|
||||||
|
.title("Import a Secret Key or Bunker Connection")
|
||||||
|
.show_close(true)
|
||||||
|
.pb_2()
|
||||||
|
.child(import.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let connect = cx.new(|cx| ConnectSigner::new(window, cx));
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
|
this.width(px(460.))
|
||||||
|
.title("Scan QR Code to Connect")
|
||||||
|
.show_close(true)
|
||||||
|
.pb_2()
|
||||||
|
.child(connect.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AccountSelector {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let persons = PersonRegistry::global(cx);
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let npubs = nostr.read(cx).npubs();
|
||||||
|
let loading = self.logging_in.read(cx).is_some();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.gap_2()
|
||||||
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.italic()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().text_danger)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.children({
|
||||||
|
let mut items = vec![];
|
||||||
|
|
||||||
|
for (ix, public_key) in npubs.read(cx).iter().enumerate() {
|
||||||
|
let profile = persons.read(cx).get(public_key, cx);
|
||||||
|
let logging_in = self.logging_in(public_key, cx);
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
h_flex()
|
||||||
|
.id(ix)
|
||||||
|
.group("")
|
||||||
|
.px_2()
|
||||||
|
.h_10()
|
||||||
|
.justify_between()
|
||||||
|
.w_full()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().ghost_element_background)
|
||||||
|
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(Avatar::new(profile.avatar()).small())
|
||||||
|
.child(div().text_sm().child(profile.name())),
|
||||||
|
)
|
||||||
|
.when(logging_in, |this| this.child(Indicator::new().small()))
|
||||||
|
.when(!logging_in, |this| {
|
||||||
|
this.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.invisible()
|
||||||
|
.group_hover("", |this| this.visible())
|
||||||
|
.child(
|
||||||
|
Button::new(format!("del-{ix}"))
|
||||||
|
.icon(IconName::Close)
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.disabled(logging_in)
|
||||||
|
.on_click(cx.listener({
|
||||||
|
let public_key = *public_key;
|
||||||
|
move |this, _ev, _window, cx| {
|
||||||
|
cx.stop_propagation();
|
||||||
|
this.remove(public_key, cx);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(!logging_in, |this| {
|
||||||
|
let public_key = *public_key;
|
||||||
|
this.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.login(public_key, window, cx);
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
})
|
||||||
|
.child(div().w_full().h_px().bg(cx.theme().border_variant))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.justify_end()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
Button::new("input")
|
||||||
|
.icon(Icon::new(IconName::Usb))
|
||||||
|
.label("Import")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.disabled(loading)
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.open_import(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("qr")
|
||||||
|
.icon(Icon::new(IconName::Scan))
|
||||||
|
.label("Scan QR to connect")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.disabled(loading)
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.open_connect(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
115
crates/coop/src/dialogs/connect.rs
Normal file
115
crates/coop/src/dialogs/connect.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use common::TextUtils;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
|
Subscription, Window, div, img, px,
|
||||||
|
};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
|
use state::{
|
||||||
|
CLIENT_NAME, CoopAuthUrlHandler, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT, NostrRegistry,
|
||||||
|
StateEvent,
|
||||||
|
};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::v_flex;
|
||||||
|
|
||||||
|
pub struct ConnectSigner {
|
||||||
|
/// QR Code
|
||||||
|
qr_code: Option<Arc<Image>>,
|
||||||
|
|
||||||
|
/// Error message
|
||||||
|
error: Entity<Option<SharedString>>,
|
||||||
|
|
||||||
|
/// Subscription to the signer event
|
||||||
|
_subscription: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectSigner {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let error = cx.new(|_| None);
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let app_keys = nostr.read(cx).keys();
|
||||||
|
|
||||||
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
|
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||||
|
|
||||||
|
// Generate the nostr connect uri
|
||||||
|
let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME);
|
||||||
|
|
||||||
|
// Generate the nostr connect
|
||||||
|
let mut signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None).unwrap();
|
||||||
|
|
||||||
|
// Handle the auth request
|
||||||
|
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
|
// Generate a QR code for quick connection
|
||||||
|
let qr_code = uri.to_string().to_qr();
|
||||||
|
|
||||||
|
// Set signer in the background
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.add_nip46_signer(&signer, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to the signer event
|
||||||
|
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
||||||
|
if let StateEvent::Error(e) = event {
|
||||||
|
this.set_error(e, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
qr_code,
|
||||||
|
error,
|
||||||
|
_subscription: Some(subscription),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.error.update(cx, |this, cx| {
|
||||||
|
*this = Some(message.into());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ConnectSigner {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
const MSG: &str = "Scan with any Nostr Connect-compatible app to connect";
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.p_4()
|
||||||
|
.when_some(self.qr_code.as_ref(), |this, qr| {
|
||||||
|
this.child(
|
||||||
|
img(qr.clone())
|
||||||
|
.size(px(256.))
|
||||||
|
.rounded(cx.theme().radius_lg)
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().border),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().text_danger)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from(MSG)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
300
crates/coop/src/dialogs/import.rs
Normal file
300
crates/coop/src/dialogs/import.rs
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{Error, anyhow};
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
|
Subscription, Task, Window, div,
|
||||||
|
};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
|
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::{Disableable, v_flex};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ImportKey {
|
||||||
|
/// Secret key input
|
||||||
|
key_input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Password input (if required)
|
||||||
|
pass_input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Error message
|
||||||
|
error: Entity<Option<SharedString>>,
|
||||||
|
|
||||||
|
/// Countdown timer for nostr connect
|
||||||
|
countdown: Entity<Option<u64>>,
|
||||||
|
|
||||||
|
/// Whether the user is currently loading
|
||||||
|
loading: bool,
|
||||||
|
|
||||||
|
/// Async tasks
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
|
/// Event subscriptions
|
||||||
|
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportKey {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let key_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
|
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
|
let error = cx.new(|_| None);
|
||||||
|
let countdown = cx.new(|_| None);
|
||||||
|
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Subscribe to key input events and process login when the user presses enter
|
||||||
|
cx.subscribe_in(&key_input, window, |this, _input, event, window, cx| {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
this.login(window, cx);
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Subscribe to the nostr signer event
|
||||||
|
cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
||||||
|
if let StateEvent::Error(e) = event {
|
||||||
|
this.set_error(e, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
key_input,
|
||||||
|
pass_input,
|
||||||
|
error,
|
||||||
|
countdown,
|
||||||
|
loading: false,
|
||||||
|
tasks: vec![],
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.loading {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Prevent duplicate login requests
|
||||||
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
|
let value = self.key_input.read(cx).value();
|
||||||
|
let password = self.pass_input.read(cx).value();
|
||||||
|
|
||||||
|
if value.starts_with("bunker://") {
|
||||||
|
self.bunker(&value, window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.starts_with("ncryptsec1") {
|
||||||
|
self.ncryptsec(value, password, window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(secret) = SecretKey::parse(&value) {
|
||||||
|
let keys = Keys::new(secret);
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
|
// Update the signer
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.add_key_signer(&keys, cx);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.set_error("Invalid key", cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Ok(uri) = NostrConnectUri::parse(content) else {
|
||||||
|
self.set_error("Bunker is not valid", cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let app_keys = nostr.read(cx).keys();
|
||||||
|
let timeout = Duration::from_secs(30);
|
||||||
|
|
||||||
|
// Construct the nostr connect signer
|
||||||
|
let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
||||||
|
|
||||||
|
// Handle auth url with the default browser
|
||||||
|
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
|
// Set signer in the background
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.add_nip46_signer(&signer, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start countdown
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
for i in (0..=30).rev() {
|
||||||
|
if i == 0 {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_countdown(None, cx);
|
||||||
|
})?;
|
||||||
|
} else {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_countdown(Some(i), cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ncryptsec<S>(&mut self, content: S, pwd: S, window: &mut Window, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let content: String = content.into();
|
||||||
|
let password: String = pwd.into();
|
||||||
|
|
||||||
|
if password.is_empty() {
|
||||||
|
self.set_error("Password is required", cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(enc) = EncryptedSecretKey::from_bech32(&content) else {
|
||||||
|
self.set_error("Secret Key is invalid", cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decrypt in the background to ensure it doesn't block the UI
|
||||||
|
let task = cx.background_spawn(async move {
|
||||||
|
if let Ok(content) = enc.decrypt(&password) {
|
||||||
|
Ok(Keys::new(content))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Invalid password"))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(keys) => {
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.add_key_signer(&keys, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_error(e.to_string(), cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<SharedString>,
|
||||||
|
{
|
||||||
|
// Reset the log in state
|
||||||
|
self.set_loading(false, cx);
|
||||||
|
|
||||||
|
// Reset the countdown
|
||||||
|
self.set_countdown(None, cx);
|
||||||
|
|
||||||
|
// Update error message
|
||||||
|
self.error.update(cx, |this, cx| {
|
||||||
|
*this = Some(message.into());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the error message after 3 secs
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.error.update(cx, |this, cx| {
|
||||||
|
*this = None;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
|
self.loading = status;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_countdown(&mut self, i: Option<u64>, cx: &mut Context<Self>) {
|
||||||
|
self.countdown.update(cx, |this, cx| {
|
||||||
|
*this = i;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ImportKey {
|
||||||
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child("nsec or bunker://")
|
||||||
|
.child(TextInput::new(&self.key_input)),
|
||||||
|
)
|
||||||
|
.when(
|
||||||
|
self.key_input.read(cx).value().starts_with("ncryptsec1"),
|
||||||
|
|this| {
|
||||||
|
this.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child("Password:")
|
||||||
|
.child(TextInput::new(&self.pass_input)),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("login")
|
||||||
|
.label("Continue")
|
||||||
|
.primary()
|
||||||
|
.loading(self.loading)
|
||||||
|
.disabled(self.loading)
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.login(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"Approve connection request from your signer in {} seconds",
|
||||||
|
i
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().text_danger)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
|
pub mod accounts;
|
||||||
|
pub mod connect;
|
||||||
|
pub mod import;
|
||||||
|
pub mod restore;
|
||||||
pub mod screening;
|
pub mod screening;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|||||||
130
crates/coop/src/dialogs/restore.rs
Normal file
130
crates/coop/src/dialogs/restore.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
|
use device::DeviceRegistry;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
|
Subscription, Task, Window, div,
|
||||||
|
};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::button::{Button, ButtonVariants};
|
||||||
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
|
use ui::{WindowExtension, v_flex};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RestoreEncryption {
|
||||||
|
/// Secret key input
|
||||||
|
key_input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Error message
|
||||||
|
error: Entity<Option<SharedString>>,
|
||||||
|
|
||||||
|
/// Async tasks
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
|
/// Event subscription
|
||||||
|
_subscription: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RestoreEncryption {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let key_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
|
let error = cx.new(|_| None);
|
||||||
|
|
||||||
|
let subscription =
|
||||||
|
cx.subscribe_in(&key_input, window, |this, _input, event, window, cx| {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
this.restore(window, cx);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
key_input,
|
||||||
|
error,
|
||||||
|
tasks: vec![],
|
||||||
|
_subscription: Some(subscription),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let device = DeviceRegistry::global(cx);
|
||||||
|
let content = self.key_input.read(cx).value();
|
||||||
|
|
||||||
|
if !content.is_empty() {
|
||||||
|
self.set_error("Secret Key cannot be empty.", cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(secret) = SecretKey::parse(&content) else {
|
||||||
|
self.set_error("Secret Key is invalid.", cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
device.update(cx, |this, cx| {
|
||||||
|
this.set_announcement(Keys::new(secret), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the current modal
|
||||||
|
window.close_modal(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<SharedString>,
|
||||||
|
{
|
||||||
|
// Update error message
|
||||||
|
self.error.update(cx, |this, cx| {
|
||||||
|
*this = Some(message.into());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the error message after 3 secs
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.error.update(cx, |this, cx| {
|
||||||
|
*this = None;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for RestoreEncryption {
|
||||||
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child("Secret Key")
|
||||||
|
.child(TextInput::new(&self.key_input)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("restore")
|
||||||
|
.label("Restore")
|
||||||
|
.primary()
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.restore(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().text_danger)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@ use anyhow::{Context as AnyhowContext, Error};
|
|||||||
use common::RenderedTimestamp;
|
use common::RenderedTimestamp;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity,
|
div, px, relative, uniform_list, App, AppContext, Context, Div, Entity, InteractiveElement,
|
||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window,
|
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, Window,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{shorten_pubkey, Person, PersonRegistry};
|
use person::{shorten_pubkey, Person, PersonRegistry};
|
||||||
@@ -41,10 +41,20 @@ pub struct Screening {
|
|||||||
|
|
||||||
/// Async tasks
|
/// Async tasks
|
||||||
tasks: SmallVec<[Task<()>; 3]>,
|
tasks: SmallVec<[Task<()>; 3]>,
|
||||||
|
|
||||||
|
/// Subscriptions
|
||||||
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Screening {
|
impl Screening {
|
||||||
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(cx.on_release_in(window, move |this, window, cx| {
|
||||||
|
this.tasks.clear();
|
||||||
|
window.close_all_modals(cx);
|
||||||
|
}));
|
||||||
|
|
||||||
cx.defer_in(window, move |this, _window, cx| {
|
cx.defer_in(window, move |this, _window, cx| {
|
||||||
this.check_contact(cx);
|
this.check_contact(cx);
|
||||||
this.check_wot(cx);
|
this.check_wot(cx);
|
||||||
@@ -59,6 +69,7 @@ impl Screening {
|
|||||||
last_active: None,
|
last_active: None,
|
||||||
mutual_contacts: vec![],
|
mutual_contacts: vec![],
|
||||||
tasks: smallvec![],
|
tasks: smallvec![],
|
||||||
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,10 +148,10 @@ impl Screening {
|
|||||||
let mut activity: Option<Timestamp> = None;
|
let mut activity: Option<Timestamp> = None;
|
||||||
|
|
||||||
// Construct target for subscription
|
// Construct target for subscription
|
||||||
let target = BOOTSTRAP_RELAYS
|
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|relay| (relay, vec![filter.clone()]))
|
.map(|relay| (relay, vec![filter.clone()]))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect();
|
||||||
|
|
||||||
if let Ok(mut stream) = client
|
if let Ok(mut stream) = client
|
||||||
.stream_events(target)
|
.stream_events(target)
|
||||||
@@ -243,7 +254,7 @@ impl Screening {
|
|||||||
let total = contacts.len();
|
let total = contacts.len();
|
||||||
|
|
||||||
this.title(SharedString::from("Mutual contacts")).child(
|
this.title(SharedString::from("Mutual contacts")).child(
|
||||||
v_flex().gap_1().pb_4().child(
|
v_flex().gap_1().pb_2().child(
|
||||||
uniform_list("contacts", total, move |range, _window, cx| {
|
uniform_list("contacts", total, move |range, _window, cx| {
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let mut items = Vec::with_capacity(total);
|
let mut items = Vec::with_capacity(total);
|
||||||
@@ -263,7 +274,7 @@ impl Screening {
|
|||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||||
.child(Avatar::new(profile.avatar()).size(rems(1.75)))
|
.child(Avatar::new(profile.avatar()).small())
|
||||||
.child(profile.name()),
|
.child(profile.name()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -279,11 +290,21 @@ impl Screening {
|
|||||||
|
|
||||||
impl Render for Screening {
|
impl Render for Screening {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
const CONTACT: &str = "This person is one of your contacts.";
|
||||||
|
const NOT_CONTACT: &str = "This person is not one of your contacts.";
|
||||||
|
const NO_ACTIVITY: &str = "This person hasn't had any activity.";
|
||||||
|
const RELAY_INFO: &str = "Only checked on public relays; may be inaccurate.";
|
||||||
|
const NO_MUTUAL: &str = "You don't have any mutual contacts.";
|
||||||
|
const NIP05_MATCH: &str = "The address matches the user's public key.";
|
||||||
|
const NIP05_NOT_MATCH: &str = "The address does not match the user's public key.";
|
||||||
|
const NO_NIP05: &str = "This person has not set up their friendly address";
|
||||||
|
|
||||||
let profile = self.profile(cx);
|
let profile = self.profile(cx);
|
||||||
let shorten_pubkey = shorten_pubkey(self.public_key, 8);
|
let shorten_pubkey = shorten_pubkey(self.public_key, 8);
|
||||||
|
|
||||||
let total_mutuals = self.mutual_contacts.len();
|
|
||||||
let last_active = self.last_active.map(|_| true);
|
let last_active = self.last_active.map(|_| true);
|
||||||
|
let mutuals = self.mutual_contacts.len();
|
||||||
|
let mutuals_str = format!("You have {} mutual contacts with this person.", mutuals);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
@@ -293,7 +314,7 @@ impl Render for Screening {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.text_center()
|
.text_center()
|
||||||
.child(Avatar::new(profile.avatar()).size(rems(4.)))
|
.child(Avatar::new(profile.avatar()).large())
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
@@ -335,8 +356,9 @@ impl Render for Screening {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("report")
|
Button::new("report")
|
||||||
.tooltip("Report as a scam or impostor")
|
.tooltip("Report as a scam or impostor")
|
||||||
.icon(IconName::Boom)
|
.icon(IconName::Warning)
|
||||||
.danger()
|
.small()
|
||||||
|
.warning()
|
||||||
.rounded()
|
.rounded()
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||||
this.report(window, cx);
|
this.report(window, cx);
|
||||||
@@ -363,9 +385,9 @@ impl Render for Screening {
|
|||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child({
|
.child({
|
||||||
if self.followed {
|
if self.followed {
|
||||||
SharedString::from("This person is one of your contacts.")
|
SharedString::from(CONTACT)
|
||||||
} else {
|
} else {
|
||||||
SharedString::from("This person is not one of your contacts.")
|
SharedString::from(NOT_CONTACT)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -390,7 +412,7 @@ impl Render for Screening {
|
|||||||
.xsmall()
|
.xsmall()
|
||||||
.ghost()
|
.ghost()
|
||||||
.rounded()
|
.rounded()
|
||||||
.tooltip("This may be inaccurate if the user only publishes to their private relays."),
|
.tooltip(RELAY_INFO),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -399,13 +421,13 @@ impl Render for Screening {
|
|||||||
.line_clamp(1)
|
.line_clamp(1)
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if let Some(date) = self.last_active {
|
if let Some(t) = self.last_active {
|
||||||
this.child(SharedString::from(format!(
|
this.child(SharedString::from(format!(
|
||||||
"Last active: {}.",
|
"Last active: {}.",
|
||||||
date.to_human_time()
|
t.to_human_time()
|
||||||
)))
|
)))
|
||||||
} else {
|
} else {
|
||||||
this.child(SharedString::from("This person hasn't had any activity."))
|
this.child(SharedString::from(NO_ACTIVITY))
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -423,7 +445,9 @@ impl Render for Screening {
|
|||||||
if let Some(addr) = self.address(cx) {
|
if let Some(addr) = self.address(cx) {
|
||||||
SharedString::from(format!("{} validation", addr))
|
SharedString::from(format!("{} validation", addr))
|
||||||
} else {
|
} else {
|
||||||
SharedString::from("Friendly Address (NIP-05) validation")
|
SharedString::from(
|
||||||
|
"Friendly Address (NIP-05) validation",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
@@ -433,12 +457,12 @@ impl Render for Screening {
|
|||||||
.child({
|
.child({
|
||||||
if self.address(cx).is_some() {
|
if self.address(cx).is_some() {
|
||||||
if self.verified {
|
if self.verified {
|
||||||
SharedString::from("The address matches the user's public key.")
|
SharedString::from(NIP05_MATCH)
|
||||||
} else {
|
} else {
|
||||||
SharedString::from("The address does not match the user's public key.")
|
SharedString::from(NIP05_NOT_MATCH)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SharedString::from("This person has not set up their friendly address")
|
SharedString::from(NO_NIP05)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -448,7 +472,7 @@ impl Render for Screening {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.items_start()
|
.items_start()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(status_badge(Some(total_mutuals > 0), cx))
|
.child(status_badge(Some(mutuals > 0), cx))
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
@@ -474,13 +498,10 @@ impl Render for Screening {
|
|||||||
.line_clamp(1)
|
.line_clamp(1)
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child({
|
.child({
|
||||||
if total_mutuals > 0 {
|
if mutuals > 0 {
|
||||||
SharedString::from(format!(
|
SharedString::from(mutuals_str)
|
||||||
"You have {} mutual contacts with this person.",
|
|
||||||
total_mutuals
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
SharedString::from("You don't have any mutual contacts with this person.")
|
SharedString::from(NO_MUTUAL)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use gpui::http_client::Url;
|
use gpui::http_client::Url;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
|
App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
Styled, Window,
|
Window, div, px,
|
||||||
};
|
};
|
||||||
use settings::{AppSettings, AuthMode};
|
use settings::{AppSettings, AuthMode};
|
||||||
use theme::{ActiveTheme, ThemeMode};
|
use theme::{ActiveTheme, ThemeMode};
|
||||||
@@ -11,7 +11,7 @@ use ui::input::{InputState, TextInput};
|
|||||||
use ui::menu::{DropdownMenu, PopupMenuItem};
|
use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::switch::Switch;
|
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> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
||||||
cx.new(|cx| Preferences::new(window, cx))
|
cx.new(|cx| Preferences::new(window, cx))
|
||||||
@@ -41,7 +41,7 @@ impl Preferences {
|
|||||||
AppSettings::update_file_server(url, cx);
|
AppSettings::update_file_server(url, cx);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
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 assets::Assets;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, point, px, size, App, AppContext, Bounds, KeyBinding, Menu, MenuItem, SharedString,
|
App, AppContext, Bounds, KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions,
|
||||||
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
|
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
||||||
WindowOptions,
|
actions, point, px, size,
|
||||||
};
|
};
|
||||||
use gpui_platform::application;
|
use gpui_platform::application;
|
||||||
use state::{APP_ID, CLIENT_NAME};
|
use state::{APP_ID, CLIENT_NAME};
|
||||||
@@ -79,16 +79,14 @@ fn main() {
|
|||||||
// Initialize theme registry
|
// Initialize theme registry
|
||||||
theme::init(cx);
|
theme::init(cx);
|
||||||
|
|
||||||
|
// Initialize settings
|
||||||
|
settings::init(window, cx);
|
||||||
|
|
||||||
// Initialize the nostr client
|
// Initialize the nostr client
|
||||||
state::init(window, cx);
|
state::init(window, cx);
|
||||||
|
|
||||||
// Initialize device signer
|
// Initialize person registry
|
||||||
//
|
person::init(window, cx);
|
||||||
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
|
||||||
device::init(window, cx);
|
|
||||||
|
|
||||||
// Initialize settings
|
|
||||||
settings::init(window, cx);
|
|
||||||
|
|
||||||
// Initialize relay auth registry
|
// Initialize relay auth registry
|
||||||
relay_auth::init(window, cx);
|
relay_auth::init(window, cx);
|
||||||
@@ -96,8 +94,10 @@ fn main() {
|
|||||||
// Initialize app registry
|
// Initialize app registry
|
||||||
chat::init(window, cx);
|
chat::init(window, cx);
|
||||||
|
|
||||||
// Initialize person registry
|
// Initialize device signer
|
||||||
person::init(cx);
|
//
|
||||||
|
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
device::init(window, cx);
|
||||||
|
|
||||||
// Initialize auto update
|
// Initialize auto update
|
||||||
auto_update::init(window, cx);
|
auto_update::init(window, cx);
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window,
|
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::KEYRING;
|
use state::KEYRING;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{InputState, TextInput};
|
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. \
|
const MSG: &str = "Store your account keys in a safe location. \
|
||||||
You can restore your account or move to another client anytime you want.";
|
You can restore your account or move to another client anytime you want.";
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use common::TextUtils;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, img, px, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
|
||||||
FocusHandle, Focusable, Image, IntoElement, ParentElement, Render, SharedString, Styled, Task,
|
|
||||||
Window,
|
|
||||||
};
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
|
||||||
use state::NostrRegistry;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
|
||||||
use ui::dock_area::ClosePanel;
|
|
||||||
use ui::notification::Notification;
|
|
||||||
use ui::{v_flex, StyledExt, WindowExtension};
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ConnectPanel> {
|
|
||||||
cx.new(|cx| ConnectPanel::new(window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ConnectPanel {
|
|
||||||
name: SharedString,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
|
|
||||||
/// QR Code
|
|
||||||
qr_code: Option<Arc<Image>>,
|
|
||||||
|
|
||||||
/// Background tasks
|
|
||||||
_tasks: SmallVec<[Task<()>; 1]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectPanel {
|
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let weak_state = nostr.downgrade();
|
|
||||||
let (signer, uri) = nostr.read(cx).client_connect(None);
|
|
||||||
|
|
||||||
// Generate a QR code for quick connection
|
|
||||||
let qr_code = uri.to_string().to_qr();
|
|
||||||
|
|
||||||
let mut tasks = smallvec![];
|
|
||||||
|
|
||||||
tasks.push(
|
|
||||||
// Wait for nostr connect
|
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
|
||||||
let result = signer.bunker_uri().await;
|
|
||||||
|
|
||||||
weak_state
|
|
||||||
.update_in(cx, |this, window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(uri) => {
|
|
||||||
this.persist_bunker(uri, cx);
|
|
||||||
this.set_signer(signer, true, cx);
|
|
||||||
// Close the current panel after setting the signer
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
name: "Nostr Connect".into(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
qr_code,
|
|
||||||
_tasks: tasks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for ConnectPanel {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
self.name.clone().into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for ConnectPanel {}
|
|
||||||
|
|
||||||
impl Focusable for ConnectPanel {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ConnectPanel {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.p_2()
|
|
||||||
.gap_10()
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.justify_center()
|
|
||||||
.items_center()
|
|
||||||
.text_center()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.font_semibold()
|
|
||||||
.line_height(relative(1.25))
|
|
||||||
.child(SharedString::from("Continue with Nostr Connect")),
|
|
||||||
)
|
|
||||||
.child(div().text_sm().text_color(cx.theme().text_muted).child(
|
|
||||||
SharedString::from("Use Nostr Connect apps to scan the code"),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.when_some(self.qr_code.as_ref(), |this, qr| {
|
|
||||||
this.child(
|
|
||||||
img(qr.clone())
|
|
||||||
.size(px(256.))
|
|
||||||
.rounded(cx.theme().radius_lg)
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
358
crates/coop/src/panels/contact_list.rs
Normal file
358
crates/coop/src/panels/contact_list.rs
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{Context as AnyhowContext, Error};
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||||
|
Task, TextAlign, Window, div, rems,
|
||||||
|
};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use person::PersonRegistry;
|
||||||
|
use smallvec::{SmallVec, smallvec};
|
||||||
|
use state::NostrRegistry;
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::avatar::Avatar;
|
||||||
|
use ui::button::{Button, ButtonVariants};
|
||||||
|
use ui::dock::{Panel, PanelEvent};
|
||||||
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ContactListPanel {
|
||||||
|
name: SharedString,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
|
||||||
|
/// Npub input
|
||||||
|
input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Whether the panel is updating
|
||||||
|
updating: bool,
|
||||||
|
|
||||||
|
/// Error message
|
||||||
|
error: Option<SharedString>,
|
||||||
|
|
||||||
|
/// All contacts
|
||||||
|
contacts: HashSet<PublicKey>,
|
||||||
|
|
||||||
|
/// Event subscriptions
|
||||||
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
|
|
||||||
|
/// Background tasks
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContactListPanel {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let input = cx.new(|cx| InputState::new(window, cx).placeholder("npub1..."));
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Subscribe to user's input events
|
||||||
|
cx.subscribe_in(&input, window, move |this, _input, event, window, cx| {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
this.add(window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Run at the end of current cycle
|
||||||
|
cx.defer_in(window, |this, window, cx| {
|
||||||
|
this.load(window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
name: "Contact List".into(),
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
input,
|
||||||
|
updating: false,
|
||||||
|
contacts: HashSet::new(),
|
||||||
|
error: None,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
tasks: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let task: Task<Result<HashSet<PublicKey>, Error>> = cx.background_spawn(async move {
|
||||||
|
let signer = client.signer().context("Signer not found")?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
let contact_list = client.database().contacts_public_keys(public_key).await?;
|
||||||
|
|
||||||
|
Ok(contact_list)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let public_keys = task.await?;
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.contacts.extend(public_keys);
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let value = self.input.read(cx).value().to_string();
|
||||||
|
|
||||||
|
if let Ok(public_key) = PublicKey::parse(&value) {
|
||||||
|
if self.contacts.insert(public_key) {
|
||||||
|
self.input.update(cx, |this, cx| {
|
||||||
|
this.set_value("", window, cx);
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.set_error("Public Key is invalid", window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
||||||
|
self.contacts.remove(public_key);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<E>(&mut self, error: E, window: &mut Window, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
E: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.error = Some(error.into());
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
// Clear the error message after a delay
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.error = None;
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_updating(&mut self, updating: bool, cx: &mut Context<Self>) {
|
||||||
|
self.updating = updating;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.contacts.is_empty() {
|
||||||
|
self.set_error("You need to add at least 1 contact", window, cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
// Get contacts
|
||||||
|
let contacts: Vec<Contact> = self
|
||||||
|
.contacts
|
||||||
|
.iter()
|
||||||
|
.map(|public_key| Contact::new(*public_key))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Set updating state
|
||||||
|
self.set_updating(true, cx);
|
||||||
|
|
||||||
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
|
// 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_nip65().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(_) => {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.set_updating(false, cx);
|
||||||
|
this.load(window, cx);
|
||||||
|
|
||||||
|
window.push_notification("Update successful", cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.set_updating(false, cx);
|
||||||
|
this.set_error(e.to_string(), window, cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_list_items(&mut self, cx: &mut Context<Self>) -> Vec<impl IntoElement> {
|
||||||
|
let persons = PersonRegistry::global(cx);
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
for (ix, public_key) in self.contacts.iter().enumerate() {
|
||||||
|
let profile = persons.read(cx).get(public_key, cx);
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
h_flex()
|
||||||
|
.id(ix)
|
||||||
|
.group("")
|
||||||
|
.flex_1()
|
||||||
|
.w_full()
|
||||||
|
.h_8()
|
||||||
|
.px_2()
|
||||||
|
.justify_between()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().secondary_background)
|
||||||
|
.text_color(cx.theme().secondary_foreground)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(Avatar::new(profile.avatar()).small())
|
||||||
|
.child(profile.name()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("remove_{ix}")
|
||||||
|
.icon(IconName::Close)
|
||||||
|
.xsmall()
|
||||||
|
.ghost()
|
||||||
|
.invisible()
|
||||||
|
.group_hover("", |this| this.visible())
|
||||||
|
.on_click({
|
||||||
|
let public_key = public_key.to_owned();
|
||||||
|
cx.listener(move |this, _ev, _window, cx| {
|
||||||
|
this.remove(&public_key, cx);
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_empty(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.h_20()
|
||||||
|
.justify_center()
|
||||||
|
.border_2()
|
||||||
|
.border_dashed()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.rounded(cx.theme().radius_lg)
|
||||||
|
.text_sm()
|
||||||
|
.text_align(TextAlign::Center)
|
||||||
|
.child(SharedString::from("Please add some relays."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for ContactListPanel {
|
||||||
|
fn panel_id(&self) -> SharedString {
|
||||||
|
self.name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self, _cx: &App) -> AnyElement {
|
||||||
|
self.name.clone().into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<PanelEvent> for ContactListPanel {}
|
||||||
|
|
||||||
|
impl Focusable for ContactListPanel {
|
||||||
|
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ContactListPanel {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex().p_3().gap_3().w_full().child(
|
||||||
|
v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.flex_1()
|
||||||
|
.w_full()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from("New contact:")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
TextInput::new(&self.input)
|
||||||
|
.small()
|
||||||
|
.bordered(false)
|
||||||
|
.cleanable(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("add")
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.tooltip("Add contact")
|
||||||
|
.ghost()
|
||||||
|
.size(rems(2.))
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.add(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(self.error.as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.italic()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_danger)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.map(|this| {
|
||||||
|
if self.contacts.is_empty() {
|
||||||
|
this.child(self.render_empty(window, cx))
|
||||||
|
} else {
|
||||||
|
this.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1()
|
||||||
|
.flex_1()
|
||||||
|
.w_full()
|
||||||
|
.children(self.render_list_items(cx)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
Button::new("submit")
|
||||||
|
.icon(IconName::CheckCircle)
|
||||||
|
.label("Update")
|
||||||
|
.primary()
|
||||||
|
.small()
|
||||||
|
.font_semibold()
|
||||||
|
.loading(self.updating)
|
||||||
|
.disabled(self.updating)
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.update(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
use anyhow::Error;
|
|
||||||
use device::DeviceRegistry;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, px, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
|
||||||
IntoElement, ParentElement, Render, SharedString, Styled, Task, Window,
|
|
||||||
};
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use person::{shorten_pubkey, PersonRegistry};
|
|
||||||
use state::Announcement;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
|
||||||
use ui::notification::Notification;
|
|
||||||
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
|
|
||||||
|
|
||||||
const MSG: &str =
|
|
||||||
"Encryption Key is a special key that used to encrypt and decrypt your messages. \
|
|
||||||
Your identity is completely decoupled from all encryption processes to protect your privacy.";
|
|
||||||
|
|
||||||
const NOTICE: &str = "By resetting your encryption key, you will lose access to \
|
|
||||||
all your encrypted messages before. This action cannot be undone.";
|
|
||||||
|
|
||||||
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<EncryptionPanel> {
|
|
||||||
cx.new(|cx| EncryptionPanel::new(public_key, window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct EncryptionPanel {
|
|
||||||
name: SharedString,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
|
|
||||||
/// User's public key
|
|
||||||
public_key: PublicKey,
|
|
||||||
|
|
||||||
/// Whether the panel is loading
|
|
||||||
loading: bool,
|
|
||||||
|
|
||||||
/// Whether the encryption is resetting
|
|
||||||
resetting: bool,
|
|
||||||
|
|
||||||
/// Tasks
|
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EncryptionPanel {
|
|
||||||
fn new(public_key: PublicKey, _window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
Self {
|
|
||||||
name: "Encryption".into(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
public_key,
|
|
||||||
loading: false,
|
|
||||||
resetting: false,
|
|
||||||
tasks: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.loading = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let device = DeviceRegistry::global(cx);
|
|
||||||
let task = device.read(cx).approve(event, cx);
|
|
||||||
let id = event.id;
|
|
||||||
|
|
||||||
// Update loading status
|
|
||||||
self.set_loading(true, cx);
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
match task.await {
|
|
||||||
Ok(_) => {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
// Reset loading status
|
|
||||||
this.set_loading(false, cx);
|
|
||||||
|
|
||||||
// Remove request
|
|
||||||
device.update(cx, |this, cx| {
|
|
||||||
this.remove_request(&id, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.push_notification("Approved", cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.set_loading(false, cx);
|
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_resetting(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.resetting = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let device = DeviceRegistry::global(cx);
|
|
||||||
let task = device.read(cx).create_encryption(cx);
|
|
||||||
|
|
||||||
// Update the reset status
|
|
||||||
self.set_resetting(true, cx);
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
match task.await {
|
|
||||||
Ok(keys) => {
|
|
||||||
this.update_in(cx, |this, _window, cx| {
|
|
||||||
this.set_resetting(false, cx);
|
|
||||||
|
|
||||||
device.update(cx, |this, cx| {
|
|
||||||
this.set_signer(keys, cx);
|
|
||||||
this.listen_request(cx);
|
|
||||||
});
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.set_resetting(false, cx);
|
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_requests(&mut self, cx: &mut Context<Self>) -> Vec<impl IntoElement> {
|
|
||||||
const TITLE: &str = "You've requested for the Encryption Key from:";
|
|
||||||
|
|
||||||
let device = DeviceRegistry::global(cx);
|
|
||||||
let requests = device.read(cx).requests.clone();
|
|
||||||
let mut items = Vec::new();
|
|
||||||
|
|
||||||
for event in requests.into_iter() {
|
|
||||||
let request = Announcement::from(&event);
|
|
||||||
let client_name = request.client_name();
|
|
||||||
let target = request.public_key();
|
|
||||||
|
|
||||||
items.push(
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.text_sm()
|
|
||||||
.child(SharedString::from(TITLE))
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.h_12()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.px_2()
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.bg(cx.theme().warning_background)
|
|
||||||
.text_color(cx.theme().warning_foreground)
|
|
||||||
.child(client_name.clone()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.h_7()
|
|
||||||
.w_full()
|
|
||||||
.px_2()
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.bg(cx.theme().elevated_surface_background)
|
|
||||||
.child(SharedString::from(target.to_hex())),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex().justify_end().gap_2().child(
|
|
||||||
Button::new("approve")
|
|
||||||
.label("Approve")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.disabled(self.loading)
|
|
||||||
.loading(self.loading)
|
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
|
||||||
this.approve(&event, window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for EncryptionPanel {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
self.name.clone().into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for EncryptionPanel {}
|
|
||||||
|
|
||||||
impl Focusable for EncryptionPanel {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for EncryptionPanel {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let device = DeviceRegistry::global(cx);
|
|
||||||
let state = device.read(cx).state();
|
|
||||||
let has_requests = device.read(cx).has_requests();
|
|
||||||
|
|
||||||
let persons = PersonRegistry::global(cx);
|
|
||||||
let profile = persons.read(cx).get(&self.public_key, cx);
|
|
||||||
|
|
||||||
let Some(announcement) = profile.announcement() else {
|
|
||||||
return div();
|
|
||||||
};
|
|
||||||
|
|
||||||
let pubkey = SharedString::from(shorten_pubkey(announcement.public_key(), 16));
|
|
||||||
let client_name = announcement.client_name();
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.p_3()
|
|
||||||
.gap_3()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from(MSG)),
|
|
||||||
)
|
|
||||||
.child(divider(cx))
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_3()
|
|
||||||
.text_sm()
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from("Device Name:")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.h_12()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.bg(cx.theme().elevated_surface_background)
|
|
||||||
.child(client_name.clone()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from("Encryption Public Key:")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.h_7()
|
|
||||||
.w_full()
|
|
||||||
.px_2()
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.bg(cx.theme().elevated_surface_background)
|
|
||||||
.child(pubkey),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.when(has_requests, |this| {
|
|
||||||
this.child(divider(cx)).child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from("Requests:")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.flex_1()
|
|
||||||
.w_full()
|
|
||||||
.children(self.render_requests(cx)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(divider(cx))
|
|
||||||
.when(state.requesting(), |this| {
|
|
||||||
this.child(
|
|
||||||
h_flex()
|
|
||||||
.h_8()
|
|
||||||
.justify_center()
|
|
||||||
.text_xs()
|
|
||||||
.text_center()
|
|
||||||
.text_color(cx.theme().text_accent)
|
|
||||||
.bg(cx.theme().elevated_surface_background)
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.child(SharedString::from(
|
|
||||||
"Please open other device and approve the request",
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
Button::new("reset")
|
|
||||||
.icon(IconName::Reset)
|
|
||||||
.label("Reset")
|
|
||||||
.warning()
|
|
||||||
.small()
|
|
||||||
.font_semibold()
|
|
||||||
.on_click(
|
|
||||||
cx.listener(move |this, _ev, window, cx| this.reset(window, cx)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.italic()
|
|
||||||
.text_size(px(10.))
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from(NOTICE)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
use chat::{ChatRegistry, InboxState};
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg,
|
||||||
};
|
};
|
||||||
use state::{NostrRegistry, RelayState};
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::dock::DockPlacement;
|
use ui::dock::{DockPlacement, Panel, PanelEvent};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
||||||
use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
|
|
||||||
|
|
||||||
use crate::panels::{connect, import, messaging_relays, profile, relay_list};
|
use crate::panels::profile;
|
||||||
use crate::workspace::Workspace;
|
use crate::workspace::Workspace;
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
||||||
@@ -82,18 +79,6 @@ impl Render for GreeterPanel {
|
|||||||
const TITLE: &str = "Welcome to Coop!";
|
const TITLE: &str = "Welcome to Coop!";
|
||||||
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
|
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();
|
|
||||||
|
|
||||||
let signer = nostr.read(cx).signer();
|
|
||||||
let owned = signer.owned();
|
|
||||||
|
|
||||||
let required_actions =
|
|
||||||
nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable;
|
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -133,118 +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,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(!owned, |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("Use your own identity"))
|
|
||||||
.child(div().flex_1().h_px().bg(cx.theme().border)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
Button::new("connect")
|
|
||||||
.icon(Icon::new(IconName::Door))
|
|
||||||
.label("Connect account via Nostr Connect")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.justify_start()
|
|
||||||
.on_click(move |_ev, window, cx| {
|
|
||||||
Workspace::add_panel(
|
|
||||||
connect::init(window, cx),
|
|
||||||
DockPlacement::Center,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("import")
|
|
||||||
.icon(Icon::new(IconName::Usb))
|
|
||||||
.label("Import a secret key or bunker")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.justify_start()
|
|
||||||
.on_click(move |_ev, window, cx| {
|
|
||||||
Workspace::add_panel(
|
|
||||||
import::init(window, cx),
|
|
||||||
DockPlacement::Center,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
|
|||||||
@@ -1,371 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
|
||||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
|
||||||
};
|
|
||||||
use nostr_connect::prelude::*;
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
|
||||||
use state::{CoopAuthUrlHandler, NostrRegistry};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
|
||||||
use ui::dock_area::ClosePanel;
|
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
|
||||||
use ui::notification::Notification;
|
|
||||||
use ui::{v_flex, Disableable, StyledExt, WindowExtension};
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ImportPanel> {
|
|
||||||
cx.new(|cx| ImportPanel::new(window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ImportPanel {
|
|
||||||
name: SharedString,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
|
|
||||||
/// Secret key input
|
|
||||||
key_input: Entity<InputState>,
|
|
||||||
|
|
||||||
/// Password input (if required)
|
|
||||||
pass_input: Entity<InputState>,
|
|
||||||
|
|
||||||
/// Error message
|
|
||||||
error: Entity<Option<SharedString>>,
|
|
||||||
|
|
||||||
/// Countdown timer for nostr connect
|
|
||||||
countdown: Entity<Option<u64>>,
|
|
||||||
|
|
||||||
/// Whether the user is currently logging in
|
|
||||||
logging_in: bool,
|
|
||||||
|
|
||||||
/// Event subscriptions
|
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ImportPanel {
|
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let key_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
|
||||||
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
|
||||||
|
|
||||||
let error = cx.new(|_| None);
|
|
||||||
let countdown = cx.new(|_| None);
|
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Subscribe to key input events and process login when the user presses enter
|
|
||||||
cx.subscribe_in(&key_input, window, |this, _input, event, window, cx| {
|
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
|
||||||
this.login(window, cx);
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
key_input,
|
|
||||||
pass_input,
|
|
||||||
error,
|
|
||||||
countdown,
|
|
||||||
name: "Import".into(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
logging_in: false,
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if self.logging_in {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
// Prevent duplicate login requests
|
|
||||||
self.set_logging_in(true, cx);
|
|
||||||
|
|
||||||
let value = self.key_input.read(cx).value();
|
|
||||||
let password = self.pass_input.read(cx).value();
|
|
||||||
|
|
||||||
if value.starts_with("bunker://") {
|
|
||||||
self.login_with_bunker(&value, window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.starts_with("ncryptsec1") {
|
|
||||||
self.login_with_password(&value, &password, window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(secret) = SecretKey::parse(&value) {
|
|
||||||
let keys = Keys::new(secret);
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
// Update the signer
|
|
||||||
nostr.update(cx, |this, cx| {
|
|
||||||
this.set_signer(keys, true, cx);
|
|
||||||
});
|
|
||||||
// Close the current panel after setting the signer
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
} else {
|
|
||||||
self.set_error("Invalid", cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn login_with_bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Ok(uri) = NostrConnectUri::parse(content) else {
|
|
||||||
self.set_error("Bunker is not valid", cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let weak_state = nostr.downgrade();
|
|
||||||
|
|
||||||
let app_keys = nostr.read(cx).app_keys();
|
|
||||||
let timeout = Duration::from_secs(30);
|
|
||||||
let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
|
||||||
|
|
||||||
// Handle auth url with the default browser
|
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
|
||||||
|
|
||||||
// Start countdown
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
for i in (0..=30).rev() {
|
|
||||||
if i == 0 {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_countdown(None, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
} else {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_countdown(Some(i), cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
cx.background_executor().timer(Duration::from_secs(1)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
// Handle connection
|
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
|
||||||
let result = signer.bunker_uri().await;
|
|
||||||
|
|
||||||
weak_state
|
|
||||||
.update_in(cx, |this, window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(uri) => {
|
|
||||||
this.persist_bunker(uri, cx);
|
|
||||||
this.set_signer(signer, true, cx);
|
|
||||||
// Close the current panel after setting the signer
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn login_with_password(
|
|
||||||
&mut self,
|
|
||||||
content: &str,
|
|
||||||
pwd: &str,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if pwd.is_empty() {
|
|
||||||
self.set_error("Password is required", cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Ok(enc) = EncryptedSecretKey::from_bech32(content) else {
|
|
||||||
self.set_error("Secret Key is invalid", cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let password = pwd.to_owned();
|
|
||||||
|
|
||||||
// Decrypt in the background to ensure it doesn't block the UI
|
|
||||||
let task = cx.background_spawn(async move {
|
|
||||||
if let Ok(content) = enc.decrypt(&password) {
|
|
||||||
Ok(Keys::new(content))
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Invalid password"))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
let result = task.await;
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(keys) => {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
// Update the signer
|
|
||||||
nostr.update(cx, |this, cx| {
|
|
||||||
this.set_signer(keys, true, cx);
|
|
||||||
});
|
|
||||||
// Close the current panel after setting the signer
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.set_error(e.to_string(), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
|
||||||
where
|
|
||||||
S: Into<SharedString>,
|
|
||||||
{
|
|
||||||
// Reset the log in state
|
|
||||||
self.set_logging_in(false, cx);
|
|
||||||
|
|
||||||
// Reset the countdown
|
|
||||||
self.set_countdown(None, cx);
|
|
||||||
|
|
||||||
// Update error message
|
|
||||||
self.error.update(cx, |this, cx| {
|
|
||||||
*this = Some(message.into());
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear the error message after 3 secs
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
cx.background_executor().timer(Duration::from_secs(3)).await;
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.error.update(cx, |this, cx| {
|
|
||||||
*this = None;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.logging_in = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_countdown(&mut self, i: Option<u64>, cx: &mut Context<Self>) {
|
|
||||||
self.countdown.update(cx, |this, cx| {
|
|
||||||
*this = i;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for ImportPanel {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
self.name.clone().into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for ImportPanel {}
|
|
||||||
|
|
||||||
impl Focusable for ImportPanel {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ImportPanel {
|
|
||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
const SECRET_WARN: &str = "* Coop doesn't store your secret key. \
|
|
||||||
It will be cleared when you close the app. \
|
|
||||||
To persist your identity, please connect via Nostr Connect.";
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.p_2()
|
|
||||||
.gap_10()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_center()
|
|
||||||
.font_semibold()
|
|
||||||
.line_height(relative(1.25))
|
|
||||||
.child(SharedString::from("Import a Secret Key or Bunker")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.w_112()
|
|
||||||
.gap_2()
|
|
||||||
.text_sm()
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child("nsec or bunker://")
|
|
||||||
.child(TextInput::new(&self.key_input)),
|
|
||||||
)
|
|
||||||
.when(
|
|
||||||
self.key_input.read(cx).value().starts_with("ncryptsec1"),
|
|
||||||
|this| {
|
|
||||||
this.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child("Password:")
|
|
||||||
.child(TextInput::new(&self.pass_input)),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("login")
|
|
||||||
.label("Continue")
|
|
||||||
.primary()
|
|
||||||
.loading(self.logging_in)
|
|
||||||
.disabled(self.logging_in)
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.login(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_center()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from(format!(
|
|
||||||
"Approve connection request from your signer in {} seconds",
|
|
||||||
i
|
|
||||||
))),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_center()
|
|
||||||
.text_color(cx.theme().danger_foreground)
|
|
||||||
.child(error.clone()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.mt_2()
|
|
||||||
.italic()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from(SECRET_WARN)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
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,
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||||
Task, TextAlign, Window,
|
Task, TextAlign, Window, div, rems,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{NostrRegistry, TIMEOUT};
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
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::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. \
|
const MSG: &str = "Messaging Relays are relays that hosted all your messages. \
|
||||||
Other users will find your relays and send messages to it.";
|
Other users will find your relays and send messages to it.";
|
||||||
@@ -187,11 +187,7 @@ impl MessagingRelayPanel {
|
|||||||
let event = client.sign_event_builder(builder).await?;
|
let event = client.sign_event_builder(builder).await?;
|
||||||
|
|
||||||
// Set messaging relays
|
// Set messaging relays
|
||||||
client
|
client.send_event(&event).to_nip65().await?;
|
||||||
.send_event(&event)
|
|
||||||
.to_nip65()
|
|
||||||
.ok_timeout(Duration::from_secs(TIMEOUT))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
@@ -342,7 +338,7 @@ impl Render for MessagingRelayPanel {
|
|||||||
div()
|
div()
|
||||||
.italic()
|
.italic()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().danger_foreground)
|
.text_color(cx.theme().text_danger)
|
||||||
.child(error.clone()),
|
.child(error.clone()),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
pub mod backup;
|
pub mod backup;
|
||||||
pub mod connect;
|
pub mod contact_list;
|
||||||
pub mod encryption_key;
|
|
||||||
pub mod greeter;
|
pub mod greeter;
|
||||||
pub mod import;
|
|
||||||
pub mod messaging_relays;
|
pub mod messaging_relays;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod relay_list;
|
pub mod relay_list;
|
||||||
|
|||||||
@@ -3,21 +3,21 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{Context as AnyhowContext, Error};
|
use anyhow::{Context as AnyhowContext, Error};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, rems, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter,
|
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||||
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
|
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
|
||||||
Styled, Task, Window,
|
Window, div,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{shorten_pubkey, Person, PersonRegistry};
|
use person::{Person, PersonRegistry, shorten_pubkey};
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use state::{upload, NostrRegistry};
|
use state::{NostrRegistry, upload};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::input::{InputState, TextInput};
|
||||||
use ui::notification::Notification;
|
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> {
|
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> {
|
||||||
cx.new(|cx| ProfilePanel::new(public_key, window, cx))
|
cx.new(|cx| ProfilePanel::new(public_key, window, cx))
|
||||||
@@ -186,7 +186,10 @@ impl ProfilePanel {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.set_uploading(false, 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) => {
|
Err(e) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
window.push_notification(
|
||||||
|
Notification::error(e.to_string()).autohide(false),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -322,7 +328,7 @@ impl Render for ProfilePanel {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.child(Avatar::new(avatar).size(rems(4.25)))
|
.child(Avatar::new(avatar).large())
|
||||||
.child(
|
.child(
|
||||||
Button::new("upload")
|
Button::new("upload")
|
||||||
.icon(IconName::PlusCircle)
|
.icon(IconName::PlusCircle)
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, rems, Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||||
Subscription, Task, TextAlign, Window,
|
Task, TextAlign, Window, div, px, rems,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
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::input::{InputEvent, InputState, TextInput};
|
||||||
use ui::menu::DropdownMenu;
|
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 \
|
const MSG: &str = "Relay List (or Gossip Relays) are a set of relays \
|
||||||
where you will publish all your events. Others also publish events \
|
where you will publish all your events. Others also publish events \
|
||||||
@@ -408,7 +408,7 @@ impl Render for RelayListPanel {
|
|||||||
div()
|
div()
|
||||||
.italic()
|
.italic()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().danger_foreground)
|
.text_color(cx.theme().text_danger)
|
||||||
.child(error.clone()),
|
.child(error.clone()),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ use std::rc::Rc;
|
|||||||
use chat::RoomKind;
|
use chat::RoomKind;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, rems, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce,
|
App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString,
|
||||||
SharedString, StatefulInteractiveElement, Styled, Window,
|
StatefulInteractiveElement, Styled, Window, div,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::dock_area::ClosePanel;
|
use ui::dock::ClosePanel;
|
||||||
use ui::modal::ModalButtonProps;
|
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;
|
use crate::dialogs::screening;
|
||||||
|
|
||||||
@@ -106,14 +106,7 @@ impl RenderOnce for RoomEntry {
|
|||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.when(!hide_avatar, |this| {
|
.when(!hide_avatar, |this| {
|
||||||
this.when_some(self.avatar, |this, avatar| {
|
this.when_some(self.avatar, |this, avatar| {
|
||||||
this.child(
|
this.child(Avatar::new(avatar).small().flex_shrink_0())
|
||||||
div()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.size_6()
|
|
||||||
.rounded_full()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(Avatar::new(avatar).size(rems(1.5))),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
@@ -155,26 +148,27 @@ impl RenderOnce for RoomEntry {
|
|||||||
this.on_click(move |event, window, cx| {
|
this.on_click(move |event, window, cx| {
|
||||||
handler(event, window, cx);
|
handler(event, window, cx);
|
||||||
|
|
||||||
if let Some(public_key) = public_key {
|
if let Some(public_key) = public_key
|
||||||
if self.kind != Some(RoomKind::Ongoing) && screening {
|
&& self.kind != Some(RoomKind::Ongoing)
|
||||||
let screening = screening::init(public_key, window, cx);
|
&& screening
|
||||||
|
{
|
||||||
|
let screening = screening::init(public_key, window, cx);
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
this.confirm()
|
this.confirm()
|
||||||
.child(screening.clone())
|
.child(screening.clone())
|
||||||
.button_props(
|
.button_props(
|
||||||
ModalButtonProps::default()
|
ModalButtonProps::default()
|
||||||
.cancel_text("Ignore")
|
.cancel_text("Ignore")
|
||||||
.ok_text("Response"),
|
.ok_text("Response"),
|
||||||
)
|
)
|
||||||
.on_cancel(move |_event, window, cx| {
|
.on_cancel(move |_event, window, cx| {
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
window.dispatch_action(Box::new(ClosePanel), cx);
|
||||||
// Prevent closing the modal on click
|
// Prevent closing the modal on click
|
||||||
// modal will be automatically closed after closing panel
|
// modal will be automatically closed after closing panel
|
||||||
false
|
false
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,22 +8,22 @@ use common::{DebouncedDelay, RenderedTimestamp};
|
|||||||
use entry::RoomEntry;
|
use entry::RoomEntry;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
|
||||||
IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled, Subscription,
|
ParentElement, Render, RetainAllImageCache, SharedString, Styled, Subscription, Task,
|
||||||
Task, UniformListScrollHandle, Window,
|
UniformListScrollHandle, Window, div, uniform_list,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{NostrRegistry, FIND_DELAY};
|
use state::{FIND_DELAY, NostrRegistry};
|
||||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT};
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::scroll::Scrollbar;
|
use ui::scroll::Scrollbar;
|
||||||
use ui::{h_flex, v_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension};
|
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
mod entry;
|
mod entry;
|
||||||
|
|
||||||
@@ -180,7 +180,10 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
cx.update(|window, cx| {
|
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;
|
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 chat = ChatRegistry::global(cx);
|
||||||
let rooms = chat.read(cx).rooms(self.filter.read(cx), cx);
|
let rooms = chat.read(cx).rooms(self.filter.read(cx), cx);
|
||||||
|
|
||||||
@@ -395,7 +402,11 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render the contact list
|
/// 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);
|
let persons = PersonRegistry::global(cx);
|
||||||
|
|
||||||
// Get the contact list
|
// Get the contact list
|
||||||
@@ -428,7 +439,11 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render the contact list
|
/// 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);
|
let persons = PersonRegistry::global(cx);
|
||||||
|
|
||||||
// Get the contact list
|
// Get the contact list
|
||||||
@@ -500,7 +515,7 @@ impl Render for Sidebar {
|
|||||||
.h(TABBAR_HEIGHT)
|
.h(TABBAR_HEIGHT)
|
||||||
.border_b_1()
|
.border_b_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.bg(cx.theme().elevated_surface_background)
|
.bg(cx.theme().tab_background)
|
||||||
.child(
|
.child(
|
||||||
TextInput::new(&self.find_input)
|
TextInput::new(&self.find_input)
|
||||||
.appearance(false)
|
.appearance(false)
|
||||||
@@ -585,10 +600,11 @@ impl Render for Sidebar {
|
|||||||
)
|
)
|
||||||
.when(!show_find_panel && !loading && total_rooms == 0, |this| {
|
.when(!show_find_panel && !loading && total_rooms == 0, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
div().px_2().child(
|
div().w(SIDEBAR_WIDTH).px_2().child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.p_3()
|
.p_3()
|
||||||
.h_24()
|
.h_24()
|
||||||
|
.w_full()
|
||||||
.border_2()
|
.border_2()
|
||||||
.border_dashed()
|
.border_dashed()
|
||||||
.border_color(cx.theme().border_variant)
|
.border_color(cx.theme().border_variant)
|
||||||
@@ -612,11 +628,9 @@ impl Render for Sidebar {
|
|||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.h_full()
|
.size_full()
|
||||||
.px_1p5()
|
|
||||||
.gap_1()
|
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.overflow_y_hidden()
|
.gap_1()
|
||||||
.when(show_find_panel, |this| {
|
.when(show_find_panel, |this| {
|
||||||
this.gap_3()
|
this.gap_3()
|
||||||
.when_some(self.find_results.read(cx).as_ref(), |this, results| {
|
.when_some(self.find_results.read(cx).as_ref(), |this, results| {
|
||||||
@@ -639,7 +653,7 @@ impl Render for Sidebar {
|
|||||||
uniform_list(
|
uniform_list(
|
||||||
"rooms",
|
"rooms",
|
||||||
results.len(),
|
results.len(),
|
||||||
cx.processor(|this, range, _window, cx| {
|
cx.processor(move |this, range, _window, cx| {
|
||||||
this.render_results(range, cx)
|
this.render_results(range, cx)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -666,7 +680,7 @@ impl Render for Sidebar {
|
|||||||
uniform_list(
|
uniform_list(
|
||||||
"contacts",
|
"contacts",
|
||||||
contacts.len(),
|
contacts.len(),
|
||||||
cx.processor(move |this, range, _window, cx| {
|
cx.processor(|this, range, _window, cx| {
|
||||||
this.render_contacts(range, cx)
|
this.render_contacts(range, cx)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -687,7 +701,8 @@ impl Render for Sidebar {
|
|||||||
)
|
)
|
||||||
.track_scroll(&self.scroll_handle)
|
.track_scroll(&self.scroll_handle)
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.h_full(),
|
.h_full()
|
||||||
|
.px_2(),
|
||||||
)
|
)
|
||||||
.child(Scrollbar::vertical(&self.scroll_handle))
|
.child(Scrollbar::vertical(&self.scroll_handle))
|
||||||
}),
|
}),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,8 @@ publish.workspace = true
|
|||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
state = { path = "../state" }
|
state = { path = "../state" }
|
||||||
person = { path = "../person" }
|
person = { path = "../person" }
|
||||||
|
ui = { path = "../ui" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,3 +15,4 @@ smallvec.workspace = true
|
|||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
|||||||
@@ -3,19 +3,19 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{Error, anyhow};
|
||||||
use common::EventUtils;
|
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 nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{Announcement, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT};
|
use state::{Announcement, BOOTSTRAP_RELAYS, NostrRegistry, TIMEOUT};
|
||||||
|
|
||||||
mod person;
|
mod person;
|
||||||
|
|
||||||
pub use person::*;
|
pub use person::*;
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(window: &mut Window, cx: &mut App) {
|
||||||
PersonRegistry::set_global(cx.new(PersonRegistry::new), cx);
|
PersonRegistry::set_global(cx.new(|cx| PersonRegistry::new(window, cx)), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GlobalPersonRegistry(Entity<PersonRegistry>);
|
struct GlobalPersonRegistry(Entity<PersonRegistry>);
|
||||||
@@ -36,13 +36,13 @@ pub struct PersonRegistry {
|
|||||||
persons: HashMap<PublicKey, Entity<Person>>,
|
persons: HashMap<PublicKey, Entity<Person>>,
|
||||||
|
|
||||||
/// Set of public keys that have been seen
|
/// Set of public keys that have been seen
|
||||||
seen: Rc<RefCell<HashSet<PublicKey>>>,
|
seens: Rc<RefCell<HashSet<PublicKey>>>,
|
||||||
|
|
||||||
/// Sender for requesting metadata
|
/// Sender for requesting metadata
|
||||||
sender: flume::Sender<PublicKey>,
|
sender: flume::Sender<PublicKey>,
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
/// Tasks for asynchronous operations
|
||||||
_tasks: SmallVec<[Task<()>; 4]>,
|
tasks: SmallVec<[Task<()>; 4]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersonRegistry {
|
impl PersonRegistry {
|
||||||
@@ -57,13 +57,13 @@ impl PersonRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new person registry instance
|
/// 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 nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
// Channel for communication between nostr and gpui
|
// Channel for communication between nostr and gpui
|
||||||
let (tx, rx) = flume::bounded::<Dispatch>(100);
|
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![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
@@ -111,33 +111,16 @@ impl PersonRegistry {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
tasks.push(
|
// Load all user profiles from the database
|
||||||
// Load all user profiles from the database
|
cx.defer_in(window, |this, _window, cx| {
|
||||||
cx.spawn(async move |this, cx| {
|
this.load(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}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
persons: HashMap::new(),
|
persons: HashMap::new(),
|
||||||
seen: Rc::new(RefCell::new(HashSet::new())),
|
seens: Rc::new(RefCell::new(HashSet::new())),
|
||||||
sender: mta_tx,
|
sender: mta_tx,
|
||||||
_tasks: tasks,
|
tasks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,25 +146,21 @@ impl PersonRegistry {
|
|||||||
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
||||||
let person = Person::new(event.pubkey, metadata);
|
let person = Person::new(event.pubkey, metadata);
|
||||||
let val = Box::new(person);
|
let val = Box::new(person);
|
||||||
|
|
||||||
// Send
|
// Send
|
||||||
tx.send_async(Dispatch::Person(val)).await.ok();
|
tx.send_async(Dispatch::Person(val)).await.ok();
|
||||||
}
|
}
|
||||||
Kind::ContactList => {
|
Kind::ContactList => {
|
||||||
let public_keys = event.extract_public_keys();
|
let public_keys = event.extract_public_keys();
|
||||||
|
|
||||||
// Get metadata for all public keys
|
// Get metadata for all public keys
|
||||||
get_metadata(client, public_keys).await.ok();
|
get_metadata(client, public_keys).await.ok();
|
||||||
}
|
}
|
||||||
Kind::InboxRelays => {
|
Kind::InboxRelays => {
|
||||||
let val = Box::new(event.into_owned());
|
let val = Box::new(event.into_owned());
|
||||||
|
|
||||||
// Send
|
// Send
|
||||||
tx.send_async(Dispatch::Relays(val)).await.ok();
|
tx.send_async(Dispatch::Relays(val)).await.ok();
|
||||||
}
|
}
|
||||||
Kind::Custom(10044) => {
|
Kind::Custom(10044) => {
|
||||||
let val = Box::new(event.into_owned());
|
let val = Box::new(event.into_owned());
|
||||||
|
|
||||||
// Send
|
// Send
|
||||||
tx.send_async(Dispatch::Announcement(val)).await.ok();
|
tx.send_async(Dispatch::Announcement(val)).await.ok();
|
||||||
}
|
}
|
||||||
@@ -208,40 +187,81 @@ impl PersonRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
get_metadata(client, std::mem::take(&mut batch)).await.ok();
|
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
|
/// Set profile encryption keys announcement
|
||||||
fn set_announcement(&mut self, event: &Event, cx: &mut App) {
|
fn set_announcement(&mut self, event: &Event, cx: &mut App) {
|
||||||
if let Some(person) = self.persons.get(&event.pubkey) {
|
let announcement = Announcement::from(event);
|
||||||
let announcement = Announcement::from(event);
|
|
||||||
|
|
||||||
|
if let Some(person) = self.persons.get(&event.pubkey) {
|
||||||
person.update(cx, |person, cx| {
|
person.update(cx, |person, cx| {
|
||||||
person.set_announcement(announcement);
|
person.set_announcement(announcement);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
let person =
|
||||||
|
Person::new(event.pubkey, Metadata::default()).with_announcement(announcement);
|
||||||
|
self.insert(person, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set messaging relays for a person
|
/// Set messaging relays for a person
|
||||||
fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) {
|
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();
|
||||||
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.update(cx, |person, cx| {
|
||||||
person.set_messaging_relays(urls);
|
person.set_messaging_relays(urls);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
let person = Person::new(event.pubkey, Metadata::default()).with_messaging_relays(urls);
|
||||||
|
self.insert(person, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert batch of persons
|
/// Insert batch of persons
|
||||||
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
|
fn bulk_inserts(&mut self, persons: Vec<Person>, cx: &mut Context<Self>) {
|
||||||
for person in persons.into_iter() {
|
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();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -253,7 +273,7 @@ impl PersonRegistry {
|
|||||||
match self.persons.get(&public_key) {
|
match self.persons.get(&public_key) {
|
||||||
Some(this) => {
|
Some(this) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
*this = person;
|
this.set_metadata(person.metadata());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -270,7 +290,7 @@ impl PersonRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let public_key = *public_key;
|
let public_key = *public_key;
|
||||||
let mut seen = self.seen.borrow_mut();
|
let mut seen = self.seens.borrow_mut();
|
||||||
|
|
||||||
if seen.insert(public_key) {
|
if seen.insert(public_key) {
|
||||||
let sender = self.sender.clone();
|
let sender = self.sender.clone();
|
||||||
@@ -307,40 +327,18 @@ where
|
|||||||
.timeout(Some(Duration::from_secs(TIMEOUT)));
|
.timeout(Some(Duration::from_secs(TIMEOUT)));
|
||||||
|
|
||||||
// Construct the filter for metadata
|
// Construct the filter for metadata
|
||||||
let metadata = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::Metadata)
|
.kind(Kind::Metadata)
|
||||||
.authors(authors.clone())
|
|
||||||
.limit(limit);
|
|
||||||
|
|
||||||
// Construct the filter for relay list
|
|
||||||
let gossip = Filter::new()
|
|
||||||
.kind(Kind::RelayList)
|
|
||||||
.authors(authors)
|
.authors(authors)
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
// Construct target for subscription
|
// Construct target for subscription
|
||||||
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
let target: HashMap<&str, Vec<Filter>> = BOOTSTRAP_RELAYS
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|relay| (relay, vec![metadata.clone(), gossip.clone()]))
|
.map(|relay| (relay, vec![filter.clone()]))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
client.subscribe(target).close_on(opts).await?;
|
client.subscribe(target).close_on(opts).await?;
|
||||||
|
|
||||||
Ok(())
|
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
|
/// Get profile public key
|
||||||
pub fn public_key(&self) -> PublicKey {
|
pub fn public_key(&self) -> PublicKey {
|
||||||
self.public_key
|
self.public_key
|
||||||
@@ -80,12 +95,6 @@ impl Person {
|
|||||||
self.announcement.clone()
|
self.announcement.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set profile encryption keys announcement
|
|
||||||
pub fn set_announcement(&mut self, announcement: Announcement) {
|
|
||||||
self.announcement = Some(announcement);
|
|
||||||
log::info!("Updated announcement for: {}", self.public_key());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get profile messaging relays
|
/// Get profile messaging relays
|
||||||
pub fn messaging_relays(&self) -> &Vec<RelayUrl> {
|
pub fn messaging_relays(&self) -> &Vec<RelayUrl> {
|
||||||
&self.messaging_relays
|
&self.messaging_relays
|
||||||
@@ -96,15 +105,6 @@ impl Person {
|
|||||||
self.messaging_relays.first().cloned()
|
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();
|
|
||||||
log::info!("Updated messaging relays for: {}", self.public_key());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get profile avatar
|
/// Get profile avatar
|
||||||
pub fn avatar(&self) -> SharedString {
|
pub fn avatar(&self) -> SharedString {
|
||||||
self.metadata()
|
self.metadata()
|
||||||
@@ -112,8 +112,9 @@ impl Person {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|picture| !picture.is_empty())
|
.filter(|picture| !picture.is_empty())
|
||||||
.map(|picture| {
|
.map(|picture| {
|
||||||
|
let encoded_picture = urlencoding::encode(picture);
|
||||||
let url = format!(
|
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()
|
url.into()
|
||||||
})
|
})
|
||||||
@@ -122,20 +123,38 @@ impl Person {
|
|||||||
|
|
||||||
/// Get profile name
|
/// Get profile name
|
||||||
pub fn name(&self) -> SharedString {
|
pub fn name(&self) -> SharedString {
|
||||||
if let Some(display_name) = self.metadata().display_name.as_ref() {
|
if let Some(display_name) = self.metadata().display_name.as_ref()
|
||||||
if !display_name.is_empty() {
|
&& !display_name.is_empty()
|
||||||
return SharedString::from(display_name);
|
{
|
||||||
}
|
return SharedString::from(display_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = self.metadata().name.as_ref() {
|
if let Some(name) = self.metadata().name.as_ref()
|
||||||
if !name.is_empty() {
|
&& !name.is_empty()
|
||||||
return SharedString::from(name);
|
{
|
||||||
}
|
return SharedString::from(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedString::from(shorten_pubkey(self.public_key(), 4))
|
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
|
/// Shorten a [`PublicKey`] to a string with the first and last `len` characters
|
||||||
@@ -145,7 +164,7 @@ pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String {
|
|||||||
let Ok(pubkey) = public_key.to_bech32();
|
let Ok(pubkey) = public_key.to_bech32();
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"{}:{}",
|
"{}...{}",
|
||||||
&pubkey[0..(len + 1)],
|
&pubkey[0..(len + 1)],
|
||||||
&pubkey[pubkey.len() - len..]
|
&pubkey[pubkey.len() - len..]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,19 +5,19 @@ use std::hash::Hash;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
|
App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled,
|
||||||
Task, Window,
|
Task, Window, div, relative,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use settings::{AppSettings, AuthMode};
|
use settings::{AppSettings, AuthMode};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::{v_flex, Disableable, IconName, Sizable, WindowExtension};
|
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, v_flex};
|
||||||
|
|
||||||
const AUTH_MESSAGE: &str =
|
const AUTH_MESSAGE: &str =
|
||||||
"Approve the authentication request to allow Coop to continue sending or receiving events.";
|
"Approve the authentication request to allow Coop to continue sending or receiving events.";
|
||||||
@@ -34,7 +34,10 @@ struct AuthRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
Self {
|
||||||
challenge: challenge.into(),
|
challenge: challenge.into(),
|
||||||
url,
|
url,
|
||||||
@@ -67,7 +70,7 @@ pub struct RelayAuth {
|
|||||||
pending_events: HashSet<(EventId, RelayUrl)>,
|
pending_events: HashSet<(EventId, RelayUrl)>,
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
/// Tasks for asynchronous operations
|
||||||
tasks: SmallVec<[Task<()>; 2]>,
|
_tasks: SmallVec<[Task<()>; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RelayAuth {
|
impl RelayAuth {
|
||||||
@@ -83,26 +86,15 @@ impl RelayAuth {
|
|||||||
|
|
||||||
/// Create a new relay auth instance
|
/// Create a new relay auth instance
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
cx.defer_in(window, |this, window, cx| {
|
|
||||||
this.handle_notifications(window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
pending_events: HashSet::default(),
|
|
||||||
tasks: smallvec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle nostr notifications
|
|
||||||
fn handle_notifications(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
// Channel for communication between nostr and gpui
|
// Channel for communication between nostr and gpui
|
||||||
let (tx, rx) = flume::bounded::<Signal>(256);
|
let (tx, rx) = flume::bounded::<Signal>(256);
|
||||||
|
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
tasks.push(cx.background_spawn(async move {
|
||||||
log::info!("Started handling nostr notifications");
|
|
||||||
let mut notifications = client.notifications();
|
let mut notifications = client.notifications();
|
||||||
let mut challenges: HashSet<Cow<'_, str>> = HashSet::default();
|
let mut challenges: HashSet<Cow<'_, str>> = HashSet::default();
|
||||||
|
|
||||||
@@ -134,7 +126,7 @@ impl RelayAuth {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
while let Ok(signal) = rx.recv_async().await {
|
while let Ok(signal) = rx.recv_async().await {
|
||||||
match signal {
|
match signal {
|
||||||
Signal::Auth(req) => {
|
Signal::Auth(req) => {
|
||||||
@@ -152,6 +144,11 @@ impl RelayAuth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pending_events: HashSet::default(),
|
||||||
|
_tasks: tasks,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a pending event waiting for resend after authentication
|
/// Insert a pending event waiting for resend after authentication
|
||||||
@@ -162,15 +159,12 @@ impl RelayAuth {
|
|||||||
|
|
||||||
/// Get all pending events for a specific relay,
|
/// Get all pending events for a specific relay,
|
||||||
fn get_pending_events(&self, relay: &RelayUrl, _cx: &App) -> Vec<EventId> {
|
fn get_pending_events(&self, relay: &RelayUrl, _cx: &App) -> Vec<EventId> {
|
||||||
let pending_events: Vec<EventId> = self
|
self.pending_events
|
||||||
.pending_events
|
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, pending_relay)| pending_relay == relay)
|
.filter(|(_, pending_relay)| pending_relay == relay)
|
||||||
.map(|(id, _relay)| id)
|
.map(|(id, _relay)| id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect()
|
||||||
|
|
||||||
pending_events
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all pending events for a specific relay,
|
/// Clear all pending events for a specific relay,
|
||||||
@@ -266,7 +260,7 @@ impl RelayAuth {
|
|||||||
fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) {
|
fn response(&self, req: &Arc<AuthRequest>, window: &Window, cx: &Context<Self>) {
|
||||||
let settings = AppSettings::global(cx);
|
let settings = AppSettings::global(cx);
|
||||||
let req = req.clone();
|
let req = req.clone();
|
||||||
let challenge = req.challenge().to_string();
|
let challenge = SharedString::from(req.challenge().to_string());
|
||||||
|
|
||||||
// Create a task for authentication
|
// Create a task for authentication
|
||||||
let task = self.auth(&req, cx);
|
let task = self.auth(&req, cx);
|
||||||
@@ -276,20 +270,31 @@ impl RelayAuth {
|
|||||||
let url = req.url();
|
let url = req.url();
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
window.clear_notification(challenge, cx);
|
window.clear_notification_by_id::<AuthNotification>(challenge, cx);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Clear pending events for the authenticated relay
|
// Clear pending events for the authenticated relay
|
||||||
this.clear_pending_events(url, cx);
|
this.clear_pending_events(url, cx);
|
||||||
|
|
||||||
// Save the authenticated relay to automatically authenticate future requests
|
// Save the authenticated relay to automatically authenticate future requests
|
||||||
settings.update(cx, |this, cx| {
|
settings.update(cx, |this, cx| {
|
||||||
this.add_trusted_relay(url, cx);
|
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) => {
|
Err(e) => {
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
window.push_notification(
|
||||||
|
Notification::error(e.to_string()).autohide(false),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -314,33 +319,39 @@ impl RelayAuth {
|
|||||||
/// Build a notification for the authentication request.
|
/// Build a notification for the authentication request.
|
||||||
fn notification(&self, req: &Arc<AuthRequest>, cx: &Context<Self>) -> Notification {
|
fn notification(&self, req: &Arc<AuthRequest>, cx: &Context<Self>) -> Notification {
|
||||||
let req = req.clone();
|
let req = req.clone();
|
||||||
|
let challenge = SharedString::from(req.challenge.clone());
|
||||||
let url = SharedString::from(req.url().to_string());
|
let url = SharedString::from(req.url().to_string());
|
||||||
let entity = cx.entity().downgrade();
|
let entity = cx.entity().downgrade();
|
||||||
let loading = Rc::new(Cell::new(false));
|
let loading = Rc::new(Cell::new(false));
|
||||||
|
|
||||||
Notification::new()
|
Notification::new()
|
||||||
.custom_id(SharedString::from(&req.challenge))
|
.type_id::<AuthNotification>(challenge)
|
||||||
.autohide(false)
|
.autohide(false)
|
||||||
.icon(IconName::Info)
|
.icon(IconName::Warning)
|
||||||
.title(SharedString::from("Authentication Required"))
|
.title(SharedString::from("Authentication Required"))
|
||||||
.content(move |_window, cx| {
|
.content(move |_this, _window, cx| {
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.text_sm()
|
.child(
|
||||||
.child(SharedString::from(AUTH_MESSAGE))
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.line_height(relative(1.25))
|
||||||
|
.child(SharedString::from(AUTH_MESSAGE)),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.py_1()
|
.py_1()
|
||||||
.px_1p5()
|
.px_1p5()
|
||||||
.rounded_sm()
|
.rounded_sm()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.bg(cx.theme().warning_background)
|
.font_semibold()
|
||||||
.text_color(cx.theme().warning_foreground)
|
.bg(cx.theme().elevated_surface_background)
|
||||||
|
.text_color(cx.theme().text)
|
||||||
.child(url.clone()),
|
.child(url.clone()),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
})
|
})
|
||||||
.action(move |_window, _cx| {
|
.action(move |_this, _window, _cx| {
|
||||||
let view = entity.clone();
|
let view = entity.clone();
|
||||||
let req = req.clone();
|
let req = req.clone();
|
||||||
|
|
||||||
@@ -352,11 +363,9 @@ impl RelayAuth {
|
|||||||
.disabled(loading.get())
|
.disabled(loading.get())
|
||||||
.on_click({
|
.on_click({
|
||||||
let loading = Rc::clone(&loading);
|
let loading = Rc::clone(&loading);
|
||||||
|
|
||||||
move |_ev, window, cx| {
|
move |_ev, window, cx| {
|
||||||
// Set loading state to true
|
// Set loading state to true
|
||||||
loading.set(true);
|
loading.set(true);
|
||||||
|
|
||||||
// Process to approve the request
|
// Process to approve the request
|
||||||
view.update(cx, |this, cx| {
|
view.update(cx, |this, cx| {
|
||||||
this.response(&req, window, cx);
|
this.response(&req, window, cx);
|
||||||
@@ -367,3 +376,5 @@ impl RelayAuth {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AuthNotification;
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{Error, anyhow};
|
||||||
use common::config_dir;
|
use common::config_dir;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
|
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use theme::{Theme, ThemeFamily, ThemeMode};
|
use theme::{Theme, ThemeFamily, ThemeMode};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) {
|
pub fn init(window: &mut Window, cx: &mut App) {
|
||||||
@@ -94,21 +94,28 @@ pub struct RoomConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RoomConfig {
|
impl RoomConfig {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
backup: true,
|
||||||
|
signer_kind: SignerKind::Auto,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get backup config
|
/// Get backup config
|
||||||
pub fn backup(&self) -> bool {
|
pub fn backup(&self) -> bool {
|
||||||
self.backup
|
self.backup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set backup config
|
||||||
|
pub fn toggle_backup(&mut self) {
|
||||||
|
self.backup = !self.backup;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get signer kind config
|
/// Get signer kind config
|
||||||
pub fn signer_kind(&self) -> &SignerKind {
|
pub fn signer_kind(&self) -> &SignerKind {
|
||||||
&self.signer_kind
|
&self.signer_kind
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set backup config
|
|
||||||
pub fn set_backup(&mut self, backup: bool) {
|
|
||||||
self.backup = backup;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set signer kind config
|
/// Set signer kind config
|
||||||
pub fn set_signer_kind(&mut self, kind: &SignerKind) {
|
pub fn set_signer_kind(&mut self, kind: &SignerKind) {
|
||||||
self.signer_kind = kind.to_owned();
|
self.signer_kind = kind.to_owned();
|
||||||
@@ -284,6 +291,8 @@ impl AppSettings {
|
|||||||
/// Reset theme
|
/// Reset theme
|
||||||
pub fn reset_theme(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn reset_theme(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.values.theme = None;
|
self.values.theme = None;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
self.apply_theme(window, cx);
|
self.apply_theme(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ common = { path = "../common" }
|
|||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
nostr-lmdb.workspace = true
|
nostr-lmdb.workspace = true
|
||||||
|
nostr-memory.workspace = true
|
||||||
|
nostr-gossip-memory.workspace = true
|
||||||
nostr-connect.workspace = true
|
nostr-connect.workspace = true
|
||||||
nostr-gossip-sqlite.workspace = true
|
|
||||||
nostr-blossom.workspace = true
|
nostr-blossom.workspace = true
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub const APP_ID: &str = "su.reya.coop";
|
|||||||
/// Keyring name
|
/// Keyring name
|
||||||
pub const KEYRING: &str = "Coop Safe Storage";
|
pub const KEYRING: &str = "Coop Safe Storage";
|
||||||
|
|
||||||
/// Default timeout in second for subscription
|
/// Default timeout for subscription
|
||||||
pub const TIMEOUT: u64 = 2;
|
pub const TIMEOUT: u64 = 2;
|
||||||
|
|
||||||
/// Default delay for searching
|
/// Default delay for searching
|
||||||
@@ -21,28 +21,31 @@ pub const FIND_DELAY: u64 = 600;
|
|||||||
/// Default limit for searching
|
/// Default limit for searching
|
||||||
pub const FIND_LIMIT: usize = 20;
|
pub const FIND_LIMIT: usize = 20;
|
||||||
|
|
||||||
/// Default timeout for Nostr Connect
|
|
||||||
pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
|
|
||||||
|
|
||||||
/// Default Nostr Connect relay
|
|
||||||
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
|
||||||
|
|
||||||
/// Default subscription id for device gift wrap events
|
/// Default subscription id for device gift wrap events
|
||||||
pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps";
|
pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps";
|
||||||
|
|
||||||
/// Default subscription id for user gift wrap events
|
/// Default subscription id for user gift wrap events
|
||||||
pub const USER_GIFTWRAP: &str = "user-gift-wraps";
|
pub const USER_GIFTWRAP: &str = "user-gift-wraps";
|
||||||
|
|
||||||
|
/// Default timeout for Nostr Connect
|
||||||
|
pub const NOSTR_CONNECT_TIMEOUT: u64 = 60;
|
||||||
|
|
||||||
|
/// Default Nostr Connect relay
|
||||||
|
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nip46.com";
|
||||||
|
|
||||||
/// Default vertex relays
|
/// Default vertex relays
|
||||||
pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
|
pub const WOT_RELAYS: [&str; 1] = ["wss://relay.vertexlab.io"];
|
||||||
|
|
||||||
/// Default search relays
|
/// Default search relays
|
||||||
pub const SEARCH_RELAYS: [&str; 1] = ["wss://antiprimal.net"];
|
pub const INDEXER_RELAYS: [&str; 1] = ["wss://indexer.coracle.social"];
|
||||||
|
|
||||||
|
/// Default search relays
|
||||||
|
pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos.today"];
|
||||||
|
|
||||||
/// Default bootstrap relays
|
/// Default bootstrap relays
|
||||||
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
"wss://nos.lol",
|
"wss://relay.primal.net",
|
||||||
"wss://user.kindpag.es",
|
"wss://user.kindpag.es",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,6 @@
|
|||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
|
||||||
pub enum DeviceState {
|
|
||||||
#[default]
|
|
||||||
Idle,
|
|
||||||
Requesting,
|
|
||||||
Set,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeviceState {
|
|
||||||
pub fn idle(&self) -> bool {
|
|
||||||
matches!(self, DeviceState::Idle)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn requesting(&self) -> bool {
|
|
||||||
matches!(self, DeviceState::Requesting)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set(&self) -> bool {
|
|
||||||
matches!(self, DeviceState::Set)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Announcement
|
/// Announcement
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct Announcement {
|
pub struct Announcement {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::result::Result;
|
use std::result::Result;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -16,11 +15,6 @@ pub struct CoopSigner {
|
|||||||
|
|
||||||
/// Specific signer for encryption purposes
|
/// Specific signer for encryption purposes
|
||||||
encryption_signer: RwLock<Option<Arc<dyn NostrSigner>>>,
|
encryption_signer: RwLock<Option<Arc<dyn NostrSigner>>>,
|
||||||
|
|
||||||
/// By default, Coop generates a new signer for new users.
|
|
||||||
///
|
|
||||||
/// This flag indicates whether the signer is user-owned or Coop-generated.
|
|
||||||
owned: AtomicBool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CoopSigner {
|
impl CoopSigner {
|
||||||
@@ -32,7 +26,6 @@ impl CoopSigner {
|
|||||||
signer: RwLock::new(signer.into_nostr_signer()),
|
signer: RwLock::new(signer.into_nostr_signer()),
|
||||||
signer_pkey: RwLock::new(None),
|
signer_pkey: RwLock::new(None),
|
||||||
encryption_signer: RwLock::new(None),
|
encryption_signer: RwLock::new(None),
|
||||||
owned: AtomicBool::new(false),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,17 +40,15 @@ impl CoopSigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get public key
|
/// Get public key
|
||||||
|
///
|
||||||
|
/// Ensure to call this method after the signer has been initialized.
|
||||||
|
/// Otherwise, this method will panic.
|
||||||
pub fn public_key(&self) -> Option<PublicKey> {
|
pub fn public_key(&self) -> Option<PublicKey> {
|
||||||
self.signer_pkey.read_blocking().to_owned()
|
*self.signer_pkey.read_blocking()
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the flag indicating whether the signer is user-owned.
|
|
||||||
pub fn owned(&self) -> bool {
|
|
||||||
self.owned.load(Ordering::SeqCst)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switch the current signer to a new signer.
|
/// Switch the current signer to a new signer.
|
||||||
pub async fn switch<T>(&self, new: T, owned: bool)
|
pub async fn switch<T>(&self, new: T)
|
||||||
where
|
where
|
||||||
T: IntoNostrSigner,
|
T: IntoNostrSigner,
|
||||||
{
|
{
|
||||||
@@ -75,9 +66,6 @@ impl CoopSigner {
|
|||||||
|
|
||||||
// Reset the encryption signer
|
// Reset the encryption signer
|
||||||
*encryption_signer = None;
|
*encryption_signer = None;
|
||||||
|
|
||||||
// Update the owned flag
|
|
||||||
self.owned.store(owned, Ordering::SeqCst);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the encryption signer.
|
/// Set the encryption signer.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use gpui::{hsla, Hsla, Rgba};
|
use gpui::{Hsla, Rgba, hsla};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -30,6 +30,8 @@ pub struct ThemeColors {
|
|||||||
pub text_muted: Hsla,
|
pub text_muted: Hsla,
|
||||||
pub text_placeholder: Hsla,
|
pub text_placeholder: Hsla,
|
||||||
pub text_accent: Hsla,
|
pub text_accent: Hsla,
|
||||||
|
pub text_danger: Hsla,
|
||||||
|
pub text_warning: Hsla,
|
||||||
|
|
||||||
// Icon colors
|
// Icon colors
|
||||||
pub icon: Hsla,
|
pub icon: Hsla,
|
||||||
@@ -77,11 +79,11 @@ pub struct ThemeColors {
|
|||||||
pub ghost_element_disabled: Hsla,
|
pub ghost_element_disabled: Hsla,
|
||||||
|
|
||||||
// Tab colors
|
// Tab colors
|
||||||
pub tab_inactive_background: Hsla,
|
pub tab_background: Hsla,
|
||||||
pub tab_inactive_foreground: Hsla,
|
pub tab_foreground: Hsla,
|
||||||
|
pub tab_hover_background: Hsla,
|
||||||
pub tab_active_background: Hsla,
|
pub tab_active_background: Hsla,
|
||||||
pub tab_active_foreground: Hsla,
|
pub tab_active_foreground: Hsla,
|
||||||
pub tab_hover_foreground: Hsla,
|
|
||||||
|
|
||||||
// Scrollbar colors
|
// Scrollbar colors
|
||||||
pub scrollbar_thumb_background: Hsla,
|
pub scrollbar_thumb_background: Hsla,
|
||||||
@@ -110,8 +112,8 @@ impl ThemeColors {
|
|||||||
elevated_surface_background: neutral().light().step_3(),
|
elevated_surface_background: neutral().light().step_3(),
|
||||||
panel_background: neutral().light().step_1(),
|
panel_background: neutral().light().step_1(),
|
||||||
overlay: neutral().light_alpha().step_3(),
|
overlay: neutral().light_alpha().step_3(),
|
||||||
title_bar: neutral().light().step_2(),
|
title_bar: neutral().light().step_3(),
|
||||||
title_bar_inactive: neutral().light().step_3(),
|
title_bar_inactive: neutral().light().step_1(),
|
||||||
window_border: hsl(240.0, 5.9, 78.0),
|
window_border: hsl(240.0, 5.9, 78.0),
|
||||||
|
|
||||||
border: neutral().light().step_6(),
|
border: neutral().light().step_6(),
|
||||||
@@ -125,7 +127,9 @@ impl ThemeColors {
|
|||||||
text: neutral().light().step_12(),
|
text: neutral().light().step_12(),
|
||||||
text_muted: neutral().light().step_11(),
|
text_muted: neutral().light().step_11(),
|
||||||
text_placeholder: neutral().light().step_10(),
|
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: neutral().light().step_11(),
|
||||||
icon_muted: neutral().light().step_10(),
|
icon_muted: neutral().light().step_10(),
|
||||||
@@ -166,17 +170,17 @@ impl ThemeColors {
|
|||||||
ghost_element_selected: neutral().light().step_5(),
|
ghost_element_selected: neutral().light().step_5(),
|
||||||
ghost_element_disabled: neutral().light_alpha().step_2(),
|
ghost_element_disabled: neutral().light_alpha().step_2(),
|
||||||
|
|
||||||
tab_inactive_background: neutral().light().step_2(),
|
tab_background: neutral().light().step_3(),
|
||||||
tab_inactive_foreground: neutral().light().step_11(),
|
tab_foreground: neutral().light().step_11(),
|
||||||
|
tab_hover_background: neutral().light_alpha().step_4(),
|
||||||
tab_active_background: neutral().light().step_1(),
|
tab_active_background: neutral().light().step_1(),
|
||||||
tab_active_foreground: neutral().light().step_12(),
|
tab_active_foreground: neutral().light().step_12(),
|
||||||
tab_hover_foreground: brand().light().step_9(),
|
|
||||||
|
|
||||||
scrollbar_thumb_background: neutral().light_alpha().step_3(),
|
scrollbar_thumb_background: neutral().light_alpha().step_3(),
|
||||||
scrollbar_thumb_hover_background: neutral().light_alpha().step_4(),
|
scrollbar_thumb_hover_background: neutral().light_alpha().step_4(),
|
||||||
scrollbar_thumb_border: gpui::transparent_black(),
|
scrollbar_thumb_border: gpui::transparent_black(),
|
||||||
scrollbar_track_background: 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(),
|
drop_target_background: brand().light_alpha().step_2(),
|
||||||
cursor: hsl(200., 100., 50.),
|
cursor: hsl(200., 100., 50.),
|
||||||
@@ -192,9 +196,9 @@ impl ThemeColors {
|
|||||||
background: neutral().dark().step_1(),
|
background: neutral().dark().step_1(),
|
||||||
surface_background: neutral().dark().step_2(),
|
surface_background: neutral().dark().step_2(),
|
||||||
elevated_surface_background: neutral().dark().step_3(),
|
elevated_surface_background: neutral().dark().step_3(),
|
||||||
panel_background: gpui::black(),
|
panel_background: neutral().dark().step_1(),
|
||||||
overlay: neutral().dark_alpha().step_3(),
|
overlay: neutral().dark_alpha().step_3(),
|
||||||
title_bar: gpui::transparent_black(),
|
title_bar: neutral().dark().step_3(),
|
||||||
title_bar_inactive: neutral().dark().step_1(),
|
title_bar_inactive: neutral().dark().step_1(),
|
||||||
window_border: hsl(240.0, 3.7, 28.0),
|
window_border: hsl(240.0, 3.7, 28.0),
|
||||||
|
|
||||||
@@ -209,7 +213,9 @@ impl ThemeColors {
|
|||||||
text: neutral().dark().step_12(),
|
text: neutral().dark().step_12(),
|
||||||
text_muted: neutral().dark().step_11(),
|
text_muted: neutral().dark().step_11(),
|
||||||
text_placeholder: neutral().dark().step_10(),
|
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: neutral().dark().step_11(),
|
||||||
icon_muted: neutral().dark().step_10(),
|
icon_muted: neutral().dark().step_10(),
|
||||||
@@ -250,17 +256,17 @@ impl ThemeColors {
|
|||||||
ghost_element_selected: neutral().dark().step_5(),
|
ghost_element_selected: neutral().dark().step_5(),
|
||||||
ghost_element_disabled: neutral().dark_alpha().step_2(),
|
ghost_element_disabled: neutral().dark_alpha().step_2(),
|
||||||
|
|
||||||
tab_inactive_background: neutral().dark().step_2(),
|
tab_background: neutral().dark().step_3(),
|
||||||
tab_inactive_foreground: neutral().dark().step_11(),
|
tab_foreground: neutral().dark().step_11(),
|
||||||
tab_active_background: neutral().dark().step_3(),
|
tab_hover_background: neutral().dark_alpha().step_4(),
|
||||||
|
tab_active_background: neutral().dark().step_1(),
|
||||||
tab_active_foreground: neutral().dark().step_12(),
|
tab_active_foreground: neutral().dark().step_12(),
|
||||||
tab_hover_foreground: brand().dark().step_9(),
|
|
||||||
|
|
||||||
scrollbar_thumb_background: neutral().dark_alpha().step_3(),
|
scrollbar_thumb_background: neutral().dark_alpha().step_3(),
|
||||||
scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),
|
scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),
|
||||||
scrollbar_thumb_border: gpui::transparent_black(),
|
scrollbar_thumb_border: gpui::transparent_black(),
|
||||||
scrollbar_track_background: 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(),
|
drop_target_background: brand().dark_alpha().step_2(),
|
||||||
cursor: hsl(200., 100., 50.),
|
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 {
|
match axis {
|
||||||
Axis::Vertical => match self {
|
Axis::Vertical => match self {
|
||||||
Self::TopLeft => Self::BottomLeft,
|
Self::TopLeft => Self::BottomLeft,
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{px, App, Global, Pixels, SharedString, Window};
|
use gpui::{App, Global, Pixels, SharedString, Window, px};
|
||||||
|
|
||||||
mod colors;
|
mod colors;
|
||||||
|
mod geometry;
|
||||||
|
mod notification;
|
||||||
mod platform_kind;
|
mod platform_kind;
|
||||||
mod registry;
|
mod registry;
|
||||||
mod scale;
|
mod scale;
|
||||||
@@ -11,6 +13,8 @@ mod scrollbar_mode;
|
|||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
pub use colors::*;
|
pub use colors::*;
|
||||||
|
pub use geometry::*;
|
||||||
|
pub use notification::*;
|
||||||
pub use platform_kind::PlatformKind;
|
pub use platform_kind::PlatformKind;
|
||||||
pub use registry::*;
|
pub use registry::*;
|
||||||
pub use scale::*;
|
pub use scale::*;
|
||||||
@@ -82,6 +86,9 @@ pub struct Theme {
|
|||||||
/// Show the scrollbar mode, default: scrolling
|
/// Show the scrollbar mode, default: scrolling
|
||||||
pub scrollbar_mode: ScrollbarMode,
|
pub scrollbar_mode: ScrollbarMode,
|
||||||
|
|
||||||
|
/// Notification settings
|
||||||
|
pub notification: NotificationSettings,
|
||||||
|
|
||||||
/// Platform kind
|
/// Platform kind
|
||||||
pub platform: PlatformKind,
|
pub platform: PlatformKind,
|
||||||
}
|
}
|
||||||
@@ -200,10 +207,11 @@ impl From<ThemeFamily> for Theme {
|
|||||||
Theme {
|
Theme {
|
||||||
font_size: px(15.),
|
font_size: px(15.),
|
||||||
font_family: font_family.into(),
|
font_family: font_family.into(),
|
||||||
radius: px(5.),
|
radius: px(6.),
|
||||||
radius_lg: px(10.),
|
radius_lg: px(10.),
|
||||||
shadow: true,
|
shadow: true,
|
||||||
scrollbar_mode: ScrollbarMode::default(),
|
scrollbar_mode: ScrollbarMode::default(),
|
||||||
|
notification: NotificationSettings::default(),
|
||||||
mode,
|
mode,
|
||||||
colors: *colors,
|
colors: *colors,
|
||||||
theme: Rc::new(family),
|
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,14 +1,12 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use gpui::MouseButton;
|
use gpui::MouseButton;
|
||||||
#[cfg(not(target_os = "windows"))]
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::Pixels;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
|
AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement, ParentElement,
|
||||||
ParentElement, Render, StatefulInteractiveElement as _, Styled, Window, WindowControlArea,
|
Pixels, Render, StatefulInteractiveElement as _, Styled, Window, WindowControlArea, px,
|
||||||
};
|
};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use theme::{ActiveTheme, PlatformKind, CLIENT_SIDE_DECORATION_ROUNDING};
|
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, PlatformKind};
|
||||||
use ui::h_flex;
|
use ui::h_flex;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
//! This is a fork of gpui's anchored element that adds support for offsetting
|
//! 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
|
//! https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/elements/anchored.rs
|
||||||
use gpui::{
|
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,
|
InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
|
||||||
Window,
|
Window, point, px,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
use theme::Anchor;
|
||||||
use crate::Anchor;
|
|
||||||
|
|
||||||
/// The state that the anchored element element uses to track its children.
|
/// The state that the anchored element element uses to track its children.
|
||||||
pub struct AnchoredState {
|
pub struct AnchoredState {
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, px, rems, AbsoluteLength, App, Hsla, ImageSource, Img, IntoElement, ParentElement,
|
AbsoluteLength, App, Div, Hsla, ImageSource, Img, InteractiveElement, Interactivity,
|
||||||
RenderOnce, Styled, StyledImage, Window,
|
IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, StyledImage, Window, div, img,
|
||||||
|
px,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
|
use crate::{Selectable, Sizable, Size};
|
||||||
|
|
||||||
|
/// Returns the size of the avatar based on the given [`Size`].
|
||||||
|
pub(super) fn avatar_size(size: Size) -> AbsoluteLength {
|
||||||
|
match size {
|
||||||
|
Size::Large => px(64.).into(),
|
||||||
|
Size::Medium => px(32.).into(),
|
||||||
|
Size::Small => px(24.).into(),
|
||||||
|
Size::XSmall => px(20.).into(),
|
||||||
|
Size::Size(size) => size.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An element that renders a user avatar with customizable appearance options.
|
/// An element that renders a user avatar with customizable appearance options.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
@@ -18,18 +32,24 @@ use theme::ActiveTheme;
|
|||||||
/// ```
|
/// ```
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Avatar {
|
pub struct Avatar {
|
||||||
|
base: Div,
|
||||||
image: Img,
|
image: Img,
|
||||||
size: Option<AbsoluteLength>,
|
style: StyleRefinement,
|
||||||
|
size: Size,
|
||||||
border_color: Option<Hsla>,
|
border_color: Option<Hsla>,
|
||||||
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Avatar {
|
impl Avatar {
|
||||||
/// Creates a new avatar element with the specified image source.
|
/// Creates a new avatar element with the specified image source.
|
||||||
pub fn new(src: impl Into<ImageSource>) -> Self {
|
pub fn new(src: impl Into<ImageSource>) -> Self {
|
||||||
Avatar {
|
Avatar {
|
||||||
|
base: div(),
|
||||||
image: img(src),
|
image: img(src),
|
||||||
size: None,
|
style: StyleRefinement::default(),
|
||||||
|
size: Size::Medium,
|
||||||
border_color: None,
|
border_color: None,
|
||||||
|
selected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,14 +76,38 @@ impl Avatar {
|
|||||||
self.border_color = Some(color.into());
|
self.border_color = Some(color.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Size overrides the avatar size. By default they are 1rem.
|
impl Sizable for Avatar {
|
||||||
pub fn size<L: Into<AbsoluteLength>>(mut self, size: impl Into<Option<L>>) -> Self {
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||||
self.size = size.into().map(Into::into);
|
self.size = size.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Styled for Avatar {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RenderOnce for Avatar {
|
impl RenderOnce for Avatar {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let border_width = if self.border_color.is_some() {
|
let border_width = if self.border_color.is_some() {
|
||||||
@@ -71,8 +115,7 @@ impl RenderOnce for Avatar {
|
|||||||
} else {
|
} else {
|
||||||
px(0.)
|
px(0.)
|
||||||
};
|
};
|
||||||
|
let image_size = avatar_size(self.size);
|
||||||
let image_size = self.size.unwrap_or_else(|| rems(1.).into());
|
|
||||||
let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.;
|
let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.;
|
||||||
|
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -3,20 +3,26 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, App, AppContext, Axis, Context, Element, Entity, IntoElement, MouseMoveEvent,
|
App, AppContext, Axis, Context, Element, Empty, Entity, IntoElement, MouseMoveEvent,
|
||||||
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, Styled as _, WeakEntity,
|
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, StyleRefinement, Styled as _,
|
||||||
Window,
|
WeakEntity, Window, div, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{DockArea, DockItem};
|
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::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;
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum DockPlacement {
|
pub enum DockPlacement {
|
||||||
Center,
|
Center,
|
||||||
@@ -265,22 +271,22 @@ impl Dock {
|
|||||||
let mut right_dock_size = px(0.0);
|
let mut right_dock_size = px(0.0);
|
||||||
|
|
||||||
// Get the size of the left dock if it's open and not the current dock
|
// 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 let Some(left_dock) = &dock_area.left_dock
|
||||||
if left_dock.entity_id() != cx.entity().entity_id() {
|
&& left_dock.entity_id() != cx.entity().entity_id()
|
||||||
let left_dock_read = left_dock.read(cx);
|
{
|
||||||
if left_dock_read.is_open() {
|
let left_dock_read = left_dock.read(cx);
|
||||||
left_dock_size = left_dock_read.size;
|
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
|
// 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 let Some(right_dock) = &dock_area.right_dock
|
||||||
if right_dock.entity_id() != cx.entity().entity_id() {
|
&& right_dock.entity_id() != cx.entity().entity_id()
|
||||||
let right_dock_read = right_dock.read(cx);
|
{
|
||||||
if right_dock_read.is_open() {
|
let right_dock_read = right_dock.read(cx);
|
||||||
right_dock_size = right_dock_read.size;
|
if right_dock_read.is_open() {
|
||||||
}
|
right_dock_size = right_dock_read.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +327,8 @@ impl Render for Dock {
|
|||||||
return div();
|
return div();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cache_style = StyleRefinement::default().absolute().size_full();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
@@ -336,7 +344,7 @@ impl Render for Dock {
|
|||||||
.map(|this| match &self.panel {
|
.map(|this| match &self.panel {
|
||||||
DockItem::Split { view, .. } => this.child(view.clone()),
|
DockItem::Split { view, .. } => this.child(view.clone()),
|
||||||
DockItem::Tabs { 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(self.render_resize_handle(window, cx))
|
||||||
.child(DockElement {
|
.child(DockElement {
|
||||||
@@ -2,21 +2,24 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges, Entity,
|
AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations, Edges, Entity,
|
||||||
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
|
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||||
Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
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;
|
use crate::ElementExt;
|
||||||
|
|
||||||
pub mod dock;
|
#[allow(clippy::module_inception)]
|
||||||
pub mod panel;
|
mod dock;
|
||||||
pub mod stack_panel;
|
mod panel;
|
||||||
pub mod tab_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]);
|
actions!(dock, [ToggleZoom, ClosePanel]);
|
||||||
|
|
||||||
@@ -202,19 +205,16 @@ impl DockItem {
|
|||||||
/// Returns all panel ids
|
/// Returns all panel ids
|
||||||
pub fn panel_ids(&self, cx: &App) -> Vec<SharedString> {
|
pub fn panel_ids(&self, cx: &App) -> Vec<SharedString> {
|
||||||
match self {
|
match self {
|
||||||
Self::Tabs { view, .. } => view.read(cx).panel_ids(cx),
|
|
||||||
Self::Split { items, .. } => {
|
|
||||||
let mut total = vec![];
|
|
||||||
|
|
||||||
for item in items.iter() {
|
|
||||||
if let DockItem::Tabs { view, .. } = item {
|
|
||||||
total.extend(view.read(cx).panel_ids(cx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
total
|
|
||||||
}
|
|
||||||
Self::Panel { .. } => vec![],
|
Self::Panel { .. } => vec![],
|
||||||
|
Self::Tabs { view, .. } => view.read(cx).panel_ids(cx),
|
||||||
|
Self::Split { items, .. } => items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| match item {
|
||||||
|
DockItem::Tabs { view, .. } => Some(view.read(cx).panel_ids(cx)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,6 +745,7 @@ impl EventEmitter<DockEvent> for DockArea {}
|
|||||||
impl Render for DockArea {
|
impl Render for DockArea {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let view = cx.entity().clone();
|
let view = cx.entity().clone();
|
||||||
|
let decorations = window.window_decorations();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id("dock-area")
|
.id("dock-area")
|
||||||
@@ -754,7 +755,17 @@ impl Render for DockArea {
|
|||||||
.on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds))
|
.on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds))
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if let Some(zoom_view) = self.zoom_view.clone() {
|
if let Some(zoom_view) = self.zoom_view.clone() {
|
||||||
this.child(zoom_view)
|
this.map(|this| match decorations {
|
||||||
|
Decorations::Server => this,
|
||||||
|
Decorations::Client { tiling } => this
|
||||||
|
.when(!(tiling.top || tiling.right), |div| {
|
||||||
|
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
})
|
||||||
|
.when(!(tiling.top || tiling.left), |div| {
|
||||||
|
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.child(zoom_view)
|
||||||
} else {
|
} else {
|
||||||
// render dock
|
// render dock
|
||||||
this.child(
|
this.child(
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Render,
|
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Render,
|
||||||
SharedString, Window,
|
SharedString, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,12 +21,6 @@ pub enum PanelStyle {
|
|||||||
TabBar,
|
TabBar,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct TitleStyle {
|
|
||||||
pub background: Hsla,
|
|
||||||
pub foreground: Hsla,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
|
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
|
||||||
/// The name of the panel used to serialize, deserialize and identify the panel.
|
/// The name of the panel used to serialize, deserialize and identify the panel.
|
||||||
///
|
///
|
||||||
@@ -7,16 +7,16 @@ use gpui::{
|
|||||||
Window,
|
Window,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
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 super::{DockArea, PanelEvent};
|
||||||
use crate::dock_area::panel::{Panel, PanelView};
|
use crate::dock::panel::{Panel, PanelView};
|
||||||
use crate::dock_area::tab_panel::TabPanel;
|
use crate::dock::tab_panel::TabPanel;
|
||||||
|
use crate::h_flex;
|
||||||
use crate::resizable::{
|
use crate::resizable::{
|
||||||
resizable_panel, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
|
PANEL_MIN_SIZE, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
|
||||||
PANEL_MIN_SIZE,
|
resizable_panel,
|
||||||
};
|
};
|
||||||
use crate::{h_flex, AxisExt as _, Placement};
|
|
||||||
|
|
||||||
pub struct StackPanel {
|
pub struct StackPanel {
|
||||||
pub(super) parent: Option<WeakEntity<StackPanel>>,
|
pub(super) parent: Option<WeakEntity<StackPanel>>,
|
||||||
@@ -70,10 +70,10 @@ impl StackPanel {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(parent) = &self.parent {
|
if let Some(parent) = &self.parent
|
||||||
if let Some(parent) = parent.upgrade() {
|
&& let Some(parent) = parent.upgrade()
|
||||||
return parent.read(cx).is_last_panel(cx);
|
{
|
||||||
}
|
return parent.read(cx).is_last_panel(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
@@ -297,12 +297,11 @@ impl StackPanel {
|
|||||||
|
|
||||||
/// Find the first top left in the stack.
|
/// Find the first top left in the stack.
|
||||||
pub fn left_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
pub fn left_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
||||||
if check_parent {
|
if check_parent
|
||||||
if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) {
|
&& 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) {
|
&& let Some(panel) = parent.read(cx).left_top_tab_panel(true, cx)
|
||||||
return Some(panel);
|
{
|
||||||
}
|
return Some(panel);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let first_panel = self.panels.first();
|
let first_panel = self.panels.first();
|
||||||
@@ -321,12 +320,11 @@ impl StackPanel {
|
|||||||
|
|
||||||
/// Find the first top right in the stack.
|
/// Find the first top right in the stack.
|
||||||
pub fn right_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
pub fn right_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
|
||||||
if check_parent {
|
if check_parent
|
||||||
if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) {
|
&& 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) {
|
&& let Some(panel) = parent.read(cx).right_top_tab_panel(true, cx)
|
||||||
return Some(panel);
|
{
|
||||||
}
|
return Some(panel);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let panel = if self.axis.is_vertical() {
|
let panel = if self.axis.is_vertical() {
|
||||||
@@ -2,22 +2,22 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent,
|
App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity,
|
||||||
Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
|
EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, MouseButton,
|
||||||
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
|
ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
|
||||||
StatefulInteractiveElement, Styled, WeakEntity, Window,
|
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::button::{Button, ButtonVariants as _};
|
||||||
use crate::dock_area::dock::DockPlacement;
|
use crate::dock::dock::DockPlacement;
|
||||||
use crate::dock_area::panel::{Panel, PanelView};
|
use crate::dock::panel::{Panel, PanelView};
|
||||||
use crate::dock_area::stack_panel::StackPanel;
|
use crate::dock::stack_panel::StackPanel;
|
||||||
use crate::dock_area::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
use crate::dock::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
||||||
use crate::menu::{DropdownMenu, PopupMenu};
|
use crate::menu::{DropdownMenu, PopupMenu};
|
||||||
use crate::tab::tab_bar::TabBar;
|
|
||||||
use crate::tab::Tab;
|
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)]
|
#[derive(Clone)]
|
||||||
struct TabState {
|
struct TabState {
|
||||||
@@ -42,22 +42,20 @@ impl DragPanel {
|
|||||||
|
|
||||||
impl Render for DragPanel {
|
impl Render for DragPanel {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
div()
|
h_flex()
|
||||||
.id("drag-panel")
|
.id("drag-panel")
|
||||||
.cursor_grab()
|
.cursor_grab()
|
||||||
.py_1()
|
.p_2()
|
||||||
.px_2()
|
.min_w_24()
|
||||||
.w_24()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.whitespace_nowrap()
|
.whitespace_nowrap()
|
||||||
.rounded(cx.theme().radius_lg)
|
.rounded(cx.theme().radius)
|
||||||
.text_xs()
|
.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)
|
.bg(cx.theme().background)
|
||||||
.text_color(cx.theme().text_accent)
|
|
||||||
.child(self.panel.title(cx))
|
.child(self.panel.title(cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,14 +232,13 @@ impl TabPanel {
|
|||||||
.any(|p| p.panel_id(cx) == panel.panel_id(cx))
|
.any(|p| p.panel_id(cx) == panel.panel_id(cx))
|
||||||
{
|
{
|
||||||
// Set the active panel to the matched panel
|
// Set the active panel to the matched panel
|
||||||
if active {
|
if active
|
||||||
if let Some(ix) = self
|
&& let Some(ix) = self
|
||||||
.panels
|
.panels
|
||||||
.iter()
|
.iter()
|
||||||
.position(|p| p.panel_id(cx) == panel.panel_id(cx))
|
.position(|p| p.panel_id(cx) == panel.panel_id(cx))
|
||||||
{
|
{
|
||||||
self.set_active_ix(ix, window, cx);
|
self.set_active_ix(ix, window, cx);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -374,12 +371,11 @@ impl TabPanel {
|
|||||||
|
|
||||||
/// Return true if self or parent only have last panel.
|
/// Return true if self or parent only have last panel.
|
||||||
fn is_last_panel(&self, cx: &App) -> bool {
|
fn is_last_panel(&self, cx: &App) -> bool {
|
||||||
if let Some(parent) = &self.stack_panel {
|
if let Some(parent) = &self.stack_panel
|
||||||
if let Some(stack_panel) = parent.upgrade() {
|
&& let Some(stack_panel) = parent.upgrade()
|
||||||
if !stack_panel.read(cx).is_last_panel(cx) {
|
&& !stack_panel.read(cx).is_last_panel(cx)
|
||||||
return false;
|
{
|
||||||
}
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.panels.len() <= 1
|
self.panels.len() <= 1
|
||||||
@@ -425,14 +421,13 @@ impl TabPanel {
|
|||||||
let view = cx.entity().clone();
|
let view = cx.entity().clone();
|
||||||
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
|
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
|
||||||
let toolbar = self.toolbar_buttons(window, cx);
|
let toolbar = self.toolbar_buttons(window, cx);
|
||||||
let has_toolbar = !toolbar.is_empty();
|
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.p_0p5()
|
.p_0p5()
|
||||||
.gap_1()
|
.gap_1p5()
|
||||||
.occlude()
|
.occlude()
|
||||||
.rounded_full()
|
.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| {
|
.when(self.zoomed, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Button::new("zoom")
|
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(
|
.child(
|
||||||
Button::new("menu")
|
Button::new("menu")
|
||||||
.icon(IconName::Ellipsis)
|
.icon(IconName::Ellipsis)
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.rounded()
|
|
||||||
.dropdown_menu({
|
.dropdown_menu({
|
||||||
let zoomable = state.zoomable;
|
let zoomable = state.zoomable;
|
||||||
let closable = state.closable;
|
let closable = state.closable;
|
||||||
@@ -578,6 +569,7 @@ impl TabPanel {
|
|||||||
let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx);
|
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 has_extend_dock_button = left_dock_button.is_some() || bottom_dock_button.is_some();
|
||||||
let tabs_count = self.panels.len();
|
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 {
|
if tabs_count == 1 && dock_area.read(cx).panel_style == PanelStyle::Default {
|
||||||
let panel = self.panels.first().unwrap();
|
let panel = self.panels.first().unwrap();
|
||||||
@@ -646,7 +638,7 @@ impl TabPanel {
|
|||||||
.into_any_element();
|
.into_any_element();
|
||||||
}
|
}
|
||||||
|
|
||||||
TabBar::new()
|
TabBar::new("tab-bar")
|
||||||
.track_scroll(&self.tab_bar_scroll_handle)
|
.track_scroll(&self.tab_bar_scroll_handle)
|
||||||
.h(TABBAR_HEIGHT)
|
.h(TABBAR_HEIGHT)
|
||||||
.when(has_extend_dock_button, |this| {
|
.when(has_extend_dock_button, |this| {
|
||||||
@@ -659,8 +651,9 @@ impl TabPanel {
|
|||||||
.border_b_1()
|
.border_b_1()
|
||||||
.h_full()
|
.h_full()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.bg(cx.theme().surface_background)
|
.bg(cx.theme().tab_background)
|
||||||
.px_2()
|
.pl_0p5()
|
||||||
|
.pr_1()
|
||||||
.children(left_dock_button)
|
.children(left_dock_button)
|
||||||
.children(bottom_dock_button),
|
.children(bottom_dock_button),
|
||||||
)
|
)
|
||||||
@@ -682,16 +675,43 @@ impl TabPanel {
|
|||||||
Some(
|
Some(
|
||||||
Tab::new()
|
Tab::new()
|
||||||
.ix(ix)
|
.ix(ix)
|
||||||
.label(panel.title(cx))
|
.tab_bar_prefix(has_extend_dock_button)
|
||||||
.py_2()
|
.child(panel.title(cx))
|
||||||
.selected(active)
|
.selected(active)
|
||||||
.disabled(disabled)
|
.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| {
|
.when(!disabled, |this| {
|
||||||
this.on_mouse_down(
|
this.on_mouse_down(
|
||||||
MouseButton::Middle,
|
MouseButton::Middle,
|
||||||
cx.listener({
|
cx.listener({
|
||||||
let panel = panel.clone();
|
let panel = panel.clone();
|
||||||
move |view, _, window, cx| {
|
move |view, _ev, window, cx| {
|
||||||
view.remove_panel(&panel, window, cx);
|
view.remove_panel(&panel, window, cx);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -757,14 +777,15 @@ impl TabPanel {
|
|||||||
this.suffix(
|
this.suffix(
|
||||||
h_flex()
|
h_flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.px_2()
|
|
||||||
.gap_1()
|
|
||||||
.top_0()
|
.top_0()
|
||||||
.right_0()
|
.right_0()
|
||||||
.h_full()
|
.h_full()
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.border_l_1()
|
.border_l_1()
|
||||||
.border_b_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))
|
.child(self.render_toolbar(state, window, cx))
|
||||||
.when_some(right_dock_button, |this, btn| this.child(btn)),
|
.when_some(right_dock_button, |this, btn| this.child(btn)),
|
||||||
)
|
)
|
||||||
@@ -1080,7 +1101,9 @@ impl TabPanel {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
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);
|
self.remove_panel(&panel, window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1097,6 +1120,7 @@ impl Focusable for TabPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for TabPanel {}
|
impl EventEmitter<DismissEvent> for TabPanel {}
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for TabPanel {}
|
impl EventEmitter<PanelEvent> for TabPanel {}
|
||||||
|
|
||||||
impl Render for TabPanel {
|
impl Render for TabPanel {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
svg, AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render,
|
AnyElement, App, AppContext, Context, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
|
||||||
RenderOnce, SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
SharedString, StyleRefinement, Styled, Svg, Transformation, Window, svg,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ pub enum IconName {
|
|||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Boom,
|
Boom,
|
||||||
|
Book,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
CaretDown,
|
CaretDown,
|
||||||
CaretRight,
|
CaretRight,
|
||||||
@@ -33,10 +34,12 @@ pub enum IconName {
|
|||||||
CloseCircle,
|
CloseCircle,
|
||||||
CloseCircleFill,
|
CloseCircleFill,
|
||||||
Copy,
|
Copy,
|
||||||
|
Device,
|
||||||
Door,
|
Door,
|
||||||
Ellipsis,
|
Ellipsis,
|
||||||
Emoji,
|
Emoji,
|
||||||
Eye,
|
Eye,
|
||||||
|
Input,
|
||||||
Info,
|
Info,
|
||||||
Invite,
|
Invite,
|
||||||
Inbox,
|
Inbox,
|
||||||
@@ -51,11 +54,14 @@ pub enum IconName {
|
|||||||
Relay,
|
Relay,
|
||||||
Reply,
|
Reply,
|
||||||
Refresh,
|
Refresh,
|
||||||
|
Scan,
|
||||||
Search,
|
Search,
|
||||||
Settings,
|
Settings,
|
||||||
|
Settings2,
|
||||||
Sun,
|
Sun,
|
||||||
Ship,
|
Ship,
|
||||||
Shield,
|
Shield,
|
||||||
|
Group,
|
||||||
UserKey,
|
UserKey,
|
||||||
Upload,
|
Upload,
|
||||||
Usb,
|
Usb,
|
||||||
@@ -89,6 +95,7 @@ impl IconNamed for IconName {
|
|||||||
Self::ArrowLeft => "icons/arrow-left.svg",
|
Self::ArrowLeft => "icons/arrow-left.svg",
|
||||||
Self::ArrowRight => "icons/arrow-right.svg",
|
Self::ArrowRight => "icons/arrow-right.svg",
|
||||||
Self::Boom => "icons/boom.svg",
|
Self::Boom => "icons/boom.svg",
|
||||||
|
Self::Book => "icons/book.svg",
|
||||||
Self::ChevronDown => "icons/chevron-down.svg",
|
Self::ChevronDown => "icons/chevron-down.svg",
|
||||||
Self::CaretDown => "icons/caret-down.svg",
|
Self::CaretDown => "icons/caret-down.svg",
|
||||||
Self::CaretRight => "icons/caret-right.svg",
|
Self::CaretRight => "icons/caret-right.svg",
|
||||||
@@ -99,10 +106,12 @@ impl IconNamed for IconName {
|
|||||||
Self::CloseCircle => "icons/close-circle.svg",
|
Self::CloseCircle => "icons/close-circle.svg",
|
||||||
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
||||||
Self::Copy => "icons/copy.svg",
|
Self::Copy => "icons/copy.svg",
|
||||||
|
Self::Device => "icons/device.svg",
|
||||||
Self::Door => "icons/door.svg",
|
Self::Door => "icons/door.svg",
|
||||||
Self::Ellipsis => "icons/ellipsis.svg",
|
Self::Ellipsis => "icons/ellipsis.svg",
|
||||||
Self::Emoji => "icons/emoji.svg",
|
Self::Emoji => "icons/emoji.svg",
|
||||||
Self::Eye => "icons/eye.svg",
|
Self::Eye => "icons/eye.svg",
|
||||||
|
Self::Input => "icons/input.svg",
|
||||||
Self::Info => "icons/info.svg",
|
Self::Info => "icons/info.svg",
|
||||||
Self::Invite => "icons/invite.svg",
|
Self::Invite => "icons/invite.svg",
|
||||||
Self::Inbox => "icons/inbox.svg",
|
Self::Inbox => "icons/inbox.svg",
|
||||||
@@ -117,14 +126,17 @@ impl IconNamed for IconName {
|
|||||||
Self::Relay => "icons/relay.svg",
|
Self::Relay => "icons/relay.svg",
|
||||||
Self::Reply => "icons/reply.svg",
|
Self::Reply => "icons/reply.svg",
|
||||||
Self::Refresh => "icons/refresh.svg",
|
Self::Refresh => "icons/refresh.svg",
|
||||||
|
Self::Scan => "icons/scan.svg",
|
||||||
Self::Search => "icons/search.svg",
|
Self::Search => "icons/search.svg",
|
||||||
Self::Settings => "icons/settings.svg",
|
Self::Settings => "icons/settings.svg",
|
||||||
|
Self::Settings2 => "icons/settings2.svg",
|
||||||
Self::Sun => "icons/sun.svg",
|
Self::Sun => "icons/sun.svg",
|
||||||
Self::Ship => "icons/ship.svg",
|
Self::Ship => "icons/ship.svg",
|
||||||
Self::Shield => "icons/shield.svg",
|
Self::Shield => "icons/shield.svg",
|
||||||
Self::UserKey => "icons/user-key.svg",
|
Self::UserKey => "icons/user-key.svg",
|
||||||
Self::Upload => "icons/upload.svg",
|
Self::Upload => "icons/upload.svg",
|
||||||
Self::Usb => "icons/usb.svg",
|
Self::Usb => "icons/usb.svg",
|
||||||
|
Self::Group => "icons/group.svg",
|
||||||
Self::PanelLeft => "icons/panel-left.svg",
|
Self::PanelLeft => "icons/panel-left.svg",
|
||||||
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
||||||
Self::PanelRight => "icons/panel-right.svg",
|
Self::PanelRight => "icons/panel-right.svg",
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ use std::ops::Range;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
|
App, Bounds, Corners, Element, ElementId, ElementInputHandler, Entity, GlobalElementId, Hitbox,
|
||||||
Entity, GlobalElementId, Hitbox, IntoElement, LayoutId, MouseButton, MouseMoveEvent, Path,
|
IntoElement, LayoutId, MouseButton, MouseMoveEvent, Path, Pixels, Point, ShapedLine,
|
||||||
Pixels, Point, ShapedLine, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle,
|
SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window, fill, point, px,
|
||||||
Window,
|
relative, size,
|
||||||
};
|
};
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
@@ -348,10 +348,10 @@ impl TextElement {
|
|||||||
let mut rev_line_corners = line_corners.iter().rev().peekable();
|
let mut rev_line_corners = line_corners.iter().rev().peekable();
|
||||||
while let Some(corners) = rev_line_corners.next() {
|
while let Some(corners) = rev_line_corners.next() {
|
||||||
points.push(corners.top_left);
|
points.push(corners.top_left);
|
||||||
if let Some(next) = rev_line_corners.peek() {
|
if let Some(next) = rev_line_corners.peek()
|
||||||
if next.top_left.x > corners.top_left.x {
|
&& next.top_left.x > corners.top_left.x
|
||||||
points.push(point(next.top_left.x, corners.top_left.y));
|
{
|
||||||
}
|
points.push(point(next.top_left.x, corners.top_left.y));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,10 +376,10 @@ impl TextElement {
|
|||||||
) -> Option<Path<Pixels>> {
|
) -> Option<Path<Pixels>> {
|
||||||
let state = self.state.read(cx);
|
let state = self.state.read(cx);
|
||||||
let mut selected_range = state.selected_range;
|
let mut selected_range = state.selected_range;
|
||||||
if let Some(ime_marked_range) = &state.ime_marked_range {
|
if let Some(ime_marked_range) = &state.ime_marked_range
|
||||||
if !ime_marked_range.is_empty() {
|
&& !ime_marked_range.is_empty()
|
||||||
selected_range = (ime_marked_range.end..ime_marked_range.end).into();
|
{
|
||||||
}
|
selected_range = (ime_marked_range.end..ime_marked_range.end).into();
|
||||||
}
|
}
|
||||||
if selected_range.is_empty() {
|
if selected_range.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
@@ -830,11 +830,12 @@ impl Element for TextElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Paint blinking cursor
|
// Paint blinking cursor
|
||||||
if focused && show_cursor {
|
if focused
|
||||||
if let Some(mut cursor_bounds) = prepaint.cursor_bounds.take() {
|
&& show_cursor
|
||||||
cursor_bounds.origin.y += prepaint.cursor_scroll_offset.y;
|
&& let Some(mut cursor_bounds) = prepaint.cursor_bounds.take()
|
||||||
window.paint_quad(fill(cursor_bounds, cx.theme().cursor));
|
{
|
||||||
}
|
cursor_bounds.origin.y += prepaint.cursor_scroll_offset.y;
|
||||||
|
window.paint_quad(fill(cursor_bounds, cx.theme().cursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint line numbers
|
// Paint line numbers
|
||||||
|
|||||||
@@ -225,13 +225,12 @@ impl MaskPattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if the fraction part is valid
|
// check if the fraction part is valid
|
||||||
if let Some(frac) = frac_part {
|
if let Some(frac) = frac_part
|
||||||
if !frac
|
&& !frac
|
||||||
.chars()
|
.chars()
|
||||||
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
|
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
@@ -255,10 +254,10 @@ impl MaskPattern {
|
|||||||
|
|
||||||
if token.is_sep() {
|
if token.is_sep() {
|
||||||
// If next token is match, it's valid
|
// If next token is match, it's valid
|
||||||
if let Some(next_token) = tokens.get(pos + 1) {
|
if let Some(next_token) = tokens.get(pos + 1)
|
||||||
if next_token.is_match(ch) {
|
&& next_token.is_match(ch)
|
||||||
return true;
|
{
|
||||||
}
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, point, px, Action, App, AppContext, Bounds, ClipboardItem, Context, Entity,
|
Action, App, AppContext, Bounds, ClipboardItem, Context, Entity, EntityInputHandler,
|
||||||
EntityInputHandler, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
|
EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding,
|
||||||
KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _,
|
||||||
ParentElement as _, Pixels, Point, Render, ScrollHandle, ScrollWheelEvent, SharedString,
|
Pixels, Point, Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription,
|
||||||
Styled as _, Subscription, UTF16Selection, Window, WrappedLine,
|
UTF16Selection, Window, WrappedLine, actions, div, point, px,
|
||||||
};
|
};
|
||||||
use lsp_types::Position;
|
use lsp_types::Position;
|
||||||
use rope::{OffsetUtf16, Rope};
|
use rope::{OffsetUtf16, Rope};
|
||||||
@@ -25,9 +25,9 @@ use super::mask_pattern::MaskPattern;
|
|||||||
use super::mode::{InputMode, TabSize};
|
use super::mode::{InputMode, TabSize};
|
||||||
use super::rope_ext::RopeExt;
|
use super::rope_ext::RopeExt;
|
||||||
use super::text_wrapper::{LineItem, TextWrapper};
|
use super::text_wrapper::{LineItem, TextWrapper};
|
||||||
|
use crate::Root;
|
||||||
use crate::history::History;
|
use crate::history::History;
|
||||||
use crate::input::element::RIGHT_MARGIN;
|
use crate::input::element::RIGHT_MARGIN;
|
||||||
use crate::Root;
|
|
||||||
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
#[action(namespace = input, no_json)]
|
#[action(namespace = input, no_json)]
|
||||||
@@ -521,16 +521,16 @@ impl InputState {
|
|||||||
let new_row = new_row as usize;
|
let new_row = new_row as usize;
|
||||||
if new_row >= last_layout.visible_range.start {
|
if new_row >= last_layout.visible_range.start {
|
||||||
let visible_row = new_row.saturating_sub(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 Some(line) = last_layout.lines.get(visible_row)
|
||||||
if let Ok(x) = line.closest_index_for_position(
|
&& let Ok(x) = line.closest_index_for_position(
|
||||||
Point {
|
Point {
|
||||||
x: preferred_x,
|
x: preferred_x,
|
||||||
y: px(0.),
|
y: px(0.),
|
||||||
},
|
},
|
||||||
last_layout.line_height,
|
last_layout.line_height,
|
||||||
) {
|
)
|
||||||
new_offset = line_start_offset + x;
|
{
|
||||||
}
|
new_offset = line_start_offset + x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1355,10 +1355,10 @@ impl InputState {
|
|||||||
) {
|
) {
|
||||||
// If there have IME marked range and is empty (Means pressed Esc to abort IME typing)
|
// If there have IME marked range and is empty (Means pressed Esc to abort IME typing)
|
||||||
// Clear the marked range.
|
// Clear the marked range.
|
||||||
if let Some(ime_marked_range) = &self.ime_marked_range {
|
if let Some(ime_marked_range) = &self.ime_marked_range
|
||||||
if ime_marked_range.is_empty() {
|
&& ime_marked_range.is_empty()
|
||||||
self.ime_marked_range = None;
|
{
|
||||||
}
|
self.ime_marked_range = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.selecting = true;
|
self.selecting = true;
|
||||||
@@ -1842,23 +1842,21 @@ impl InputState {
|
|||||||
|
|
||||||
fn previous_boundary(&self, offset: usize) -> usize {
|
fn previous_boundary(&self, offset: usize) -> usize {
|
||||||
let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left);
|
let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left);
|
||||||
if let Some(ch) = self.text.char_at(offset) {
|
if let Some(ch) = self.text.char_at(offset)
|
||||||
if ch == '\r' {
|
&& ch == '\r'
|
||||||
offset -= 1;
|
{
|
||||||
}
|
offset -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
offset
|
offset
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_boundary(&self, offset: usize) -> usize {
|
fn next_boundary(&self, offset: usize) -> usize {
|
||||||
let mut offset = self.text.clip_offset(offset + 1, Bias::Right);
|
let mut offset = self.text.clip_offset(offset + 1, Bias::Right);
|
||||||
if let Some(ch) = self.text.char_at(offset) {
|
if let Some(ch) = self.text.char_at(offset)
|
||||||
if ch == '\r' {
|
&& ch == '\r'
|
||||||
offset += 1;
|
{
|
||||||
}
|
offset += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
offset
|
offset
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1927,10 +1925,10 @@ impl InputState {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(validate) = &self.validate {
|
if let Some(validate) = &self.validate
|
||||||
if !validate(new_text, cx) {
|
&& !validate(new_text, cx)
|
||||||
return false;
|
{
|
||||||
}
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.mask_pattern.is_valid(new_text) {
|
if !self.mask_pattern.is_valid(new_text) {
|
||||||
@@ -1979,19 +1977,19 @@ impl InputState {
|
|||||||
self.input_bounds = new_bounds;
|
self.input_bounds = new_bounds;
|
||||||
|
|
||||||
// Update text_wrapper wrap_width if changed.
|
// Update text_wrapper wrap_width if changed.
|
||||||
if let Some(last_layout) = self.last_layout.as_ref() {
|
if let Some(last_layout) = self.last_layout.as_ref()
|
||||||
if wrap_width_changed {
|
&& wrap_width_changed
|
||||||
let wrap_width = if !self.soft_wrap {
|
{
|
||||||
// None to disable wrapping (will use Pixels::MAX)
|
let wrap_width = if !self.soft_wrap {
|
||||||
None
|
// None to disable wrapping (will use Pixels::MAX)
|
||||||
} else {
|
None
|
||||||
last_layout.wrap_width
|
} else {
|
||||||
};
|
last_layout.wrap_width
|
||||||
|
};
|
||||||
|
|
||||||
self.text_wrapper.set_wrap_width(wrap_width, cx);
|
self.text_wrapper.set_wrap_width(wrap_width, cx);
|
||||||
self.mode.update_auto_grow(&self.text_wrapper);
|
self.mode.update_auto_grow(&self.text_wrapper);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2209,20 +2207,18 @@ impl EntityInputHandler for InputState {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if start_origin.is_none() {
|
if start_origin.is_none()
|
||||||
if let Some(p) =
|
&& let Some(p) =
|
||||||
line.position_for_index(range.start.saturating_sub(index_offset), line_height)
|
line.position_for_index(range.start.saturating_sub(index_offset), line_height)
|
||||||
{
|
{
|
||||||
start_origin = Some(p + point(px(0.), y_offset));
|
start_origin = Some(p + point(px(0.), y_offset));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if end_origin.is_none() {
|
if end_origin.is_none()
|
||||||
if let Some(p) =
|
&& let Some(p) =
|
||||||
line.position_for_index(range.end.saturating_sub(index_offset), line_height)
|
line.position_for_index(range.end.saturating_sub(index_offset), line_height)
|
||||||
{
|
{
|
||||||
end_origin = Some(p + point(px(0.), y_offset));
|
end_origin = Some(p + point(px(0.), y_offset));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
index_offset += line.len() + 1;
|
index_offset += line.len() + 1;
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ pub use anchored::*;
|
|||||||
pub use element_ext::ElementExt;
|
pub use element_ext::ElementExt;
|
||||||
pub use event::InteractiveElementExt;
|
pub use event::InteractiveElementExt;
|
||||||
pub use focusable::FocusableCycle;
|
pub use focusable::FocusableCycle;
|
||||||
pub use geometry::*;
|
|
||||||
pub use icon::*;
|
pub use icon::*;
|
||||||
pub use index_path::IndexPath;
|
pub use index_path::IndexPath;
|
||||||
pub use kbd::*;
|
pub use kbd::*;
|
||||||
pub use root::{window_paddings, Root};
|
pub use root::{Root, window_paddings};
|
||||||
pub use styled::*;
|
pub use styled::*;
|
||||||
pub use window_ext::*;
|
pub use window_ext::*;
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ pub mod avatar;
|
|||||||
pub mod button;
|
pub mod button;
|
||||||
pub mod checkbox;
|
pub mod checkbox;
|
||||||
pub mod divider;
|
pub mod divider;
|
||||||
pub mod dock_area;
|
pub mod dock;
|
||||||
pub mod group_box;
|
pub mod group_box;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod indicator;
|
pub mod indicator;
|
||||||
@@ -39,7 +38,6 @@ mod anchored;
|
|||||||
mod element_ext;
|
mod element_ext;
|
||||||
mod event;
|
mod event;
|
||||||
mod focusable;
|
mod focusable;
|
||||||
mod geometry;
|
|
||||||
mod icon;
|
mod icon;
|
||||||
mod index_path;
|
mod index_path;
|
||||||
mod kbd;
|
mod kbd;
|
||||||
|
|||||||
@@ -3,21 +3,21 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, size, uniform_list, App, AppContext, AvailableSpace, ClickEvent, Context,
|
App, AppContext, AvailableSpace, ClickEvent, Context, DefiniteLength, EdgesRefinement, Entity,
|
||||||
DefiniteLength, EdgesRefinement, Entity, EventEmitter, FocusHandle, Focusable,
|
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, Length,
|
||||||
InteractiveElement, IntoElement, KeyBinding, Length, ListSizingBehavior, MouseButton,
|
ListSizingBehavior, MouseButton, ParentElement, Render, RenderOnce, ScrollStrategy,
|
||||||
ParentElement, Render, RenderOnce, ScrollStrategy, SharedString, StatefulInteractiveElement,
|
SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task,
|
||||||
StyleRefinement, Styled, Subscription, Task, UniformListScrollHandle, Window,
|
UniformListScrollHandle, Window, div, px, size, uniform_list,
|
||||||
};
|
};
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
|
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
|
||||||
use crate::input::{InputEvent, InputState, TextInput};
|
use crate::input::{InputEvent, InputState, TextInput};
|
||||||
use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache};
|
|
||||||
use crate::list::ListDelegate;
|
use crate::list::ListDelegate;
|
||||||
|
use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache};
|
||||||
use crate::scroll::{Scrollbar, ScrollbarHandle};
|
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) {
|
pub(crate) fn init(cx: &mut App) {
|
||||||
let context: Option<&str> = Some("List");
|
let context: Option<&str> = Some("List");
|
||||||
@@ -578,10 +578,10 @@ where
|
|||||||
self.prepare_items_if_needed(window, cx);
|
self.prepare_items_if_needed(window, cx);
|
||||||
|
|
||||||
// Scroll to the selected item if it is set.
|
// Scroll to the selected item if it is set.
|
||||||
if let Some((ix, strategy)) = self.deferred_scroll_to_index.take() {
|
if let Some((ix, strategy)) = self.deferred_scroll_to_index.take()
|
||||||
if let Some(item_ix) = self.rows_cache.position_of(&ix) {
|
&& let Some(item_ix) = self.rows_cache.position_of(&ix)
|
||||||
self.scroll_handle.scroll_to_item(item_ix, strategy);
|
{
|
||||||
}
|
self.scroll_handle.scroll_to_item(item_ix, strategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
let loading = self.delegate().loading(cx);
|
let loading = self.delegate().loading(cx);
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ use gpui::{
|
|||||||
RenderOnce, SharedString, StyleRefinement, Styled, Window,
|
RenderOnce, SharedString, StyleRefinement, Styled, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::Selectable;
|
||||||
|
use crate::avatar::Avatar;
|
||||||
use crate::button::Button;
|
use crate::button::Button;
|
||||||
use crate::menu::PopupMenu;
|
use crate::menu::PopupMenu;
|
||||||
use crate::popover::Popover;
|
use crate::popover::Popover;
|
||||||
use crate::Selectable;
|
|
||||||
|
|
||||||
/// A dropdown menu trait for buttons and other interactive elements
|
/// A dropdown menu trait for buttons and other interactive elements
|
||||||
pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static {
|
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 Button {}
|
||||||
|
|
||||||
|
impl DropdownMenu for Avatar {}
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
|
pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, div, px, rems, Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent,
|
Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent, Context, Corner, DismissEvent,
|
||||||
Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, Half,
|
Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, InteractiveElement, IntoElement,
|
||||||
InteractiveElement, IntoElement, KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement,
|
KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement, Pixels, Point, Render, ScrollHandle,
|
||||||
Pixels, Point, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
|
SharedString, StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, anchored,
|
||||||
Subscription, WeakEntity, Window,
|
div, px, rems,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::{ActiveTheme, Side};
|
||||||
|
|
||||||
use crate::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp};
|
use crate::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp};
|
||||||
use crate::kbd::Kbd;
|
use crate::kbd::Kbd;
|
||||||
use crate::menu::menu_item::MenuItemElement;
|
use crate::menu::menu_item::MenuItemElement;
|
||||||
use crate::scroll::ScrollableElement;
|
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";
|
const CONTEXT: &str = "PopupMenu";
|
||||||
|
|
||||||
@@ -719,13 +719,13 @@ impl PopupMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
|
pub(crate) fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
|
||||||
if let Some(ix) = self.selected_index {
|
if let Some(ix) = self.selected_index
|
||||||
if let Some(item) = self.menu_items.get(ix) {
|
&& let Some(item) = self.menu_items.get(ix)
|
||||||
return match item {
|
{
|
||||||
PopupMenuItem::Submenu { menu, .. } => Some(menu.clone()),
|
return match item {
|
||||||
_ => None,
|
PopupMenuItem::Submenu { menu, .. } => Some(menu.clone()),
|
||||||
};
|
_ => None,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
@@ -965,12 +965,11 @@ impl PopupMenu {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
// Do not dismiss, if click inside the parent menu
|
// Do not dismiss, if click inside the parent menu
|
||||||
if let Some(parent) = self.parent_menu.as_ref() {
|
if let Some(parent) = self.parent_menu.as_ref()
|
||||||
if let Some(parent) = parent.upgrade() {
|
&& let Some(parent) = parent.upgrade()
|
||||||
if parent.read(cx).bounds.contains(position) {
|
&& parent.read(cx).bounds.contains(position)
|
||||||
return;
|
{
|
||||||
}
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dismiss(&Cancel, window, cx);
|
self.dismiss(&Cancel, window, cx);
|
||||||
@@ -1026,7 +1025,7 @@ impl PopupMenu {
|
|||||||
} else if checked {
|
} else if checked {
|
||||||
Icon::new(IconName::Check)
|
Icon::new(IconName::Check)
|
||||||
} else {
|
} else {
|
||||||
return None;
|
Icon::empty()
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(icon.small())
|
Some(icon.small())
|
||||||
@@ -1112,25 +1111,17 @@ impl PopupMenu {
|
|||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.disabled(true),
|
.disabled(true),
|
||||||
PopupMenuItem::Label(label) => this.disabled(true).cursor_default().child(
|
PopupMenuItem::Label(label) => this.disabled(true).cursor_default().child(
|
||||||
h_flex()
|
h_flex().cursor_default().items_center().gap_x_1().child(
|
||||||
.cursor_default()
|
div()
|
||||||
.items_center()
|
.flex_1()
|
||||||
.gap_x_1()
|
.text_xs()
|
||||||
.children(Self::render_icon(has_left_icon, false, None, window, cx))
|
.font_semibold()
|
||||||
.child(
|
.text_color(cx.theme().text_muted)
|
||||||
div()
|
.child(label.clone()),
|
||||||
.flex_1()
|
),
|
||||||
.text_xs()
|
|
||||||
.font_semibold()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(label.clone()),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
PopupMenuItem::ElementItem {
|
PopupMenuItem::ElementItem {
|
||||||
render,
|
render, disabled, ..
|
||||||
icon,
|
|
||||||
disabled,
|
|
||||||
..
|
|
||||||
} => this
|
} => this
|
||||||
.when(!disabled, |this| {
|
.when(!disabled, |this| {
|
||||||
this.on_click(
|
this.on_click(
|
||||||
@@ -1144,13 +1135,6 @@ impl PopupMenu {
|
|||||||
.min_h(item_height)
|
.min_h(item_height)
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_x_2()
|
.gap_x_2()
|
||||||
.children(Self::render_icon(
|
|
||||||
has_left_icon,
|
|
||||||
is_left_check,
|
|
||||||
icon.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
.child((render)(window, cx))
|
.child((render)(window, cx))
|
||||||
.children(right_check_icon.map(|icon| icon.ml_3())),
|
.children(right_check_icon.map(|icon| icon.ml_3())),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, div, hsla, point, px, Animation, AnimationExt as _, AnyElement, App, Bounds,
|
Animation, AnimationExt as _, AnyElement, App, Bounds, BoxShadow, ClickEvent, Div, FocusHandle,
|
||||||
BoxShadow, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
|
||||||
MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, StyleRefinement, Styled,
|
RenderOnce, SharedString, StyleRefinement, Styled, Window, anchored, div, hsla, point, px,
|
||||||
Window,
|
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ use crate::actions::{Cancel, Confirm};
|
|||||||
use crate::animation::cubic_bezier;
|
use crate::animation::cubic_bezier;
|
||||||
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
|
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
|
||||||
use crate::scroll::ScrollableElement;
|
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";
|
const CONTEXT: &str = "Modal";
|
||||||
|
|
||||||
@@ -297,10 +296,10 @@ impl RenderOnce for Modal {
|
|||||||
let on_close = on_close.clone();
|
let on_close = on_close.clone();
|
||||||
|
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if let Some(on_ok) = &on_ok {
|
if let Some(on_ok) = &on_ok
|
||||||
if !on_ok(&ClickEvent::default(), window, cx) {
|
&& !on_ok(&ClickEvent::default(), window, cx)
|
||||||
return;
|
{
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
on_close(&ClickEvent::default(), window, cx);
|
on_close(&ClickEvent::default(), window, cx);
|
||||||
@@ -343,7 +342,7 @@ impl RenderOnce for Modal {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let window_paddings = crate::root::window_paddings(window, cx);
|
let window_paddings = crate::root::window_paddings(window, cx);
|
||||||
let radius = (cx.theme().radius_lg * 2.).min(px(20.));
|
let radius = cx.theme().radius_lg;
|
||||||
|
|
||||||
let view_size = window.viewport_size()
|
let view_size = window.viewport_size()
|
||||||
- gpui::size(
|
- gpui::size(
|
||||||
@@ -360,8 +359,8 @@ impl RenderOnce for Modal {
|
|||||||
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
||||||
let x = bounds.center().x - self.width / 2.;
|
let x = bounds.center().x - self.width / 2.;
|
||||||
|
|
||||||
let mut padding_right = px(16.);
|
let mut padding_right = px(8.);
|
||||||
let mut padding_left = px(16.);
|
let mut padding_left = px(8.);
|
||||||
|
|
||||||
if let Some(pl) = self.style.padding.left {
|
if let Some(pl) = self.style.padding.left {
|
||||||
padding_left = pl.to_pixels(self.width.into(), window.rem_size());
|
padding_left = pl.to_pixels(self.width.into(), window.rem_size());
|
||||||
@@ -500,6 +499,7 @@ impl RenderOnce for Modal {
|
|||||||
.child(self.content),
|
.child(self.content),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.when_none(&self.footer, |this| this.child(div().pt(padding_left)))
|
||||||
.when_some(self.footer, |this, footer| {
|
.when_some(self.footer, |this, footer| {
|
||||||
this.child(
|
this.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context,
|
Animation, AnimationExt, AnyElement, App, AppContext, ClickEvent, Context, DismissEvent,
|
||||||
DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
|
Render, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription,
|
||||||
Subscription, Window,
|
Window, div, px, relative,
|
||||||
};
|
};
|
||||||
use smol::Timer;
|
use theme::{ActiveTheme, Anchor};
|
||||||
use theme::ActiveTheme;
|
|
||||||
|
|
||||||
use crate::animation::cubic_bezier;
|
use crate::animation::cubic_bezier;
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt};
|
use crate::{Icon, IconName, Sizable as _, Size, StyledExt, h_flex, v_flex};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
pub enum NotificationType {
|
pub enum NotificationKind {
|
||||||
#[default]
|
#[default]
|
||||||
Info,
|
Info,
|
||||||
Success,
|
Success,
|
||||||
@@ -27,13 +25,21 @@ pub enum NotificationType {
|
|||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NotificationType {
|
impl NotificationKind {
|
||||||
fn icon(&self, cx: &App) -> Icon {
|
fn icon(&self, cx: &App) -> Icon {
|
||||||
match self {
|
match self {
|
||||||
Self::Info => Icon::new(IconName::Info).text_color(cx.theme().element_foreground),
|
Self::Info => Icon::new(IconName::Info)
|
||||||
Self::Success => Icon::new(IconName::Info).text_color(cx.theme().secondary_foreground),
|
.with_size(Size::Medium)
|
||||||
Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().warning_foreground),
|
.text_color(cx.theme().icon),
|
||||||
Self::Error => Icon::new(IconName::Warning).text_color(cx.theme().danger_foreground),
|
Self::Success => Icon::new(IconName::CheckCircle)
|
||||||
|
.with_size(Size::Medium)
|
||||||
|
.text_color(cx.theme().icon_accent),
|
||||||
|
Self::Warning => Icon::new(IconName::Warning)
|
||||||
|
.with_size(Size::Medium)
|
||||||
|
.text_color(cx.theme().text_warning),
|
||||||
|
Self::Error => Icon::new(IconName::CloseCircle)
|
||||||
|
.with_size(Size::Medium)
|
||||||
|
.text_color(cx.theme().danger_foreground),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,6 +62,7 @@ impl From<(TypeId, ElementId)> for NotificationId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
/// A notification element.
|
/// A notification element.
|
||||||
pub struct Notification {
|
pub struct Notification {
|
||||||
/// The id is used make the notification unique.
|
/// The id is used make the notification unique.
|
||||||
@@ -64,16 +71,13 @@ pub struct Notification {
|
|||||||
/// None means the notification will be added to the end of the list.
|
/// None means the notification will be added to the end of the list.
|
||||||
id: NotificationId,
|
id: NotificationId,
|
||||||
style: StyleRefinement,
|
style: StyleRefinement,
|
||||||
type_: Option<NotificationType>,
|
kind: Option<NotificationKind>,
|
||||||
title: Option<SharedString>,
|
title: Option<SharedString>,
|
||||||
message: Option<SharedString>,
|
message: Option<SharedString>,
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
autohide: bool,
|
autohide: bool,
|
||||||
#[allow(clippy::type_complexity)]
|
action_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> Button>>,
|
||||||
action_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> Button>>,
|
content_builder: Option<Rc<dyn Fn(&mut Self, &mut Window, &mut Context<Self>) -> AnyElement>>,
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
content_builder: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>>,
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||||
closing: bool,
|
closing: bool,
|
||||||
}
|
}
|
||||||
@@ -84,12 +88,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 {
|
impl From<SharedString> for Notification {
|
||||||
fn from(s: SharedString) -> Self {
|
fn from(s: SharedString) -> Self {
|
||||||
Self::new().message(s)
|
Self::new().message(s)
|
||||||
@@ -102,24 +100,24 @@ impl From<&'static str> for Notification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(NotificationType, &'static str)> for Notification {
|
impl From<(NotificationKind, &'static str)> for Notification {
|
||||||
fn from((type_, content): (NotificationType, &'static str)) -> Self {
|
fn from((kind, content): (NotificationKind, &'static str)) -> Self {
|
||||||
Self::new().message(content).with_type(type_)
|
Self::new().message(content).with_kind(kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(NotificationType, SharedString)> for Notification {
|
impl From<(NotificationKind, SharedString)> for Notification {
|
||||||
fn from((type_, content): (NotificationType, SharedString)) -> Self {
|
fn from((kind, content): (NotificationKind, SharedString)) -> Self {
|
||||||
Self::new().message(content).with_type(type_)
|
Self::new().message(content).with_kind(kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DefaultIdType;
|
struct DefaultIdType;
|
||||||
|
|
||||||
impl Notification {
|
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 {
|
pub fn new() -> Self {
|
||||||
let id: SharedString = uuid::Uuid::new_v4().to_string().into();
|
let id: SharedString = uuid::Uuid::new_v4().to_string().into();
|
||||||
let id = (TypeId::of::<DefaultIdType>(), id.into());
|
let id = (TypeId::of::<DefaultIdType>(), id.into());
|
||||||
@@ -129,7 +127,7 @@ impl Notification {
|
|||||||
style: StyleRefinement::default(),
|
style: StyleRefinement::default(),
|
||||||
title: None,
|
title: None,
|
||||||
message: None,
|
message: None,
|
||||||
type_: None,
|
kind: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
autohide: true,
|
autohide: true,
|
||||||
action_builder: None,
|
action_builder: None,
|
||||||
@@ -139,33 +137,38 @@ impl Notification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the message of the notification, default is None.
|
||||||
pub fn message(mut self, message: impl Into<SharedString>) -> Self {
|
pub fn message(mut self, message: impl Into<SharedString>) -> Self {
|
||||||
self.message = Some(message.into());
|
self.message = Some(message.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create an info notification with the given message.
|
||||||
pub fn info(message: impl Into<SharedString>) -> Self {
|
pub fn info(message: impl Into<SharedString>) -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
.message(message)
|
.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 {
|
pub fn success(message: impl Into<SharedString>) -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
.message(message)
|
.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 {
|
pub fn warning(message: impl Into<SharedString>) -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
.message(message)
|
.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 {
|
pub fn error(message: impl Into<SharedString>) -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
.message(message)
|
.message(message)
|
||||||
.with_type(NotificationType::Error)
|
.with_kind(NotificationKind::Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the type for unique identification of the notification.
|
/// Set the type for unique identification of the notification.
|
||||||
@@ -180,8 +183,8 @@ impl Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the type and id of the notification, used to uniquely identify the 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 {
|
pub fn type_id<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
|
||||||
self.id = (TypeId::of::<DefaultIdType>(), key.into()).into();
|
self.id = (TypeId::of::<T>(), key.into()).into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,8 +205,8 @@ impl Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the type of the notification, default is NotificationType::Info.
|
/// Set the type of the notification, default is NotificationType::Info.
|
||||||
pub fn with_type(mut self, type_: NotificationType) -> Self {
|
pub fn with_kind(mut self, kind: NotificationKind) -> Self {
|
||||||
self.type_ = Some(type_);
|
self.kind = Some(kind);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,22 +226,31 @@ impl Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the action button of the 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
|
pub fn action<F>(mut self, action: F) -> Self
|
||||||
where
|
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.action_builder = Some(Rc::new(action));
|
||||||
|
self.autohide = false;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dismiss the notification.
|
/// Dismiss the notification.
|
||||||
pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
pub fn dismiss(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.closing {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.closing = true;
|
self.closing = true;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
// Dismiss the notification after 0.15s to show the animation.
|
// Dismiss the notification after 0.15s to show the animation.
|
||||||
cx.spawn(async move |view, cx| {
|
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| {
|
cx.update(|cx| {
|
||||||
if let Some(view) = view.upgrade() {
|
if let Some(view) = view.upgrade() {
|
||||||
view.update(cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
@@ -248,13 +260,13 @@ impl Notification {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.detach()
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the content of the notification.
|
/// Set the content of the notification.
|
||||||
pub fn content(
|
pub fn content(
|
||||||
mut self,
|
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 {
|
||||||
self.content_builder = Some(Rc::new(content));
|
self.content_builder = Some(Rc::new(content));
|
||||||
self
|
self
|
||||||
@@ -276,33 +288,58 @@ impl Styled for Notification {
|
|||||||
&mut self.style
|
&mut self.style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Notification {
|
impl Render for Notification {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let closing = self.closing;
|
let content = self
|
||||||
let icon = match self.type_ {
|
.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(),
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
let closing = self.closing;
|
||||||
|
let has_title = self.title.is_some();
|
||||||
|
let only_message = !has_title && content.is_none() && action.is_none();
|
||||||
|
let placement = cx.theme().notification.placement;
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("notification")
|
.id("notification")
|
||||||
.refine_style(&self.style)
|
|
||||||
.group("")
|
.group("")
|
||||||
.occlude()
|
.occlude()
|
||||||
.relative()
|
.relative()
|
||||||
.w_96()
|
.w_112()
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.bg(cx.theme().surface_background)
|
.bg(background)
|
||||||
|
.text_color(text_color)
|
||||||
.rounded(cx.theme().radius_lg)
|
.rounded(cx.theme().radius_lg)
|
||||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||||
.p_2()
|
.p_2()
|
||||||
.gap_3()
|
.gap_2()
|
||||||
.justify_start()
|
.justify_start()
|
||||||
.items_start()
|
.items_start()
|
||||||
|
.when(only_message, |this| this.items_center())
|
||||||
|
.refine_style(&self.style)
|
||||||
.when_some(icon, |this, icon| {
|
.when_some(icon, |this, icon| {
|
||||||
this.child(div().flex_shrink_0().pt_1().child(icon))
|
this.child(div().flex_shrink_0().size_5().child(icon))
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -310,23 +347,34 @@ impl Render for Notification {
|
|||||||
.gap_1()
|
.gap_1()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.when_some(self.title.clone(), |this, title| {
|
.when_some(self.title.clone(), |this, title| {
|
||||||
this.child(div().text_sm().font_semibold().child(title))
|
this.child(h_flex().h_5().text_sm().font_semibold().child(title))
|
||||||
})
|
})
|
||||||
.when_some(self.message.clone(), |this, message| {
|
.when_some(self.message.clone(), |this, message| {
|
||||||
this.child(div().text_sm().child(message))
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.when(has_title, |this| this.text_color(cx.theme().text_muted))
|
||||||
|
.line_height(relative(1.3))
|
||||||
|
.child(message),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.when_some(self.content_builder.clone(), |this, child_builder| {
|
.when_some(content, |this, content| this.child(content))
|
||||||
this.child(child_builder(window, cx))
|
.when_some(action, |this, action| {
|
||||||
})
|
this.child(
|
||||||
.when_some(self.action_builder.clone(), |this, action_builder| {
|
h_flex()
|
||||||
this.child(action_builder(window, cx).small().w_full().my_2())
|
.w_full()
|
||||||
|
.flex_1()
|
||||||
|
.gap_1()
|
||||||
|
.justify_end()
|
||||||
|
.child(action),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
.top_2p5()
|
.top(px(6.5))
|
||||||
.right_2p5()
|
.right(px(6.5))
|
||||||
.invisible()
|
.invisible()
|
||||||
.group_hover("", |this| this.visible())
|
.group_hover("", |this| this.visible())
|
||||||
.child(
|
.child(
|
||||||
@@ -334,7 +382,7 @@ impl Render for Notification {
|
|||||||
.icon(IconName::Close)
|
.icon(IconName::Close)
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
this.dismiss(window, cx);
|
this.dismiss(window, cx);
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
@@ -345,21 +393,47 @@ impl Render for Notification {
|
|||||||
on_click(event, window, cx);
|
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(
|
.with_animation(
|
||||||
ElementId::NamedInteger("slide-down".into(), closing as u64),
|
ElementId::NamedInteger("slide-down".into(), closing as u64),
|
||||||
Animation::new(Duration::from_secs_f64(0.25))
|
Animation::new(Duration::from_secs_f64(0.25))
|
||||||
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
|
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
|
||||||
move |this, delta| {
|
move |this, delta| {
|
||||||
if closing {
|
if closing {
|
||||||
let x_offset = px(0.) + delta * px(45.);
|
|
||||||
let opacity = 1. - delta;
|
let opacity = 1. - delta;
|
||||||
this.left(px(0.) + x_offset)
|
let that = this
|
||||||
.shadow_none()
|
.shadow_none()
|
||||||
.opacity(opacity)
|
.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 {
|
} else {
|
||||||
let y_offset = px(-45.) + delta * px(45.);
|
|
||||||
let opacity = delta;
|
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)
|
this.top(px(0.) + y_offset)
|
||||||
.opacity(opacity)
|
.opacity(opacity)
|
||||||
.when(opacity < 0.85, |this| this.shadow_none())
|
.when(opacity < 0.85, |this| this.shadow_none())
|
||||||
@@ -373,7 +447,11 @@ impl Render for Notification {
|
|||||||
pub struct NotificationList {
|
pub struct NotificationList {
|
||||||
/// Notifications that will be auto hidden.
|
/// Notifications that will be auto hidden.
|
||||||
pub(crate) notifications: VecDeque<Entity<Notification>>,
|
pub(crate) notifications: VecDeque<Entity<Notification>>,
|
||||||
|
|
||||||
|
/// Whether the notification list is expanded.
|
||||||
expanded: bool,
|
expanded: bool,
|
||||||
|
|
||||||
|
/// Subscriptions
|
||||||
_subscriptions: HashMap<NotificationId, Subscription>,
|
_subscriptions: HashMap<NotificationId, Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,10 +464,12 @@ impl NotificationList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push<T>(&mut self, notification: T, window: &mut Window, cx: &mut Context<Self>)
|
pub fn push(
|
||||||
where
|
&mut self,
|
||||||
T: Into<Notification>,
|
notification: impl Into<Notification>,
|
||||||
{
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
let notification = notification.into();
|
let notification = notification.into();
|
||||||
let id = notification.id.clone();
|
let id = notification.id.clone();
|
||||||
let autohide = notification.autohide;
|
let autohide = notification.autohide;
|
||||||
@@ -411,36 +491,35 @@ impl NotificationList {
|
|||||||
|
|
||||||
if autohide {
|
if autohide {
|
||||||
// Sleep for 5 seconds to autohide the notification
|
// Sleep for 5 seconds to autohide the notification
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
cx.spawn_in(window, async move |_this, cx| {
|
||||||
Timer::after(Duration::from_secs(5)).await;
|
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))
|
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();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close<T>(&mut self, key: T, window: &mut Window, cx: &mut Context<Self>)
|
pub(crate) fn close(
|
||||||
where
|
&mut self,
|
||||||
T: Into<ElementId>,
|
id: impl Into<NotificationId>,
|
||||||
{
|
window: &mut Window,
|
||||||
let id = (TypeId::of::<DefaultIdType>(), key.into()).into();
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let id: NotificationId = id.into();
|
||||||
if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) {
|
if let Some(n) = self.notifications.iter().find(|n| n.read(cx).id == id) {
|
||||||
n.update(cx, |note, cx| {
|
n.update(cx, |note, cx| note.dismiss(window, cx))
|
||||||
note.dismiss(window, cx);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
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();
|
self.notifications.clear();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -451,25 +530,46 @@ impl NotificationList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Render for 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 size = window.viewport_size();
|
||||||
let items = self.notifications.iter().rev().take(10).rev().cloned();
|
let items = self.notifications.iter().rev().take(10).rev().cloned();
|
||||||
|
|
||||||
div()
|
let placement = cx.theme().notification.placement;
|
||||||
.id("notification-wrapper")
|
let margins = &cx.theme().notification.margins;
|
||||||
.absolute()
|
|
||||||
.top_4()
|
v_flex()
|
||||||
.right_4()
|
.id("notification-list")
|
||||||
.child(
|
.max_h(size.height)
|
||||||
v_flex()
|
.pt(margins.top)
|
||||||
.id("notification-list")
|
.pb(margins.bottom)
|
||||||
.h(size.height - px(8.))
|
.gap_3()
|
||||||
.gap_3()
|
.when(
|
||||||
.children(items)
|
matches!(placement, Anchor::TopRight),
|
||||||
.on_hover(cx.listener(|view, hovered, _, cx| {
|
|this| this.pr(margins.right), // ignore left
|
||||||
view.expanded = *hovered;
|
|
||||||
cx.notify()
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
|
.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::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
deferred, div, px, AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId,
|
AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId, EventEmitter,
|
||||||
EventEmitter, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding,
|
FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
|
||||||
MouseButton, ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement,
|
ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement, Styled,
|
||||||
Styled, Subscription, Window,
|
Subscription, Window, deferred, div, px,
|
||||||
};
|
};
|
||||||
|
use theme::Anchor;
|
||||||
|
|
||||||
use crate::actions::Cancel;
|
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";
|
const CONTEXT: &str = "Popover";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use gpui::{
|
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;
|
mod panel;
|
||||||
@@ -142,10 +142,10 @@ impl ResizableState {
|
|||||||
pub(crate) fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
|
pub(crate) fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
|
||||||
self.panels.remove(panel_ix);
|
self.panels.remove(panel_ix);
|
||||||
self.sizes.remove(panel_ix);
|
self.sizes.remove(panel_ix);
|
||||||
if let Some(resizing_panel_ix) = self.resizing_panel_ix {
|
if let Some(resizing_panel_ix) = self.resizing_panel_ix
|
||||||
if resizing_panel_ix > panel_ix {
|
&& resizing_panel_ix > panel_ix
|
||||||
self.resizing_panel_ix = Some(resizing_panel_ix - 1);
|
{
|
||||||
}
|
self.resizing_panel_ix = Some(resizing_panel_ix - 1);
|
||||||
}
|
}
|
||||||
self.adjust_to_container_size(cx);
|
self.adjust_to_container_size(cx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty,
|
Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, Entity,
|
||||||
Entity, EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent,
|
EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, MouseUpEvent,
|
||||||
MouseUpEvent, ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window,
|
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::resizable::PANEL_MIN_SIZE;
|
||||||
use crate::{h_flex, v_flex, AxisExt, ElementExt};
|
use crate::{ElementExt, h_flex, v_flex};
|
||||||
|
|
||||||
pub enum ResizablePanelEvent {
|
pub enum ResizablePanelEvent {
|
||||||
Resized,
|
Resized,
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId,
|
AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, InteractiveElement,
|
||||||
InteractiveElement, IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels,
|
IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
|
||||||
Point, Render, StatefulInteractiveElement, Styled as _, Window,
|
StatefulInteractiveElement, Styled as _, Window, div, px,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::{ActiveTheme, AxisExt};
|
||||||
|
|
||||||
use crate::dock_area::dock::DockPlacement;
|
use crate::dock::DockPlacement;
|
||||||
use crate::AxisExt;
|
|
||||||
|
|
||||||
pub(crate) const HANDLE_PADDING: Pixels = px(4.);
|
pub(crate) const HANDLE_PADDING: Pixels = px(4.);
|
||||||
pub(crate) const HANDLE_SIZE: Pixels = px(1.);
|
pub(crate) const HANDLE_SIZE: Pixels = px(1.);
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
use std::any::TypeId;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
canvas, div, point, px, size, AnyView, App, AppContext, Bounds, Context, CursorStyle,
|
AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, Edges, ElementId, Entity,
|
||||||
Decorations, Edges, Entity, FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement,
|
FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||||
MouseButton, ParentElement as _, Pixels, Point, Render, ResizeEdge, SharedString, Size, Styled,
|
ParentElement as _, Pixels, Point, Render, ResizeEdge, Size, Styled, Tiling, WeakFocusHandle,
|
||||||
Tiling, WeakFocusHandle, Window,
|
Window, canvas, div, point, px, size,
|
||||||
};
|
};
|
||||||
use theme::{
|
use theme::{
|
||||||
ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
|
ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
|
||||||
@@ -91,7 +92,7 @@ impl Root {
|
|||||||
pub fn render_notification_layer(
|
pub fn render_notification_layer(
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Option<impl IntoElement> {
|
) -> Option<impl IntoElement + use<>> {
|
||||||
let root = window.root::<Root>()??;
|
let root = window.root::<Root>()??;
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
@@ -104,7 +105,10 @@ impl Root {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render the modal layer.
|
/// 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 root = window.root::<Root>()??;
|
||||||
let active_modals = root.read(cx).active_modals.clone();
|
let active_modals = root.read(cx).active_modals.clone();
|
||||||
|
|
||||||
@@ -138,10 +142,10 @@ impl Root {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if let Some(ix) = show_overlay_ix {
|
if let Some(ix) = show_overlay_ix
|
||||||
if let Some(modal) = modals.get_mut(ix) {
|
&& let Some(modal) = modals.get_mut(ix)
|
||||||
modal.overlay_visible = true;
|
{
|
||||||
}
|
modal.overlay_visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(div().children(modals))
|
Some(div().children(modals))
|
||||||
@@ -213,13 +217,30 @@ impl Root {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear a notification by its ID.
|
/// Clear a notification by its type.
|
||||||
pub fn clear_notification<T>(&mut self, id: T, window: &mut Window, cx: &mut Context<Self>)
|
pub fn clear_notification<T: Sized + 'static>(
|
||||||
where
|
&mut self,
|
||||||
T: Into<SharedString>,
|
window: &mut Window,
|
||||||
{
|
cx: &mut Context<'_, Root>,
|
||||||
self.notification
|
) {
|
||||||
.update(cx, |view, cx| view.close(id.into(), window, cx));
|
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();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +270,6 @@ impl Render for Root {
|
|||||||
div()
|
div()
|
||||||
.id("window")
|
.id("window")
|
||||||
.size_full()
|
.size_full()
|
||||||
.bg(gpui::transparent_black())
|
|
||||||
.map(|div| match decorations {
|
.map(|div| match decorations {
|
||||||
Decorations::Server => div,
|
Decorations::Server => div,
|
||||||
Decorations::Client { tiling } => div
|
Decorations::Client { tiling } => div
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, App, Div, Element, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
App, Div, Element, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||||
ScrollHandle, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window,
|
ScrollHandle, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window, div,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Scrollbar, ScrollbarAxis};
|
use super::{Scrollbar, ScrollbarAxis};
|
||||||
use crate::scroll::ScrollbarHandle;
|
|
||||||
use crate::StyledExt;
|
use crate::StyledExt;
|
||||||
|
use crate::scroll::ScrollbarHandle;
|
||||||
|
|
||||||
/// A trait for elements that can be made scrollable with scrollbars.
|
/// A trait for elements that can be made scrollable with scrollbars.
|
||||||
pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Element {
|
pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Element {
|
||||||
@@ -160,6 +160,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollableElement for Div {}
|
impl ScrollableElement for Div {}
|
||||||
|
|
||||||
impl<E> ScrollableElement for Stateful<E>
|
impl<E> ScrollableElement for Stateful<E>
|
||||||
where
|
where
|
||||||
E: ParentElement + Styled + Element,
|
E: ParentElement + Styled + Element,
|
||||||
@@ -195,6 +196,7 @@ fn render_scrollbar<H: ScrollbarHandle + Clone>(
|
|||||||
// Do not render scrollbar when inspector is picking elements,
|
// Do not render scrollbar when inspector is picking elements,
|
||||||
// to allow us to pick the background elements.
|
// to allow us to pick the background elements.
|
||||||
let is_inspector_picking = window.is_inspector_picking(cx);
|
let is_inspector_picking = window.is_inspector_picking(cx);
|
||||||
|
|
||||||
if is_inspector_picking {
|
if is_inspector_picking {
|
||||||
return div();
|
return div();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
px, relative, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId,
|
App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId, EntityId,
|
||||||
EntityId, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels,
|
GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point,
|
||||||
Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window,
|
Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window, px, relative,
|
||||||
};
|
};
|
||||||
|
use theme::AxisExt;
|
||||||
use crate::AxisExt;
|
|
||||||
|
|
||||||
/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening.
|
/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ use std::rc::Rc;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner,
|
App, Axis, BorderStyle, Bounds, ContentMask, Corner, CursorStyle, Edges, Element, ElementId,
|
||||||
CursorStyle, Edges, Element, ElementId, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
|
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId, IntoElement, IsZero,
|
||||||
InspectorElementId, IntoElement, IsZero, LayoutId, ListState, MouseDownEvent, MouseMoveEvent,
|
LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
||||||
MouseUpEvent, PaintQuad, Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style,
|
Position, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, fill,
|
||||||
UniformListScrollHandle, Window,
|
point, px, relative, size,
|
||||||
};
|
};
|
||||||
use theme::{ActiveTheme, ScrollbarMode};
|
use theme::{ActiveTheme, AxisExt, ScrollbarMode};
|
||||||
|
|
||||||
use crate::AxisExt;
|
|
||||||
|
|
||||||
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
|
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
|
||||||
const WIDTH: Pixels = px(1. * 2. + 8.);
|
const WIDTH: Pixels = px(1. * 2. + 8.);
|
||||||
@@ -54,7 +52,7 @@ impl ScrollbarHandle for ScrollHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn content_size(&self) -> Size<Pixels> {
|
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> {
|
fn content_size(&self) -> Size<Pixels> {
|
||||||
let base_handle = &self.0.borrow().base_handle;
|
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> {
|
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) {
|
fn start_drag(&self) {
|
||||||
@@ -407,7 +405,6 @@ impl Scrollbar {
|
|||||||
ScrollbarMode::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
|
ScrollbarMode::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
|
||||||
_ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
|
_ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
|
||||||
};
|
};
|
||||||
|
|
||||||
(
|
(
|
||||||
cx.theme().scrollbar_thumb_background,
|
cx.theme().scrollbar_thumb_background,
|
||||||
cx.theme().scrollbar_track_background,
|
cx.theme().scrollbar_track_background,
|
||||||
@@ -522,6 +519,7 @@ impl Element for Scrollbar {
|
|||||||
|
|
||||||
let mut states = vec![];
|
let mut states = vec![];
|
||||||
let mut has_both = self.axis.is_both();
|
let mut has_both = self.axis.is_both();
|
||||||
|
|
||||||
let scroll_size = self
|
let scroll_size = self
|
||||||
.scroll_size
|
.scroll_size
|
||||||
.unwrap_or(self.scroll_handle.content_size());
|
.unwrap_or(self.scroll_handle.content_size());
|
||||||
|
|||||||
@@ -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 serde::{Deserialize, Serialize};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
@@ -46,6 +46,30 @@ pub trait StyledExt: Styled + Sized {
|
|||||||
self.flex().flex_col()
|
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_thin, THIN);
|
||||||
font_weight!(font_extralight, EXTRA_LIGHT);
|
font_weight!(font_extralight, EXTRA_LIGHT);
|
||||||
font_weight!(font_light, LIGHT);
|
font_weight!(font_light, LIGHT);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user