210 Commits

Author SHA1 Message Date
dcf28e2b60 chore: better dropdown menu (#11)
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m44s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Reviewed-on: #11
2026-02-28 06:05:44 +00:00
624140c061 feat: add contact list panel (#10)
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m41s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Reviewed-on: #10
2026-02-28 01:50:33 +00:00
fcb2b671e7 chore: update deps
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m59s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
2026-02-27 18:39:07 +07:00
a86219dcb0 chore: bump version
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 5m41s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
2026-02-27 15:51:24 +07:00
c22a7291c7 .
Some checks failed
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m40s
Rust / build (ubuntu-latest, stable) (push) Failing after 1m41s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-27 15:39:33 +07:00
d7996bf32e .
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 2m56s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m43s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-27 15:17:18 +07:00
2dcf825105 .
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m50s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m40s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-27 08:11:40 +07:00
3debfa81d7 Revert "wip"
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m50s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m50s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
This reverts commit e152154c3b.
2026-02-27 05:46:41 +07:00
4ba2049756 Revert "."
This reverts commit b7ffdc8431.
2026-02-27 05:46:40 +07:00
b7ffdc8431 .
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m45s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m42s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-26 18:28:24 +07:00
e152154c3b wip 2026-02-26 15:09:27 +07:00
ff5ae8280c .
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m55s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m43s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-26 14:28:20 +07:00
41cc8f4032 add rose pine theme
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 2m3s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 2m5s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-26 13:16:59 +07:00
8ebd1c3525 .
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m50s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m51s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-26 12:50:10 +07:00
bd1910ce03 add theme selector
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m42s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m57s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-26 10:48:06 +07:00
cba3f976c6 add preferences dialog
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 2m3s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m50s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-26 07:07:15 +07:00
971a82df1b add backup panel
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m47s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m52s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-25 09:11:23 +07:00
6d863d8bbe add support for blossom
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m42s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m53s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-25 06:58:46 +07:00
10ded51d2f update encryption panel
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m58s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m39s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-24 15:48:58 +07:00
e44ce7e3f6 update profile panel
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m55s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 2m42s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-24 09:43:10 +07:00
a7f9a7ceeb update relay list panel
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 2m0s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 2m23s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-24 09:08:31 +07:00
ebf0e86828 update messaging relays panel
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m41s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m46s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-23 19:23:56 +07:00
2ec98e14d0 wip: revamp title bar elements
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 12m59s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 9m53s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-23 15:48:35 +07:00
31df6d7937 clean up
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m24s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m26s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-22 16:54:51 +07:00
67ccfcb132 update send message and chat panel ui
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m26s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m20s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
2026-02-22 14:34:41 +07:00
e3141aba19 update theme
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 9m53s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
2026-02-21 10:43:36 +07:00
bc588114c4 remove keystore
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m46s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
2026-02-21 07:54:09 +07:00
4c0beb2a2a update deps
Some checks failed
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (ubuntu-latest, stable) (push) Has been cancelled
2026-02-21 07:53:24 +07:00
b88955e62c merged previous stuffs on master 2026-02-20 19:48:03 +07:00
014757cfc9 chore: update gpui 2026-01-14 15:00:52 +07:00
ac9afb1790 chore: refactor app settings (#2)
# Changelog

### Added

- [x] Add `Auth Mode` setting.
- [x] Add `Room Config` setting.

### Changed

- [x] Rename `media server` setting to `file server`

### Removed

- [x] Remove `proxy` setting. Coop is no longer depend on any 3rd party services.
- [x] Remove `contact bypass` settings. All chat requests from known contacts will be bypass by default.

**Note:**
- The Settings UI has been removed. It will be re-added in a separate PR.

Reviewed-on: #2
2026-01-14 09:48:15 +08:00
75c3783522 feat: rewrite the nip-4e implementation (#1)
Make NIP-4e a core feature, not an optional feature.

Note:
- The UI is broken and needs to be updated in a separate PR.

Reviewed-on: #1
2026-01-13 16:00:08 +08:00
reya
bb455871e5 remove i18n crate (#213) 2025-12-31 09:23:59 +07:00
0507fa7ac5 chore: update deps 2025-12-26 08:25:22 +07:00
af115321b4 Merge branch 'master' of github.com:lumehq/coop 2025-12-26 08:23:57 +07:00
reya
34e026751b feat: add support for multi-themes (#210)
* chore: update deps

* wip

* add themes

* add matrix theme

* add flexoki and spaceduck themes

* .

* simple theme change function

* .

* respect shadow and radius settings

* add rose pine themes

* toggle theme
2025-12-26 08:20:18 +07:00
e9e662dccc chore: update deps 2025-12-23 10:10:50 +07:00
5b7780ec9b chore: update deps 2025-12-17 08:10:02 +07:00
782efd7498 chore: update deps 2025-12-13 10:06:09 +07:00
8192023479 chore: release version 0.3.0 2025-12-08 11:10:54 +07:00
4637478a0b chore: minor fixes 2025-12-05 10:26:24 +07:00
6b5adb0a56 chore: update deps 2025-12-05 08:10:36 +07:00
9fd55cf3ff chore: update deps 2025-11-30 09:47:40 +07:00
reya
14c36e4731 feat: refine the search bar (#207)
* update deps

* refactor the search cancellation

* .

* .
2025-11-22 07:25:08 +07:00
reya
a6e00b47d8 feat: refine user profile popup (#206)
* update user popup

* .

* .
2025-11-20 08:23:02 +07:00
0784a20be5 chore: update deps 2025-11-18 07:36:31 +07:00
reya
6023063cf4 feat: revamp the onboarding process (#205)
* redesign

* restructure

* .

* .

* .

* .

* .
2025-11-17 15:10:14 +07:00
67c92cb319 chore: update deps 2025-11-15 14:51:16 +07:00
reya
122299f548 chore: improve nip4e implementation (#204)
* patch

* update ui

* add load response

* fix

* .

* wip: rewrite gossip

* new gossip implementation

* clean up

* .

* debug

* .

* .

* update

* .

* fix

* fix
2025-11-15 08:30:45 +07:00
d87bcfbd65 chore: follow up on #203 2025-11-11 11:02:37 +07:00
de5134676d chore: update deps 2025-11-11 09:25:46 +07:00
reya
512834b640 chore: rewrite the backend (not tested) (#203)
* wip: refactor

* refactor

* clean up

* .

* rename

* add relay auth

* .

* .

* optimize

* .

* clean up

* add encryption crate

* .

* .

* .

* .

* .

* add encryption crate

* .

* refactor nip4e

* .

* fix endless loop

* fix metadata fetching
2025-11-11 09:09:33 +07:00
a1a0a7ecd4 chore: update deps 2025-11-03 19:09:59 +07:00
reya
a4067d2c00 chore: fix crash when failing to parse message (#202)
* clean up

* .

* fix rich text component

* clean up
2025-11-03 19:04:16 +07:00
4ebe590f8a chore: update deps 2025-11-02 20:27:47 +07:00
reya
9da624dd0c feat: nostr based auto updater (#200)
* .

* refactor

* fix

* .

* clean up

* clean up
2025-11-02 08:22:55 +07:00
reya
7091fa1cab chore: restructure and refine the ui (#199)
* update deps

* clean up

* add account crate

* add person crate

* add chat and chat ui crates

* .

* clean up the ui crate

* .

* .
2025-11-01 09:16:02 +07:00
a1bd4954eb chore: clean up 2025-10-29 08:30:04 +07:00
fde1499796 chore: update deps 2025-10-29 07:52:53 +07:00
reya
649cdff49c re add verify relay connection (#197) 2025-10-29 07:42:50 +07:00
reya
b0fa98831d chore: fix nip4e implementation (#196)
* push

* debug

* disable verify sender

* .
2025-10-28 20:35:34 +07:00
reya
b9297d3a01 chore: follow up on nip-4e (#195)
* update deps

* .

* remove resend button

* clean up

* .

* .

* .

* .

* .
2025-10-28 14:37:30 +07:00
reya
b5ed079a0e chore: improve nip-4e (#194)
* update texts

* update ui

* .

* .
2025-10-27 17:10:38 +07:00
6017eebaed chore: update gpui & components 2025-10-27 08:20:37 +07:00
reya
15bbe82a87 feat: nip4e (#188)
* encryption keys

* .

* .

* move nip4e to device crate

* .

* .

* use i18n for device crate

* refactor

* refactor

* .

* add reset button

* send message with encryption keys

* clean up

* .

* choose signer

* fix

* update i18n

* fix sending
2025-10-26 18:10:40 +07:00
alltheseas
83687e5448 Adds outbound dm relay hint (#193)
* Include relay hints in DM rumor tags

* Add unit test for DM relay hints

---------

Co-authored-by: alltheseas <alltheseas@users.noreply.github.com>
2025-10-25 07:40:55 +07:00
alltheseas
48c90f5bb0 Store DM rumors without re-signing (#192)
Co-authored-by: alltheseas <alltheseas@users.noreply.github.com>
2025-10-25 07:39:02 +07:00
alltheseas
47abd2909b Reject gift wraps whose rumor pubkey doesn’t match the seal signer (#190)
* Verify seal sender before caching rumors

* Test rumor sender verification logic

---------

Co-authored-by: alltheseas <alltheseas@users.noreply.github.com>
2025-10-24 16:30:25 +07:00
reya
ac0b233089 feat: implement multiple keystores (#187)
* keystore

* .

* fix

* .

* allow user disable keyring

* update texts
2025-10-20 07:40:02 +07:00
reya
a1e0934fc3 chore: clean up codebase (#186)
* refactor app state

* clean up

* clean up

* .
2025-10-18 09:46:45 +07:00
32a0401907 chore: simplify codebase 2025-10-17 08:51:34 +07:00
1742031901 chore: update deps 2025-10-12 20:51:46 +07:00
reya
2415374567 chore: improve gossip implementation (#184)
* add send event function

* add set nip17 and set nip65 functions

* setup gossip relays

* .
2025-10-12 20:22:57 +07:00
reya
7fc727461e chore: follow up #181 (#183)
* update deps

* .

* fix
2025-10-11 16:27:14 +07:00
reya
68a8ec7a69 feat: custom gossip implementation (#181)
* .

* rename global to app_state

* refactor event tracker

* gossip

* .

* .
2025-10-10 17:36:38 +07:00
b7693444e6 chore: update deps 2025-10-07 14:45:44 +07:00
6e7f63d79a chore: release version 0.2.11 2025-10-01 13:50:57 +07:00
ee693aa503 chore: update the release script 2025-10-01 13:49:39 +07:00
reya
ebcc60cd92 chore: follow up on #172 (#173)
* clean up

* wip

* clean up

* remove unused picture field
2025-10-01 13:45:13 +07:00
reya
0db48bc003 chore: refactor message sending (#172)
* refactor send message

* refactor resend

* fix

* refactor

* clean up
2025-09-30 08:59:38 +07:00
880ba30d20 chore: bump version 2025-09-28 08:01:41 +07:00
reya
d889f9b25d chore: always call get_public_key on nip46 (#171)
* get public key on login

* .
2025-09-28 07:57:04 +07:00
reya
0de1b20951 feat: add context menu for quick profile viewing (#170)
* add profile context menu

* add context menu for avatar
2025-09-27 15:15:00 +07:00
reya
338a947b57 chore: fix duplicate reply (#169)
* prevent duplicate reply

* .
2025-09-27 08:02:16 +07:00
reya
98ce928f0c chore: fix double message on sent (#166)
* .

* fix

* update
2025-09-26 14:17:31 +07:00
reya
61cad5dd96 chore: refactor the input component (#165)
* refactor the input component

* fix clippy

* clean up
2025-09-25 08:03:14 +07:00
a87184214f chore: add release script 2025-09-23 09:36:49 +07:00
fff3a44f62 chore: bump version 2025-09-23 09:05:35 +07:00
reya
9abcc25f32 chore: optimize resource usage (#162)
* avoid string allocation

* cache image

* .

* .

* .

* fix
2025-09-23 09:03:48 +07:00
reya
fb3da096f8 chore: improve the media uploader (#161)
* refactor upload

* .

* .
2025-09-22 07:30:32 +07:00
1de3045505 chore: update deps 2025-09-19 08:42:08 +07:00
reya
9f369bf57f chore: improve auth handling in startup screen (#160)
* cancel auth

* .
2025-09-18 20:01:10 +07:00
reya
4164651342 chore: refactor the compose modal (#156)
* .

* update

* clean up
2025-09-18 08:39:24 +07:00
c12856cda0 chore: bump version 2025-09-16 20:31:10 +07:00
reya
c67b223a53 chore: add missing ui elements (#153)
* add empty state

* .

* update welcome panel
2025-09-16 19:59:03 +07:00
reya
9880a3ed3d chore: follow up on #151 (#152)
* improve ui

* .

* clean up
2025-09-15 20:53:25 +07:00
reya
d13ffd5a54 feat: detect user dm relays when opening chat panel (#151)
* preconnect to user messaging relays

* .
2025-09-15 19:34:48 +07:00
cc79f0ed1c chore: clean up 2025-09-15 09:10:37 +07:00
reya
5127eaadbb feat: add seen-on-relays viewer per message (#149)
* chore: bump version

* add seen on

* seen on menu
2025-09-14 11:50:14 +07:00
d38e70ecbf chore: update deps 2025-09-13 07:51:33 +07:00
reya
b142982ab1 chore: refactor event fetching (#148)
* use stream for nip65 and nip17 relays fetching

* .
2025-09-13 07:42:17 +07:00
reya
2ea2519e8b feat: resend failed messages (#147)
* .

* .

* fix

* fix

* update

* fix

* .

* .
2025-09-12 17:07:57 +07:00
reya
2ea5feaf4b chore: improve handling of user profiles (#146)
* resubscribe metadata for all pubkeys

* .
2025-09-10 10:06:45 +07:00
4ec7530b91 chore: update deps 2025-09-10 08:21:43 +07:00
df82861101 chore: bump version 2025-09-10 07:25:15 +07:00
reya
fc99ef4dfe chore: improve the activity check (#145)
* better check for activity

* .
2025-09-09 10:23:40 +07:00
reya
d0f7a1abd3 feat: extended screening (#144)
* improve mutual contacts check

* .

* .
2025-09-08 17:11:29 +07:00
reya
71140beb52 feat: relay status viewer (#143)
* add relay status

* .
2025-09-07 14:54:28 +07:00
reya
e177facef4 chore: better handle async tasks (#142)
* improve some codes

* .
2025-09-07 08:33:21 +07:00
60bca49200 chore: update deps and refactor the event loop 2025-09-06 20:55:11 +07:00
reya
ede41c41c3 chore: improve the event loop (#141)
* improve wait for signer

* refactor gift wrap processor

* .

* .

* .

* .

* .
2025-09-05 19:01:26 +07:00
reya
70e235dcc2 chore: minor ui components improvements (#140)
* improve ui

* .

* .
2025-09-04 07:30:03 +07:00
b11b0e0115 chore: update deps 2025-09-03 12:12:40 +07:00
reya
d8edac0bb9 chore: fix rooms out of order while loading (#139)
* fix room out of order while loading

* .

* .
2025-09-03 09:16:36 +07:00
reya
d392602ed6 feat: resend messages after authentication (#137)
* resend failed message

* update settings
2025-09-02 18:19:53 +07:00
reya
5a36354cc8 chore: fix handling of ongoing room kind incorrectly (#136) 2025-09-01 19:44:23 +07:00
a1df66e176 chore: bump hotfix version 2025-09-01 17:31:34 +07:00
reya
78d913ae38 chore: fix high cpu usage and incorrect use of list indices (#135)
* .

* fix cpu usage
2025-09-01 17:30:33 +07:00
b4691aa689 chore: temporary disable room's announcement 2025-09-01 10:16:48 +07:00
c49530b030 chore: bump hotfix version 2025-09-01 08:24:10 +07:00
reya
e7ffe7627c chore: fix double messages on load (#134) 2025-09-01 08:22:04 +07:00
6a5304514f chore: bump version 2025-08-31 19:08:37 +07:00
reya
f2be8fca08 feat: add setting for relay authentication (#133)
* remember auth relay

* .

* .
2025-08-31 18:06:04 +07:00
reya
807851518a feat: manually handle NIP-42 auth request (#132)
* improve fetch relays

* .

* .

* .

* refactor

* refactor

* remove identity

* manually auth

* auth

* prevent duplicate message

* clean up
2025-08-30 14:38:00 +07:00
49a3dedd9c chore: clean up 2025-08-25 13:46:46 +07:00
reya
b19bb01003 feat: support triple-click to select entire line 2025-08-25 12:37:20 +07:00
3a6fc2bcc5 chore: fix messages not loading 2025-08-25 12:13:45 +07:00
reya
5edcc97ada chore: rework login and identity (#129)
* .

* redesign onboarding screen

* .

* add signer proxy

* .

* .

* .

* .

* fix proxy

* clean up

* fix new account
2025-08-25 09:22:09 +07:00
a8ccda259c chore: update deps 2025-08-20 11:50:56 +07:00
reya
23ad28e96e fix cpu spike (#127) 2025-08-19 14:31:13 +07:00
07a2f6980e chore: update deps 2025-08-18 14:24:13 +07:00
reya
c2b276f3f3 chore: improve chat panel (#121)
* .

* .

* .

* skip sent message

* improve sent reports

* .

* .

* .
2025-08-18 13:20:29 +07:00
5bef1a2c6c chore: fix flatpak 2025-08-10 14:58:59 +07:00
cd26244538 chore: bump version 2025-08-10 12:50:52 +07:00
reya
ca622d1262 chore: improve logout behavior (#118)
* resubscribe on logout

* .

* .
2025-08-10 10:43:28 +07:00
reya
5011becacb chore: use a newer flatpak runtime (#117)
* use newer flatpak runtime

* .
2025-08-10 07:40:13 +07:00
reya
17f92d767e chore: improve ui consistency (#115)
* .

* .
2025-08-09 14:58:01 +07:00
be660cb14b chore: update deps 2025-08-08 13:14:11 +07:00
reya
8fca202c05 chore: refactor subscription (#113)
* fix duplicate set signer request

* refactor

* .
2025-08-07 20:53:21 +07:00
7b20131e3b chore: bump version 2025-08-06 10:11:04 +07:00
reya
9127696517 chore: fix missing message during initial fetch (#110)
* remove fetched flag

* .

* improve
2025-08-06 09:15:00 +07:00
reya
af74a4ed23 . (#109) 2025-08-06 07:10:00 +07:00
reya
bd2b72a57a chore: fix duplicate messages (#108)
* prevent duplicate message on load

* refactor
2025-08-05 21:15:10 +07:00
d6edc8b546 chore: update metadata and version 2025-08-05 15:06:51 +07:00
053ecc6a15 chore: update readme 2025-08-05 13:22:29 +07:00
reya
fe864e4a7f chore: improve github action (#105)
* fix build step

* fix ci

* fix build

* fix ci on linux

* .

* fix flatpak

* .

* .

* .

* fix snap

* .

* .

* .

* .

* fix

* .

* .

* fix path

* .

* .

* fix upload artifacts

* fix build on arm

* fix snap arm

* .

* .
2025-08-05 13:14:35 +07:00
871bbdac78 chore: fix ci 2025-08-04 11:47:50 +07:00
0ea919901e chore: fix ci 2025-08-04 10:57:05 +07:00
3772853141 chore: update gh action 2025-08-04 10:09:36 +07:00
ab4597cb6f chore: add release action 2025-08-04 08:59:24 +07:00
ed6e4f2082 chore: update deps 2025-08-04 07:27:53 +07:00
493223276c chore: improve message fetching 2025-08-03 20:34:35 +07:00
c8c5a6668d chore: refactor auto updater 2025-08-02 17:28:27 +07:00
86d24ccbd5 chore: fix identifier 2025-08-02 13:17:43 +07:00
80c649f9a0 chore: prepare to release 2025-08-02 13:17:06 +07:00
reya
c188f12993 chore: Refine the UI (#102)
* update deps

* update window options

* linux title bar

* fix build

* .

* fix build

* rounded corners on linux

* .

* .

* fix i18n key

* fix change subject modal

* .

* update new account

* .

* update relay modal

* .

* fix i18n keys

---------

Co-authored-by: reya <reya@macbook.local>
2025-08-02 11:37:15 +07:00
reya
3cf9dde882 chore: Improve Request Screening (#101)
* open chat while screening

* close panel on ignore

* bypass screening

* .

* improve settings

* refine modal

* .

* .

* .

* .

* .
2025-07-27 07:22:31 +07:00
reya
91cca37d69 chore: Improve Font Rendering on Linux (#100)
* add zed plex sans

* .
2025-07-25 07:20:47 +07:00
12168c6084 chore: update gpui-component 2025-07-23 20:47:15 +07:00
reya
a631dd90d2 feat: screening (#96)
* .

* .

* refactor

* .

* screening

* add report user function

* add danger and warning styles

* update deps

* update

* fix line height

* .
2025-07-23 12:45:01 +07:00
reya
00b40db82c chore: improve text input (#94)
* update history

* hide cursor & selection when window is deactivated - gpui-component

* .

* update input to catch up with gpui-component

* adjust history
2025-07-18 09:25:55 +07:00
59cfdb9ae2 chore: improve render modal and update deps 2025-07-17 07:54:40 +07:00
73a2678278 chore: update deps 2025-07-16 15:18:18 +07:00
reya
c7ab75d310 chore: fix translations (#93)
* fix

* .
2025-07-16 14:57:57 +07:00
reya
8195eedaf6 chore: improve render message (#84)
* .

* refactor upload button

* refactor

* dispatch action on mention clicked

* add profile modal

* .

* .

* .

* improve rich_text

* improve handle url

* make registry simpler

* refactor

* .

* clean up
2025-07-16 14:37:26 +07:00
alltheseas
9f02942d87 Update README.md with additional Linux prerequisites (#90)
Updated README.md with Linux prerequisites - openSSL, X11, build-essential
2025-07-16 07:19:54 +07:00
reya
2e3a4b3634 chore: improve search (#83)
* .

* .

* wip: add nip05 search

* add nip05 search

* .

* support cancel search

* .
2025-07-10 09:03:54 +07:00
reya
8bfad30a99 chore: improve data requests (#81)
* refactor

* refactor

* add documents

* clean up

* refactor

* clean up

* refactor identity

* .

* .

* rename
2025-07-08 15:23:35 +07:00
122dbaf693 chore: add document for actions 2025-07-05 08:33:40 +07:00
9bb784652d chore: update deps 2025-07-04 15:01:57 +07:00
reya
c1d5c7e719 feat: add support for multi languages (#79)
* update backup settings description

* add rust-i18n

* translate

* .

* update translations

* fix

* update translate

* .
2025-07-04 14:57:22 +07:00
f9bf29df09 chore: fix copy/paste on linux 2025-07-02 15:53:52 +07:00
2e046ec5d7 chore: update deps 2025-07-02 15:30:28 +07:00
abb1474300 chore: update gpui and nostr-sdk 2025-06-30 08:35:48 +07:00
reya
b212095334 chore: Improve Auto Login (#71)
* improve auto login

* add auto login status

* add reset button on startup
2025-06-29 08:01:08 +07:00
@RandyMcMillan
2dfb48b538 build tooling (#69)
* script/macos:add

* script/linux:add libx11-dev

* Cargo.toml:pin nostr nostr-sdk nostr nostr-connect

---------

Co-authored-by: reya <123083837+reyamir@users.noreply.github.com>
2025-06-28 14:14:54 +07:00
reya
14076054c0 chore: Improve Login Process (#70)
* wip

* simplify nostr connect uri logic

* improve wait for connection

* improve handle password

* .

* add countdown
2025-06-28 10:09:31 +07:00
3c2eaabab2 chore: update gpui and nostr sdk 2025-06-25 20:00:05 +07:00
reya
edee9305cc feat: improve search and handle input in compose (#67)
* feat: support search by npub or nprofile

* .

* .

* .

* chore: prevent update local search with empty result

* clean up

* .
2025-06-25 15:03:05 +07:00
reya
c7e3331eb0 feat: wait for processing to complete (#66)
* wait instead of check eose

* refactor

* refactor

* refactor

* improve extend rooms function

* .
2025-06-23 09:00:56 +07:00
1d77fd443e chore: always show title bar on linux and windows 2025-06-18 12:26:04 +07:00
5f5bb33654 chore: update gpui components 2025-06-17 14:50:46 +07:00
052b0163cb chore: add goreleaser 2025-06-17 13:04:17 +07:00
5f8e886a34 chore: update gpui 2025-06-17 08:00:47 +07:00
reya
440f17af18 refactor: Client keys and Identity (#61)
* .

* .

* .

* .

* refactor client keys

* .

* .

* refactor

* .

* .

* .

* update new account
2025-06-17 07:16:16 +07:00
reya
cc36adeafe feat: Basic Application Settings (#58)
* .

* .

* .

* update modal
2025-06-13 07:56:59 +07:00
reya
e687204361 chore: refactor the global state and improve signer (#56)
* refactor

* update

* .

* rustfmt

* .

* .

* .

* .

* .

* add document

* .

* add logout

* handle error

* chore: update gpui

* adjust timeout
2025-06-07 14:52:21 +07:00
reya
50beaebd2c chore: improve message copying functionality (#53)
* chore: improve message copying functionality

* .

* clean up
2025-05-31 07:29:56 +07:00
reya
7cc512331b press the down key to move to the end of the line (#52) 2025-05-30 12:14:51 +07:00
63191c16bd chore: use primary clipboard when pasting (linux only) 2025-05-30 07:39:30 +07:00
reya
a674ac898a feat: Middle Click (#51)
* paste on middle click

* middle click to close tab

* middle click to reply
2025-05-29 17:24:51 +07:00
reya
557ff18714 chore: improve room kind handling (#48)
* chore: improve room kind handling

* .

* add some tooltips

* .

* fix button hovered style

* .

* improve prevent duplicate message

* .
2025-05-29 09:05:08 +07:00
7a447da447 chore: improve nostr connect and search 2025-05-27 08:59:51 +07:00
92d862e1fa chore: update gpui 2025-05-27 07:44:17 +07:00
reya
0f884f8142 chore: improve performance (#42)
* use uniform list for rooms list

* move profile cache to outside gpui context

* update comment

* refactor

* refactor

* .

* .

* add avatar component

* .

* refactor

* .
2025-05-27 07:34:22 +07:00
Vitor Pamplona
45564c7722 Massively improves the boot speed by: (#41)
1. ignoring duplicated events coming to the client
2. avoiding checking for trust in disk for every event (uses a simple cache)
2025-05-22 07:18:09 +07:00
b0a6b73801 chore: update text input 2025-05-21 18:48:00 +07:00
e851063de9 chore: update gpui 2025-05-21 17:45:48 +07:00
reya
3fd236de73 feat: Reply or Reference a specific message (#39)
* add reply to when send message

* show reply message

* refactor

* multiple quote
2025-05-21 17:44:43 +07:00
ba42bafc3a chore: fix input auto-grow height 2025-05-19 07:25:39 +07:00
71fbd97bad chore: adjust global consts 2025-05-18 16:00:28 +07:00
reya
443dbc82a6 chore: Improve Chat Performance (#35)
* refactor

* optimistically update message list

* fix

* update

* handle duplicate messages

* update ui

* refactor input

* update multi line input

* clean up
2025-05-18 15:35:33 +07:00
reya
4f066b7c00 feat: Improve Blink Cursor (#34)
* add cursor color to theme

* adjust
2025-05-13 13:26:02 +07:00
reya
4e24061817 feat: Redesign New Chat (#31)
* make subject is optional

* redesign

* search

* fix

* adjust
2025-05-12 20:46:01 +07:00
2f83b5091e chore: revamp theme 2025-05-07 14:12:31 +07:00
reya
97e66fbeb7 chore: improve nostr connect (#21)
* ref

* update

* temporary switch to rust-nostr fork

* use nip46 branch
2025-05-06 07:38:15 +07:00
3fea18f038 feat: automatically open the chat room 2025-05-04 10:12:15 +07:00
3bd8592f86 chore: clean up current dock when logout 2025-05-03 08:39:17 +07:00
reya
8c211be11a feat: add search and refactor modal (#19)
* add find button to sidebar

* update

* improve search

* add error msg
2025-05-02 17:03:49 +07:00
2c2aeb915e feat: add option for toggle chat folders 2025-04-30 13:10:18 +07:00
303 changed files with 34756 additions and 18524 deletions

View File

@@ -1,81 +0,0 @@
name: Packager Release Process
run-name: Triggered by ${{ github.actor }}.
on: workflow_dispatch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CN_APPLICATION: lume/coop
jobs:
draft:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create draft release
uses: crabnebula-dev/cloud-release@v0
with:
command: release draft ${{ env.CN_APPLICATION }} --framework packager
api-key: ${{ secrets.CN_API_KEY }}
build:
needs: draft
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Install stable toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
cache: true
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y gcc g++ libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libzstd-dev libvulkan1 libgit2-dev make cmake clang jq netcat-openbsd git curl gettext-base elfutils libsqlite3-dev musl-tools musl-dev build-essential
- name: install cargo packager
run: |
cargo install cargo-packager --locked
- name: Build packager app
run: |
cargo packager --release
- name: Move assets to workdir
run: |
mv target/release/* .
- name: Upload assets
uses: crabnebula-dev/cloud-release@v0
with:
command: release upload ${{ env.CN_APPLICATION }} --framework packager
api-key: ${{ secrets.CN_API_KEY }}
publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Publish release
uses: crabnebula-dev/cloud-release@v0
with:
command: release publish ${{ env.CN_APPLICATION }} --framework packager
api-key: ${{ secrets.CN_API_KEY }}

172
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,172 @@
name: Build and Release
on:
workflow_dispatch:
push:
tags:
- "v*"
jobs:
build:
name: Build ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- platform: windows-x64
os: windows-latest
target: x86_64-pc-windows-msvc
- platform: windows-arm64
os: windows-11-arm
target: aarch64-pc-windows-msvc
- platform: macos-x64
os: macos-13
target: x86_64-apple-darwin
- platform: macos-arm64
os: macos-latest
target: aarch64-apple-darwin
- platform: linux-x64
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- platform: linux-arm64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
# Windows and macOS builds using cargo-packager
- name: Build with cargo-packager (Windows/macOS)
if: runner.os != 'Linux'
working-directory: crates/coop
run: |
cargo install cargo-packager --locked
cargo packager --release
- name: Upload Windows/macOS artifacts
if: runner.os != 'Linux'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform }}-artifacts
path: |
dist/*.dmg
dist/*.msi
dist/*.exe
if-no-files-found: error
# Linux builds using custom scripts
- name: Install Linux build dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y flatpak flatpak-builder snapd squashfs-tools jq gettext-base
- name: Install Snapcraft
if: runner.os == 'Linux'
run: sudo snap install snapcraft --classic
- name: Make scripts executable
if: runner.os == 'Linux'
run: |
chmod +x script/get-crate-version
chmod +x script/linux
chmod +x script/bundle-snap
chmod +x script/bundle-linux
chmod +x script/flatpak/deps
chmod +x script/flatpak/bundle-flatpak
- name: Install required dependencies
if: runner.os == 'Linux'
run: ./script/linux
# Only build Flatpak and Snap for x86_64 (most common use case)
- name: Build Flatpak
if: runner.os == 'Linux'
run: |
./script/bundle-linux --flatpak
./script/flatpak/deps
./script/flatpak/bundle-flatpak
- name: Build Snap
if: runner.os == 'Linux'
run: |
VERSION=$(script/get-crate-version coop)
./script/bundle-linux
./script/bundle-snap $VERSION
- name: Collect Linux artifacts
if: runner.os == 'Linux'
working-directory: ${{ github.workspace }}
run: |
mkdir -p linux-artifacts
# Copy the tarball created by bundle-linux
find target/release -name "*.tar.gz" -exec cp {} linux-artifacts/ \;
# Find and copy flatpak files (if they exist)
find . -name "*.flatpak" -exec cp {} linux-artifacts/ \; || true
# Find and copy snap files (if they exist)
find . -name "*.snap" -exec cp {} linux-artifacts/ \; || true
ls -la linux-artifacts/
- name: Upload Linux artifacts
if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform }}-artifacts
path: linux-artifacts/**/*
if-no-files-found: error
release:
name: Create Release
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Make get-crate-version executable
run: chmod +x script/get-crate-version
- name: Get version
id: version
run: |
VERSION=$(script/get-crate-version coop)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Display artifacts structure
run: |
echo "Artifacts structure:"
find artifacts -type f -exec ls -la {} \;
- name: Create draft release
id: create_release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
name: ${{ steps.version.outputs.tag }}
draft: true
prerelease: false
generate_release_notes: true
files: |
artifacts/**/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Output release info
run: |
echo "Created draft release: ${{ steps.create_release.outputs.url }}"
echo "Release ID: ${{ steps.create_release.outputs.id }}"

32
.github/workflows/rust.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Rust
on:
push:
branches: ["**"]
pull_request:
branches: ["m**"]
env:
CARGO_TERM_COLOR: always
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rustup: [stable]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: script/linux
run: chmod +x ./script/linux && ./script/linux
if: matrix.os == 'ubuntu-latest'
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

2
.gitignore vendored
View File

@@ -19,3 +19,5 @@ dist/
# Useless stuffs
.DS_Store
# Added by goreleaser init:
.intentionally-empty-file.o

5371
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,53 +1,55 @@
[workspace]
resolver = "2"
members = ["crates/*"]
default-members = ["crates/coop"]
[workspace.package]
version = "0.1.5"
edition = "2021"
publish = false
[workspace.dependencies]
coop = { path = "crates/*" }
# UI
gpui = { git = "https://github.com/zed-industries/zed" }
reqwest_client = { git = "https://github.com/zed-industries/zed" }
# Nostr
nostr = { git = "https://github.com/rust-nostr/nostr", features = ["parser"] }
nostr-relay-builder = { git = "https://github.com/rust-nostr/nostr" }
nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
nostr-keyring = { git = "https://github.com/rust-nostr/nostr" }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
"lmdb",
"nip96",
"nip59",
"nip49",
"nip44",
"nip05",
] }
# Others
emojis = "0.6.4"
smol = "2"
oneshot = "0.1.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dirs = "5.0"
itertools = "0.13.0"
futures = "0.3.30"
chrono = "0.4.38"
tracing = "0.1.40"
anyhow = "1.0.44"
smallvec = "1.14.0"
rust-embed = "8.5.0"
log = "0.4"
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
[workspace]
resolver = "2"
members = ["crates/*"]
default-members = ["crates/coop"]
[workspace.package]
version = "1.0.0-beta"
edition = "2021"
publish = false
[workspace.dependencies]
# GPUI
gpui = { git = "https://github.com/zed-industries/zed" }
gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["screen-capture", "x11", "wayland", "runtime_shaders"] }
gpui_linux = { git = "https://github.com/zed-industries/zed" }
gpui_windows = { git = "https://github.com/zed-industries/zed" }
gpui_macos = { git = "https://github.com/zed-industries/zed" }
gpui_tokio = { git = "https://github.com/zed-industries/zed" }
reqwest_client = { git = "https://github.com/zed-industries/zed" }
# Nostr
nostr-lmdb = { 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-sdk = { git = "https://github.com/rust-nostr/nostr" }
nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] }
# Others
anyhow = "1.0.44"
chrono = "0.4.38"
futures = "0.3"
itertools = "0.13.0"
log = "0.4"
oneshot = "0.1.10"
flume = { version = "0.11.1", default-features = false, features = ["async", "select"] }
rust-embed = "8.5.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
schemars = "1"
smallvec = "1.14.0"
smol = "2"
tracing = "0.1.40"
webbrowser = "1.0.4"
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
[profile.profiling]
inherits = "release"
debug = true

View File

@@ -1,18 +0,0 @@
name = "coop"
description = "Coop is a cross-platform Nostr client designed for secure communication focus on simplicity and customizability."
product-name = "Coop"
version = "0.1.5"
category = "SocialNetworking"
identifier = "su.reya.coop"
resources = ["assets/*/*", "Cargo.toml", "./LICENSE", "./README.md"]
binaries = [ { path = "coop", main = true } ]
before-packaging-command = "cargo build --release"
out-dir = "./target/release"
icons = [
"crates/coop/resources/32x32.png",
"crates/coop/resources/128x128.png",
"crates/coop/resources/128x128@2x.png",
"crates/coop/resources/app-icon.icns",
"crates/coop/resources/app-icon.png",
"crates/coop/resources/app-icon.ico",
]

View File

@@ -1,19 +1,34 @@
![CoopDemo](/docs/coop.jpg)
![Coop](/docs/coop.png)
<p>
<a href="https://github.com/lumehq/coop/actions/workflows/main.yml">
<img alt="Actions" src="https://github.com/lumehq/coop/actions/workflows/main.yml/badge.svg">
<a href="https://github.com/lumehq/coop/actions/workflows/rust.yml">
<img alt="Actions" src="https://github.com/lumehq/coop/actions/workflows/rust.yml/badge.svg">
</a>
<img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/lumehq/coop">
<img alt="GitHub issues" src="https://img.shields.io/github/issues-raw/lumehq/coop">
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/lumehq/coop">
</p>
Coop is a cross-platform Nostr client designed for secure communication focus on simplicity and customizability.
Coop is a simple, fast, and reliable nostr client for secure messaging across all platforms.
**New**✨: A blog post introducing Coop in details has been posted [here](#).
### Screenshots
> Coop is currently in the **alpha stage** of development. This means the app may contain bugs, incomplete features, or unexpected behavior. We recommend using it for testing purposes only and not for critical or sensitive communications. Your feedback is invaluable in helping us improve Coop, so please report any issues or suggestions via the [GitHub Issue Tracker](https://github.com/lumehq/coop/issues). Thank you for your understanding and support!
<p float="left">
<img src="/docs/mac_01.png" width="250" />
<img src="/docs/mac_02.png" width="250" />
<img src="/docs/mac_03.png" width="250" />
<img src="/docs/mac_04.png" width="250" />
<img src="/docs/mac_05.png" width="250" />
<img src="/docs/mac_06.png" width="250" />
<img src="/docs/mac_07.png" width="250" />
<img src="/docs/mac_08.png" width="250" />
<img src="/docs/mac_09.png" width="250" />
<img src="/docs/linux_01.png" width="250" />
<img src="/docs/linux_02.png" width="250" />
<img src="/docs/linux_03.png" width="250" />
<img src="/docs/linux_04.png" width="250" />
<img src="/docs/linux_05.png" width="250" />
</p>
### Installation
@@ -28,9 +43,7 @@ To install Coop, follow these steps:
- **Windows**: Run the downloaded `.exe` installer and follow the on-screen instructions.
- **macOS**: Open the downloaded `.dmg` file and drag Coop to your Applications folder.
- **Ubuntu**: Run the downloaded `.deb` or `.AppImage` installer and follow the on-screen instructions.
- **Arch Linux**: For `.tar.gz` packages, extract and install manually. For PKGBUILD, use `makepkg -si` to build and install.
- **Flatpak**: Coming soon.
- **Linux**: Run the downloaded `.flatpak` or `.snap` installer and follow the on-screen instructions.
3. **Run Coop**:
- Launch Coop from your Applications folder (macOS) or by double-clicking the executable (Windows/Linux).
@@ -56,13 +69,25 @@ Coop is built using Rust and GPUI. All Nostr related stuffs handled by [Rust Nos
cd coop
```
2. Install dependencies:
2.1 Install Linux dependencies:
```bash
./script/linux
```
2.2 Install FreeBSD dependencies:
```bash
./script/freebsd
```
3. Install Rust dependencies:
```bash
cargo build
```
3. Run the app:
4. Run the app:
```bash
cargo run
```
@@ -88,14 +113,6 @@ If you'd like to contribute to Coop, please follow these steps:
For more information, see the [Contributing](#contributing) section.
#### Debugging
To debug Coop, you can use `cargo`'s built-in debugging tools or attach a debugger like `gdb` or `lldb`. For example:
```bash
cargo run -- --debug
```
#### Additional Resources
- [Rust Nostr](https://github.com/rust-nostr/nostr/)

View File

BIN
assets/brand/group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
assets/brand/system.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.25 21.25H6c-.69 0-1.25-.56-1.25-1.25M11.5 9.25h1M4.75 20V4.75a2 2 0 0 1 2-2h12.5v16H6c-.69 0-1.25.56-1.25 1.25Zm5-6.25s0-1.5 2.25-1.5 2.25 1.5 2.25 1.5h-4.5ZM13 9.25a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 404 B

View File

@@ -1,3 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10 5.75 3.75 12 10 18.25M4.5 12h15.75"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path
d="M10 5.75L3.75 12L10 18.25"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M4.5 12H20.25"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

Before

Width:  |  Height:  |  Size: 244 B

After

Width:  |  Height:  |  Size: 418 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14 5.75 20.25 12 14 18.25M19.5 12H3.75"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M14 5.75L20.25 12L14 18.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M19.5 12H3.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 320 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm6.22-1.53 3.25-3.25a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 1 1-1.06 1.06l-1.97-1.97v6.69a.75.75 0 0 1-1.5 0V9.56l-1.97 1.97a.75.75 0 0 1-1.06-1.06Z" clip-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 397 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,53.66,163.31,104H192a8,8,0,0,1,0,16H144a8,8,0,0,1-8-8V64a8,8,0,0,1,16,0V92.69l50.34-50.35a8,8,0,0,1,11.32,11.32ZM112,136H64a8,8,0,0,0,0,16H92.69L42.34,202.34a8,8,0,0,0,11.32,11.32L104,163.31V192a8,8,0,0,0,16,0V144A8,8,0,0,0,112,136Z"></path></svg>

Before

Width:  |  Height:  |  Size: 370 B

3
assets/icons/book.svg Normal file
View 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/boom.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M17.25 14C17.25 18.0041 14.0041 21.25 10 21.25C5.99594 21.25 2.75 18.0041 2.75 14C2.75 9.99594 5.99594 6.75 10 6.75C14.0041 6.75 17.25 9.99594 17.25 14Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.5 8.5L17.5 6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M16.75 1.75V3.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M20.75 7.25H22.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M20 4L21.25 2.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 800 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill="#000" fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10a9.967 9.967 0 0 1-4.098-.876.313.313 0 0 0-.195-.026l-3.471.78a1.75 1.75 0 0 1-2.084-2.12l.809-3.33a.313.313 0 0 0-.028-.204A9.965 9.965 0 0 1 2 12Zm4.5 0a1 1 0 1 0 2 0 1 1 0 0 0-2 0Zm4.5 0a1 1 0 1 0 2 0 1 1 0 0 0-2 0Zm5.5 1a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z" clip-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 488 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"></path></svg>

Before

Width:  |  Height:  |  Size: 218 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m20 9-6.586 6.586a2 2 0 0 1-2.828 0L4 9"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M5.75 9.5L12 15.75L18.25 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 209 B

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M9.5 18.25L15.75 12L9.5 5.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 249 B

After

Width:  |  Height:  |  Size: 209 B

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,165.66a8,8,0,0,1-11.32,0L128,91.31,53.66,165.66a8,8,0,0,1-11.32-11.32l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,213.66,165.66Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M5.75 14.5L12 8.25L18.25 14.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 210 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,85.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"></path></svg>

Before

Width:  |  Height:  |  Size: 298 B

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M173.66,98.34a8,8,0,0,1,0,11.32l-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35A8,8,0,0,1,173.66,98.34ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="9.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.75 12.9231L10.5625 15.75L15.25 8.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 369 B

After

Width:  |  Height:  |  Size: 341 B

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M6.75 13.0625L9.9 16.25L17.25 7.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 215 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M9.8007 10.25C8.74816 10.25 8.16683 11.4713 8.83056 12.2882L11.0301 14.9953C11.5303 15.611 12.4701 15.611 12.9704 14.9953L15.1699 12.2882C15.8336 11.4713 15.2523 10.25 14.1997 10.25H9.8007Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 302 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm7.53-3.53a.75.75 0 0 0-1.06 1.06L10.94 12l-2.47 2.47a.75.75 0 1 0 1.06 1.06L12 13.06l2.47 2.47a.75.75 0 1 0 1.06-1.06L13.06 12l2.47-2.47a.75.75 0 0 0-1.06-1.06L12 10.94 9.53 8.47Z" clip-rule="evenodd"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM9.53033 8.46967C9.23744 8.17678 8.76256 8.17678 8.46967 8.46967C8.17678 8.76256 8.17678 9.23744 8.46967 9.53033L10.9393 12L8.46967 14.4697C8.17678 14.7626 8.17678 15.2374 8.46967 15.5303C8.76256 15.8232 9.23744 15.8232 9.53033 15.5303L12 13.0607L14.4697 15.5303C14.7626 15.8232 15.2374 15.8232 15.5303 15.5303C15.8232 15.2374 15.8232 14.7626 15.5303 14.4697L13.0607 12L15.5303 9.53033C15.8232 9.23744 15.8232 8.76256 15.5303 8.46967C15.2374 8.17678 14.7626 8.17678 14.4697 8.46967L12 10.9393L9.53033 8.46967Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 774 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm7.53-3.53a.75.75 0 0 0-1.06 1.06L10.94 12l-2.47 2.47a.75.75 0 1 0 1.06 1.06L12 13.06l2.47 2.47a.75.75 0 1 0 1.06-1.06L13.06 12l2.47-2.47a.75.75 0 0 0-1.06-1.06L12 10.94 9.53 8.47Z" clip-rule="evenodd"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M15 9L9 15M15 15L9 9M21.25 12C21.25 17.1086 17.1086 21.25 12 21.25C6.89137 21.25 2.75 17.1086 2.75 12C2.75 6.89137 6.89137 2.75 12 2.75C17.1086 2.75 21.25 6.89137 21.25 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 329 B

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M6.25 6.25L17.75 17.75M17.75 6.25L6.25 17.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 201 B

3
assets/icons/copy.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M8.75 8.75V4C8.75 3.30964 9.30964 2.75 10 2.75H20C20.6904 2.75 21.25 3.30964 21.25 4V14C21.25 14.6904 20.6904 15.25 20 15.25H15.25M14 8.75H4C3.30964 8.75 2.75 9.30964 2.75 10V20C2.75 20.6904 3.30964 21.25 4 21.25H14C14.6904 21.25 15.25 20.6904 15.25 20V10C15.25 9.30964 14.6904 8.75 14 8.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 472 B

3
assets/icons/door.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M2.75 21.25L21.25 21.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M4.75 21.25V4.75C4.75 3.64543 5.64543 2.75 6.75 2.75H17.25C18.3546 2.75 19.25 3.64543 19.25 4.75V21.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.75 12.25H8.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 522 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M4 4.75A2.75 2.75 0 0 1 6.75 2h10.5A2.75 2.75 0 0 1 20 4.75v7.917a3.9 3.9 0 0 0-4.091.909l-3.75 3.75a2.25 2.25 0 0 0-.659 1.59v2.334c0 .263.045.515.128.75H6.75A2.75 2.75 0 0 1 4 19.25V4.75Z"/>
<path fill="currentColor" fill-rule="evenodd" d="M19.303 15.697a.9.9 0 0 0-1.273 0l-3.53 3.53V20.5h1.273l3.53-3.53a.9.9 0 0 0 0-1.273Zm-2.333-1.06a2.4 2.4 0 1 1 3.394 3.393l-3.75 3.75a.75.75 0 0 1-.53.22H13.75a.75.75 0 0 1-.75-.75v-2.333a.75.75 0 0 1 .22-.53l3.75-3.75Z" clip-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 622 B

View File

@@ -1,4 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm8.25 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm-16.5 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"/>
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm8.25 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm-16.5 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M3.75 10.25C2.7835 10.25 2 11.0335 2 12C2 12.9665 2.7835 13.75 3.75 13.75C4.7165 13.75 5.5 12.9665 5.5 12C5.5 11.0335 4.7165 10.25 3.75 10.25Z" fill="currentColor"/><path d="M12 10.25C11.0335 10.25 10.25 11.0335 10.25 12C10.25 12.9665 11.0335 13.75 12 13.75C12.9665 13.75 13.75 12.9665 13.75 12C13.75 11.0335 12.9665 10.25 12 10.25Z" fill="currentColor"/><path d="M20.25 10.25C19.2835 10.25 18.5 11.0335 18.5 12C18.5 12.9665 19.2835 13.75 20.25 13.75C21.2165 13.75 22 12.9665 22 12C22 11.0335 21.2165 10.25 20.25 10.25Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 457 B

After

Width:  |  Height:  |  Size: 632 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm7.75-5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 9.75 7Zm4.5 0a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5a.75.75 0 0 1 .75-.75Zm-6.143 7.864a.75.75 0 0 1 1.029-.257c1.04.624 1.97.905 2.864.905.894 0 1.824-.281 2.864-.905a.75.75 0 1 1 .772 1.286c-1.21.726-2.405 1.12-3.636 1.12-1.23 0-2.426-.394-3.636-1.12a.75.75 0 0 1-.257-1.029Z" clip-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 604 B

3
assets/icons/emoji.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M21.25 12C21.25 17.1086 17.1086 21.25 12 21.25C6.89137 21.25 2.75 17.1086 2.75 12C2.75 6.89137 6.89137 2.75 12 2.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M19 1.75V8.25M15.75 5H22.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M10.75 9.9C10.75 11.0046 10.0784 11.75 9.25 11.75C8.42157 11.75 7.75 11.0046 7.75 9.9C7.75 8.79543 8.42157 8 9.25 8C10.0784 8 10.75 8.79543 10.75 9.9Z" fill="currentColor"/><path d="M16.25 9.9C16.25 11.0046 15.5784 11.75 14.75 11.75C13.9216 11.75 13.25 11.0046 13.25 9.9C13.25 8.79543 13.9216 8 14.75 8C15.5784 8 16.25 8.79543 16.25 9.9Z" fill="currentColor"/><path d="M16.1123 14.8493C16.1942 14.7105 16.2249 14.545 16.192 14.3857C16.1592 14.2263 16.0665 14.0867 15.933 13.9968C15.7996 13.9069 15.6354 13.8733 15.4754 13.9028C15.3154 13.9321 15.1736 14.0226 15.0757 14.1507C15.0008 14.2469 14.9237 14.3367 14.8415 14.4241C14.1096 15.2083 13.061 15.628 12.0035 15.625C10.946 15.6265 9.8972 15.2055 9.16254 14.4222C9.08002 14.3348 9.00261 14.2451 8.92738 14.1491C8.8291 14.0214 8.68699 13.9313 8.52686 13.9024C8.36679 13.8735 8.20268 13.9076 8.06954 13.9979C7.9364 14.0882 7.84406 14.2281 7.81174 14.3875C7.77938 14.547 7.81054 14.7123 7.89293 14.8509C7.97731 14.99 8.06686 15.1223 8.16553 15.2526C9.04297 16.4311 10.5292 17.1343 12.0024 17.125C13.4754 17.1367 14.965 16.4342 15.8405 15.2521C15.939 15.1215 16.0282 14.9888 16.1123 14.8493Z" fill="currentColor"/><path d="M21.25 12C21.25 17.1086 17.1086 21.25 12 21.25C6.89137 21.25 2.75 17.1086 2.75 12C2.75 6.89137 6.89137 2.75 12 2.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

3
assets/icons/eye.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M3.75 13.0199C8.54029 18.1132 15.4597 18.1132 20.25 13.0199" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.75 7.62257C6.14516 5.07587 9.0726 3.80251 12 3.80249C14.9274 3.80247 17.8549 5.07576 20.25 7.62238" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 17V20.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.25 16.5L6.75 18.9821" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.5 16.5L17.25 18.9821" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 800 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M12.7507 3.75C12.7507 3.33579 12.4149 3 12.0007 3C11.5865 3 11.2507 3.33579 11.2507 3.75V6.25C11.2507 6.66421 11.5865 7 12.0007 7C12.4149 7 12.7507 6.66421 12.7507 6.25V3.75Z" fill="currentColor"/><path d="M7.26594 5.19799C6.99969 4.88068 6.52662 4.8393 6.20932 5.10555C5.89201 5.3718 5.85062 5.84487 6.11687 6.16217L7.72384 8.07728C7.99009 8.39459 8.46316 8.43598 8.78047 8.16973C9.09777 7.90347 9.13916 7.43041 8.87291 7.1131L7.26594 5.19799Z" fill="currentColor"/><path d="M17.8726 6.16227C18.1389 5.84496 18.0975 5.37189 17.7802 5.10564C17.4629 4.83939 16.9898 4.88078 16.7235 5.19809L15.1166 7.1132C14.8503 7.4305 14.8917 7.90357 15.209 8.16982C15.5263 8.43607 15.9994 8.39468 16.2656 8.07738L17.8726 6.16227Z" fill="currentColor"/><path d="M5.22073 9C4.33013 9 3.52687 9.5355 3.18434 10.3576L2.78846 11.3077C2.61378 11.7269 2.20416 12 1.75 12C1.33579 12 1 12.3358 1 12.75V19.25C1 19.6642 1.33579 20 1.75 20H7.38937C9.39779 20 11.0763 18.4715 11.2637 16.4719L11.4255 14.746C11.6391 12.468 9.84697 10.5 7.559 10.5C7.46053 10.5 7.36858 10.4508 7.31396 10.3689L7.0563 9.98237C6.64715 9.36864 5.95834 9 5.22073 9Z" fill="currentColor"/><path d="M18.722 9C17.9844 9 17.2956 9.36864 16.8865 9.98237L16.6288 10.3689C16.5742 10.4508 16.4822 10.5 16.3838 10.5C14.0958 10.5 12.3037 12.468 12.5172 14.746L12.679 16.4719C12.8665 18.4715 14.545 20 16.5534 20H22.1928C22.607 20 22.9428 19.6642 22.9428 19.25V12.75C22.9428 12.3358 22.607 12 22.1928 12C21.7386 12 21.329 11.7269 21.1543 11.3077L20.7584 10.3576C20.4159 9.5355 19.6126 9 18.722 9Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M1.75 19.25H7.38937C9.0107 19.25 10.3657 18.0161 10.517 16.4019L10.6788 14.676C10.8511 12.8379 9.40511 11.25 7.559 11.25C7.20977 11.25 6.88364 11.0755 6.68992 10.7849L6.43226 10.3984C6.16221 9.99331 5.70757 9.75 5.22073 9.75C4.6329 9.75 4.10273 10.1034 3.87664 10.6461L3.48077 11.5962C3.18964 12.2949 2.50694 12.75 1.75 12.75M22.1928 19.25H16.5534C14.9321 19.25 13.5771 18.0161 13.4258 16.4019L13.264 14.676C13.0916 12.8379 14.5377 11.25 16.3838 11.25C16.733 11.25 17.0591 11.0755 17.2528 10.7849L17.5105 10.3984C17.7806 9.99331 18.2352 9.75 18.722 9.75C19.3099 9.75 19.84 10.1034 20.0661 10.6461L20.462 11.5962C20.7531 12.2949 21.4358 12.75 22.1928 12.75M12.0007 3.75V6.25M6.69141 5.68008L8.29838 7.59519M17.2981 5.68018L15.6911 7.59529" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 918 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.75 5.75v11.5a2 2 0 0 0 2 2h14.5a2 2 0 0 0 2-2v-8.5a2 2 0 0 0-2-2h-6.18a2 2 0 0 1-1.664-.89l-.812-1.22a2 2 0 0 0-1.664-.89H4.75a2 2 0 0 0-2 2Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 350 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.00001 13.7453L3.00004 5.75C3.00004 4.23122 4.23126 3.00001 5.75004 3L18.25 3C19.7688 3 21 4.23122 21 5.75V13.7466C21 13.7477 21 13.7489 21 13.75L21 18.25C21 19.7688 19.7688 21 18.25 21H5.75C4.32614 21 3.15502 19.9179 3.0142 18.5312C3.00481 18.4387 3 18.3449 3 18.25M5.75004 4.5L18.25 4.5C18.9403 4.5 19.5 5.05964 19.5 5.75V13L15.9298 13C15.5695 13 15.2601 13.2562 15.1929 13.6102C14.9078 15.1135 13.5858 16.25 12 16.25C10.4142 16.25 9.09221 15.1135 8.80706 13.6102C8.73991 13.2562 8.43051 13 8.0702 13H4.50002L4.50004 5.75C4.50004 5.05965 5.05968 4.50001 5.75004 4.5Z" fill="currentColor"/><path d="M3 18.25V13.75C3 13.7484 3 13.7469 3.00001 13.7453" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 805 B

3
assets/icons/inbox.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M3.75 12.75H8.0702C8.42126 14.6006 10.0472 16 12 16C13.9528 16 15.5787 14.6006 15.9298 12.75L20.25 12.75M18.25 20.25H5.75001C4.64543 20.25 3.75 19.3546 3.75001 18.25L3.75005 5.75C3.75005 4.64543 4.64548 3.75001 5.75005 3.75L18.25 3.75C19.3546 3.75 20.25 4.64543 20.25 5.75V18.25C20.25 19.3546 19.3546 20.25 18.25 20.25Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 501 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill="#000" fill-rule="evenodd" d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm-2 9a.75.75 0 0 1 .75-.75H12a.75.75 0 0 1 .75.75v5.25a.75.75 0 0 1-1.5 0v-4.5h-.5A.75.75 0 0 1 10 11Zm2-3.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" clip-rule="evenodd"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M10.75 11H12L12 16.25M21.25 12C21.25 17.1086 17.1086 21.25 12 21.25C6.89137 21.25 2.75 17.1086 2.75 12C2.75 6.89137 6.89137 2.75 12 2.75C17.1086 2.75 21.25 6.89137 21.25 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 7.375C11.6548 7.375 11.375 7.65482 11.375 8C11.375 8.34518 11.6548 8.625 12 8.625C12.3452 8.625 12.625 8.34518 12.625 8C12.625 7.65482 12.3452 7.375 12 7.375Z" fill="currentColor" stroke="currentColor" stroke-width="0.25"/>
</svg>

Before

Width:  |  Height:  |  Size: 396 B

After

Width:  |  Height:  |  Size: 590 B

3
assets/icons/invite.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M4.75 10.9853V4.75C4.75 3.64543 5.64543 2.75 6.75 2.75H17.25C18.3546 2.75 19.25 3.64543 19.25 4.75V10.9853M9.75 7.75H14.25M12.617 13.5499L19.9415 11.1744C20.5875 10.9649 21.25 11.4465 21.25 12.1256V18.25C21.25 19.3546 20.3546 20.25 19.25 20.25H4.75C3.64543 20.25 2.75 19.3546 2.75 18.25V12.1256C2.75 11.4465 3.41249 10.9649 4.0585 11.1744L11.383 13.5499C11.784 13.68 12.216 13.68 12.617 13.5499Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 576 B

3
assets/icons/link.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M9.75027 5.52371L10.7168 4.55722C13.1264 2.14759 17.0332 2.14759 19.4428 4.55722C21.8524 6.96684 21.8524 10.8736 19.4428 13.2832L18.4742 14.2519M5.52886 9.74513L4.55722 10.7168C2.14759 13.1264 2.1476 17.0332 4.55722 19.4428C6.96684 21.8524 10.8736 21.8524 13.2832 19.4428L14.2478 18.4782M9.5 14.5L14.5 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 462 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M104,152a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H96A8,8,0,0,1,104,152ZM168,32h24a8,8,0,0,0,0-16H160a8,8,0,0,0-8,8V56h16Zm72,84v60a16,16,0,0,1-16,16H136v32a8,8,0,0,1-16,0V192H32a16,16,0,0,1-16-16V116A60.07,60.07,0,0,1,76,56h76v88a8,8,0,0,0,16,0V56h12A60.07,60.07,0,0,1,240,116Zm-120,0a44,44,0,0,0-88,0v60h88Z"></path></svg>

Before

Width:  |  Height:  |  Size: 429 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.75 3.75v6.5m0 0h6.5m-6.5 0 6.5-6.5m-10 16.5v-6.5m0 0h-6.5m6.5 0-6.5 6.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 281 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21.248 11.811a6.5 6.5 0 0 1-9.06-9.06 9.25 9.25 0 1 0 9.06 9.06Z"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M21.2481 11.8112C20.1889 12.56 18.8958 13 17.5 13C13.9101 13 11 10.0899 11 6.5C11 5.10416 11.44 3.81108 12.1888 2.75189C12.126 2.75063 12.0631 2.75 12 2.75C6.89137 2.75 2.75 6.89137 2.75 12C2.75 17.1086 6.89137 21.25 12 21.25C17.1086 21.25 21.25 17.1086 21.25 12C21.25 11.9369 21.2494 11.874 21.2481 11.8112Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 489 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" fill-rule="evenodd" d="M12 2a7.795 7.795 0 0 0-7.696 6.554l-1.17 7.258A2.75 2.75 0 0 0 5.848 19h1.66c.849 1.75 2.512 3 4.492 3s3.643-1.25 4.492-3h1.66a2.75 2.75 0 0 0 2.714-3.188l-1.17-7.258A7.795 7.795 0 0 0 12 2Zm2.754 17H9.245c.678.937 1.68 1.5 2.754 1.5s2.076-.563 2.754-1.5Z" clip-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 434 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill="#000" fill-rule="evenodd" d="M9 4.5v15h9.25c.69 0 1.25-.56 1.25-1.25V5.75c0-.69-.56-1.25-1.25-1.25H9ZM3 5.75A2.75 2.75 0 0 1 5.75 3h12.5A2.75 2.75 0 0 1 21 5.75v12.5A2.75 2.75 0 0 1 18.25 21H5.75A2.75 2.75 0 0 1 3 18.25V5.75Z" clip-rule="evenodd"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 5.5V18.5H19.25C19.9404 18.5 20.5 17.9404 20.5 17.25V6.75C20.5 6.05964 19.9404 5.5 19.25 5.5H9ZM2 6.75C2 5.23122 3.23122 4 4.75 4H19.25C20.7688 4 22 5.23122 22 6.75V17.25C22 18.7688 20.7688 20 19.25 20H4.75C3.23122 20 2 18.7688 2 17.25V6.75Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 374 B

After

Width:  |  Height:  |  Size: 396 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.75 5.75a2 2 0 0 1 2-2h12.5a2 2 0 0 1 2 2v12.5a2 2 0 0 1-2 2H5.75a2 2 0 0 1-2-2V5.75Zm5-1.75v16"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M2.75 6.75C2.75 5.64543 3.64543 4.75 4.75 4.75H19.25C20.3546 4.75 21.25 5.64543 21.25 6.75V17.25C21.25 18.3546 20.3546 19.25 19.25 19.25H4.75C3.64543 19.25 2.75 18.3546 2.75 17.25V6.75Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M8.25 5V12V19" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 433 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill="#000" fill-rule="evenodd" d="M15 4.5v15H5.75c-.69 0-1.25-.56-1.25-1.25V5.75c0-.69.56-1.25 1.25-1.25H15Zm6 1.25A2.75 2.75 0 0 0 18.25 3H5.75A2.75 2.75 0 0 0 3 5.75v12.5A2.75 2.75 0 0 0 5.75 21h12.5A2.75 2.75 0 0 0 21 18.25V5.75Z" clip-rule="evenodd"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 5.5V18.5H4.75C4.05964 18.5 3.5 17.9404 3.5 17.25V6.75C3.5 6.05964 4.05964 5.5 4.75 5.5H15ZM22 6.75C22 5.23122 20.7688 4 19.25 4H4.75C3.23122 4 2 5.23122 2 6.75V17.25C2 18.7688 3.23122 20 4.75 20H19.25C20.7688 20 22 18.7688 22 17.25V6.75Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 376 B

After

Width:  |  Height:  |  Size: 394 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.25 4v16M3.75 5.75a2 2 0 0 1 2-2h12.5a2 2 0 0 1 2 2v12.5a2 2 0 0 1-2 2H5.75a2 2 0 0 1-2-2V5.75Z"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M15.75 5V19" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M2.75 6.75C2.75 5.64543 3.64543 4.75 4.75 4.75H19.25C20.3546 4.75 21.25 5.64543 21.25 6.75V17.25C21.25 18.3546 20.3546 19.25 19.25 19.25H4.75C3.64543 19.25 2.75 18.3546 2.75 17.25V6.75Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 304 B

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M2.66936 5.12886C2.12122 3.64104 3.6759 2.24953 5.09409 2.95862L20.0468 10.435C21.3366 11.0799 21.3366 12.9205 20.0468 13.5655L5.09409 21.0418C3.67589 21.7509 2.12122 20.3594 2.66936 18.8715L4.92467 12.75H9.25021C9.66442 12.75 10.0002 12.4142 10.0002 12C10.0002 11.5858 9.66442 11.25 9.25021 11.25H4.92452L2.66936 5.12886Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 435 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.13,104.13,0,0,0,128,24Zm40,112H136v32a8,8,0,0,1-16,0V136H88a8,8,0,0,1,0-16h32V88a8,8,0,0,1,16,0v32h32a8,8,0,0,1,0,16Z"></path></svg>

Before

Width:  |  Height:  |  Size: 281 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M16.2426 12.0005H7.75736M12 16.2431V7.75781M21.25 12C21.25 17.1086 17.1086 21.25 12 21.25C6.89137 21.25 2.75 17.1086 2.75 12C2.75 6.89137 6.89137 2.75 12 2.75C17.1086 2.75 21.25 6.89137 21.25 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 352 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 3.75V12m0 0v8.25M12 12H3.75M12 12h8.25"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M12 6.75V12M12 12V17.25M12 12H6.75M12 12H17.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 248 B

After

Width:  |  Height:  |  Size: 203 B

3
assets/icons/profile.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M17.75 2.75H6.25C5.14543 2.75 4.25 3.64543 4.25 4.75V19.25C4.25 20.3546 5.14543 21.25 6.25 21.25H17.75C18.8546 21.25 19.75 20.3546 19.75 19.25V4.75C19.75 3.64543 18.8546 2.75 17.75 2.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12.25" r="2.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M16 21C16 18.7909 14.2091 17 12 17C9.79086 17 8 18.7909 8 21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M9.75 6.25H14.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 696 B

3
assets/icons/refresh.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M13 21C13.5523 21 14 20.5523 14 20C14 19.4477 13.5523 19 13 19C12.4477 19 12 19.4477 12 20C12 20.5523 12.4477 21 13 21Z" fill="currentColor"/><path d="M21 11C21 10.4477 20.5523 9.99999 20 9.99999C19.4477 9.99999 19 10.4477 19 11C19 11.5523 19.4477 12 20 12C20.5523 12 21 11.5523 21 11Z" fill="currentColor"/><path d="M19.9295 14.2679C20.4078 14.5441 20.5716 15.1557 20.2955 15.634C20.0193 16.1123 19.4078 16.2761 18.9295 16C18.4512 15.7238 18.2873 15.1123 18.5634 14.634C18.8396 14.1557 19.4512 13.9918 19.9295 14.2679Z" fill="currentColor"/><path d="M17.3676 19.2942C17.8459 19.0181 18.0098 18.4065 17.7336 17.9282C17.4575 17.4499 16.8459 17.286 16.3676 17.5621C15.8893 17.8383 15.7254 18.4499 16.0016 18.9282C16.2777 19.4065 16.8893 19.5703 17.3676 19.2942Z" fill="currentColor"/><path d="M18.9269 7.99998C18.4487 8.27612 17.8371 8.11225 17.5609 7.63396C17.2848 7.15566 17.4487 6.54407 17.9269 6.26793C18.4052 5.99179 19.0168 6.15566 19.293 6.63396C19.5691 7.11225 19.4052 7.72384 18.9269 7.99998Z" fill="currentColor"/><path d="M9.25 14.75V20.25H3.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.2493 4.41452C14.2521 3.98683 13.1537 3.75 12 3.75C7.44365 3.75 3.75 7.44365 3.75 12C3.75 15.498 5.92698 18.4875 9 19.6876" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

3
assets/icons/relay.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="9.25" r="1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.75 21.25L11.75 9.25H12.25L16.25 21.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9.5 17.75H14.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.75693 12.7501C6.08102 10.7234 6.08103 7.77679 7.75693 5.75M16.2431 5.75C17.919 7.77679 17.919 10.7234 16.2431 12.7501" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.06494 2.7574C1.64285 6.40823 1.64502 12.1018 5.07145 15.75M18.9281 2.75C22.3572 6.40053 22.3573 12.0993 18.9285 15.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 899 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m7.5 3.25 4.5 3.5 4.5-3.5m-11.75 17h14.5a2 2 0 0 0 2-2v-9.5a2 2 0 0 0-2-2H4.75a2 2 0 0 0-2 2v9.5a2 2 0 0 0 2 2Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 317 B

3
assets/icons/reply.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M1.84521 11.4494L9.99071 3.91478C10.471 3.47055 11.25 3.81116 11.25 4.46535V7.99994C11.25 8.27608 11.478 8.49949 11.7541 8.50388C19.8394 8.63247 22 11.9205 22 20.2499C20.5303 17.3105 19.7806 15.5711 11.7551 15.5021C11.4789 15.4997 11.25 15.7238 11.25 15.9999V19.5345C11.25 20.1887 10.471 20.5293 9.99071 20.0851L1.84521 12.5505C1.52425 12.2536 1.52425 11.7463 1.84521 11.4494Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 534 B

3
assets/icons/reset.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M13 17.25C16.4518 17.25 19.25 14.4518 19.25 11V10.5C19.25 6.77208 16.2279 3.75 12.5 3.75C8.77208 3.75 5.75 6.77208 5.75 10.5V20" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 16.5L5.75 20.25L9.5 16.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 435 B

View File

@@ -1,9 +0,0 @@
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="list-filter" transform="translate(16.142767, 16.107233) rotate(-45.000000) translate(-16.142767, -16.107233) translate(3.642767, 10.491117)" stroke="#000000" stroke-width="2">
<line x1="0.454058454" y1="0.48959236" x2="24.1421356" y2="0.843145751" stroke-linecap="square"></line>
<line x1="4.69669914" y1="6.14644661" x2="20.1188954" y2="5.79289322"></line>
<line x1="9.06066017" y1="10.732233" x2="15.3033009" y2="10.3890873"></line>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 730 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m20.25 20.25-4.123-4.123m0 0A7.25 7.25 0 1 0 5.873 5.873a7.25 7.25 0 0 0 10.253 10.253Z"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M20 20L16.1265 16.1265M16.1265 16.1265C17.4385 14.8145 18.25 13.002 18.25 11C18.25 6.99594 15.0041 3.75 11 3.75C6.99594 3.75 3.75 6.99594 3.75 11C3.75 15.0041 6.99594 18.25 11 18.25C13.002 18.25 14.8145 17.4385 16.1265 16.1265Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 293 B

After

Width:  |  Height:  |  Size: 408 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M7.878 5.21415L7.17474 5.05186C6.58003 4.91462 5.95657 5.09343 5.525 5.525C5.09343 5.95657 4.91462 6.58003 5.05186 7.17474L5.21415 7.878C5.40122 8.6886 5.06696 9.53036 4.37477 9.99182L3.51965 10.5619C3.03881 10.8825 2.75 11.4221 2.75 12C2.75 12.5779 3.03881 13.1175 3.51965 13.4381L4.37477 14.0082C5.06696 14.4696 5.40122 15.3114 5.21415 16.122L5.05186 16.8253C4.91462 17.42 5.09343 18.0434 5.525 18.475C5.95657 18.9066 6.58003 19.0854 7.17474 18.9481L7.878 18.7858C8.6886 18.5988 9.53036 18.933 9.99182 19.6252L10.5619 20.4804C10.8825 20.9612 11.4221 21.25 12 21.25C12.5779 21.25 13.1175 20.9612 13.4381 20.4804L14.0082 19.6252C14.4696 18.933 15.3114 18.5988 16.122 18.7858L16.8253 18.9481C17.42 19.0854 18.0434 18.9066 18.475 18.475C18.9066 18.0434 19.0854 17.42 18.9481 16.8253L18.7858 16.122C18.5988 15.3114 18.933 14.4696 19.6252 14.0082L20.4804 13.4381C20.9612 13.1175 21.25 12.5779 21.25 12C21.25 11.4221 20.9612 10.8825 20.4804 10.5619L19.6252 9.99182C18.933 9.53036 18.5988 8.6886 18.7858 7.878L18.9481 7.17473C19.0854 6.58003 18.9066 5.95657 18.475 5.525C18.0434 5.09343 17.42 4.91462 16.8253 5.05186L16.122 5.21415C15.3114 5.40122 14.4696 5.06696 14.0082 4.37477L13.4381 3.51965C13.1175 3.03881 12.5779 2.75 12 2.75C11.4221 2.75 10.8825 3.03881 10.5619 3.51965L9.99182 4.37477C9.53036 5.06696 8.6886 5.40122 7.878 5.21415Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M14.75 12C14.75 13.5188 13.5188 14.75 12 14.75C10.4812 14.75 9.25 13.5188 9.25 12C9.25 10.4812 10.4812 9.25 12 9.25C13.5188 9.25 14.75 10.4812 14.75 12Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View 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

3
assets/icons/shield.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M20.25 6.94155C20.25 6.08069 19.6991 5.31641 18.8825 5.04418L12.6325 2.96085C12.2219 2.824 11.7781 2.824 11.3675 2.96085L5.11754 5.04418C4.30086 5.31641 3.75 6.08069 3.75 6.94155V11.9124C3.75 16.8848 8 19.25 12 21.4079C16 19.25 20.25 16.8848 20.25 11.9124V6.94155Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 446 B

3
assets/icons/ship.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M2.75 20.25L6.26353 19.4903M6.26353 19.4903L6.95233 19.3414C7.23089 19.2812 7.51911 19.2812 7.79767 19.3414L11.5773 20.1586C11.8559 20.2188 12.1441 20.2188 12.4227 20.1586L16.2023 19.3414C16.4809 19.2812 16.7691 19.2812 17.0477 19.3414L17.7365 19.4903M6.26353 19.4903C5.08645 17.9188 4.46034 16.5675 4.08992 15.0117C3.8539 14.0205 4.52677 13.0678 5.51689 12.827L11.5273 11.365C11.8379 11.2894 12.1621 11.2894 12.4727 11.365L18.4831 12.827C19.4732 13.0678 20.1461 14.0205 19.9101 15.0117C19.5397 16.5675 18.9136 17.9188 17.7365 19.4903M17.7365 19.4903L21.25 20.25M5.75 12.75V7.75C5.75 7.19772 6.19772 6.75 6.75 6.75H17.25C17.8023 6.75 18.25 7.19772 18.25 7.75V12.75M9.75 6.75V3.75C9.75 3.19772 10.1977 2.75 10.75 2.75H13.25C13.8023 2.75 14.25 3.19772 14.25 3.75V6.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 946 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.998 3.29V1.769M5.84 18.158l-1.077 1.078m7.235 2.997v-1.524m7.235-15.944-1.077 1.077M20.707 12h1.523m-4.074 6.159 1.077 1.077M1.766 12h1.523m1.474-7.235L5.84 5.842m9.87 2.446a5.25 5.25 0 1 1-7.424 7.424 5.25 5.25 0 0 1 7.424-7.424Z"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M11.9982 3.29083V1.76758M5.83985 18.1586L4.76275 19.2357M11.9982 22.2327V20.7094M19.2334 4.76468L18.1562 5.84179M20.707 12.0001H22.2303M18.1562 18.1586L19.2334 19.2357M1.76562 12.0001H3.28888M4.76267 4.76462L5.83977 5.84173M15.7104 8.28781C17.7606 10.3381 17.7606 13.6622 15.7104 15.7124C13.6601 17.7627 10.336 17.7627 8.28574 15.7124C6.23548 13.6622 6.23548 10.3381 8.28574 8.28781C10.336 6.23756 13.6601 6.23756 15.7104 8.28781Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 440 B

After

Width:  |  Height:  |  Size: 611 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" fill-rule="evenodd" d="M2 4.75A2.75 2.75 0 0 1 4.75 2h8.5A2.75 2.75 0 0 1 16 4.75V8h3.25A2.75 2.75 0 0 1 22 10.75v8.5A2.75 2.75 0 0 1 19.25 22h-8.5A2.75 2.75 0 0 1 8 19.25V16H4.75A2.75 2.75 0 0 1 2 13.25v-8.5ZM14.5 8V4.75c0-.69-.56-1.25-1.25-1.25h-8.5c-.69 0-1.25.56-1.25 1.25v5.991l.983-.644a2.75 2.75 0 0 1 3.033.012l.5.334A2.75 2.75 0 0 1 10.75 8h3.75ZM5 6.25a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0Zm8.39 6.292a.75.75 0 0 1 .766.027l2.8 1.8a.75.75 0 0 1 0 1.262l-2.8 1.8A.75.75 0 0 1 13 16.8v-3.6a.75.75 0 0 1 .39-.658Z" clip-rule="evenodd"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M12 19.25V13L14.5 15.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 13L9.5 15.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M7.375 19.25H4.75C3.64543 19.25 2.75 18.3546 2.75 17.25V5.75C2.75 4.64543 3.64543 3.75 4.75 3.75H8.92963C9.59834 3.75 10.2228 4.0842 10.5937 4.6406L11.7031 6.3047C11.8886 6.5829 12.2008 6.75 12.5352 6.75H19.25C20.3546 6.75 21.25 7.64543 21.25 8.75V17.25C21.25 18.3546 20.3546 19.25 19.25 19.25H16.625" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 682 B

After

Width:  |  Height:  |  Size: 718 B

3
assets/icons/usb.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M10 5.75V7.25M14 5.75V7.25M3.75 10.25H20.25V19.25C20.25 20.3546 19.3546 21.25 18.25 21.25H5.75C4.64543 21.25 3.75 20.3546 3.75 19.25V10.25ZM5.75 2.75H18.25V10.25H5.75V2.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 353 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M12.9996 12.8145C12.675 12.7719 12.3415 12.75 11.9996 12.75C8.55174 12.75 5.94978 14.981 4.9305 18.114C4.56744 19.23 5.50919 20.25 6.68275 20.25H13.9996M15.7496 6.5C15.7496 8.57107 14.0706 10.25 11.9996 10.25C9.92851 10.25 8.24958 8.57107 8.24958 6.5C8.24958 4.42893 9.92851 2.75 11.9996 2.75C14.0706 2.75 15.7496 4.42893 15.7496 6.5ZM15.7496 14C15.7496 12.7574 16.7569 11.75 17.9996 11.75C19.2422 11.75 20.2496 12.7574 20.2496 14C20.2496 14.7801 19.8526 15.4675 19.2496 15.8711V17L18.7496 17.9356L19.2496 18.9678V20L17.9996 21L16.7496 20V15.8711C16.1466 15.4675 15.7496 14.7801 15.7496 14Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 771 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M64.12,147.8a4,4,0,0,1-4,4.2H16a8,8,0,0,1-7.8-6.17,8.35,8.35,0,0,1,1.62-6.93A67.79,67.79,0,0,1,37,117.51a40,40,0,1,1,66.46-35.8,3.94,3.94,0,0,1-2.27,4.18A64.08,64.08,0,0,0,64,144C64,145.28,64,146.54,64.12,147.8Zm182-8.91A67.76,67.76,0,0,0,219,117.51a40,40,0,1,0-66.46-35.8,3.94,3.94,0,0,0,2.27,4.18A64.08,64.08,0,0,1,192,144c0,1.28,0,2.54-.12,3.8a4,4,0,0,0,4,4.2H240a8,8,0,0,0,7.8-6.17A8.33,8.33,0,0,0,246.17,138.89Zm-89,43.18a48,48,0,1,0-58.37,0A72.13,72.13,0,0,0,65.07,212,8,8,0,0,0,72,224H184a8,8,0,0,0,6.93-12A72.15,72.15,0,0,0,157.19,182.07Z"></path></svg>

Before

Width:  |  Height:  |  Size: 676 B

3
assets/icons/warning.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="9.25" stroke="currentColor" stroke-width="1.5"/><path d="M11.3121 12.3511L11.0582 7.9983C11.0266 7.45662 11.4574 7 12 7C12.5426 7 12.9734 7.45662 12.9418 7.9983L12.6879 12.3511C12.6666 12.7154 12.365 13 12 13C11.635 13 11.3334 12.7154 11.3121 12.3511Z" fill="currentColor"/><circle cx="11.9999" cy="15.8998" r="1.1" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 445 B

58
assets/icons/zoom.svg Normal file
View File

@@ -0,0 +1,58 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path
d="M4.75 9.25V4.75H9.25"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M19.25 9.25V4.75H14.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M19.25 14.75V19.25H14.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M4.75 14.75V19.25H9.25"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5 5L9.5 9.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M19 5L14.5 9.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M19 19L14.5 14.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5 19L9.5 14.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,140 @@
{
"id": "catppuccin-frappe",
"name": "Catppuccin Frappé",
"author": "Catppuccin",
"url": "https://github.com/catppuccin/catppuccin",
"light": {
"background": "#303446",
"surface_background": "#292c3c",
"elevated_surface_background": "#232634",
"panel_background": "#303446",
"overlay": "#c6d0f51a",
"title_bar": "#292c3c",
"title_bar_inactive": "#232634",
"window_border": "#737994",
"border": "#626880",
"border_variant": "#51576d",
"border_focused": "#8caaee",
"border_selected": "#8caaee",
"border_transparent": "#00000000",
"border_disabled": "#414559",
"ring": "#8caaee",
"text": "#c6d0f5",
"text_muted": "#b5bfe2",
"text_placeholder": "#a5adce",
"text_accent": "#8caaee",
"icon": "#b5bfe2",
"icon_muted": "#a5adce",
"icon_accent": "#8caaee",
"element_foreground": "#232634",
"element_background": "#8caaee",
"element_hover": "#babbf1",
"element_active": "#7e99d6",
"element_selected": "#7088bf",
"element_disabled": "#8caaee4d",
"secondary_foreground": "#7088bf",
"secondary_background": "#292c3c",
"secondary_hover": "#8caaee33",
"secondary_active": "#232634",
"secondary_selected": "#232634",
"secondary_disabled": "#8caaee4d",
"danger_foreground": "#232634",
"danger_background": "#e78284",
"danger_hover": "#ea999c",
"danger_active": "#d07576",
"danger_selected": "#b96869",
"danger_disabled": "#e782844d",
"warning_foreground": "#232634",
"warning_background": "#e5c890",
"warning_hover": "#ef9f76",
"warning_active": "#ceb482",
"warning_selected": "#b7a074",
"warning_disabled": "#e5c8904d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#414559",
"ghost_element_hover": "#c6d0f533",
"ghost_element_active": "#51576d",
"ghost_element_selected": "#51576d",
"ghost_element_disabled": "#c6d0f50d",
"tab_inactive_background": "#292c3c",
"tab_inactive_foreground": "#b5bfe2",
"tab_active_background": "#303446",
"tab_active_foreground": "#c6d0f5",
"tab_hover_foreground": "#babbf1",
"scrollbar_thumb_background": "#c6d0f533",
"scrollbar_thumb_hover_background": "#c6d0f580",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#51576d",
"drop_target_background": "#8caaee1a",
"cursor": "#f2d5cf",
"selection": "#949cbb40"
},
"dark": {
"background": "#303446",
"surface_background": "#292c3c",
"elevated_surface_background": "#232634",
"panel_background": "#303446",
"overlay": "#c6d0f51a",
"title_bar": "#292c3c",
"title_bar_inactive": "#232634",
"window_border": "#737994",
"border": "#626880",
"border_variant": "#51576d",
"border_focused": "#8caaee",
"border_selected": "#8caaee",
"border_transparent": "#00000000",
"border_disabled": "#414559",
"ring": "#8caaee",
"text": "#c6d0f5",
"text_muted": "#b5bfe2",
"text_placeholder": "#a5adce",
"text_accent": "#8caaee",
"icon": "#b5bfe2",
"icon_muted": "#a5adce",
"icon_accent": "#8caaee",
"element_foreground": "#232634",
"element_background": "#8caaee",
"element_hover": "#babbf1",
"element_active": "#7e99d6",
"element_selected": "#7088bf",
"element_disabled": "#8caaee4d",
"secondary_foreground": "#7088bf",
"secondary_background": "#292c3c",
"secondary_hover": "#8caaee33",
"secondary_active": "#232634",
"secondary_selected": "#232634",
"secondary_disabled": "#8caaee4d",
"danger_foreground": "#232634",
"danger_background": "#e78284",
"danger_hover": "#ea999c",
"danger_active": "#d07576",
"danger_selected": "#b96869",
"danger_disabled": "#e782844d",
"warning_foreground": "#232634",
"warning_background": "#e5c890",
"warning_hover": "#ef9f76",
"warning_active": "#ceb482",
"warning_selected": "#b7a074",
"warning_disabled": "#e5c8904d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#414559",
"ghost_element_hover": "#c6d0f533",
"ghost_element_active": "#51576d",
"ghost_element_selected": "#51576d",
"ghost_element_disabled": "#c6d0f50d",
"tab_inactive_background": "#292c3c",
"tab_inactive_foreground": "#b5bfe2",
"tab_active_background": "#303446",
"tab_active_foreground": "#c6d0f5",
"tab_hover_foreground": "#babbf1",
"scrollbar_thumb_background": "#c6d0f533",
"scrollbar_thumb_hover_background": "#c6d0f580",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#51576d",
"drop_target_background": "#8caaee1a",
"cursor": "#f2d5cf",
"selection": "#949cbb40"
}
}

View File

@@ -0,0 +1,140 @@
{
"id": "catppuccin-latte",
"name": "Catppuccin Latte",
"author": "Catppuccin",
"url": "https://github.com/catppuccin/catppuccin",
"light": {
"background": "#eff1f5",
"surface_background": "#e6e9ef",
"elevated_surface_background": "#dce0e8",
"panel_background": "#eff1f5",
"overlay": "#4c4f691a",
"title_bar": "#e6e9ef",
"title_bar_inactive": "#dce0e8",
"window_border": "#9ca0b0",
"border": "#acb0be",
"border_variant": "#bcc0cc",
"border_focused": "#1e66f5",
"border_selected": "#1e66f5",
"border_transparent": "#00000000",
"border_disabled": "#ccd0da",
"ring": "#1e66f5",
"text": "#4c4f69",
"text_muted": "#5c5f77",
"text_placeholder": "#6c6f85",
"text_accent": "#1e66f5",
"icon": "#5c5f77",
"icon_muted": "#6c6f85",
"icon_accent": "#1e66f5",
"element_foreground": "#eff1f5",
"element_background": "#1e66f5",
"element_hover": "#8839ef",
"element_active": "#1c5ce0",
"element_selected": "#1a52cc",
"element_disabled": "#1e66f54d",
"secondary_foreground": "#1a52cc",
"secondary_background": "#e6e9ef",
"secondary_hover": "#8839ef33",
"secondary_active": "#dce0e8",
"secondary_selected": "#dce0e8",
"secondary_disabled": "#1e66f54d",
"danger_foreground": "#eff1f5",
"danger_background": "#d20f39",
"danger_hover": "#e64553",
"danger_active": "#bd0d33",
"danger_selected": "#a80b2d",
"danger_disabled": "#d20f394d",
"warning_foreground": "#4c4f69",
"warning_background": "#df8e1d",
"warning_hover": "#fe640b",
"warning_active": "#c9801a",
"warning_selected": "#b47217",
"warning_disabled": "#df8e1d4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#ccd0da",
"ghost_element_hover": "#4c4f6933",
"ghost_element_active": "#bcc0cc",
"ghost_element_selected": "#bcc0cc",
"ghost_element_disabled": "#4c4f690d",
"tab_inactive_background": "#e6e9ef",
"tab_inactive_foreground": "#5c5f77",
"tab_active_background": "#eff1f5",
"tab_active_foreground": "#4c4f69",
"tab_hover_foreground": "#8839ef",
"scrollbar_thumb_background": "#4c4f6933",
"scrollbar_thumb_hover_background": "#4c4f6980",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#bcc0cc",
"drop_target_background": "#1e66f51a",
"cursor": "#dc8a78",
"selection": "#7c7f9340"
},
"dark": {
"background": "#eff1f5",
"surface_background": "#e6e9ef",
"elevated_surface_background": "#dce0e8",
"panel_background": "#eff1f5",
"overlay": "#4c4f691a",
"title_bar": "#e6e9ef",
"title_bar_inactive": "#dce0e8",
"window_border": "#9ca0b0",
"border": "#acb0be",
"border_variant": "#bcc0cc",
"border_focused": "#1e66f5",
"border_selected": "#1e66f5",
"border_transparent": "#00000000",
"border_disabled": "#ccd0da",
"ring": "#1e66f5",
"text": "#4c4f69",
"text_muted": "#5c5f77",
"text_placeholder": "#6c6f85",
"text_accent": "#1e66f5",
"icon": "#5c5f77",
"icon_muted": "#6c6f85",
"icon_accent": "#1e66f5",
"element_foreground": "#eff1f5",
"element_background": "#1e66f5",
"element_hover": "#8839ef",
"element_active": "#1c5ce0",
"element_selected": "#1a52cc",
"element_disabled": "#1e66f54d",
"secondary_foreground": "#1a52cc",
"secondary_background": "#e6e9ef",
"secondary_hover": "#8839ef33",
"secondary_active": "#dce0e8",
"secondary_selected": "#dce0e8",
"secondary_disabled": "#1e66f54d",
"danger_foreground": "#eff1f5",
"danger_background": "#d20f39",
"danger_hover": "#e64553",
"danger_active": "#bd0d33",
"danger_selected": "#a80b2d",
"danger_disabled": "#d20f394d",
"warning_foreground": "#4c4f69",
"warning_background": "#df8e1d",
"warning_hover": "#fe640b",
"warning_active": "#c9801a",
"warning_selected": "#b47217",
"warning_disabled": "#df8e1d4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#ccd0da",
"ghost_element_hover": "#4c4f6933",
"ghost_element_active": "#bcc0cc",
"ghost_element_selected": "#bcc0cc",
"ghost_element_disabled": "#4c4f690d",
"tab_inactive_background": "#e6e9ef",
"tab_inactive_foreground": "#5c5f77",
"tab_active_background": "#eff1f5",
"tab_active_foreground": "#4c4f69",
"tab_hover_foreground": "#8839ef",
"scrollbar_thumb_background": "#4c4f6933",
"scrollbar_thumb_hover_background": "#4c4f6980",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#bcc0cc",
"drop_target_background": "#1e66f51a",
"cursor": "#dc8a78",
"selection": "#7c7f9340"
}
}

View File

@@ -0,0 +1,140 @@
{
"id": "catppuccin-macchiato",
"name": "Catppuccin Macchiato",
"author": "Catppuccin",
"url": "https://github.com/catppuccin/catppuccin",
"light": {
"background": "#24273a",
"surface_background": "#1e2030",
"elevated_surface_background": "#181926",
"panel_background": "#24273a",
"overlay": "#cad3f51a",
"title_bar": "#1e2030",
"title_bar_inactive": "#181926",
"window_border": "#6e738d",
"border": "#5b6078",
"border_variant": "#494d64",
"border_focused": "#8aadf4",
"border_selected": "#8aadf4",
"border_transparent": "#00000000",
"border_disabled": "#363a4f",
"ring": "#8aadf4",
"text": "#cad3f5",
"text_muted": "#b8c0e0",
"text_placeholder": "#a5adcb",
"text_accent": "#8aadf4",
"icon": "#b8c0e0",
"icon_muted": "#a5adcb",
"icon_accent": "#8aadf4",
"element_foreground": "#181926",
"element_background": "#8aadf4",
"element_hover": "#b7bdf8",
"element_active": "#7c9cdc",
"element_selected": "#6e8bc5",
"element_disabled": "#8aadf44d",
"secondary_foreground": "#6e8bc5",
"secondary_background": "#1e2030",
"secondary_hover": "#8aadf433",
"secondary_active": "#181926",
"secondary_selected": "#181926",
"secondary_disabled": "#8aadf44d",
"danger_foreground": "#181926",
"danger_background": "#ed8796",
"danger_hover": "#ee99a0",
"danger_active": "#d57a87",
"danger_selected": "#be6d78",
"danger_disabled": "#ed87964d",
"warning_foreground": "#181926",
"warning_background": "#eed49f",
"warning_hover": "#f5a97f",
"warning_active": "#d6bf8f",
"warning_selected": "#beaa7f",
"warning_disabled": "#eed49f4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#363a4f",
"ghost_element_hover": "#cad3f533",
"ghost_element_active": "#494d64",
"ghost_element_selected": "#494d64",
"ghost_element_disabled": "#cad3f50d",
"tab_inactive_background": "#1e2030",
"tab_inactive_foreground": "#b8c0e0",
"tab_active_background": "#24273a",
"tab_active_foreground": "#cad3f5",
"tab_hover_foreground": "#b7bdf8",
"scrollbar_thumb_background": "#cad3f533",
"scrollbar_thumb_hover_background": "#cad3f580",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#494d64",
"drop_target_background": "#8aadf41a",
"cursor": "#f4dbd6",
"selection": "#939ab740"
},
"dark": {
"background": "#24273a",
"surface_background": "#1e2030",
"elevated_surface_background": "#181926",
"panel_background": "#24273a",
"overlay": "#cad3f51a",
"title_bar": "#1e2030",
"title_bar_inactive": "#181926",
"window_border": "#6e738d",
"border": "#5b6078",
"border_variant": "#494d64",
"border_focused": "#8aadf4",
"border_selected": "#8aadf4",
"border_transparent": "#00000000",
"border_disabled": "#363a4f",
"ring": "#8aadf4",
"text": "#cad3f5",
"text_muted": "#b8c0e0",
"text_placeholder": "#a5adcb",
"text_accent": "#8aadf4",
"icon": "#b8c0e0",
"icon_muted": "#a5adcb",
"icon_accent": "#8aadf4",
"element_foreground": "#181926",
"element_background": "#8aadf4",
"element_hover": "#b7bdf8",
"element_active": "#7c9cdc",
"element_selected": "#6e8bc5",
"element_disabled": "#8aadf44d",
"secondary_foreground": "#6e8bc5",
"secondary_background": "#1e2030",
"secondary_hover": "#8aadf433",
"secondary_active": "#181926",
"secondary_selected": "#181926",
"secondary_disabled": "#8aadf44d",
"danger_foreground": "#181926",
"danger_background": "#ed8796",
"danger_hover": "#ee99a0",
"danger_active": "#d57a87",
"danger_selected": "#be6d78",
"danger_disabled": "#ed87964d",
"warning_foreground": "#181926",
"warning_background": "#eed49f",
"warning_hover": "#f5a97f",
"warning_active": "#d6bf8f",
"warning_selected": "#beaa7f",
"warning_disabled": "#eed49f4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#363a4f",
"ghost_element_hover": "#cad3f533",
"ghost_element_active": "#494d64",
"ghost_element_selected": "#494d64",
"ghost_element_disabled": "#cad3f50d",
"tab_inactive_background": "#1e2030",
"tab_inactive_foreground": "#b8c0e0",
"tab_active_background": "#24273a",
"tab_active_foreground": "#cad3f5",
"tab_hover_foreground": "#b7bdf8",
"scrollbar_thumb_background": "#cad3f533",
"scrollbar_thumb_hover_background": "#cad3f580",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#494d64",
"drop_target_background": "#8aadf41a",
"cursor": "#f4dbd6",
"selection": "#939ab740"
}
}

View File

@@ -0,0 +1,140 @@
{
"id": "catppuccin-mocha",
"name": "Catppuccin Mocha",
"author": "Catppuccin",
"url": "https://github.com/catppuccin/catppuccin",
"light": {
"background": "#1e1e2e",
"surface_background": "#181825",
"elevated_surface_background": "#11111b",
"panel_background": "#1e1e2e",
"overlay": "#cdd6f41a",
"title_bar": "#181825",
"title_bar_inactive": "#11111b",
"window_border": "#6c7086",
"border": "#585b70",
"border_variant": "#45475a",
"border_focused": "#89b4fa",
"border_selected": "#89b4fa",
"border_transparent": "#00000000",
"border_disabled": "#313244",
"ring": "#89b4fa",
"text": "#cdd6f4",
"text_muted": "#bac2de",
"text_placeholder": "#a6adc8",
"text_accent": "#89b4fa",
"icon": "#bac2de",
"icon_muted": "#a6adc8",
"icon_accent": "#89b4fa",
"element_foreground": "#11111b",
"element_background": "#89b4fa",
"element_hover": "#b4befe",
"element_active": "#7ba2e1",
"element_selected": "#6d90c9",
"element_disabled": "#89b4fa4d",
"secondary_foreground": "#6d90c9",
"secondary_background": "#181825",
"secondary_hover": "#89b4fa33",
"secondary_active": "#11111b",
"secondary_selected": "#11111b",
"secondary_disabled": "#89b4fa4d",
"danger_foreground": "#11111b",
"danger_background": "#f38ba8",
"danger_hover": "#eba0ac",
"danger_active": "#db7d98",
"danger_selected": "#c46f88",
"danger_disabled": "#f38ba84d",
"warning_foreground": "#11111b",
"warning_background": "#f9e2af",
"warning_hover": "#fab387",
"warning_active": "#e0cb9e",
"warning_selected": "#c8b48d",
"warning_disabled": "#f9e2af4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#313244",
"ghost_element_hover": "#cdd6f433",
"ghost_element_active": "#45475a",
"ghost_element_selected": "#45475a",
"ghost_element_disabled": "#cdd6f40d",
"tab_inactive_background": "#181825",
"tab_inactive_foreground": "#bac2de",
"tab_active_background": "#1e1e2e",
"tab_active_foreground": "#cdd6f4",
"tab_hover_foreground": "#b4befe",
"scrollbar_thumb_background": "#cdd6f433",
"scrollbar_thumb_hover_background": "#cdd6f580",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#45475a",
"drop_target_background": "#89b4fa1a",
"cursor": "#f5e0dc",
"selection": "#9399b240"
},
"dark": {
"background": "#1e1e2e",
"surface_background": "#181825",
"elevated_surface_background": "#11111b",
"panel_background": "#1e1e2e",
"overlay": "#cdd6f41a",
"title_bar": "#181825",
"title_bar_inactive": "#11111b",
"window_border": "#6c7086",
"border": "#585b70",
"border_variant": "#45475a",
"border_focused": "#89b4fa",
"border_selected": "#89b4fa",
"border_transparent": "#00000000",
"border_disabled": "#313244",
"ring": "#89b4fa",
"text": "#cdd6f4",
"text_muted": "#bac2de",
"text_placeholder": "#a6adc8",
"text_accent": "#89b4fa",
"icon": "#bac2de",
"icon_muted": "#a6adc8",
"icon_accent": "#89b4fa",
"element_foreground": "#11111b",
"element_background": "#89b4fa",
"element_hover": "#b4befe",
"element_active": "#7ba2e1",
"element_selected": "#6d90c9",
"element_disabled": "#89b4fa4d",
"secondary_foreground": "#6d90c9",
"secondary_background": "#181825",
"secondary_hover": "#89b4fa33",
"secondary_active": "#11111b",
"secondary_selected": "#11111b",
"secondary_disabled": "#89b4fa4d",
"danger_foreground": "#11111b",
"danger_background": "#f38ba8",
"danger_hover": "#eba0ac",
"danger_active": "#db7d98",
"danger_selected": "#c46f88",
"danger_disabled": "#f38ba84d",
"warning_foreground": "#11111b",
"warning_background": "#f9e2af",
"warning_hover": "#fab387",
"warning_active": "#e0cb9e",
"warning_selected": "#c8b48d",
"warning_disabled": "#f9e2af4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#313244",
"ghost_element_hover": "#cdd6f433",
"ghost_element_active": "#45475a",
"ghost_element_selected": "#45475a",
"ghost_element_disabled": "#cdd6f40d",
"tab_inactive_background": "#181825",
"tab_inactive_foreground": "#bac2de",
"tab_active_background": "#1e1e2e",
"tab_active_foreground": "#cdd6f4",
"tab_hover_foreground": "#b4befe",
"scrollbar_thumb_background": "#cdd6f433",
"scrollbar_thumb_hover_background": "#cdd6f580",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#45475a",
"drop_target_background": "#89b4fa1a",
"cursor": "#f5e0dc",
"selection": "#9399b240"
}
}

140
assets/themes/flexoki.json Normal file
View File

@@ -0,0 +1,140 @@
{
"id": "flexoki",
"name": "Flexoki",
"author": "Stephan Ango",
"url": "https://stephango.com/flexoki",
"light": {
"background": "#FFFCF0",
"surface_background": "#F2F0E5",
"elevated_surface_background": "#E6E4D9",
"panel_background": "#FFFCF0",
"overlay": "#100F0F1a",
"title_bar": "#F2F0E5",
"title_bar_inactive": "#E6E4D9",
"window_border": "#B7B5AC",
"border": "#CECDC3",
"border_variant": "#DAD8CE",
"border_focused": "#205EA6",
"border_selected": "#205EA6",
"border_transparent": "#00000000",
"border_disabled": "#E6E4D9",
"ring": "#205EA6",
"text": "#100F0F",
"text_muted": "#6F6E69",
"text_placeholder": "#9F9D96",
"text_accent": "#205EA6",
"icon": "#6F6E69",
"icon_muted": "#9F9D96",
"icon_accent": "#205EA6",
"element_foreground": "#FFFCF0",
"element_background": "#205EA6",
"element_hover": "#1A4F8C",
"element_active": "#163B66",
"element_selected": "#133051",
"element_disabled": "#205EA64d",
"secondary_foreground": "#163B66",
"secondary_background": "#F2F0E5",
"secondary_hover": "#205EA61a",
"secondary_active": "#E6E4D9",
"secondary_selected": "#E6E4D9",
"secondary_disabled": "#205EA64d",
"danger_foreground": "#FFFCF0",
"danger_background": "#D14D41",
"danger_hover": "#C03E35",
"danger_active": "#AF3029",
"danger_selected": "#942822",
"danger_disabled": "#D14D414d",
"warning_foreground": "#100F0F",
"warning_background": "#D0A215",
"warning_hover": "#BE9207",
"warning_active": "#AD8301",
"warning_selected": "#8E6B01",
"warning_disabled": "#D0A2154d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#E6E4D9",
"ghost_element_hover": "#100F0F1a",
"ghost_element_active": "#DAD8CE",
"ghost_element_selected": "#DAD8CE",
"ghost_element_disabled": "#100F0F0d",
"tab_inactive_background": "#F2F0E5",
"tab_inactive_foreground": "#6F6E69",
"tab_active_background": "#FFFCF0",
"tab_active_foreground": "#100F0F",
"tab_hover_foreground": "#205EA6",
"scrollbar_thumb_background": "#100F0F33",
"scrollbar_thumb_hover_background": "#100F0F4d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#DAD8CE",
"drop_target_background": "#205EA61a",
"cursor": "#205EA6",
"selection": "#205EA640"
},
"dark": {
"background": "#100F0F",
"surface_background": "#1C1B1A",
"elevated_surface_background": "#282726",
"panel_background": "#100F0F",
"overlay": "#FFFCF01a",
"title_bar": "#1C1B1A",
"title_bar_inactive": "#282726",
"window_border": "#575653",
"border": "#403E3C",
"border_variant": "#343331",
"border_focused": "#4385BE",
"border_selected": "#4385BE",
"border_transparent": "#00000000",
"border_disabled": "#282726",
"ring": "#4385BE",
"text": "#FFFCF0",
"text_muted": "#878580",
"text_placeholder": "#6F6E69",
"text_accent": "#4385BE",
"icon": "#878580",
"icon_muted": "#6F6E69",
"icon_accent": "#4385BE",
"element_foreground": "#100F0F",
"element_background": "#4385BE",
"element_hover": "#3171B2",
"element_active": "#205EA6",
"element_selected": "#1A4F8C",
"element_disabled": "#4385BE4d",
"secondary_foreground": "#205EA6",
"secondary_background": "#1C1B1A",
"secondary_hover": "#4385BE1a",
"secondary_active": "#282726",
"secondary_selected": "#282726",
"secondary_disabled": "#4385BE4d",
"danger_foreground": "#100F0F",
"danger_background": "#E8705F",
"danger_hover": "#D14D41",
"danger_active": "#C03E35",
"danger_selected": "#AF3029",
"danger_disabled": "#E8705F4d",
"warning_foreground": "#100F0F",
"warning_background": "#DFB431",
"warning_hover": "#D0A215",
"warning_active": "#BE9207",
"warning_selected": "#AD8301",
"warning_disabled": "#DFB4314d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#282726",
"ghost_element_hover": "#FFFCF01a",
"ghost_element_active": "#343331",
"ghost_element_selected": "#343331",
"ghost_element_disabled": "#FFFCF00d",
"tab_inactive_background": "#1C1B1A",
"tab_inactive_foreground": "#878580",
"tab_active_background": "#100F0F",
"tab_active_foreground": "#FFFCF0",
"tab_hover_foreground": "#4385BE",
"scrollbar_thumb_background": "#FFFCF033",
"scrollbar_thumb_hover_background": "#FFFCF04d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#343331",
"drop_target_background": "#4385BE1a",
"cursor": "#4385BE",
"selection": "#4385BE40"
}
}

View File

@@ -0,0 +1,140 @@
{
"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"
}
}

View File

@@ -0,0 +1,140 @@
{
"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"
}
}

View File

@@ -0,0 +1,140 @@
{
"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"
}
}

View File

@@ -1,230 +0,0 @@
use std::time::Duration;
use anyhow::Error;
use global::{
constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID},
get_client,
};
use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
use nostr_sdk::prelude::*;
use ui::{notification::Notification, ContextModal};
struct GlobalAccount(Entity<Account>);
impl Global for GlobalAccount {}
pub fn init(cx: &mut App) {
Account::set_global(cx.new(|_| Account { profile: None }), cx);
}
#[derive(Debug, Clone)]
pub struct Account {
pub profile: Option<Profile>,
}
impl Account {
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalAccount>().0.clone()
}
pub fn set_global(account: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalAccount(account));
}
/// Login to the account using the given signer.
pub fn login<S>(&mut self, signer: S, window: &mut Window, cx: &mut Context<Self>)
where
S: NostrSigner + 'static,
{
let task: Task<Result<Profile, Error>> = cx.background_spawn(async move {
let client = get_client();
// Use user's signer for main signer
_ = client.set_signer(signer).await;
// Verify nostr signer and get public key
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
log::info!("Logged in with public key: {:?}", public_key);
// Fetch user's metadata
let metadata = client
.fetch_metadata(public_key, Duration::from_secs(2))
.await?
.unwrap_or_default();
Ok(Profile::new(public_key, metadata))
});
cx.spawn_in(window, async move |this, cx| match task.await {
Ok(profile) => {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.profile(profile, cx);
cx.defer_in(window, |this, _, cx| {
this.subscribe(cx);
});
})
.ok();
})
.ok();
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(Notification::error(e.to_string()), cx)
})
.ok();
}
})
.detach();
}
/// Create a new account with the given metadata.
pub fn new_account(&mut self, metadata: Metadata, window: &mut Window, cx: &mut Context<Self>) {
const DEFAULT_NIP_65_RELAYS: [&str; 4] = [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://relay.nostr.net",
"wss://nos.lol",
];
const DEFAULT_MESSAGING_RELAYS: [&str; 2] =
["wss://auth.nostr1.com", "wss://relay.0xchat.com"];
let keys = Keys::generate();
let public_key = keys.public_key();
let task: Task<Result<Profile, Error>> = cx.background_spawn(async move {
let client = get_client();
// Update signer
client.set_signer(keys).await;
// Set metadata
client.set_metadata(&metadata).await?;
// Create relay list
let tags: Vec<Tag> = DEFAULT_NIP_65_RELAYS
.into_iter()
.filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay_metadata(url, None))
} else {
None
}
})
.collect();
let builder = EventBuilder::new(Kind::RelayList, "").tags(tags);
if let Err(e) = client.send_event_builder(builder).await {
log::error!("Failed to send relay list event: {}", e);
};
// Create messaging relay list
let tags: Vec<Tag> = DEFAULT_MESSAGING_RELAYS
.into_iter()
.filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay(url))
} else {
None
}
})
.collect();
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
if let Err(e) = client.send_event_builder(builder).await {
log::error!("Failed to send messaging relay list event: {}", e);
};
Ok(Profile::new(public_key, metadata))
});
cx.spawn_in(window, async move |this, cx| {
if let Ok(profile) = task.await {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.profile(profile, cx);
cx.defer_in(window, |this, _, cx| {
this.subscribe(cx);
});
})
.ok();
})
.ok();
} else {
cx.update(|window, cx| {
window.push_notification(Notification::error("Failed to create account."), cx)
})
.ok();
}
})
.detach();
}
/// Sets the profile for the account.
pub fn profile(&mut self, profile: Profile, cx: &mut Context<Self>) {
self.profile = Some(profile);
cx.notify();
}
/// Subscribes to the current account's metadata.
pub fn subscribe(&self, cx: &mut Context<Self>) {
let Some(profile) = self.profile.as_ref() else {
return;
};
let user = profile.public_key();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let metadata = Filter::new()
.kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::InboxRelays,
Kind::MuteList,
Kind::SimpleGroups,
])
.author(user)
.limit(10);
let data = Filter::new()
.author(user)
.since(Timestamp::now())
.kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::MuteList,
Kind::SimpleGroups,
Kind::InboxRelays,
Kind::RelayList,
]);
let msg = Filter::new().kind(Kind::GiftWrap).pubkey(user);
let new_msg = Filter::new().kind(Kind::GiftWrap).pubkey(user).limit(0);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = get_client();
client.subscribe(metadata, Some(opts)).await?;
client.subscribe(data, None).await?;
let sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
client.subscribe_with_id(sub_id, msg, Some(opts)).await?;
let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
client.subscribe_with_id(sub_id, new_msg, None).await?;
Ok(())
});
cx.spawn(async move |_, _| {
if let Err(e) = task.await {
log::error!("Error: {}", e);
}
})
.detach();
}
}

11
crates/assets/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "assets"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
gpui.workspace = true
anyhow.workspace = true
log.workspace = true
rust-embed.workspace = true

51
crates/assets/src/lib.rs Normal file
View File

@@ -0,0 +1,51 @@
use anyhow::Context;
use gpui::{App, AssetSource, Result, SharedString};
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "../../assets"]
#[include = "fonts/**/*"]
#[include = "brand/**/*"]
#[include = "icons/**/*"]
#[include = "themes/**/*"]
#[exclude = "*.DS_Store"]
pub struct Assets;
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
Self::get(path)
.map(|f| Some(f.data))
.with_context(|| format!("loading asset at path {path:?}"))
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
Ok(Self::iter()
.filter_map(|p| {
if p.starts_with(path) {
Some(p.into())
} else {
None
}
})
.collect())
}
}
impl Assets {
/// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory.
pub fn load_fonts(&self, cx: &App) -> anyhow::Result<()> {
let font_paths = self.list("fonts")?;
let mut embedded_fonts = Vec::new();
for font_path in font_paths {
if font_path.ends_with(".ttf") {
let font_bytes = cx
.asset_source()
.load(&font_path)?
.expect("Assets should never return None");
embedded_fonts.push(font_bytes);
}
}
cx.text_system().add_fonts(embedded_fonts)
}
}

View File

@@ -1,18 +1,21 @@
[package]
name = "auto_update"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
common = { path = "../common" }
global = { path = "../global" }
gpui.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
smol.workspace = true
log.workspace = true
tempfile = "3.19.1"
reqwest = { version = "0.12", features = ["stream"] }
[package]
name = "auto_update"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
common = { path = "../common" }
gpui.workspace = true
gpui_tokio.workspace = true
anyhow.workspace = true
smol.workspace = true
log.workspace = true
smallvec.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
semver = "1.0.27"
tempfile = "3.23.0"
futures.workspace = true

View File

@@ -1,350 +1,561 @@
use std::{
env::{self, consts::OS},
ffi::OsString,
path::PathBuf,
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use gpui::http_client::{AsyncBody, HttpClient};
use gpui::{
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
Window,
};
use semver::Version;
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use smol::fs::File;
use smol::io::AsyncReadExt;
use smol::process::Command;
use anyhow::{anyhow, Context as _, Error};
use global::get_client;
use gpui::{App, AppContext, Context, Entity, Global, SemanticVersion, Task};
use nostr_sdk::prelude::*;
use smol::{
fs::{self, File},
io::AsyncWriteExt,
process::Command,
};
use tempfile::TempDir;
const GITHUB_API_URL: &str = "https://api.github.com";
const COOP_UPDATE_EXPLANATION: &str = "COOP_UPDATE_EXPLANATION";
struct GlobalAutoUpdate(Entity<AutoUpdater>);
impl Global for GlobalAutoUpdate {}
pub fn init(cx: &mut App) {
let env = env!("CARGO_PKG_VERSION");
let current_version: SemanticVersion = env.parse().expect("Invalid version in Cargo.toml");
AutoUpdater::set_global(
cx.new(|_| AutoUpdater {
current_version,
status: AutoUpdateStatus::Idle,
}),
cx,
);
fn get_github_repo_owner() -> String {
std::env::var("COOP_GITHUB_REPO_OWNER").unwrap_or_else(|_| "your-username".to_string())
}
struct MacOsUnmounter {
mount_path: PathBuf,
fn get_github_repo_name() -> String {
std::env::var("COOP_GITHUB_REPO_NAME").unwrap_or_else(|_| "your-repo".to_string())
}
impl Drop for MacOsUnmounter {
fn drop(&mut self) {
let unmount_output = std::process::Command::new("hdiutil")
.args(["detach", "-force"])
.arg(&self.mount_path)
.output();
fn is_flatpak_installation() -> bool {
// Check if app is installed via Flatpak
std::env::var("FLATPAK_ID").is_ok() || std::env::var(COOP_UPDATE_EXPLANATION).is_ok()
}
match unmount_output {
Ok(output) if output.status.success() => {
log::info!("Successfully unmounted the disk image");
}
Ok(output) => {
log::error!(
"Failed to unmount disk image: {:?}",
String::from_utf8_lossy(&output.stderr)
);
}
Err(error) => {
log::error!("Error while trying to unmount disk image: {:?}", error);
}
}
pub fn init(window: &mut Window, cx: &mut App) {
// Skip auto-update initialization if installed via Flatpak
if is_flatpak_installation() {
log::info!("Skipping auto-update initialization: App is installed via Flatpak");
return;
}
AutoUpdater::set_global(cx.new(|cx| AutoUpdater::new(window, cx)), cx);
}
struct GlobalAutoUpdater(Entity<AutoUpdater>);
impl Global for GlobalAutoUpdater {}
#[cfg(not(target_os = "windows"))]
struct InstallerDir(tempfile::TempDir);
#[cfg(not(target_os = "windows"))]
impl InstallerDir {
async fn new() -> Result<Self, Error> {
Ok(Self(
tempfile::Builder::new()
.prefix("coop-auto-update")
.tempdir()?,
))
}
fn path(&self) -> &Path {
self.0.path()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(target_os = "windows")]
struct InstallerDir(PathBuf);
#[cfg(target_os = "windows")]
impl InstallerDir {
async fn new() -> Result<Self, Error> {
let installer_dir = std::env::current_exe()?
.parent()
.context("No parent dir for Coop.exe")?
.join("updates");
if smol::fs::metadata(&installer_dir).await.is_ok() {
smol::fs::remove_dir_all(&installer_dir).await?;
}
smol::fs::create_dir(&installer_dir).await?;
Ok(Self(installer_dir))
}
fn path(&self) -> &Path {
self.0.as_path()
}
}
struct MacOsUnmounter<'a> {
mount_path: PathBuf,
background_executor: &'a BackgroundExecutor,
}
impl Drop for MacOsUnmounter<'_> {
fn drop(&mut self) {
let mount_path = std::mem::take(&mut self.mount_path);
self.background_executor
.spawn(async move {
let unmount_output = Command::new("hdiutil")
.args(["detach", "-force"])
.arg(&mount_path)
.output()
.await;
match unmount_output {
Ok(output) if output.status.success() => {
log::info!("Successfully unmounted the disk image");
}
Ok(output) => {
log::error!(
"Failed to unmount disk image: {:?}",
String::from_utf8_lossy(&output.stderr)
);
}
Err(error) => {
log::error!("Error while trying to unmount disk image: {:?}", error);
}
}
})
.detach();
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum AutoUpdateStatus {
Idle,
Checking,
Downloading,
Checked { download_url: String },
Installing,
Updated { binary_path: PathBuf },
Errored,
Updated,
Errored { msg: Box<String> },
}
impl AsRef<AutoUpdateStatus> for AutoUpdateStatus {
fn as_ref(&self) -> &AutoUpdateStatus {
self
}
}
impl AutoUpdateStatus {
pub fn is_updated(&self) -> bool {
matches!(self, Self::Updated { .. })
pub fn is_updating(&self) -> bool {
matches!(self, Self::Checked { .. } | Self::Installing)
}
pub fn is_updated(&self) -> bool {
matches!(self, Self::Updated)
}
pub fn checked(download_url: String) -> Self {
Self::Checked { download_url }
}
pub fn error(e: String) -> Self {
Self::Errored { msg: Box::new(e) }
}
}
#[derive(Debug, Deserialize)]
pub struct GitHubRelease {
pub tag_name: String,
pub assets: Vec<GitHubAsset>,
}
#[derive(Debug, Deserialize)]
pub struct GitHubAsset {
pub name: String,
pub browser_download_url: String,
}
#[derive(Debug)]
pub struct AutoUpdater {
status: AutoUpdateStatus,
current_version: SemanticVersion,
/// Current status of the auto updater
pub status: AutoUpdateStatus,
/// Current version of the application
pub version: Version,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
/// Background tasks
tasks: Vec<Task<Result<(), Error>>>,
}
impl AutoUpdater {
/// Retrieve the global auto updater instance
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalAutoUpdate>().0.clone()
cx.global::<GlobalAutoUpdater>().0.clone()
}
pub fn set_global(auto_updater: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalAutoUpdate(auto_updater));
/// Set the global auto updater instance
fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalAutoUpdater(state));
}
pub fn current_version(&self) -> SemanticVersion {
self.current_version
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
let mut subscriptions = smallvec![];
subscriptions.push(
// Observe the status
cx.observe_self(|this, cx| {
if let AutoUpdateStatus::Checked { download_url } = this.status.clone() {
this.download_and_install(&download_url, cx);
}
}),
);
// Run at the end of current cycle
cx.defer_in(window, |this, _window, cx| {
this.check(cx);
});
Self {
status: AutoUpdateStatus::Idle,
version,
tasks: vec![],
_subscriptions: subscriptions,
}
}
pub fn status(&self) -> AutoUpdateStatus {
self.status.clone()
}
pub fn set_status(&mut self, status: AutoUpdateStatus, cx: &mut Context<Self>) {
fn set_status(&mut self, status: AutoUpdateStatus, cx: &mut Context<Self>) {
self.status = status;
cx.notify();
}
pub fn update(&mut self, event: Event, cx: &mut Context<Self>) {
self.set_status(AutoUpdateStatus::Checking, cx);
fn check(&mut self, cx: &mut Context<Self>) {
let version = self.version.clone();
let duration = Duration::from_secs(120);
let task = self.check_for_updates(version, cx);
// Extract the version from the identifier tag
let ident = match event.tags.identifier() {
Some(i) => match i.split('@').next_back() {
Some(i) => i,
None => return,
},
None => return,
};
// Check for updates after 2 minutes
self.tasks.push(cx.spawn(async move |this, cx| {
cx.background_executor().timer(duration).await;
// Convert the version string to a SemanticVersion
let new_version: SemanticVersion = ident.parse().expect("Invalid version");
// Update the status to checking
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Checking, cx);
})?;
// Check if the new version is the same as the current version
if self.current_version == new_version {
self.set_status(AutoUpdateStatus::Idle, cx);
return;
};
match task.await {
Ok(download_url) => {
// Update the status to checked with download URL
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::checked(download_url), cx);
})?;
}
Err(e) => {
log::warn!("Failed to check for updates: {e}");
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Idle, cx);
})?;
}
}
// Download the new version
self.set_status(AutoUpdateStatus::Downloading, cx);
Ok(())
}));
}
let task: Task<Result<(TempDir, PathBuf), Error>> = cx.background_spawn(async move {
let client = get_client();
let ids = event.tags.event_ids().copied();
let filter = Filter::new().ids(ids).kind(Kind::FileMetadata);
let events = client.database().query(filter).await?;
fn check_for_updates(&self, version: Version, cx: &App) -> Task<Result<String, Error>> {
let http_client = cx.http_client();
let repo_owner = get_github_repo_owner();
let repo_name = get_github_repo_name();
if let Some(event) = events.into_iter().find(|event| event.content == OS) {
let tag = event.tags.find(TagKind::Url).context("url not found")?;
let url = Url::parse(tag.content().context("invalid")?)?;
cx.background_spawn(async move {
let url = format!(
"{}/repos/{}/{}/releases/latest",
GITHUB_API_URL, repo_owner, repo_name
);
let temp_dir = tempfile::Builder::new().prefix("coop-update").tempdir()?;
let filename = match OS {
"macos" => Ok("Coop.dmg"),
"linux" => Ok("Coop.tar.gz"),
"windows" => Ok("CoopUpdateInstaller.exe"),
_ => Err(anyhow!("not supported: {:?}", OS)),
}?;
let async_body = AsyncBody::default();
let mut body = Vec::new();
let mut response = http_client.get(&url, async_body, false).await?;
let downloaded_asset = temp_dir.path().join(filename);
let mut target_file = File::create(&downloaded_asset).await?;
// Read the response body into a vector
response.body_mut().read_to_end(&mut body).await?;
let response = reqwest::get(url).await?;
let mut stream = response.bytes_stream();
if !response.status().is_success() {
return Err(anyhow!("GitHub API returned error: {}", response.status()));
}
while let Some(item) = stream.next().await {
let chunk = item?;
target_file.write_all(&chunk).await?;
// Parse the response body as JSON
let release: GitHubRelease = serde_json::from_slice(&body)?;
// Parse version from tag (remove 'v' prefix if present)
let tag_version = release.tag_name.trim_start_matches('v');
let new_version = Version::parse(tag_version).context(format!(
"Failed to parse version from tag: {}",
release.tag_name
))?;
if new_version > version {
// Find the appropriate asset for the current platform
let current_os = std::env::consts::OS;
let asset_name = match current_os {
"macos" => "Coop.dmg",
"linux" => "coop.tar.gz",
"windows" => "Coop.exe",
_ => return Err(anyhow!("Unsupported OS: {}", current_os)),
};
let download_url = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.map(|asset| asset.browser_download_url.clone())
.context(format!(
"No {} asset found in release {}",
asset_name, release.tag_name
))?;
Ok(download_url)
} else {
Err(anyhow!(
"No update available. Current: {}, Latest: {}",
version,
new_version
))
}
})
}
fn download_and_install(&mut self, download_url: &str, cx: &mut Context<Self>) {
let http_client = cx.http_client();
let download_url = download_url.to_string();
let task: Task<Result<(InstallerDir, PathBuf), Error>> = cx.background_spawn(async move {
let installer_dir = InstallerDir::new().await?;
let target_path = Self::target_path(&installer_dir).await?;
// Download the release
download(&download_url, &target_path, http_client).await?;
Ok((installer_dir, target_path))
});
self.tasks.push(
// Install the new release
cx.spawn(async move |this, cx| {
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Installing, cx);
})?;
match task.await {
Ok((installer_dir, target_path)) => {
if Self::install(installer_dir, target_path, cx).await.is_ok() {
// Update the status to updated
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Updated, cx);
})?;
}
}
Err(e) => {
// Update the status to error including the error message
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::error(e.to_string()), cx);
})?;
}
}
log::info!("downloaded update. path:{:?}", downloaded_asset);
Ok((temp_dir, downloaded_asset))
} else {
Err(anyhow!("Not found"))
}
});
cx.spawn(async move |this, cx| {
if let Ok((temp_dir, downloaded_asset)) = task.await {
cx.update(|cx| {
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Installing, cx);
match OS {
"macos" => this.install_release_macos(temp_dir, downloaded_asset, cx),
"linux" => this.install_release_linux(temp_dir, downloaded_asset, cx),
"windows" => this.install_release_windows(downloaded_asset, cx),
_ => {}
}
})
.ok();
})
.ok();
}
})
.detach();
Ok(())
}),
);
}
fn install_release_macos(&mut self, temp_dir: TempDir, asset: PathBuf, cx: &mut Context<Self>) {
let running_app_path = cx.app_path().unwrap();
let running_app_filename = running_app_path.file_name().unwrap();
async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf, Error> {
let filename = match std::env::consts::OS {
"macos" => anyhow::Ok("Coop.dmg"),
"linux" => Ok("coop.tar.gz"),
"windows" => Ok("Coop.exe"),
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
}?;
let mount_path = temp_dir.path().join("Coop");
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
mounted_app_path.push("/");
let task: Task<Result<PathBuf, Error>> = cx.background_spawn(async move {
let output = Command::new("hdiutil")
.args(["attach", "-nobrowse"])
.arg(&asset)
.arg("-mountroot")
.arg(temp_dir.path())
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to mount: {:?}",
String::from_utf8_lossy(&output.stderr)
);
// Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
let _unmounter = MacOsUnmounter {
mount_path: mount_path.clone(),
};
let output = Command::new("rsync")
.args(["-av", "--delete"])
.arg(&mounted_app_path)
.arg(&running_app_path)
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to copy app: {:?}",
String::from_utf8_lossy(&output.stderr)
);
Ok(running_app_path)
});
cx.spawn(async move |this, cx| {
if let Ok(binary_path) = task.await {
cx.update(|cx| {
this.update(cx, |this, cx| {
this.status = AutoUpdateStatus::Updated { binary_path };
cx.notify();
})
.ok();
})
.ok();
}
})
.detach();
Ok(installer_dir.path().join(filename))
}
fn install_release_linux(&mut self, temp_dir: TempDir, asset: PathBuf, cx: &mut Context<Self>) {
let home_dir = PathBuf::from(env::var("HOME").unwrap());
let running_app_path = cx.app_path().unwrap();
let extracted = temp_dir.path().join("coop");
let task: Task<Result<PathBuf, Error>> = cx.background_spawn(async move {
fs::create_dir_all(&extracted).await?;
let output = Command::new("tar")
.arg("-xzf")
.arg(&asset)
.arg("-C")
.arg(&extracted)
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to extract {:?} to {:?}: {:?}",
asset,
extracted,
String::from_utf8_lossy(&output.stderr)
);
let app_folder_name: String = "coop.app".into();
let from = extracted.join(&app_folder_name);
let mut to = home_dir.join(".local");
let expected_suffix = format!("{}/libexec/coop", app_folder_name);
if let Some(prefix) = running_app_path
.to_str()
.and_then(|str| str.strip_suffix(&expected_suffix))
{
to = PathBuf::from(prefix);
}
let output = Command::new("rsync")
.args(["-av", "--delete"])
.arg(&from)
.arg(&to)
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to copy Coop update from {:?} to {:?}: {:?}",
from,
to,
String::from_utf8_lossy(&output.stderr)
);
Ok(to.join(expected_suffix))
});
cx.spawn(async move |this, cx| {
if let Ok(binary_path) = task.await {
cx.update(|cx| {
this.update(cx, |this, cx| {
this.status = AutoUpdateStatus::Updated { binary_path };
cx.notify();
})
.ok();
})
.ok();
}
})
.detach();
}
fn install_release_windows(&mut self, asset: PathBuf, cx: &mut Context<Self>) {
let task: Task<Result<PathBuf, Error>> = cx.background_spawn(async move {
let output = Command::new(asset)
.arg("/verysilent")
.arg("/update=true")
.arg("!desktopicon")
.arg("!quicklaunchicon")
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to start installer: {:?}",
String::from_utf8_lossy(&output.stderr)
);
Ok(std::env::current_exe()?)
});
cx.spawn(async move |this, cx| {
if let Ok(binary_path) = task.await {
cx.update(|cx| {
this.update(cx, |this, cx| {
this.status = AutoUpdateStatus::Updated { binary_path };
cx.notify();
})
.ok();
})
.ok();
}
})
.detach();
async fn install(
installer_dir: InstallerDir,
target_path: PathBuf,
cx: &AsyncApp,
) -> Result<(), Error> {
match std::env::consts::OS {
"macos" => install_release_macos(&installer_dir, target_path, cx).await,
"linux" => install_release_linux(&installer_dir, target_path, cx).await,
"windows" => install_release_windows(target_path).await,
unsupported_os => anyhow::bail!("Not supported: {unsupported_os}"),
}
}
}
async fn download(
url: &str,
target_path: &std::path::Path,
client: Arc<dyn HttpClient>,
) -> Result<(), Error> {
let body = AsyncBody::default();
let mut target_file = File::create(&target_path).await?;
let mut response = client.get(url, body, true).await?;
// Copy the response body to the target file
smol::io::copy(response.body_mut(), &mut target_file).await?;
Ok(())
}
async fn install_release_macos(
temp_dir: &InstallerDir,
downloaded_dmg: PathBuf,
cx: &AsyncApp,
) -> Result<(), Error> {
let running_app_path = cx.update(|cx| cx.app_path())?;
let running_app_filename = running_app_path
.file_name()
.with_context(|| format!("invalid running app path {running_app_path:?}"))?;
let mount_path = temp_dir.path().join("Coop");
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
mounted_app_path.push("/");
let output = Command::new("hdiutil")
.args(["attach", "-nobrowse"])
.arg(&downloaded_dmg)
.arg("-mountroot")
.arg(temp_dir.path())
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to mount: {:?}",
String::from_utf8_lossy(&output.stderr)
);
// Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
let _unmounter = MacOsUnmounter {
mount_path: mount_path.clone(),
background_executor: cx.background_executor(),
};
let output = Command::new("rsync")
.args(["-av", "--delete"])
.arg(&mounted_app_path)
.arg(&running_app_path)
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to copy app: {:?}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
async fn install_release_linux(
temp_dir: &InstallerDir,
downloaded_tar_gz: PathBuf,
cx: &AsyncApp,
) -> Result<(), Error> {
let running_app_path = cx.update(|cx| cx.app_path())?;
// Extract the tar.gz file
let extracted = temp_dir.path().join("coop");
smol::fs::create_dir_all(&extracted)
.await
.context("failed to create directory to extract update")?;
let output = Command::new("tar")
.arg("-xzf")
.arg(&downloaded_tar_gz)
.arg("-C")
.arg(&extracted)
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to extract {:?} to {:?}: {:?}",
downloaded_tar_gz,
extracted,
String::from_utf8_lossy(&output.stderr)
);
// Find the extracted app directory
let mut entries = smol::fs::read_dir(&extracted).await?;
let mut app_dir = None;
use smol::stream::StreamExt;
while let Some(entry) = entries.next().await {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
app_dir = Some(path);
break;
}
}
let from = app_dir.context("No app directory found in archive")?;
// Copy to the current installation directory
let output = Command::new("rsync")
.args(["-av", "--delete"])
.arg(&from)
.arg(
running_app_path
.parent()
.context("No parent directory for app")?,
)
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to copy app from {:?} to {:?}: {:?}",
from,
running_app_path.parent(),
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<(), Error> {
//const CREATE_NO_WINDOW: u32 = 0x08000000;
let system_root = std::env::var("SYSTEMROOT");
let powershell_path = system_root.as_ref().map_or_else(
|_| "powershell.exe".to_string(),
|p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
);
let mut installer_path = std::ffi::OsString::new();
installer_path.push("\"");
installer_path.push(&downloaded_installer);
installer_path.push("\"");
let output = Command::new(powershell_path)
//.creation_flags(CREATE_NO_WINDOW)
.args(["-NoProfile", "-WindowStyle", "Hidden"])
.args(["Start-Process"])
.arg(installer_path)
.arg("-ArgumentList")
.args(["/P", "/R"])
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to start installer: {:?}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}

27
crates/chat/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "chat"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
common = { path = "../common" }
state = { path = "../state" }
device = { path = "../device" }
person = { path = "../person" }
settings = { path = "../settings" }
gpui.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
itertools.workspace = true
smallvec.workspace = true
smol.workspace = true
log.workspace = true
futures.workspace = true
flume.workspace = true
serde.workspace = true
serde_json.workspace = true
fuzzy-matcher = "0.3.7"

844
crates/chat/src/lib.rs Normal file
View File

@@ -0,0 +1,844 @@
use std::cmp::Reverse;
use std::collections::{HashMap, HashSet};
use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use common::EventUtils;
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use gpui::{
App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window,
};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use state::{NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT, USER_GIFTWRAP};
mod message;
mod room;
pub use message::*;
pub use room::*;
pub fn init(window: &mut Window, cx: &mut App) {
ChatRegistry::set_global(cx.new(|cx| ChatRegistry::new(window, cx)), cx);
}
struct GlobalChatRegistry(Entity<ChatRegistry>);
impl Global for GlobalChatRegistry {}
/// Chat event.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChatEvent {
/// An event to open a room by its ID
OpenRoom(u64),
/// An event to close a room by its ID
CloseRoom(u64),
/// An event to notify UI about a new chat request
Ping,
}
/// Channel signal.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Signal {
/// Message received from relay pool
Message(NewMessage),
/// Eose received from relay pool
Eose,
}
/// Inbox state.
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum InboxState {
#[default]
Idle,
Checking,
RelayNotAvailable,
RelayConfigured(Box<Event>),
Subscribing,
}
impl InboxState {
pub fn not_configured(&self) -> bool {
matches!(self, InboxState::RelayNotAvailable)
}
pub fn subscribing(&self) -> bool {
matches!(self, InboxState::Subscribing)
}
}
/// Chat Registry
#[derive(Debug)]
pub struct ChatRegistry {
/// Relay state for messaging relay list
state: Entity<InboxState>,
/// Collection of all chat rooms
rooms: Vec<Entity<Room>>,
/// Tracking the status of unwrapping gift wrap events.
tracking_flag: Arc<AtomicBool>,
/// Async tasks
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
/// Subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
}
impl EventEmitter<ChatEvent> for ChatRegistry {}
impl ChatRegistry {
/// Retrieve the global chat registry state
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalChatRegistry>().0.clone()
}
/// Set the global chat registry instance
fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalChatRegistry(state));
}
/// Create a new chat registry instance
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let state = cx.new(|_| InboxState::default());
let nostr = NostrRegistry::global(cx);
let mut subscriptions = smallvec![];
subscriptions.push(
// Observe the nip65 state and load chat rooms on every state change
cx.observe(&nostr, |this, state, cx| {
match state.read(cx).relay_list_state() {
RelayState::Idle => {
this.reset(cx);
}
RelayState::Configured => {
this.get_contact_list(cx);
this.ensure_messaging_relays(cx);
}
_ => {}
}
// Load rooms on every state change
this.get_rooms(cx);
}),
);
subscriptions.push(
// Observe the nip17 state and load chat rooms on every state change
cx.observe(&state, |this, state, cx| {
if let InboxState::RelayConfigured(event) = state.read(cx) {
let relay_urls: Vec<_> = nip17::extract_relay_list(event).cloned().collect();
this.get_messages(relay_urls, cx);
}
}),
);
// Run at the end of the current cycle
cx.defer_in(window, |this, _window, cx| {
this.get_rooms(cx);
this.handle_notifications(cx);
this.tracking(cx);
});
Self {
state,
rooms: vec![],
tracking_flag: Arc::new(AtomicBool::new(false)),
tasks: smallvec![],
_subscriptions: subscriptions,
}
}
/// Handle nostr notifications
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let status = self.tracking_flag.clone();
let initialized_at = Timestamp::now();
let sub_id1 = SubscriptionId::new(DEVICE_GIFTWRAP);
let sub_id2 = SubscriptionId::new(USER_GIFTWRAP);
// Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Signal>(1024);
self.tasks.push(cx.background_spawn(async move {
let device_signer = signer.get_encryption_signer().await;
let mut notifications = client.notifications();
let mut processed_events = HashSet::new();
while let Some(notification) = notifications.next().await {
let ClientNotification::Message { message, .. } = notification else {
// Skip non-message notifications
continue;
};
match message {
RelayMessage::Event { event, .. } => {
if !processed_events.insert(event.id) {
// Skip if the event has already been processed
continue;
}
if event.kind != Kind::GiftWrap {
// Skip non-gift wrap events
continue;
}
// Extract the rumor from the gift wrap event
match extract_rumor(&client, &device_signer, event.as_ref()).await {
Ok(rumor) => match rumor.created_at >= initialized_at {
true => {
let new_message = NewMessage::new(event.id, rumor);
let signal = Signal::Message(new_message);
tx.send_async(signal).await?;
}
false => {
status.store(true, Ordering::Release);
}
},
Err(e) => {
log::warn!("Failed to unwrap the gift wrap event: {e}");
}
}
}
RelayMessage::EndOfStoredEvents(id) => {
if id.as_ref() == &sub_id1 || id.as_ref() == &sub_id2 {
tx.send_async(Signal::Eose).await?;
}
}
_ => {}
}
}
Ok(())
}));
self.tasks.push(cx.spawn(async move |this, cx| {
while let Ok(message) = rx.recv_async().await {
match message {
Signal::Message(message) => {
this.update(cx, |this, cx| {
this.new_message(message, cx);
})?;
}
Signal::Eose => {
this.update(cx, |this, cx| {
this.get_rooms(cx);
})?;
}
};
}
Ok(())
}));
}
/// Tracking the status of unwrapping gift wrap events.
fn tracking(&mut self, cx: &mut Context<Self>) {
let status = self.tracking_flag.clone();
self.tasks.push(cx.background_spawn(async move {
let loop_duration = Duration::from_secs(15);
loop {
if status.load(Ordering::Acquire) {
_ = status.compare_exchange(true, false, Ordering::Release, Ordering::Relaxed);
}
smol::Timer::after(loop_duration).await;
}
}));
}
/// Get contact list from relays
pub fn get_contact_list(&mut self, cx: &mut Context<Self>) {
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 write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let id = SubscriptionId::new("contact-list");
let opts = SubscribeAutoCloseOptions::default()
.exit_policy(ReqExitPolicy::ExitOnEOSE)
.timeout(Some(Duration::from_secs(TIMEOUT)));
// Get user's write relays
let urls = write_relays.await;
// Construct filter for inbox relays
let filter = Filter::new()
.kind(Kind::ContactList)
.author(public_key)
.limit(1);
// Construct target for subscription
let target: HashMap<&RelayUrl, Filter> =
urls.iter().map(|relay| (relay, filter.clone())).collect();
// Subscribe
client.subscribe(target).close_on(opts).with_id(id).await?;
Ok(())
});
self.tasks.push(task);
}
/// Ensure messaging relays are set up for the current user.
pub fn ensure_messaging_relays(&mut self, cx: &mut Context<Self>) {
let task = self.verify_relays(cx);
// Set state to checking
self.set_state(InboxState::Checking, cx);
self.tasks.push(cx.spawn(async move |this, cx| {
let result = task.await?;
// Update state
this.update(cx, |this, cx| {
this.set_state(result, cx);
})?;
Ok(())
}));
}
// Verify messaging relay list for current user
fn verify_relays(&mut self, cx: &mut Context<Self>) -> Task<Result<InboxState, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let public_key = signer.public_key().unwrap();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
cx.background_spawn(async move {
let urls = write_relays.await;
// Construct filter for inbox relays
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
// Construct target for subscription
let target: HashMap<&RelayUrl, Filter> =
urls.iter().map(|relay| (relay, filter.clone())).collect();
// Stream events from user's write relays
let mut stream = client
.stream_events(target)
.timeout(Duration::from_secs(TIMEOUT))
.await?;
while let Some((_url, res)) = stream.next().await {
match res {
Ok(event) => {
return Ok(InboxState::RelayConfigured(Box::new(event)));
}
Err(e) => {
log::error!("Failed to receive relay list event: {e}");
}
}
}
Ok(InboxState::RelayNotAvailable)
})
}
/// Get all messages for current user
fn get_messages<I>(&mut self, relay_urls: I, cx: &mut Context<Self>)
where
I: IntoIterator<Item = RelayUrl>,
{
let task = self.subscribe(relay_urls, cx);
self.tasks.push(cx.spawn(async move |this, cx| {
task.await?;
// Update state
this.update(cx, |this, cx| {
this.set_state(InboxState::Subscribing, cx);
})?;
Ok(())
}));
}
/// Continuously get gift wrap events for the current user in their messaging relays
fn subscribe<I>(&mut self, urls: I, cx: &mut Context<Self>) -> Task<Result<(), Error>>
where
I: IntoIterator<Item = RelayUrl>,
{
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let urls = urls.into_iter().collect::<Vec<_>>();
cx.background_spawn(async move {
let public_key = signer.get_public_key().await?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(USER_GIFTWRAP);
// Ensure relay connections
for url in urls.iter() {
client.add_relay(url).and_connect().await?;
}
// Construct target for subscription
let target: HashMap<RelayUrl, Filter> = urls
.into_iter()
.map(|relay| (relay, filter.clone()))
.collect();
let output = client.subscribe(target).with_id(id).await?;
log::info!(
"Successfully subscribed to gift-wrap messages on: {:?}",
output.success
);
Ok(())
})
}
/// Set the state of the inbox
fn set_state(&mut self, state: InboxState, cx: &mut Context<Self>) {
self.state.update(cx, |this, cx| {
*this = state;
cx.notify();
});
}
/// Get the relay state
pub fn state(&self, cx: &App) -> InboxState {
self.state.read(cx).clone()
}
/// Get the loading status of the chat registry
pub fn loading(&self) -> bool {
self.tracking_flag.load(Ordering::Acquire)
}
/// Get a weak reference to a room by its ID.
pub fn room(&self, id: &u64, cx: &App) -> Option<WeakEntity<Room>> {
self.rooms
.iter()
.find(|this| &this.read(cx).id == id)
.map(|this| this.downgrade())
}
/// Get all rooms based on the filter.
pub fn rooms(&self, filter: &RoomKind, cx: &App) -> Vec<Entity<Room>> {
self.rooms
.iter()
.filter(|room| &room.read(cx).kind == filter)
.cloned()
.collect()
}
/// Count the number of rooms based on the filter.
pub fn count(&self, filter: &RoomKind, cx: &App) -> usize {
self.rooms
.iter()
.filter(|room| &room.read(cx).kind == filter)
.count()
}
/// Add a new room to the start of list.
pub fn add_room<I>(&mut self, room: I, cx: &mut Context<Self>)
where
I: Into<Room> + 'static,
{
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
cx.spawn(async move |this, cx| {
let signer = client.signer()?;
let public_key = signer.get_public_key().await.ok()?;
let room: Room = room.into().organize(&public_key);
this.update(cx, |this, cx| {
this.rooms.insert(0, cx.new(|_| room));
cx.emit(ChatEvent::Ping);
cx.notify();
})
.ok()
})
.detach();
}
/// Emit an open room event.
///
/// If the room is new, add it to the registry.
pub fn emit_room(&mut self, room: &Entity<Room>, cx: &mut Context<Self>) {
// Get the room's ID.
let id = room.read(cx).id;
// If the room is new, add it to the registry.
if !self.rooms.iter().any(|r| r.read(cx).id == id) {
self.rooms.insert(0, room.to_owned());
}
// Emit the open room event.
cx.emit(ChatEvent::OpenRoom(id));
}
/// Close a room.
pub fn close_room(&mut self, id: u64, cx: &mut Context<Self>) {
if self.rooms.iter().any(|r| r.read(cx).id == id) {
cx.emit(ChatEvent::CloseRoom(id));
}
}
/// Sort rooms by their created at.
pub fn sort(&mut self, cx: &mut Context<Self>) {
self.rooms.sort_by_key(|ev| Reverse(ev.read(cx).created_at));
cx.notify();
}
/// Finding rooms based on a query.
pub fn find(&self, query: &str, cx: &App) -> Vec<Entity<Room>> {
let matcher = SkimMatcherV2::default();
if let Ok(public_key) = PublicKey::parse(query) {
self.rooms
.iter()
.filter(|room| room.read(cx).members.contains(&public_key))
.cloned()
.collect()
} else {
self.rooms
.iter()
.filter(|room| {
matcher
.fuzzy_match(room.read(cx).display_name(cx).as_ref(), query)
.is_some()
})
.cloned()
.collect()
}
}
/// Reset the registry.
pub fn reset(&mut self, cx: &mut Context<Self>) {
self.rooms.clear();
cx.notify();
}
/// Extend the registry with new rooms.
fn extend_rooms(&mut self, rooms: HashSet<Room>, cx: &mut Context<Self>) {
let mut room_map: HashMap<u64, usize> = self
.rooms
.iter()
.enumerate()
.map(|(idx, room)| (room.read(cx).id, idx))
.collect();
for new_room in rooms.into_iter() {
// Check if we already have a room with this ID
if let Some(&index) = room_map.get(&new_room.id) {
self.rooms[index].update(cx, |this, cx| {
if new_room.created_at > this.created_at {
*this = new_room;
cx.notify();
}
});
} else {
let new_room_id = new_room.id;
self.rooms.push(cx.new(|_| new_room));
let new_index = self.rooms.len();
room_map.insert(new_room_id, new_index);
}
}
}
/// Load all rooms from the database.
pub fn get_rooms(&mut self, cx: &mut Context<Self>) {
let task = self.get_rooms_from_database(cx);
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(rooms) => {
this.update(cx, |this, cx| {
this.extend_rooms(rooms, cx);
this.sort(cx);
})?;
}
Err(e) => {
log::error!("Failed to load rooms: {}", e);
}
};
Ok(())
}));
}
/// Create a task to load rooms from the database
fn get_rooms_from_database(&self, cx: &App) -> Task<Result<HashSet<Room>, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Get contacts
let contacts = client
.database()
.contacts_public_keys(public_key)
.await
.unwrap_or_default();
// Construct authored filter
let authored_filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.custom_tag(SingleLetterTag::lowercase(Alphabet::A), public_key);
// Get all authored events
let authored = client.database().query(authored_filter).await?;
// Construct addressed filter
let addressed_filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.custom_tag(SingleLetterTag::lowercase(Alphabet::P), public_key);
// Get all addressed events
let addressed = client.database().query(addressed_filter).await?;
// Merge authored and addressed events
let events = authored.merge(addressed);
// Collect results
let mut rooms: HashSet<Room> = HashSet::new();
let mut grouped: HashMap<u64, Vec<UnsignedEvent>> = HashMap::new();
// Process each event and group by room hash
for raw in events.into_iter() {
if let Ok(rumor) = UnsignedEvent::from_json(&raw.content) {
if rumor.tags.public_keys().peekable().peek().is_some() {
grouped.entry(rumor.uniq_id()).or_default().push(rumor);
}
}
}
for (_id, mut messages) in grouped.into_iter() {
messages.sort_by_key(|m| Reverse(m.created_at));
// Always use the latest message
let Some(latest) = messages.first() else {
continue;
};
// Construct the room from the latest message.
//
// Call `.organize` to ensure the current user is at the end of the list.
let mut room = Room::from(latest).organize(&public_key);
// Check if the user has responded to the room
let user_sent = messages.iter().any(|m| m.pubkey == public_key);
// Check if public keys are from the user's contacts
let is_contact = room.members.iter().any(|k| contacts.contains(k));
// Set the room's kind based on status
if user_sent || is_contact {
room = room.kind(RoomKind::Ongoing);
}
rooms.insert(room);
}
Ok(rooms)
})
}
/// Parse a nostr event into a message and push it to the belonging room
///
/// If the room doesn't exist, it will be created.
/// Updates room ordering based on the most recent messages.
pub fn new_message(&mut self, message: NewMessage, cx: &mut Context<Self>) {
match self.rooms.iter().find(|e| e.read(cx).id == message.room) {
Some(room) => {
room.update(cx, |this, cx| {
this.push_message(message, cx);
});
self.sort(cx);
}
None => {
// Push the new room to the front of the list
self.add_room(message.rumor, cx);
}
}
}
/// 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>) {
if let Some(ids) = ids {
for room in self.rooms.iter() {
if ids.contains(&room.read(cx).id) {
room.update(cx, |this, cx| {
this.emit_refresh(cx);
});
}
}
}
}
}
/// Unwraps a gift-wrapped event and processes its contents.
async fn extract_rumor(
client: &Client,
device_signer: &Option<Arc<dyn NostrSigner>>,
gift_wrap: &Event,
) -> Result<UnsignedEvent, Error> {
// Try to get cached rumor first
if let Ok(event) = get_rumor(client, gift_wrap.id).await {
return Ok(event);
}
// Try to unwrap with the available signer
let unwrapped = try_unwrap(client, device_signer, gift_wrap).await?;
let mut rumor = unwrapped.rumor;
// Generate event id for the rumor if it doesn't have one
rumor.ensure_id();
// Cache the rumor
if let Err(e) = set_rumor(client, gift_wrap.id, &rumor).await {
log::error!("Failed to cache rumor: {e:?}");
}
Ok(rumor)
}
/// Helper method to try unwrapping with different signers
async fn try_unwrap(
client: &Client,
device_signer: &Option<Arc<dyn NostrSigner>>,
gift_wrap: &Event,
) -> Result<UnwrappedGift, Error> {
// Try with the device signer first
if let Some(signer) = device_signer {
if let Ok(unwrapped) = try_unwrap_with(gift_wrap, signer).await {
return Ok(unwrapped);
};
};
// Try with the user's signer
let user_signer = client.signer().context("Signer not found")?;
let unwrapped = try_unwrap_with(gift_wrap, user_signer).await?;
Ok(unwrapped)
}
/// Attempts to unwrap a gift wrap event with a given signer.
async fn try_unwrap_with(
gift_wrap: &Event,
signer: &Arc<dyn NostrSigner>,
) -> Result<UnwrappedGift, Error> {
// Get the sealed event
let seal = signer
.nip44_decrypt(&gift_wrap.pubkey, &gift_wrap.content)
.await?;
// Verify the sealed event
let seal: Event = Event::from_json(seal)?;
seal.verify_with_ctx(&SECP256K1)?;
// Get the rumor event
let rumor = signer.nip44_decrypt(&seal.pubkey, &seal.content).await?;
let rumor = UnsignedEvent::from_json(rumor)?;
Ok(UnwrappedGift {
sender: seal.pubkey,
rumor,
})
}
/// Stores an unwrapped event in local database with reference to original
async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
let rumor_id = rumor.id.context("Rumor is missing an event id")?;
let author = rumor.pubkey;
let conversation = conversation_id(rumor);
let mut tags = rumor.tags.clone().to_vec();
// Add a unique identifier
tags.push(Tag::identifier(id));
// Add a reference to the rumor's author
tags.push(Tag::custom(
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::A)),
[author],
));
// Add a conversation id
tags.push(Tag::custom(
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)),
[conversation.to_string()],
));
// Add a reference to the rumor's id
tags.push(Tag::event(rumor_id));
// Add references to the rumor's participants
for receiver in rumor.tags.public_keys().copied() {
tags.push(Tag::custom(
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)),
[receiver],
));
}
// Convert rumor to json
let content = rumor.as_json();
// Construct the event
let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
.tags(tags)
.sign(&Keys::generate())
.await?;
// Save the event to the database
client.database().save_event(&event).await?;
Ok(())
}
/// Retrieves a previously unwrapped event from local database
async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result<UnsignedEvent, Error> {
let filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.identifier(gift_wrap)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
UnsignedEvent::from_json(event.content).map_err(|e| anyhow!(e))
} else {
Err(anyhow!("Event is not cached yet."))
}
}
/// Get the conversation ID for a given rumor (message).
fn conversation_id(rumor: &UnsignedEvent) -> u64 {
let mut hasher = DefaultHasher::new();
let mut pubkeys: Vec<PublicKey> = rumor.tags.public_keys().copied().collect();
pubkeys.push(rumor.pubkey);
pubkeys.sort();
pubkeys.dedup();
pubkeys.hash(&mut hasher);
hasher.finish()
}

220
crates/chat/src/message.rs Normal file
View File

@@ -0,0 +1,220 @@
use std::hash::Hash;
use common::EventUtils;
use nostr_sdk::prelude::*;
/// New message.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NewMessage {
pub room: u64,
pub gift_wrap: EventId,
pub rumor: UnsignedEvent,
}
impl NewMessage {
pub fn new(gift_wrap: EventId, rumor: UnsignedEvent) -> Self {
let room = rumor.uniq_id();
Self {
room,
gift_wrap,
rumor,
}
}
}
/// Message.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Message {
User(RenderedMessage),
Warning(String, Timestamp),
System(Timestamp),
}
impl Message {
pub fn user<I>(user: I) -> Self
where
I: Into<RenderedMessage>,
{
Self::User(user.into())
}
pub fn warning<I>(content: I) -> Self
where
I: Into<String>,
{
Self::Warning(content.into(), Timestamp::now())
}
pub fn system() -> Self {
Self::System(Timestamp::default())
}
fn timestamp(&self) -> &Timestamp {
match self {
Message::User(msg) => &msg.created_at,
Message::Warning(_, ts) => ts,
Message::System(ts) => ts,
}
}
}
impl From<&NewMessage> for Message {
fn from(val: &NewMessage) -> Self {
Self::User(val.into())
}
}
impl From<&UnsignedEvent> for Message {
fn from(val: &UnsignedEvent) -> Self {
Self::User(val.into())
}
}
impl Ord for Message {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
// System always comes first
(Message::System(_), Message::System(_)) => self.timestamp().cmp(other.timestamp()),
(Message::System(_), _) => std::cmp::Ordering::Less,
(_, Message::System(_)) => std::cmp::Ordering::Greater,
// For non-system messages, compare by timestamp
_ => self.timestamp().cmp(other.timestamp()),
}
}
}
impl PartialOrd for Message {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
/// Rendered message.
#[derive(Debug, Clone)]
pub struct RenderedMessage {
pub id: EventId,
/// Author's public key
pub author: PublicKey,
/// The content/text of the message
pub content: String,
/// Message created time as unix timestamp
pub created_at: Timestamp,
/// List of mentioned public keys in the message
pub mentions: Vec<PublicKey>,
/// List of event of the message this message is a reply to
pub replies_to: Vec<EventId>,
}
impl From<&Event> for RenderedMessage {
fn from(val: &Event) -> Self {
let mentions = extract_mentions(&val.content);
let replies_to = extract_reply_ids(&val.tags);
Self {
id: val.id,
author: val.pubkey,
content: val.content.clone(),
created_at: val.created_at,
mentions,
replies_to,
}
}
}
impl From<&UnsignedEvent> for RenderedMessage {
fn from(val: &UnsignedEvent) -> Self {
let mentions = extract_mentions(&val.content);
let replies_to = extract_reply_ids(&val.tags);
Self {
// Event ID must be known
id: val.id.unwrap(),
author: val.pubkey,
content: val.content.clone(),
created_at: val.created_at,
mentions,
replies_to,
}
}
}
impl From<&NewMessage> for RenderedMessage {
fn from(val: &NewMessage) -> Self {
let mentions = extract_mentions(&val.rumor.content);
let replies_to = extract_reply_ids(&val.rumor.tags);
Self {
// Event ID must be known
id: val.rumor.id.unwrap(),
author: val.rumor.pubkey,
content: val.rumor.content.clone(),
created_at: val.rumor.created_at,
mentions,
replies_to,
}
}
}
impl Eq for RenderedMessage {}
impl PartialEq for RenderedMessage {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Ord for RenderedMessage {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.created_at.cmp(&other.created_at)
}
}
impl PartialOrd for RenderedMessage {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Hash for RenderedMessage {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
/// Extracts all mentions (public keys) from a content string.
fn extract_mentions(content: &str) -> Vec<PublicKey> {
let parser = NostrParser::new();
let tokens = parser.parse(content);
tokens
.filter_map(|token| match token {
Token::Nostr(nip21) => match nip21 {
Nip21::Pubkey(pubkey) => Some(pubkey),
Nip21::Profile(profile) => Some(profile.public_key),
_ => None,
},
_ => None,
})
.collect::<Vec<_>>()
}
/// Extracts all reply (ids) from the event tags.
fn extract_reply_ids(inner: &Tags) -> Vec<EventId> {
let mut replies_to = vec![];
for tag in inner.filter(TagKind::e()) {
if let Some(id) = tag.content().and_then(|id| EventId::parse(id).ok()) {
replies_to.push(id);
}
}
for tag in inner.filter(TagKind::q()) {
if let Some(id) = tag.content().and_then(|id| EventId::parse(id).ok()) {
replies_to.push(id);
}
}
replies_to
}

Some files were not shown because too many files have changed in this diff Show More