Compare commits

...

369 Commits

Author SHA1 Message Date
Jon Staab 53954aae89 classnames tweak 2026-04-17 16:06:00 -07:00
userAdityaa 24aa62a503 chore: carify Pomade login errors with actionable invalid vs network messaging (#233)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-17 23:00:03 +00:00
Jon Staab 2618bb9c63 Fix centered layout 2026-04-17 14:58:59 -07:00
Prat_09 32a31045ef fix: Improve toggle switch placement in settings screen (#208) (#232)
Co-authored-by: Pratyush Mohanty <prat_09@noreply.coracle.social>
Co-committed-by: Pratyush Mohanty <prat_09@noreply.coracle.social>
2026-04-17 21:58:52 +00:00
DeveshSingh 56edad77a8 fix: added logic for password requirements on signup (#230)
Co-authored-by: deveshanim3 <deveshsingh6986@gmail.com>
Co-committed-by: deveshanim3 <deveshsingh6986@gmail.com>
2026-04-17 19:43:27 +00:00
priyanshu_bharti fdb604e350 Use type=email for signup/login email inputs (#225) (#228)
Co-authored-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
2026-04-17 18:55:04 +00:00
DeveshSingh 3c66dfd83c fix/wrong-message-offline (#222)
Co-authored-by: deveshanim3 <deveshsingh6986@gmail.com>
Co-committed-by: deveshanim3 <deveshsingh6986@gmail.com>
2026-04-17 18:24:55 +00:00
userAdityaa 81633b0a1e fix: vertical alignment of emoji and overflow buttons in shared event action row (#219)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-17 15:22:40 +00:00
Khushvendra 4a967de184 fix(chat): suppress programmatic scroll while user is scrolling (#132) (#216)
Co-authored-by: 1amKhush <khushvendras99@gmail.com>
Co-committed-by: 1amKhush <khushvendras99@gmail.com>
2026-04-16 23:20:17 +00:00
DeveshSingh 59961cbdb5 fix: supported nip overflow in SpaceRelayStatus.svelte (#215)
Co-authored-by: deveshanim3 <deveshsingh6986@gmail.com>
Co-committed-by: deveshanim3 <deveshsingh6986@gmail.com>
2026-04-16 21:36:14 +00:00
Jon Staab 95d9d8bf23 Bump version 2026-04-16 14:10:50 -07:00
Jon Staab 2fd9741a2b Fix safe area inset for chat fab 2026-04-16 14:08:25 -07:00
Jon Staab fe9c325580 Update universal links 2026-04-16 13:50:13 -07:00
Jon Staab 61e93d4071 Update changelog, bump version 2026-04-16 11:40:24 -07:00
Jon Staab 1e4a4e43dc remove dead virtualization code 2026-04-16 11:39:11 -07:00
Jon Staab e1a7b051bd Use welshman kinds 2026-04-16 11:34:59 -07:00
sakshamjain 7a7af58f5c feat: add native share support for space invites 2026-04-16 10:16:12 -07:00
Jon Staab 016ae86d50 Stop sending duplicate requests per room 2026-04-16 10:03:01 -07:00
Jon Staab 2bff060a5e Add thumbnail url 2026-04-16 10:03:01 -07:00
userAdityaa 68231504d0 fix: modal close button stacking above emoji picker on mobile (#211)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-16 15:38:25 +00:00
DeveshSingh 0658a8ee44 bug: fixed calender modal stacking issue (#209)
Co-authored-by: deveshanim3 <deveshsingh6986@gmail.com>
Co-committed-by: deveshanim3 <deveshsingh6986@gmail.com>
2026-04-16 14:55:37 +00:00
priyanshu_bharti 43fb3d35e6 Fix #202 slow-network invite timeout handling (#207)
Co-authored-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
2026-04-15 22:01:00 +00:00
Khushvendra 4cc1cc95ca Fix voice call cold-start timeout and preserve custom timeout message (#174) (#203)
Co-authored-by: 1amKhush <khushvendras99@gmail.com>
Co-committed-by: 1amKhush <khushvendras99@gmail.com>
2026-04-15 20:37:03 +00:00
Prat_09 964ef441ec Update relay description (#195) (#197)
Co-authored-by: Pratyush Mohanty <prat_09@noreply.coracle.social>
Co-committed-by: Pratyush Mohanty <prat_09@noreply.coracle.social>
2026-04-14 15:09:46 +00:00
priyanshu_bharti 796f37d320 Make space reordering discoverable with smoother drag animation (#171)
Co-authored-by: Priyanshu Bharti <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshu Bharti <bhartipriyanshustm@gmail.com>
2026-04-13 22:38:02 +00:00
nayan9617 b46fd94578 Use relay-provided member lists as source of truth (#191)
Co-authored-by: Nayan Patidar <nayan9617@noreply.coracle.social>
Co-committed-by: Nayan Patidar <nayan9617@noreply.coracle.social>
2026-04-13 21:12:49 +00:00
Jon Staab bdc8e75640 Fix search input width 2026-04-13 12:08:11 -07:00
Jon Staab ef08821796 remove VirtualItem 2026-04-13 10:35:26 -07:00
Jon Staab 9f386f6968 remove redundant room syncing logic 2026-04-11 10:20:50 -07:00
Khushvendra ec0b6a99e2 add room mentions and clickable room/relay refs 2026-04-11 10:13:23 -07:00
Khushvendra f6d9e52c6e fix: support native clipboard image paste on mobile (#181)
Co-authored-by: Khushvendra <khushvendras99@gmail.com>
Co-committed-by: Khushvendra <khushvendras99@gmail.com>
2026-04-11 16:38:06 +00:00
Jon Staab 90f86b833d Handle quotes in RoomItem. Fixes #188 2026-04-10 15:27:36 -07:00
userAdityaa 29bb33c26c publish kind 9 quote after room content creation for cross-client interoperability 2026-04-10 14:20:24 -07:00
Jon Staab c740bd21d4 Fix space page layout on Android by adding visible prop to SecondaryNav 2026-04-10 13:26:14 -07:00
Jon Staab 1d92709c76 perf: task-fix-list-virtualization changes 2026-04-10 12:44:01 -07:00
Jon Staab a42e1df1a7 Fix feed pagination logic 2026-04-10 12:40:28 -07:00
Jon Staab e33beee17d perf: task-fix-raf-derived-to-effect changes 2026-04-10 12:30:25 -07:00
Jon Staab b10ea04cb3 Fix Android push fallback: show all notifications, retry on failure 2026-04-10 12:23:32 -07:00
priyanshu_bharti e8c94177ca Support Aegis URL scheme for NIP-46 login (#161)
Co-authored-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
2026-04-10 19:04:34 +00:00
Jon Staab f1f2083c88 Remove unnecessary snapshots, format 2026-04-10 11:09:26 -07:00
Jon Staab f42889c3c2 Improve performance #182:
increase profile timer and chat search throttle delays
reduce GC pressure in derived stores
use requestIdleCallback for non-critical storage writes
batch repository update processing in feeds
2026-04-10 10:39:38 -07:00
Jon Staab a75e1f96eb Add .claude to gitignore 2026-04-10 10:14:01 -07:00
priyanshu_bharti 85c5293082 Raise message size limit in chat (#186)
Co-authored-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
2026-04-10 16:37:23 +00:00
Jon Staab 37efa6a62c Bump pomade 2026-04-10 09:24:22 -07:00
userAdityaa 1d5f91fb6c fix: realtime updates for room members and admins (#178)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-09 21:05:30 +00:00
userAdityaa ef18655776 make close button / backdrop work on direct invite link page (#177)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-09 20:03:08 +00:00
sakshamjain b786e858d9 correct inverted arrow icon in advanced section toggle (#180)
Co-authored-by: Saksham Jain <reach2saksham2004@gmail.com>
Co-committed-by: Saksham Jain <reach2saksham2004@gmail.com>
2026-04-09 19:57:15 +00:00
mplorentz f4ebc4e99e Video in calls (#135)
#135

This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing.

When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough.

I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc.

Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social>
Reviewed-on: coracle/flotilla#135
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
Jon Staab 65ca8a7fd8 Remove follow graph building 2026-04-08 09:46:56 -07:00
nayan9617 7f1e98dcb2 Fix fallback pull race after abort (#167)
Co-authored-by: nayan9617 <nayanp4925@gmail.com>
Co-committed-by: nayan9617 <nayanp4925@gmail.com>
2026-04-08 16:43:04 +00:00
priyanshu_bharti 4c19ee823b 73-video-thumbnails (#142)
This PR implements video thumbnails for `.mov`, `.webm`, and `.mp4` files in the `ContentLinkBlock` component.

Changes:
- Added the `poster` attribute to the `<video>` tag.
- Set the poster source to `{url}#t=1` to capture a clear preview frame at the 1-second mark.
- Verified locally that thumbnails are now correctly displayed instead of a black/empty box.

Closes #73

Reviewed-on: coracle/flotilla#142
Co-authored-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
2026-04-08 16:07:11 +00:00
Jon Staab 8e2dd8b278 Upgrade daisyui/tailwind 2026-04-07 15:31:35 -07:00
Jon Staab 8d35b3aad2 Chat tweaks 2026-04-07 10:40:45 -07:00
Prat_09 613cad31c0 add start chat FAB (#152)
Co-authored-by: Pratyush Mohanty <prat_09@noreply.coracle.social>
Co-committed-by: Pratyush Mohanty <prat_09@noreply.coracle.social>
2026-04-07 17:02:40 +00:00
Jon Staab 3779a90f26 Tweak to chat item menu buttons 2026-04-07 09:51:25 -07:00
theAnuragMishra 7470f28f31 fix spacing around messages (#159)
Co-authored-by: theAnuragMishra <theanuragmishra@noreply.coracle.social>
Co-committed-by: theAnuragMishra <theanuragmishra@noreply.coracle.social>
2026-04-07 16:50:53 +00:00
hodlbod 17fb4e780b Clean up drafts implementation (#164) 2026-04-07 13:06:29 +00:00
userAdityaa 30c2a6ef79 persist drafts in memory (#155)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-07 12:06:29 +00:00
Jon Staab 0547e9513f Small css tweak 2026-04-06 09:21:59 -07:00
nayan9617 70e5172f1b fix/tooltip-clipping (#156)
Co-authored-by: nayan9617 <nayanp4925@gmail.com>
Co-committed-by: nayan9617 <nayanp4925@gmail.com>
2026-04-06 16:14:08 +00:00
Jon Staab 61c568a112 Formatting 2026-04-06 09:09:01 -07:00
Jon Staab ae2ba6f44d Tweak toast close button 2026-04-06 09:08:26 -07:00
Jon Staab f84006fbe4 Tweak button on profile page 2026-04-06 09:08:26 -07:00
priyanshu_bharti fed34a2747 show space name on hover in primary nav (#129) (#136)
Co-authored-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
2026-04-06 16:08:02 +00:00
userAdityaa 80df16f97b feat: redesign toast notifications for UX (#148)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-04 16:45:49 +00:00
junaiddshaukat 18cb245599 Remove room/space leave indications (#149)
Co-authored-by: junaiddshaukat <junaiddshaukat@noreply.coracle.social>
Co-committed-by: junaiddshaukat <junaiddshaukat@noreply.coracle.social>
2026-04-04 16:28:11 +00:00
Jon Staab fd6cc84be6 Simplify chat compose layout 2026-04-04 09:02:52 -07:00
Jon Staab 9311cab3b2 Move away from fixed positioned page elements because they act squirrely on android 2026-04-03 17:16:47 -07:00
userAdityaa fceccf47be fix(ui): hide report badge for non-admin users (#147)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-03 23:54:20 +00:00
Jon Staab fe20fbfd28 Add polls 2026-04-03 10:56:00 -07:00
junaiddshaukat 4f3a2a1660 Add space search to recent activity page (#59) (#119)
Co-authored-by: junaiddshaukat <junaiddshaukat@noreply.coracle.social>
Co-committed-by: junaiddshaukat <junaiddshaukat@noreply.coracle.social>
2026-04-03 16:58:35 +00:00
Jon Staab 1c8457a4bf Fix notification badge on mobile nav 2026-04-02 16:47:32 -07:00
Jon Staab 8710043a02 Fix env conventions again 2026-04-02 14:01:09 -07:00
Jon Staab dc46b42cb6 Fix platform logo 2026-04-02 13:49:01 -07:00
Jon Staab 2f1972e70a Add contributing file 2026-04-02 13:31:37 -07:00
mplorentz c5fcf12165 Fix error toast when failing to join room. (#113)
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-02 19:35:46 +00:00
mplorentz 61ed632579 Change audio devices in call (#112)
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-02 19:33:48 +00:00
Jon Staab 86f4b75c52 Merge subs to avoid hitting limits 2026-04-02 11:49:26 -07:00
bhavishy2801 b26ab916d5 feat: use NIP-50 relay-side search with scope selection (#114)
Co-authored-by: Bhavishy <bhavishyrocker2801@gmail.com>
Co-committed-by: Bhavishy <bhavishyrocker2801@gmail.com>
2026-04-02 18:49:18 +00:00
nayan9617 c882198206 fix: respect VITE_PLATFORM_LOGO with fallback (#116)
Co-authored-by: nayan9617 <nayanp4925@gmail.com>
Co-committed-by: nayan9617 <nayanp4925@gmail.com>
2026-04-02 17:52:44 +00:00
Jon Staab 4aef27ffd5 Fix xcode version 2026-04-02 07:08:16 -07:00
Jon Staab cf4e3f5fc6 Bump version 2026-04-02 07:05:34 -07:00
Jon Staab 57eb919c83 Bump welshman to fix poll behavior 2026-04-02 07:02:57 -07:00
Jon Staab 85cfaf2bc9 Remove redundant join space button 2026-04-02 06:29:37 -07:00
Jon Staab 25a69a8191 Small tweaks 2026-04-01 14:07:29 -07:00
Jon Staab 6feeb23b1f Bump welshman 2026-04-01 11:39:05 -07:00
Jon Staab 4b92ffe3c5 remove duplicate spaces button 2026-04-01 11:33:56 -07:00
Jon Staab 823a9c3271 Combine discover and space list into a single page 2026-03-31 14:24:09 -07:00
Jon Staab fe89df2aa3 Fix some chat related bugs 2026-03-31 11:25:59 -07:00
Jon Staab 97ff8ff802 Bump version 2026-03-31 09:55:04 -07:00
Jon Staab a10a9e7043 Bump pomade 2026-03-31 09:53:30 -07:00
Jon Staab 4f42abc2ff Bump version 2026-03-30 15:22:01 -07:00
Jon Staab fe042c88b8 Apply safe area insets to new messages button 2026-03-30 15:15:41 -07:00
Jon Staab 55e3a31b61 Show notifications on non-nip29 chat 2026-03-30 14:29:24 -07:00
Jon Staab 5760be4313 Add back button to chat detail 2026-03-30 14:24:40 -07:00
Jon Staab 2fd7556a52 Fix new messages button, improve room load 2026-03-30 14:20:30 -07:00
Jon Staab e8ed9cd379 Fix chat new messages fixed button 2026-03-30 13:22:22 -07:00
Jon Staab eeeb3c96d2 Bump welshman 2026-03-30 11:46:12 -07:00
Jon Staab 2da5dee6bd Add pow to wrapped messages 2026-03-30 11:46:12 -07:00
Jon Staab a66193ff45 Fix some display bugs 2026-03-30 11:46:12 -07:00
Jon Staab 55131ba7ce Remove replaceState from SpaceMenu since we're never in a drawer any more 2026-03-30 11:46:12 -07:00
Jon Staab df6282d2ba Fix nav overflow on mobile 2026-03-30 11:46:12 -07:00
Jon Staab 6ebe792ce5 Show notification badge on voice room item 2026-03-30 11:46:11 -07:00
Jon Staab 6c9bdb2ccd Detect blossom support using supported_nips 2026-03-30 11:46:11 -07:00
Jon Staab bc94c705f3 Make space syncing more robust 2026-03-30 11:46:11 -07:00
Jon Staab 2b9b4da2cc Add all rooms to notifications 2026-03-30 11:46:11 -07:00
Jon Staab 090070d1f9 Add dm relay 2026-03-30 11:46:11 -07:00
mplorentz 16a73f27c9 Add a dialog before joining voice rooms (#109)
After using the voice rooms more since we removed the option for voice-only rooms I think you were right to suggest a dialog box before joining rooms. It felt far to clunky to have to join the voice call any time you just wanted to try to view room members, edit room settings, or just view the recent text chat.

This adds a dialog that allows the user to decline to join the call but still access the text part of the room along with associated settings and controls. It also acts as another confirmation step before turning on the user's microphone, and allows them to choose an audio input so they don't have to mess with the (generally terrible) browser controls for doing so. We should probably have controls to change your audio input and output from within the call as well, but I think this is enough for an MVP.

![Screenshot 2026-03-27 at 11.10.53 AM.png](/attachments/3ac271a6-5d17-4063-9ac6-3e5bdef10ccf)

Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social>
Reviewed-on: coracle/flotilla#109
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-03-27 19:02:56 +00:00
mplorentz 82245d895c Voice Room Membership Error (#106)
Before this we were showing "Failed to join voice room" if the relay rejected our request for a livekit token because we aren't a member of the room. Now it shows the error "Failed to join voice room: you must be a member."

Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social>
Reviewed-on: coracle/flotilla#106
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-03-27 17:45:42 +00:00
Jon Staab 610b8dd171 Use secure storage for session data 2026-03-23 17:51:51 -07:00
Jon Staab f5b1e91378 Handle json parsing errors on file upload 2026-03-23 14:34:45 -07:00
Jon Staab 1de6d7a874 Set up default messaging relays 2026-03-23 14:21:04 -07:00
Jon Staab b716f3f792 Improve dm relay defaults and warnings 2026-03-23 12:53:24 -07:00
Jon Staab 75053bbbb1 Create health check framework 2026-03-23 11:17:45 -07:00
Jon Staab f9c7ed4936 Move relay action items to top of page 2026-03-19 12:41:23 -07:00
hodlbod 1f5be54cb1 Migrate Reports dialog to ActionItems dialog, add room join requests to queue 2026-03-19 15:40:34 +00:00
hodlbod 0761cdd28f Add android fallback for background push notifications (#102) 2026-03-19 15:32:32 +00:00
Jon Staab 7e2a0e9d5f Show notification badges regardless of favorite status 2026-03-17 15:11:02 -07:00
Jon Staab 7ae887561d Remove bad signers, fix some ui bugs 2026-03-17 15:05:46 -07:00
Jon Staab baa1d49b3a Tweak mobile nav 2026-03-17 15:03:40 -07:00
Jon Staab 58a6be911a Hide close button in dialog if in a noEscape modal 2026-03-17 14:55:40 -07:00
mplorentz 368f0b048b Expect pubkey in kind 39004 (#101)
This adjusts our implementation of the Livekit presence event to match the NIP (https://github.com/nostr-protocol/nips/pull/2238#issuecomment-4057645310). Specifically we now expect the user's Nostr pubkey in the `participant` tag instead of the livekit identity string.

I also fixed a bug I found where a malformed `participant` tag would crash the rendering of VoiceWidget, causing it to appear frozen.

There is a corresponding zooid PR [here](https://github.com/coracle-social/zooid/pull/11)

Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social>
Reviewed-on: coracle/flotilla#101
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-03-17 19:38:10 +00:00
Jon Staab 10894e17a5 Fix phantom badges 2026-03-16 13:46:00 -07:00
Jon Staab ec8a7a40e2 Fix room icon 2026-03-16 13:42:01 -07:00
mplorentz ce30820108 feature/23-voice-room/poc (#93)
Add voice rooms

Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-03-16 20:38:05 +00:00
Jon Staab 147c756cc1 Update readme to point to .env.local 2026-03-16 13:36:06 -07:00
Jon Staab c7fb404404 Add -s flag to readme 2026-03-13 13:23:31 -07:00
Jon Staab 2546146ca8 Tweak back button, hide on desktop 2026-03-13 09:11:08 -07:00
Jon Staab ffa776fd42 Make mobile check on relay page reactive 2026-03-12 17:31:28 -07:00
Jon Staab a59ffb8758 Fix some icons, add privacy nav item, add close button to modal dialog, make settings menu nicer 2026-03-12 17:21:53 -07:00
Jon Staab 9e74c94871 Show navigation on space landing on mobile 2026-03-12 16:44:22 -07:00
Jon Staab 77294e7f1c Factor primary nav spaces into its own component, fix non-nip29 default page 2026-03-12 15:08:31 -07:00
Jon Staab 57f2f4a619 Use new space icon 2026-03-12 14:45:19 -07:00
Jon Staab 1df2284ea3 Return more details about notification registration failure 2026-03-12 14:07:52 -07:00
Jon Staab 189af077e7 Fix file uploads on android 2026-03-12 13:51:05 -07:00
Jon Staab 10e4d83bce Hide add member button for non-members 2026-03-12 12:35:08 -07:00
Jon Staab 5d6661f964 Speed up boot, prune stores 2026-03-12 11:33:04 -07:00
Jon Staab e6e11bb8f2 Massive indexeddb optimization 2026-03-12 11:01:15 -07:00
Jon Staab 0e65e834da Bump welshman 2026-03-12 08:20:15 -07:00
Jon Staab 19f532c12e Allow nested modals 2026-03-11 16:29:24 -07:00
Jon Staab bfc997ba37 Tweak icon picker modal 2026-03-11 16:15:37 -07:00
Jon Staab 99966a976e Overhaul relay settings page 2026-03-11 15:58:05 -07:00
Jon Staab cd54bc2880 Add up/edit to chats 2026-03-10 15:46:59 -07:00
Jon Staab ffdd689331 Add another pomade signer 2026-03-10 11:09:59 -07:00
Jon Staab af41d81981 Add pomade signers 2026-03-10 10:15:53 -07:00
Jon Staab 10d28ed364 Update zapstore.yaml 2026-03-09 21:12:51 -07:00
Jon Staab b02f4bd53a Update version 2026-03-09 21:12:51 -07:00
Jon Staab 7ce8e3dbe6 Fix classified images 2026-03-09 21:12:51 -07:00
Jon Staab 2446d5cdb8 Add StringMultiInput for OTPs 2026-03-09 21:12:51 -07:00
Jon Staab d015018a16 Fix up edit 2026-03-09 21:12:51 -07:00
Jon Staab 6231c75e34 Handle prompt-with-rationale 2026-03-09 21:12:51 -07:00
Jon Staab 2f3bc6cc6f Handle profile update errors 2026-03-09 21:12:50 -07:00
Jon Staab 16c6015919 Move from .env to .env.local 2026-03-09 21:12:50 -07:00
Jon Staab e6b291cc68 Tweak some error messages 2026-03-09 21:12:37 -07:00
Jon Staab ae523c1ca6 Refactor pomade, add password reset flow 2026-03-09 21:12:37 -07:00
Jon Staab 7c86c1477f Add LogInSelect step for disambiguating pomade sessions 2026-03-09 21:12:37 -07:00
Jon Staab 71f162f20d Update pomade implementation 2026-03-09 21:12:37 -07:00
mplorentz eeacaca725 Fix a docker rebuild issue (#88)
The Docker build wasn't making use of docker's cache because the .git directory was being copied into the build context. This means that even if the app did not change, if anything in git changed then docker would rebuild the entire app.

This excludes the .git folder from the docker build, instead relying on the user to pass in the build hash at build time. Which is annoying but I don't think there's a better way around it.

This was annoying me because I am deploying a self-hosted version of flotilla from a git branch via ansible and it was rebuilding flotilla every time.

Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social>
Reviewed-on: coracle/flotilla#88
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-03-09 21:12:35 -07:00
Jon Staab af52ee25eb Bring back some notification badges 2026-03-09 21:12:10 -07:00
Jon Staab eef32ca11e Make sync logic more robust 2026-03-09 21:12:10 -07:00
Jon Staab 1ae821bff8 Bump welshman 2026-03-09 21:12:10 -07:00
Jon Staab 65483a6ef0 Support unban/unallow 2026-03-09 21:12:10 -07:00
Jon Staab 606a9343d9 Fix WalletPay 2026-03-09 21:12:10 -07:00
Jon Staab 7dfa6538be Bump welshman 2026-03-09 21:12:10 -07:00
Jon Staab 476d010ebe Blobify images so users can open them easier 2026-03-09 21:12:10 -07:00
Jon Staab 96d2efebc8 Add manual invoice payment 2026-03-09 21:12:10 -07:00
Jon Staab f60f5af424 Update link_deps 2026-03-09 21:12:10 -07:00
Jon Staab 3da0334083 Fix enter selecting an option when there is no term. Closes #84 2026-03-09 21:12:10 -07:00
triesap c970038943 Bootstrap Tauri desktop shell for evaluation (#66)
Adds a minimal Tauri desktop bootstrap. Run with: pnpm run tauri:dev

Reviewed-on: coracle/flotilla#66
Co-authored-by: triesap <tyson@radroots.org>
Co-committed-by: triesap <tyson@radroots.org>
2026-03-09 21:12:10 -07:00
triesap 4000477bdb Classifieds tags (#18) (#65)
Closes #18

Reviewed-on: coracle/flotilla#65
Co-authored-by: triesap <tyson@radroots.org>
Co-committed-by: triesap <tyson@radroots.org>
2026-03-09 21:12:10 -07:00
triesap ba11d53922 Show wallet status when wallet is unreachable 2026-03-09 21:12:10 -07:00
Jon Staab beef606024 Remove capacitor plugin from overrides 2026-03-09 21:12:10 -07:00
Jon Staab 2adf64da55 Bump welshman and pomade 2026-03-09 21:12:10 -07:00
Jon Staab fd3fb8573c Update nostr signer capacitor plugin 2026-03-09 21:12:09 -07:00
Jon Staab e0d94d9794 Fix safe area inset for modal footer 2026-03-09 21:12:09 -07:00
Jon Staab 7d049150a0 Bump nip55 signer 2026-03-09 21:12:09 -07:00
Jon Staab 527ef59adc Update pomade version 2026-03-09 21:12:09 -07:00
Jon Staab b39775daef Fix svgs with 302 redirects on safari 2026-03-09 21:12:09 -07:00
Jon Staab 4bdb21560a Fix mask-repeat property 2026-03-09 21:12:09 -07:00
Jon Staab 797a9c32aa Refine space join dialogs and discover page 2026-03-09 21:12:09 -07:00
mplorentz bc864b29f8 Reopen the last DM that was open when navigating back to chat (#81)
#60

Co-authored-by: mplorentz <mplorentz@users.noreply.github.com>
Reviewed-on: coracle/flotilla#81
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-03-09 21:12:09 -07:00
Jon Staab 482121db5c Add content-visibility class 2026-03-09 21:12:09 -07:00
Jon Staab 0fa26c8d0a Get rid of ChatEnable, automatically enable unwrapping when the user first visits the dms page. Closes #72 2026-03-09 21:12:09 -07:00
Jon Staab f5c768d6a7 Enable auth for relays we're publishing to 2026-03-09 21:12:09 -07:00
triesap c43544734a Drag and drop space icons (#17) (#78)
Closes #17

Co-authored-by: Jon Staab <shtaab@gmail.com>
Reviewed-on: coracle/flotilla#78
Co-authored-by: triesap <tyson@radroots.org>
Co-committed-by: triesap <tyson@radroots.org>
2026-03-09 21:12:09 -07:00
Jon Staab 86d99916f7 re-order some menu items 2026-03-09 21:12:09 -07:00
Jon Staab 135dbc8789 Fix iOS zoom bug 2026-03-09 21:12:09 -07:00
Jon Staab fc14de9b0f Reset to old home page 2026-03-09 21:12:09 -07:00
Jon Staab c77197d959 Continue working on feed page 2026-03-09 21:12:09 -07:00
Jon Staab 56dddbdd86 Add better muting, add EventReducer 2026-03-09 21:12:09 -07:00
mplorentz cbafcf6939 Add back button to settings menu 2026-03-09 21:12:09 -07:00
Jon Staab 4b156ee699 Work on feed page 2026-03-09 21:12:09 -07:00
Jon Staab a4e883b09a Prevent error loop on images 2026-03-09 21:12:09 -07:00
triesap b114a724e2 Page titles (#16) (#62)
Closes #16

Reviewed-on: coracle/flotilla#62
Co-authored-by: triesap <tyson@radroots.org>
Co-committed-by: triesap <tyson@radroots.org>
2026-03-09 21:12:09 -07:00
Jon Staab 621c0d839c tweak how at works 2026-03-09 21:12:09 -07:00
Jon Staab 021c1fc7c4 Fix scroll to event behavior 2026-03-09 21:12:08 -07:00
Jon Staab bda91080ab Pin scroll position to at'd event until user scrolls 2026-03-09 21:12:08 -07:00
Jon Staab a9828be25c Simplify goToEvent 2026-03-09 21:12:08 -07:00
Jon Staab dde9dbfbfe Add forward scrolling to makeMakeFeed 2026-03-09 21:12:08 -07:00
Jon Staab ca7d126a3c Make createScroller honor reverse param 2026-03-09 21:12:08 -07:00
Jon Staab 7f6450375b Fix duplicate ids in chat 2026-03-09 21:12:08 -07:00
Jon Staab c9954db3fe Use compressorjs-next 2026-03-09 21:12:08 -07:00
Jon Staab 3d268f1f9d Refactor SpaceSearch into its own component 2026-03-09 21:12:08 -07:00
Ben 66a7a2a7af Space search 2026-03-09 21:12:08 -07:00
Jon Staab 7823e1d803 Fix editing messages with html tags 2026-03-09 21:12:08 -07:00
Jon Staab d5e91ce874 Fix DM media detection 2026-03-09 21:12:08 -07:00
Jon Staab 6f32c1932f Make hover target for menu button more reasonable 2026-03-09 21:12:08 -07:00
Jon Staab cb06c4e954 Watch tracker in feed utils 2026-03-09 21:12:08 -07:00
Jon Staab 9188c0a8bc Revert makeFeed changes 2026-03-09 21:12:08 -07:00
Jon Staab 30653fe344 Clean up report item design, bad/restore user actions, space description input, add feed to home page 2026-03-09 21:12:07 -07:00
Jon Staab 5bb55c453f Tweak wallet page 2026-03-09 21:12:07 -07:00
Jon Staab 3024e08ca5 Fix makeFeed (maybe) 2026-03-09 21:12:07 -07:00
Jon Staab aaf1f25167 Tweak room detail 2026-03-09 21:12:07 -07:00
Jon Staab aabbb758a4 Fix scroll to bottom button safe insets 2026-03-09 21:12:07 -07:00
Jon Staab d824f928b5 Disable wallet on ios 2026-03-09 21:12:07 -07:00
Jon Staab 445ed27eb8 Add rewrite to dockerfile 2026-02-17 12:01:12 -08:00
Jon Staab 21f3970ca8 Use explicit image name in workflow file 2026-02-17 11:48:52 -08:00
Jon Staab 919fe29ffb Update docker image creation for gitea ci 2026-02-17 10:45:39 -08:00
Jon Staab 3b2870a318 Bump version 2026-02-10 17:14:02 -08:00
Jon Staab 1076e8531c Slightly tweak notifications function 2026-02-10 17:04:21 -08:00
Jon Staab 72f2effda4 Clean up modals 2026-02-10 11:39:29 -08:00
Jon Staab 7566f56858 Fix tippy placement for icon picker 2026-02-09 17:40:10 -08:00
Jon Staab c1f9c9e25e Bump welshman 2026-02-09 17:29:30 -08:00
Jon Staab 380a52efb3 Use space url as relay hint 2026-02-09 17:24:55 -08:00
Jon Staab 028c3ba92b Fix badge showing on current page 2026-02-09 17:11:03 -08:00
Jon Staab f80aba33f1 Optimistically load messaging relays 2026-02-09 16:57:44 -08:00
Jon Staab bb15c9e2d0 Fix last activity reactivity 2026-02-09 13:21:38 -08:00
Jon Staab 518c80bb1d Fix indexeddb failure 2026-02-09 11:59:58 -08:00
Jon Staab 0067d049e6 Tweak signer error 2026-02-09 11:44:50 -08:00
Jon Staab bf60dd24aa Handle sai in modals on small screens 2026-02-09 11:44:50 -08:00
Jon Staab 38d9cc4892 Add modal body to some menus 2026-02-09 11:44:50 -08:00
hodlbod c4f2f55617 Simplify notification badges, improve performance (#57)
Co-authored-by: Jon Staab <shtaab@gmail.com>
2026-02-09 11:44:32 -08:00
Jon Staab 8f73fb85e9 Show space url in page top bar 2026-02-09 08:42:30 -08:00
Jon Staab 3bd126c11d Fix a modal 2026-02-06 16:27:57 -08:00
Jon Staab 7e7aba06a6 Fix safe area insets 2026-02-06 14:57:36 -08:00
Jon Staab 2bf00f7ddc Tweak room/space icon buttons 2026-02-06 14:57:35 -08:00
Jon Staab 24b88e4ac0 Fix calendar detail 2026-02-06 14:57:35 -08:00
Jon Staab 3df3130395 remove pomade from links 2026-02-06 14:57:35 -08:00
Jon Staab c0c388d1b9 Tweak syncing so it works better for picky relays 2026-02-06 14:57:35 -08:00
Jon Staab 9f27cc61da Ignore another invite code error 2026-02-06 14:57:35 -08:00
Jon Staab 8fa1987ec0 Slight tweaks to wallet receive 2026-02-05 13:23:56 -08:00
Jon Staab 39eae42b05 Make space/room images a little bigger 2026-02-05 13:20:18 -08:00
Tyson Lupul 4dfbb437f9 Wallet receive flow (#15) (#52)
* Pin sharp via pnpm override, add wallet receive

* Revert toast success styling

* Route receive through wallet connect

* Simplify receive invoice validation

* Polish receive modal layout

* Clarify NWC client config

* Adjust wallet action layout on mobile
2026-02-05 20:51:59 +00:00
Jon Staab f132d22308 Revert safari fix (merged into nostr-editor) 2026-02-04 13:18:18 -08:00
Jon Staab b7dd2ff8b4 Bump welshman 2026-02-04 13:16:00 -08:00
Jon Staab b6b78591bc Update push impl 2026-02-04 10:37:50 -08:00
Jon Staab ec54a0dbce Use item components on recent page 2026-02-04 09:07:26 -08:00
Jon Staab 8793912b65 Prompt to add space members when adding room members 2026-02-03 17:38:22 -08:00
Jon Staab 70c430ddc2 Add classified status 2026-02-03 17:09:30 -08:00
Jon Staab 815dbba497 Use address for page param for replaceable events 2026-02-03 16:35:32 -08:00
Jon Staab dc5bac67aa Add image uploads to classifieds 2026-02-03 14:18:58 -08:00
Jon Staab 5427fd7860 Add a currency input 2026-02-03 13:25:24 -08:00
Jon Staab 119c09d730 Add classified listings 2026-02-03 12:43:36 -08:00
Jon Staab 1da6833c71 Rework recent activity page 2026-02-03 09:51:33 -08:00
Jon Staab 4b8cf53731 Rework recent activity page 2026-02-02 15:36:50 -08:00
Jon Staab d646ddd91d Fix edit 2026-02-02 14:33:45 -08:00
Jon Staab 764719afde Fix some notification related bugs 2026-02-02 14:29:12 -08:00
Jon Staab 75ec7688b1 Fix image uploads on ios 2026-02-02 14:06:36 -08:00
Jon Staab 7fc508603f Bring back service worker 2026-02-02 10:35:05 -08:00
Jon Staab fb2d78fd57 Rework modal header structure 2026-02-02 10:09:14 -08:00
Jon Staab 4480132c74 Add sticky submit buttons to settings pages 2026-02-02 09:51:36 -08:00
Jon Staab 38c0a9d403 Re work modal scrolling 2026-01-30 15:36:20 -08:00
Jon Staab 4169db33e6 Rework alert settings and UI 2026-01-30 09:13:50 -08:00
Jon Staab ee48072137 Add AGENTS.md 2026-01-29 15:09:26 -08:00
Jon Staab a3c1a5c731 Prevent icon picker from going off screen 2026-01-29 13:52:36 -08:00
Jon Staab e74f922e8d Fix tippy falling off the page 2026-01-29 13:52:36 -08:00
Jon Staab 16cd90f7b7 Refine discover page a bit to avoid slowness 2026-01-29 13:52:36 -08:00
Jon Staab e2ba10d224 Fix has alerts store 2026-01-29 13:52:36 -08:00
Jon Staab 459e9359db Disable macos build 2026-01-29 13:52:36 -08:00
Jon Staab d2a044f958 Small ui fixes 2026-01-29 13:52:36 -08:00
Jon Staab 2fbcd644d0 Tag event author when tagging parent event 2026-01-29 13:52:36 -08:00
Jon Staab cf8e736f46 Handle encrypted notifications 2026-01-29 13:52:04 -08:00
Jon Staab d4378731ae Fix a few bugs with push notifications 2026-01-28 14:08:15 -08:00
Jon Staab 000344a942 Fixing bugs with push notifications 2026-01-28 13:36:19 -08:00
Jon Staab bf6abd301c refactor notification syncing 2026-01-27 17:15:22 -08:00
Jon Staab 143a1dd39b Update notification subscriptions reactively 2026-01-27 13:38:24 -08:00
Jon Staab 9b3a8258ce Disable alerts on logout 2026-01-26 10:08:43 -08:00
Jon Staab 646b8f8736 Rework subscription storage 2026-01-23 16:51:02 -08:00
Jon Staab 2528e4acad Clean up and fix push notifications implementation 2026-01-23 15:35:54 -08:00
Jon Staab 286d939097 Moar upgrades 2026-01-23 10:53:50 -08:00
Jon Staab ca3d661830 Generally just refactor alerts, upgrade some deps 2026-01-23 10:05:47 -08:00
Jon Staab 63fee653e8 Add muted rooms, rework alert settings 2026-01-22 12:49:09 -08:00
Jon Staab 9da2473976 Add apns/fcm push notifications with new architecture 2026-01-21 16:20:48 -08:00
Jon Staab 6d1eeacc49 Add new alerts 2026-01-20 13:42:58 -08:00
Jon Staab f85748fef9 Remove old alerts 2026-01-19 16:33:49 -08:00
Jon Staab 9f34b33b7e Remove sourcemaps command 2026-01-19 10:40:56 -08:00
Jon Staab 1510f39a8a Bump ios version 2026-01-19 10:07:44 -08:00
Jon Staab bbbe011482 Publish default relay selections on signup 2026-01-16 16:11:45 -08:00
Jon Staab 82ab7a043f Remove glitchtip integration 2026-01-16 15:19:52 -08:00
Jon Staab 798253a50e Bump welshman 2026-01-16 15:07:55 -08:00
Jon Staab 52432ca068 Add sign in with private key 2026-01-16 14:25:44 -08:00
Jon Staab b3f1d8464b Add authentication policy setting 2026-01-16 13:49:35 -08:00
Jon Staab 87bb62b359 Add support for blocked relays 2026-01-16 13:10:48 -08:00
Jon Staab 3f914d02cc Fix signer disconnection flash, nav icon sizes 2026-01-16 11:33:03 -08:00
Jon Staab d1db77d0f5 Bump version 2026-01-16 11:01:24 -08:00
Jon Staab 6aa297c1a4 Rework onboarding flow, add recovery 2026-01-16 11:01:07 -08:00
Jon Staab f3647e9bc1 Use simple OTPs 2026-01-16 11:01:07 -08:00
Jon Staab 5b43c62f2d Remove pomade signers 2026-01-16 11:01:07 -08:00
Jon Staab 23ffb15a8d Fix incorrect secret being downloaded 2026-01-16 11:01:07 -08:00
Jon Staab adb2ce4846 Split key recovery components, bump deps 2026-01-16 11:01:06 -08:00
Jon Staab cdee6ca743 Add pomade key recovery 2026-01-16 11:00:46 -08:00
Jon Staab fe30aa4af2 Fix ContentLinkInline 2026-01-16 11:00:45 -08:00
Jon Staab 9943728eab Add pomade session list 2026-01-16 11:00:00 -08:00
Jon Staab 8ae7cf05cc Fix profile publishing on email sign up 2026-01-16 11:00:00 -08:00
Jon Staab a7c944e8ef Tweak breakpoint for field inline 2026-01-16 11:00:00 -08:00
Jon Staab 102339d7e8 Add link_peers script 2026-01-16 10:59:59 -08:00
Jon Staab 9a0ad0c663 Improve space join flow 2026-01-16 10:59:54 -08:00
Jon Staab f86afc08fa Normalize relay URLs 2026-01-16 10:59:46 -08:00
Jon Staab cd1b328b1b Add pomade signing 2026-01-16 10:59:45 -08:00
Jon Staab 48f2bb1c75 Bump gradle 2026-01-16 10:59:30 -08:00
Jon Staab d416fe913e Fix memory leak, notification badge not showing 2026-01-16 10:59:29 -08:00
Jon Staab 7f8744725c Improve signer status 2026-01-16 10:59:25 -08:00
Jon Staab e5d1b82a9d Fix chat list responsiveness 2026-01-16 10:59:22 -08:00
Jon Staab 619cf2e134 Update default relays 2026-01-16 10:59:16 -08:00
Jon Staab 28b522f015 Report pending signer to user 2026-01-16 10:59:10 -08:00
Jon Staab 39233f261e Force reload relay more simply 2026-01-16 10:59:05 -08:00
Jon Staab 00f0127caf Tweak room edit form 2026-01-16 10:59:03 -08:00
Jon Staab f69b575381 Fix some duplicates in eaches 2026-01-16 10:58:57 -08:00
Jon Staab 986973a605 Accept hex pubkeys/npubs/nprofiles in ProfileMultiSelect 2026-01-16 10:58:55 -08:00
Jon Staab 0d6b4591f1 Hide tooltips on mobile, sort comments ascending, make video embeds rounded 2026-01-16 10:58:40 -08:00
Jon Staab 2c62749d9b Attempt to fix new messages button 2026-01-16 10:56:18 -08:00
Jon Staab 4be4288ef0 Fix phantom notifications on mobile 2025-12-11 10:27:10 -08:00
Jon Staab c7eec167cf Fix scroll down z index 2025-12-08 09:27:38 -08:00
Jon Staab 7bae956ffa Release 1.6.2 2025-12-08 09:22:49 -08:00
Jon Staab a2f59a5b1b Fix some modal bugs 2025-12-08 09:19:41 -08:00
Jon Staab df56af9b0e Bump version 2025-12-05 09:51:15 -08:00
Jon Staab 83f7f9584f Fix duplicate rooms 2025-12-04 17:06:50 -08:00
Jon Staab a2d440e54f Fix dialog z index 2025-12-04 16:01:39 -08:00
Jon Staab 4132e8449b Fix recent missing events in feeds 2025-12-04 15:56:05 -08:00
Jon Staab ee444416e4 Fall back to file name as hash for images 2025-12-04 14:37:59 -08:00
Jon Staab 10c12c3c48 Improve time based chat partitioning 2025-12-04 14:29:12 -08:00
Jon Staab db3775ae99 Fix timezone parsing in AlertAdd 2025-12-04 11:20:54 -08:00
Jon Staab 393acce884 Fix removing non-normalized urls 2025-12-02 17:27:14 -08:00
Jon Staab 68fe663730 Fix chat content bottom offset when keyboard is open 2025-12-02 17:20:10 -08:00
Jon Staab f65a4b0db0 Handle relay urls in content and link within the app 2025-12-02 17:09:56 -08:00
Jon Staab cdfb502e6e Fix skinny profile circles 2025-12-02 13:49:04 -08:00
Jon Staab 1a2c83e49b Bump version 2025-12-02 13:38:03 -08:00
Jon Staab e6c7a675a9 Bump welshman 2025-12-02 13:24:43 -08:00
Jon Staab 69c04f29f4 Tweak zap button 2025-12-02 09:31:38 -08:00
Jon Staab 04c6f9b4fe Add date to chats 2025-12-01 11:09:26 -08:00
Jon Staab 86ec12a9db Tweak some mobile menu components 2025-12-01 11:04:11 -08:00
Jon Staab 72b3111c64 Refine sync 2025-12-01 10:56:37 -08:00
Jon Staab 6709c91779 Fix discover social proof 2025-12-01 10:26:43 -08:00
Jon Staab bb6e7495f5 Add editor props from nostr-editor 2025-12-01 08:45:35 -08:00
Jon Staab df17929681 Fix new messages indicator 2025-11-25 17:06:21 -08:00
Jon Staab e083719ceb Hide nav when keyboard is open 2025-11-25 15:43:21 -08:00
Jon Staab bfdc69f18c Fix chats 2025-11-25 15:05:45 -08:00
Jon Staab e7ae20afb7 Fix content type nav items 2025-11-25 14:13:28 -08:00
Jon Staab 229d92055f Debounce search 2025-11-25 11:55:32 -08:00
Jon Staab 64c77cfd13 Migrate to new welshman stores 2025-11-21 12:40:59 -08:00
Jon Staab 3a63894562 Switch wording to messaging from inbox 2025-11-20 15:12:16 -08:00
Jon Staab 1d272f8b37 Tweak nav icon size 2025-11-14 15:02:23 -08:00
Jon Staab bac433b640 Re-work storage adapter a bit 2025-11-14 14:59:27 -08:00
Jon Staab 62f573eac0 Merge report detail components 2025-11-14 11:36:51 -08:00
Jon Staab b3ea62c53c Remove landlubber link 2025-11-13 17:01:23 -08:00
Jon Staab b0731503a8 Fix indexeddb deletes 2025-11-13 16:39:44 -08:00
Jon Staab 2421c02c24 Add room membership management 2025-11-13 15:25:18 -08:00
Jon Staab 25e868118d Slight optimization 2025-11-13 14:40:02 -08:00
Jon Staab 2880044e0e Add event admin deletion 2025-11-13 14:25:59 -08:00
Jon Staab 5300404b46 Add option to ban users from profile detail dialog 2025-11-13 13:44:52 -08:00
Jon Staab d949d58076 Add space membership management 2025-11-13 13:25:34 -08:00
Jon Staab 997b223e95 Rename space menu components 2025-11-13 10:36:28 -08:00
Jon Staab ba52a97e26 Tweak relay icon size in nav 2025-11-13 10:34:02 -08:00
Jon Staab cc4c7b5fe9 Fix image modal, only show + room if the user is allowed 2025-11-13 10:32:37 -08:00
Jon Staab 8e2ebd11fc remove some alts 2025-11-13 08:59:32 -08:00
Jon Staab 9cae4da9f4 Add lightning invoice payments 2025-11-12 16:24:58 -08:00
Jon Staab c05d7e99e2 remove old icon picker 2025-11-12 14:56:14 -08:00
Jon Staab 2390599e8f Fix relay updating and relay icons 2025-11-11 17:48:24 -08:00
Jon Staab 1a4d45fa9c Upload svgs for room icon 2025-11-11 17:34:15 -08:00
467 changed files with 27328 additions and 12031 deletions
+2
View File
@@ -1,8 +1,10 @@
--ignore-dir=.svelte-kit
--ignore-dir=android
--ignore-dir=target
--ignore-dir=build
--ignore-dir=ios/DerivedData
--ignore-dir=ios/App/App/public
--ignore-dir=ios/App/Pods
--ignore-file=match:.svg
--ignore-file=match:package-lock.json
+8
View File
@@ -2,3 +2,11 @@ node_modules
android
ios
build
# Git
.git
.gitignore
# Env files (keep .env for build; exclude local overrides)
.env.local
.env.*.local
+10 -5
View File
@@ -1,6 +1,6 @@
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
VITE_BURROW_URL=
VITE_POMADE_SIGNERS=https://pomade.coracle.social,https://pomade.fiatjaf.com,https://pomade.nostrver.se,https://pomade.scuttle.works
VITE_PLATFORM_URL=https://app.flotilla.social
VITE_PLATFORM_TERMS=https://flotilla.social/terms
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
@@ -10,10 +10,15 @@ VITE_PLATFORM_RELAYS=
VITE_PLATFORM_ACCENT="#7161FF"
VITE_PLATFORM_SECONDARY="#EB5E28"
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
VITE_INDEXER_RELAYS=wss://purplepag.es/,wss://relay.damus.io/,wss://relay.nostr.band/,wss://indexer.coracle.social/
VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://offchain.pub/
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/
VITE_PUSH_SERVER=https://nps.flotilla.social/
VITE_PUSH_BRIDGE=wss://npb.coracle.social/
VITE_BLOCKED_RELAYS=brb.io,relay.nostr.band,nostr.mutinywallet.com,feeds.nostr.band,nostr.zbd.gg,wot.utxo.one,blastr.f7z.xyz,relay.current.fyi
VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom
VITE_DEFAULT_SEARCH_RELAYS=relay.ditto.pub,antiprimal.net,relay.vertexlab.io
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com,relay.keychat.io,relay.ditto.pub
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
VITE_THUMBNAIL_URL=https://vthumbs.coracle.social
VITE_GLITCHTIP_API_KEY=
GLITCHTIP_AUTH_TOKEN=
+3 -1
View File
@@ -1,4 +1,6 @@
src/assets
.claude
target
build
.idea
.gradle
@@ -12,4 +14,4 @@ ios/App/Pods/
android/capacitor-cordova-android-plugins
android/app/src/androidTest
android/app/src/test
node_modules
@@ -2,11 +2,11 @@ name: Docker
on:
push:
branches: ['master']
branches: [master]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
IMAGE_NAME: coracle-social/flotilla
jobs:
build-and-push-image:
@@ -14,8 +14,6 @@ jobs:
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
@@ -25,8 +23,8 @@ jobs:
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
@@ -50,10 +48,3 @@ jobs:
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
+10 -1
View File
@@ -1,5 +1,6 @@
# Env
.env
.env.local
.env.*.local
# Vite
vite.config.js.timestamp-*
@@ -24,8 +25,14 @@ android/app/src/main/assets/public/
# Web/JavaScript
node_modules/
.pnpm-store/
build/
.svelte-kit/
.next/
# Rust/Tauri
*target/
src-tauri/binaries/
# iOS
ios/App/App/public
@@ -63,7 +70,9 @@ GoogleService-Info.plist
.roo
.idea/
.vscode/
.claude/
# OS generated
.DS_Store
Thumbs.db
package-lock.json
+1 -1
View File
@@ -5,6 +5,6 @@
"svelteSortOrder": "options-styles-scripts-markup",
"arrowParens": "avoid",
"bracketSpacing": false,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"plugins": ["prettier-plugin-tailwindcss", "prettier-plugin-svelte"],
"overrides": [{"files": "*.svelte", "options": {"parser": "svelte"}}]
}
+268
View File
@@ -0,0 +1,268 @@
## Project Overview
Flotilla is a Nostr "relays as groups" community chat client. It implements NIP-29 (relay-based groups) to create Discord-like spaces (servers) and rooms (channels).
**Tech Stack:**
- SvelteKit 5.48+ with TypeScript 5.9+
- Capacitor for cross-platform (Web/PWA, Android, iOS)
- TailwindCSS + DaisyUI for styling
- Welshman library suite for Nostr protocol
- IndexedDB for local storage
- Vite for building
**Key Concepts:**
- **Spaces** - Relays used as community groups (like Discord servers)
- **Rooms** - NIP-29 groups within spaces (like Discord channels), identified by `h`
- **Chats** - Direct message conversations (NIP-04/NIP-44 encrypted)
## Architecture & Dependency Graph
The project follows a **strict acyclic dependency hierarchy**:
```
routes/ (top layer - can depend on anything)
app/components/ (can depend on app/* and lib/*)
app/core/ & app/util/ (can only depend on lib/*)
lib/ (can only depend on external libraries)
external libraries (bottom layer)
```
**Import Ordering Convention (CRITICAL):**
Always sort imports by dependency level:
1. Third-party libraries first
2. Then `lib/` imports
3. Then `app/` imports
Example:
```typescript
import {derived} from "svelte/store"
import {throttle} from "throttle-debounce"
import {Dialog} from "$lib/components"
import {repository} from "$app/core/state"
```
## File Structure
```
src/
├── lib/ # Generic reusable code
│ ├── components/ # 38 UI components (Button, Dialog, etc.)
│ ├── html.ts # DOM utilities
│ ├── indexeddb.ts # IndexedDB helpers
│ └── util.ts # Generic utilities
├── app/
│ ├── core/
│ │ ├── state.ts # State management, stores, constants (687 lines)
│ │ ├── commands.ts # Publishing events and other write operations (440+ lines)
│ │ ├── requests.ts # Loading data from network (191 lines)
│ │ ├── sync.ts # Data synchronization (296 lines)
│ │ └── storage.ts # IndexedDB setup
│ │
│ ├── util/
│ │ ├── notifications.ts # Push notifications (731 lines)
│ │ ├── policies.ts # Relay policies
│ │ ├── routes.ts # Routing helpers
│ │ ├── modal.ts # Modal management
│ │ ├── toast.ts # Toast notifications
│ │ ├── theme.ts # Theme switching
│ │ └── keyboard.ts # Keyboard handling
│ │
│ ├── editor/ # Rich text editor config
│ │ ├── index.ts # TipTap setup with Nostr integration
│ │ ├── EditorContent.svelte
│ │ └── MentionNodeView.ts
│ │
│ └── components/ # 188 app-specific components
│ ├── Space*.svelte # Space/relay management
│ ├── Room*.svelte # Room/channel management
│ ├── Chat*.svelte # Direct messaging
│ ├── Profile*.svelte # User profiles
│ ├── Thread*.svelte # Threaded posts
│ └── ...
├── routes/ # SvelteKit file-based routing
│ ├── +layout.svelte # Root layout (sync logic here)
│ ├── spaces/ # Space management
│ │ └── [relay]/ # Specific space
│ │ ├── chat/ # Space chat
│ │ ├── threads/ # Thread posts
│ │ ├── calendar/ # Events
│ │ └── [h]/ # Specific room (h = room id)
│ ├── chat/ # Direct messages
│ ├── settings/ # User settings
│ └── [bech32]/ # Bech32 entity viewer
├── assets/icons/ # ~1,277 SVG icons
├── app.html # HTML template
├── app.css # Global styles
└── types.d.ts # Type definitions
```
## State Management
**Core Principles:**
- Use Svelte 4 **stores** for all state (NOT runes outside UI components)
- Most global state flows through Welshman's `repository` (unidirectional)
- Query state using `deriveEventsMapped` or `deriveProfile` etc
- Update state by publishing events via `publishThunk`
**Thunks:**
- Reduce UI latency by handling signatures and sending in background
- Return status that should be displayed to user
- Allow cancellation and error handling
- Immediately publish to local repository for optimistic updates
## Nostr Integration
**Welshman Library Suite:**
- `@welshman/app` - High-level state (pubkey, signer, repository, tracker)
- `@welshman/net` - Network layer (Pool, Socket, load, pull, request)
- `@welshman/store` - Svelte integration (deriveEventsMapped, etc.)
- `@welshman/util` - Event utilities (kinds, tags, validation)
- `@welshman/signer` - Signing abstraction (NIP-01, NIP-07, NIP-46)
- `@welshman/router` - Relay routing (inbox/outbox model)
- `@welshman/editor` - Rich text editor with Nostr
- `@welshman/content` - Content parsing
- `@welshman/feeds` - Feed management
**Key NIPs Implemented:**
- NIP-01: Basic protocol
- NIP-44/59/17: Encrypted DMs
- NIP-07: Browser extension signing
- NIP-19: Bech32 encoding
- NIP-29: Relay-based Groups
- NIP-42: Relay authentication
- NIP-43: Relay membership
- NIP-46: Nostr Connect (remote signing)
- NIP-57: Lightning Zaps
## Development Conventions
**Component Parameterization:**
- Only pass entity identifiers (`url` for spaces, `h` for rooms)
- Derive all other data inside the component from identifiers
- Example: Don't pass `members` prop, derive it from `h` inside component
**CRITICAL Code Style Guidelines:**
- **No `null`** - only use `undefined`
- Svelte 5 runes (`$state`, `$derived`, `$effect`) only in UI components
- TailwindCSS and DaisyUI styling
- Only add comments for really weird stuff
- Do not call functions in components unless a parameter is reactive. Instead, use a svelte store or rune to make it reactive.
- Do not use `any`. If there are type errors related to `unknown`, they are likely because the upstream definition of the data is incorrect.
- When dynamically building classes, use `cx` from `classnames` rather than embedded ternaries or svelte 4's old `class:` syntax.
- When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
- Instead of `getTag(tagName, event.tags)?.[1] || ""`, use `getTagValue(tagName, event.tags)`
**Human-First Simplicity (Jon Staab Style):**
- Prefer direct, readable code over layered abstractions.
- Do not add indirection (extra helpers, wrappers, stores, or derived state) unless it removes real repeated complexity.
- Reuse existing Welshman and Flotilla primitives before introducing new utilities or dependencies.
- Favor linear control flow and explicit naming over clever patterns.
- Remove defensive checks that do not apply in this runtime model.
- When two approaches work, pick the one that feels more human and easier to maintain.
## Common Tasks
### Adding a New Component
1. Determine if it's generic (`lib/components/`) or app-specific (`app/components/`)
2. Follow naming convention: `PascalCase.svelte`
3. Import in dependency order (3rd party → lib → app)
4. Use stores for state, runes only for UI reactivity
### Creating a New Route
1. Add to `src/routes/` following SvelteKit conventions
2. Use `+page.svelte` for page component
3. Use `+layout.svelte` for shared layouts
4. Top-level sync logic goes in root `+layout.svelte`
### Loading Data from Network
1. Use utilities from `app/core/requests.ts`
2. Or create derived stores in `app/core/state.ts`
3. Use `load`, `pull`, or `request` from `@welshman/net`
### Publishing Events
1. Create `make*` function to build event template
2. Create `publish*` function using `publishThunk`
3. Display thunk status to user (for cancel/error handling)
4. These go in in `app/core/commands.ts`
### Managing Modals/Toasts
- Import from `app/util/modal.ts` or `app/util/toast.ts`
- Pass component objects with parameters
- Use `$state.snapshot` if calling component might unmount
## Development Workflow
Agents should not run the dev server or build the app. Instead, use the following commands:
```bash
pnpm run format # Format changed files
pnpm run lint # Check formatting and linting
pnpm run check # Type check
```
**Welshman Development:**
- Clone welshman to parent directory
- Use `./link_deps` script to link local welshman packages
- Avoid committing `pnpm.overrides` changes
**Git Workflow:**
- `master` branch auto-deploys to production
- Work on feature branches based on `dev` branch
- Pre-commit hooks run lint/typecheck automatically
## Environment Variables
See `.env.template` for all options.
## Important Files to Reference
- **src/app/core/state.ts** - All stores and constants
- **src/app/core/sync.ts** - Data synchronization
- **src/app/core/requests.ts** - Utilities for requesting data
- **src/app/core/commands.ts** - Publishing patterns
- **src/app/util/notifications.ts** - Notification badges and push notifications
- **src/routes/+layout.svelte** - Top-level sync logic
## Mobile Development
**Capacitor Integration:**
- Android: Full support, APK builds via `pnpm run release:android`
- iOS: Full support (zaps disabled due to App Store policy)
- PWA: Progressive Web App with service worker
**Native Features:**
- Push notifications (FCM/APNs)
- Deep linking (nostr: and https: URLs)
- Native signing plugin
- Keyboard management
- Safe area handling
- Badge management
+159
View File
@@ -1,5 +1,164 @@
# Changelog
# 1.7.4
* Fix safe area inset for FAB
# 1.7.3
* Add native share support for space invites
* Stop sending duplicate requests per room
* Add more robust thumbnail url generation
* Make space reordering discoverable with smoother drag animation
* Improve relay member list
* Add room mentions and clickable room/relay refs
* Support native clipboard image paste on mobile
* publish kind 9 quote after room content creation for cross-client interoperability
* Improve feed pagination logic and performance
* Support Aegis URL scheme for NIP-46 login
* Various UI and bug fixes
* Raise message size limit in chat
* Fix realtime updates for room members and admins
* Add video to calls
* Remove follow graph building
* Add start chat FAB
* Add drafts
* Redesign toast notifications
* Remove room/space leave indications
* Hide report badge for non-admin users
* Add polls
* Add search to recent activity page
* Fix notification badge on mobile nav
* Change audio devices in call
# 1.7.2
* Fix race condition in nip 46
* Remove duplicate spaces button
* Combine discover and space list pages
* Fix some chat related bugs
* Fix bug with joining spaces
# 1.7.1
* Fix pomade registration fallback in case of offline signer
# 1.7.0
* Enable email/password login
* Add up/edit to direct messages
* Fix a number of UI bugs
* Improve navigation on mobile
* Improve performance and syncing reliability
* Add proof of work to DMs
* Detect blossom support using supported_nips
* Improve notification badges
* Add voice rooms (@mplorentz)
* Re-design relay onboarding and settings
* Add android fallback for push notifications
* Fix file uploads on android
# 1.6.5
* Attempt to fix permission grant for notifications
* Make sync logic more robust
* Add unban/unallow support
* Improve support for downloading/opening protected images
* Add manual send/receive to wallet
* Show wallet status when wallet is unreachable
* Update nostr signer capacitor plugin
* Fix some safe area insets
* Update NIP 55 signer plugin (fixes Primal login)
* Refine space join dialogs and discover page
* Reopen the last DM that was open when navigating back to chat
* Get rid of ChatEnable interstitial
* Enable auth for relays we're publishing to
* Drag and drop space icons
* Add better muting support
* Add back button to settings menu
* Add page titles
* Improve scroll to event behavior
* Add in-memory search to rooms
* Fix editing messages with html tags
* Fix DM media detection
* Clean up reporting dialogs
* Improve room detail
# 1.6.4
* Clean up modal design
* Fix overflowing popovers
* Use space urls for relay hints
* Re-work notification badges
* Add push notification support via NIP 9a
* Optimistically load messaging relays to avoid unnecessary warning
* Recover from indexeddb not being available
* Fix safe area inset support
* Show space URL in top bar on mobile
* Fix calendar detail page
* Improve relay synchronization, especially for pyramid and relay29
* Improve invite code error handling
* Add wallet receive flow
* Fix safari image uploads
* Re-work recent activity page
* Add classified listing content type
* Use address for page param for replaceable events
* Refine discover page to avoid slowness
* Upgrade som dependencies
* Tag event author when tagging parent event
* Disable macos build
* Add room muting
# 1.6.3
* Fix scroll down button z index
* Hide tooltips on mobile
* Sort comments ascending
* Make video embeds rounded
* Fix ProfileMultiSelect styling
* Accept hex pubkeys/npubs/nprofiles in ProfileMultiSelect
* Tweak room edit form design
* Report pending signer to user
* Update default relays
* Fix chat list responsiveness
* Fix memory leak, notification badge not showing
* Improve space join flow
* Fix opening images in fullscreen dialog
* Add support for blocked relays
* Add authentication policy setting
* Add login with key if no signer is detected
* Publish default relay selections on signup
# 1.6.2
* Fix modal scrolling and style
# 1.6.1
* Fix skinny profile images
* Custom handler for relay urls
* Improve time based chat partitioning
* Improve authenticated image access interop
* Fix image detail dialog
* Fix zapper loading
* Fix recent events missing in feeds
# 1.6.0
* Switch back to indexeddb to fix memory and performance
* Add pay invoice functionality
* Add space membership management and bans
* Add event info to profile dialog
* Add better room membership management
* Refactor stores for performance
* Hide nav when keyboard is open
* Handle flotilla links in-app
* Fix new messages indicator z-index
* Fix some display bugs
* Add date to chat items
* Refine data synchronization
* Hide nav when keyboard is open on mobile
# 1.5.3
* Add space edit form
+40 -80
View File
@@ -1,96 +1,56 @@
# Contributing guidelines
## Project Overview
Flotilla is a svelte/typescript/capacitor project. It's intended to be an alternative to Discord for Nostr users. A high-quality UX is a priority, with an emphasis on well-tested, intuitive designs, and robust implementations.
Flotilla is a Nostr "relays as groups" community chat client. It implements NIP-29 (relay-based groups) to create Discord-like spaces (servers) and rooms (channels).
## Getting Started
Please visit our [issue tracker](https://gitea.coracle.social/coracle/flotilla/issues) to contribute. Any new issues should be opened without a milestone, label, or project and the project owners will triage them.
Run `pnpm run dev` to get a dev server, and `pnpm run check:watch` to watch for typescript errors. When you're ready to commit, a pre-commit hook will run to lint and typecheck your work. To run the project on Android or iOS, use Android Studio or Xcode.
### Milestones
The `master` branch is automatically deployed to production, so always work on feature branches based on the `dev` branch. This project frequently uses unreleased versions of [welshman](https://welshman.coracle.social), using `pnpm` link to hotlink a local copy of the code. To set that up, clone welshman to the parent directory of your `coracle` client, then add `link:../welshman/packages/packagename` to the `pnpm.overrides` section of your `package.json`. Below is a nodejs script that will do that automatically for you:
Milestones indicate how soon a given task should be tackled.
```javascript
#!/usr/bin/env node
- [Current](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&milestone=1) issues are immediately actionable.
- [Next](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&milestone=2) means an issue is blocked.
- [Future](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&milestone=3) means we're deferring work until a later date.
import fs from 'fs'
import path from 'path'
### Labels
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
- [Design](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&labels=15) issues need design work before being implemented. This might take the form of a high-quality mockup, wireframes, user flows, or just a couple notes about where things go, depending on the nature of the task.
- [Dev](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&labels=16) issues are ready to be implemented. Most of the work will be related to architecting and writing code.
- [Easy](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&labels=14) issues have no dependencies, and are scoped quite narrowly.
- [Priority](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&labels=6) issues include bugs and urgent feature requests. These should get attention first if possible, although sometimes long-standing performance issues or subtle bugs might end up here for a while.
- [Ideas](https://gitea.coracle.social/coracle/flotilla/issues?type=all&state=open&labels=13) are for things that aren't scoped out yet, or which need protocol work before getting designed or implemented.
packageJson.pnpm.overrides = Object.keys(packageJson.dependencies)
.filter(pkg => pkg.startsWith('@welshman/'))
.reduce((acc, pkg) => {
const packageName = pkg.split('/')[1]
acc[pkg] = `link:../welshman/packages/${packageName}`
return acc
}, {})
### Projects
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2) + '\n')
Issues may or may not have a project. Projects are used to group issues thematically just for organization.
console.log('Added welshman package overrides.')
```
## Coding conventions
Be sure to avoid committing overrides to either `package.json` or `pnpm-lock.yaml`. These overrides can generally be added, installed, and removed, and will persist until another `pnpm install` command gets run.
There are a few conventions that are helpful to know right out of the gate.
## File Structure
- Most nostr protocol functionality is implemented via the [welshman library](https://welshman.coracle.social/)
- Use Svelte 4 **stores** rather than runes for all state outside UI components
- Most global state flows through Welshman's `repository` (unidirectional)
- Query state using `deriveEventsMapped` or `deriveProfile` etc
- Events are published via `publishThunk`, which allows for optimistic UI updates during signing/pow generation.
- Components should have minimal props - e.g. instead of passing a whole `relay` through, pass its `url`.
- Use `AbortController` when possible instead of request ids
- Use `undefined` or optional properties instead of `null`
- Do not use `any`. If there are type errors related to `unknown`, they are likely because the upstream definition of the data is incorrect.
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
- When dynamically building classes, use `cx` from `classnames`.
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates.
The main parts of the application are as follows:
## Contributing Workflow
- `static` - static assets like fonts, images, etc.
- `src/assets` - svgs for use as icons.
- `src/lib` - general purpose components and utilities.
- `src/app/core/state` - environment variables, constants, custom stores, and some utilities derived from them.
- `src/app/core/requests` - utilities related to loading data from the nostr network.
- `src/app/core/commands` - utilities related to publishing nostr events and uploading media to blossom servers.
- `src/app/utils` - other application logic, including stuff related to modals, routing, etc.
- `src/app/editor` - configuration for `@welshman/editor` for use in various app views.
- `src/app/components` - reusable components that depend on other `app` stuff.
- `src/routes` - file-based routing interpreted by sveltekit.
Application organization is based on an acyclic dependency graph:
- `routes` can depend on anything
- `app/components` can depend on anything in `app` or `lib`
- `app/utils` and `app/core` can only depend on `lib`
- `lib` (and everything else) can depend only on external libraries
The main stylistic/organizational rule when working in this project is that imports should be sorted based on the dependency graph. Third-party libraries should come first, then `lib`, then `app`.
## System Architecture
Flotilla's architecture generally mirrors the file structure. State is stored using Svelte `store`s provided either by `@welshman/app` or by `app/core/state`, allowing for idiomatic svelte 4 usage (svelte 5 runes are [ghey](https://habla.news/u/hodlbod@coracle.social/1739830562159) and not allowed outside of UI components).
State is then synchronized to local storage or indexeddb using storage helpers provided by welshman in `routes/+layout.svelte`. Other top level synchronization logic generally belongs there.
`app/core/state` contains all environment variables, constants, custom stores, and utilities derived from them. Most stores are `derived` from our event `repository` using `deriveEventsMapped`, which efficiently queries the repository and maps events to custom data structures. Some of these data structures are provided by `welshman`, and some are defined in `app/core/state`. In either case, they can always be mapped back to an event, which is important for updating replaceables without dropping unknown data.
Here are a few important domain objects:
- Spaces are relays used as community groups. Their `url`s are core to a lot of data and components, and are frequently passed around from place to place.
- Chats are direct message conversations. There is currently some ambiguity in routing, since relays that don't support NIP 29 also have a "chat" tab, which uses vanilla NIP-C7.
- NIP 29 groups are called "rooms". Conventionally, "h" is a group id, while a "room" as an object representing the group's metadata.
- "Alerts" are records of requests the user has made to be notified, following [this NIP](https://github.com/nostr-protocol/nips/pull/1796)
`app/core/requests` contains utilities related to loading data from the nostr network. This might include feed manager utilities, loaders, or listeners.
`app/core/commands` contains utilities related to publishing nostr events and uploading media to blossom servers. This also includes utilities related to sending lighting payments, authenticating with relays, or probing relay policy. Event creation should generally be split into `make` functions which build the event, and `publish` functions which publish the event using `publishThunk`.
Any of these utilities can be included either in `app/components` or `routes`. Crucial to keep in mind is that nearly all global state runs through welshman's `repository` in a unidirectional way. To update state, run `publishThunk`, which immediately publishes the event to the local repository. State can be read from the repository using `deriveEventsMapped` or other utilities provided by welshman like `deriveProfile`.
Thunks are designed to reduce UI latency, handling signatures and delayed sending the background. In most cases, thunk status should be displayed to the user so that they can cancel sending or address errors.
Toast, modals, and sidebar dialogs are controlled in `app/util/modal` and `app/util/toast`. In both cases, component objects can be passed along with parameters, but care has to be taken that the calling component either doesn't unmount before the modal (as when one modal replaces another), or that `$state.snapshot` is appropriately called on any state runes. These components frequently run into weird svelte compiler bugs too, in which case you may have to do some silly things to cope.
## Issues and Pull Requests
All work by contributors should be done against an issue. If there is no issue for the work you're doing, please open one or ask the project owner to open one. All PRs should be opened against the `dev` branch (unless for hotfixes).
## Communication
Discussion about development is done [on Flotilla](https://app.flotilla.social/spaces/internal.coracle.social). The group is currently closed, so please let me know if you'd like access.
## Project License
This project is licensed under the MIT license. By contributing, you agree to waive all intellectual property rights to your contributions to this project.
To contribute, do the following:
- Find or create an issue and assign yourself (comment instead if you're not able to self-assign)
- If the issue is a design task, attach or link out to any mockups/wireframes/flowcharts
- Once a design task is completed, a maintainer will remove the `design` label and add the `dev` label
- If the issue is a development task, fork the repository and create a branch prefixed by the issue number, e.g. `105-deep-links`
- Before requesting a review, be sure to review any agent-generated code, run the pre-commit hooks, and test the changes.
- Open a PR and request a review. A maintainer will get back to you with requested changes, or will merge the PR.
- Keep your PR up-to-date by rebasing frequently on `dev`. Avoid force-pushing to `dev`.
- PRs are rebased, squashed, and merged to keep commit history simple.
- An issue may have multiple PRs. Once complete, it can be closed.
+18 -10
View File
@@ -1,24 +1,32 @@
FROM node:20-slim
# Stage 1: Build
# Uses .env from build context for config (logo, branding, etc.)
# Optional: docker build --build-arg VITE_BUILD_HASH=$(git rev-parse --short HEAD) -t flotilla .
FROM node:20-bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends curl
# Install pnpm
RUN npm install -g pnpm@latest
# Set working directory
WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./
# Install dependencies
RUN pnpm i
# Copy the rest of the application
# Copy everything (including .env when present) - build.sh will source it
COPY . .
# Build the application
ARG VITE_BUILD_HASH
ENV VITE_BUILD_HASH=${VITE_BUILD_HASH}
ENV NODE_OPTIONS=--max_old_space_size=16384
RUN pnpm run build
# Default to serving the build directory
CMD ["npx", "serve", "build"]
FROM node:20-alpine
WORKDIR /app
# Copy only the built output - no source, no .env, no dev deps
COPY --from=builder /app/build ./build
CMD ["npx", "serve", "-s", "build"]
+6 -6
View File
@@ -6,23 +6,23 @@ If you would like to be interoperable with Flotilla, please check out this guide
## Environment
You can also optionally create an `.env` file and populate it with the following environment variables (see `.env` for examples):
You can also optionally create an `.env.local` file and populate it with the following environment variables (see `.env.template` for examples):
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
- `VITE_PLATFORM_URL` - The url where the app will be hosted
- `VITE_PLATFORM_NAME` - The name of the app
- `VITE_PLATFORM_LOGO` - A logo url for the app
- `VITE_PLATFORM_LOGO` - A logo url for the app. Can be a local path or https link. Must be a PNG file.
- `VITE_PLATFORM_RELAYS` - A list of comma-separated relay urls that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the first platform relay the home page.
- `VITE_PLATFORM_ACCENT` - A hex color for the app's accent color
- `VITE_PLATFORM_DESCRIPTION` - A description of the app
- `VITE_GLITCHTIP_API_KEY` - A Sentry DSN for use with glitchtip (error reporting)
- `GLITCHTIP_AUTH_TOKEN` - A glitchtip auth token for error reporting
These values **won't** be used for a built version. Instead, env variables should be provided to `build.sh` directly or to the built container.
If you're deploying a custom version of flotilla, be sure to remove the `plausible.coracle.social` script from `app.html`. This sends analytics to a server hosted by the developer.
## Development
See [./CONTRIBUTING.md](CONTRIBUTING.md).
See [CONTRIBUTING.md](CONTRIBUTING.md).
## Deployment
@@ -31,7 +31,7 @@ To run your own Flotilla, it's as simple as:
```sh
pnpm install
pnpm run build
npx serve build
npx serve -s build
```
Or, if you prefer to use a container:
+10 -5
View File
@@ -1,19 +1,20 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace "social.flotilla"
compileSdk rootProject.ext.compileSdkVersion
namespace = "social.flotilla"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "social.flotilla"
minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion
versionCode 34
versionName "1.5.3"
versionCode 46
versionName "1.7.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
@@ -35,6 +36,10 @@ dependencies {
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation "androidx.work:work-runtime:2.10.3"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "com.squareup.okhttp3:okhttp:4.12.0"
implementation "fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.22.0"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
+3
View File
@@ -9,12 +9,15 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':aparajita-capacitor-secure-storage')
implementation project(':capacitor-community-safe-area')
implementation project(':capacitor-app')
implementation project(':capacitor-clipboard')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-keyboard')
implementation project(':capacitor-preferences')
implementation project(':capacitor-push-notifications')
implementation project(':capacitor-share')
implementation project(':capawesome-capacitor-android-dark-mode-support')
implementation project(':capawesome-capacitor-badge')
implementation project(':nostr-signer-capacitor-plugin')
+6 -1
View File
@@ -9,7 +9,7 @@
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
@@ -42,4 +42,9 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
</manifest>
@@ -1,5 +1,15 @@
package social.flotilla;
import android.os.Bundle;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}
import social.flotilla.notifications.AndroidPushFallbackPlugin;
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
registerPlugin(AndroidPushFallbackPlugin.class);
super.onCreate(savedInstanceState);
}
}
@@ -0,0 +1,101 @@
package social.flotilla.notifications
import android.content.Context
import android.content.SharedPreferences
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest
import androidx.work.OutOfQuotaPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.util.concurrent.TimeUnit
@CapacitorPlugin(name = "AndroidPushFallback")
class AndroidPushFallbackPlugin : Plugin() {
companion object {
const val PREFS_NAME = "CapacitorStorage"
const val KEY_STATE = "androidPushFallback.state"
const val UNIQUE_PERIODIC_WORK = "androidPushFallback.periodic"
const val UNIQUE_IMMEDIATE_WORK = "androidPushFallback.immediate"
}
private fun getPrefs(): SharedPreferences {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
}
@PluginMethod
fun syncState(call: PluginCall) {
val state: JSObject? = call.getObject("state")
if (state != null) {
getPrefs().edit().putString(KEY_STATE, state.toString()).apply()
if (isEnabled(state.toString())) {
scheduleWork()
} else {
cancelWork()
}
}
call.resolve()
}
private fun isEnabled(rawState: String?): Boolean {
if (rawState == null || rawState.isEmpty()) {
return false
}
return try {
val state = JSONObject(rawState)
val subscriptions: JSONArray? = state.optJSONArray("subscriptions")
subscriptions != null && subscriptions.length() > 0
} catch (_: JSONException) {
false
}
}
private fun scheduleWork() {
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
val workManager = WorkManager.getInstance(context)
val periodic = PeriodicWorkRequest.Builder(
AndroidPushFallbackWorker::class.java,
15,
TimeUnit.MINUTES,
).setConstraints(constraints).build()
val immediate = OneTimeWorkRequest.Builder(AndroidPushFallbackWorker::class.java)
.setConstraints(constraints)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
workManager.enqueueUniquePeriodicWork(
UNIQUE_PERIODIC_WORK,
ExistingPeriodicWorkPolicy.UPDATE,
periodic,
)
workManager.enqueueUniqueWork(
UNIQUE_IMMEDIATE_WORK,
ExistingWorkPolicy.REPLACE,
immediate,
)
}
private fun cancelWork() {
val workManager = WorkManager.getInstance(context)
workManager.cancelUniqueWork(UNIQUE_IMMEDIATE_WORK)
workManager.cancelUniqueWork(UNIQUE_PERIODIC_WORK)
}
}
@@ -0,0 +1,861 @@
package social.flotilla.notifications
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.util.Log
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.app.ActivityManager
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.work.Worker
import androidx.work.WorkerParameters
import fr.acinq.secp256k1.Secp256k1
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import org.json.JSONArray
import org.json.JSONObject
import social.flotilla.notifications.AndroidPushFallbackPlugin.Companion.KEY_STATE
import social.flotilla.notifications.AndroidPushFallbackPlugin.Companion.PREFS_NAME
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Arrays
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import android.util.Base64
class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
companion object {
private const val TAG = "PushFallback"
private const val CHANNEL_ID = "flotilla_fallback"
private const val CURSOR_PREFIX = "androidPushFallback.cursor."
private const val SOCKET_TIMEOUT_SECONDS = 30L
private const val REJECTED = "__REJECTED__"
private const val KIND_RELAY_AUTH = 22242
private const val KIND_NIP46_RPC = 24133
private val SECP = Secp256k1.get()
}
private val prefs: SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private val client: OkHttpClient = OkHttpClient.Builder().build()
// ---- Socket pool ----
// Opens each relay URL at most once; caller must invoke closeAll() when done.
private inner class SocketPool {
private val sockets = ConcurrentHashMap<String, WebSocket>()
fun open(url: String, listener: WebSocketListener): WebSocket =
sockets.getOrPut(url) {
client.newWebSocket(Request.Builder().url(url).build(), listener)
}
fun closeAll() {
for ((_, ws) in sockets) ws.close(1000, "done")
sockets.clear()
}
}
override fun doWork(): Result {
Log.i(TAG, "doWork() started")
if (isAppInForeground()) {
return Result.success()
}
val pool = SocketPool()
try {
val rawState = prefs.getString(KEY_STATE, "") ?: ""
if (rawState.isEmpty()) return Result.success()
val state = JSONObject(rawState)
val sessionInfo = getSessionInfo(state)
val subscriptions = parseSubscriptions(state)
if (subscriptions.isEmpty()) return Result.success()
val activeSince = state.optLong("activeSince", 0L)
val seen = mutableSetOf<String>()
val newEvents = mutableListOf<Pair<String, JSONObject>>()
for (sub in subscriptions) {
val cursorKey = CURSOR_PREFIX + sub.relay + ":" + sub.key
val since = maxOf(prefs.getLong(cursorKey, 0L), activeSince)
val result = pollRelay(sub, since, sessionInfo, pool)
if (result.lastCursor > prefs.getLong(cursorKey, 0L)) {
prefs.edit().putLong(cursorKey, result.lastCursor).apply()
}
for (event in result.events) {
val id = event.optString("id", "")
if (id.isNotEmpty() && seen.add(id)) {
newEvents.add(Pair(sub.relay, event))
}
}
}
for ((relay, event) in newEvents) {
postNotification(relay, event)
}
return Result.success()
} catch (e: Exception) {
Log.e(TAG, "Worker failed", e)
return Result.retry()
} finally {
pool.closeAll()
client.dispatcher.executorService.shutdown()
}
}
private fun isAppInForeground(): Boolean {
val am = applicationContext.getSystemService(ActivityManager::class.java) ?: return false
val tasks = am.getRunningAppProcesses() ?: return false
val pkg = applicationContext.packageName
return tasks.any { it.processName == pkg && it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND }
}
private fun getSessionInfo(state: JSONObject): SessionInfo {
val session = state.optJSONObject("session") ?: return SessionInfo("anonymous", "", JSONObject())
return SessionInfo(
session.optString("method", "anonymous"),
session.optString("pubkey", ""),
session,
)
}
private fun parseSubscriptions(state: JSONObject): List<Subscription> {
val result = mutableListOf<Subscription>()
val arr = state.optJSONArray("subscriptions") ?: return result
for (i in 0 until arr.length()) {
val item = arr.optJSONObject(i) ?: continue
val relay = item.optString("relay", "").trim()
if (!relay.startsWith("wss://") && !relay.startsWith("ws://")) continue
val filters = item.optJSONArray("filters")
if (filters == null || filters.length() == 0) continue
val key = item.optString("key", "").trim()
result.add(Subscription(relay, key, filters, item.optJSONArray("ignore")))
}
return result
}
private fun pollRelay(sub: Subscription, since: Long, sessionInfo: SessionInfo, pool: SocketPool): RelayResult {
val result = RelayResult()
val latch = CountDownLatch(1)
val listener = RelayListener(sub, since, sessionInfo, result, latch, pool)
pool.open(sub.relay, listener)
if (!latch.await(SOCKET_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
Log.d(TAG, "Relay ${sub.relay} timed out")
}
return result
}
private fun postNotification(relay: String, event: JSONObject) {
val context = applicationContext
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = context.getSystemService(NotificationManager::class.java)
if (manager != null && manager.getNotificationChannel(CHANNEL_ID) == null) {
val channel = NotificationChannel(CHANNEL_ID, "Fallback Notifications", NotificationManager.IMPORTANCE_DEFAULT)
channel.description = "Notifications delivered by Android background fallback"
manager.createNotificationChannel(channel)
}
}
val id = event.optString("id", "")
val encodedRelay = Uri.encode(relay)
val url = "https://app.flotilla.social/?relay=$encodedRelay&id=$id"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
intent.setPackage(context.packageName)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val body = "New activity"
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_notify_chat)
.setContentTitle("Flotilla")
.setContentText(body)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.build()
val notificationId = id.hashCode().let { if (it == 0) 1 else it }
NotificationManagerCompat.from(context).notify(notificationId, notification)
}
private fun matchesFilter(filter: JSONObject, event: JSONObject): Boolean {
val kinds = filter.optJSONArray("kinds")
if (kinds != null && kinds.length() > 0) {
val kind = event.optInt("kind", -1)
var found = false
for (i in 0 until kinds.length()) {
if (kinds.optInt(i, -1) == kind) { found = true; break }
}
if (!found) return false
}
val tags = event.optJSONArray("tags")
val iter = filter.keys()
while (iter.hasNext()) {
val key = iter.next()
if (!key.startsWith("#")) continue
val tagName = key.substring(1)
val allowed = filter.optJSONArray(key) ?: continue
if (allowed.length() == 0) continue
val allowedValues = mutableSetOf<String>()
for (i in 0 until allowed.length()) {
val v = allowed.optString(i, "")
if (v.isNotEmpty()) allowedValues.add(v)
}
var matched = false
if (tags != null) {
for (i in 0 until tags.length()) {
val tag = tags.optJSONArray(i) ?: continue
if (tag.optString(0, "") == tagName && allowedValues.contains(tag.optString(1, ""))) {
matched = true; break
}
}
}
if (!matched) return false
}
return true
}
// ---- Crypto helpers ----
private fun computeEventId(event: JSONObject): String {
return try {
val serialized = JSONArray()
serialized.put(0)
serialized.put(event.optString("pubkey", ""))
serialized.put(event.optLong("created_at", 0))
serialized.put(event.optInt("kind", 0))
serialized.put(event.optJSONArray("tags") ?: JSONArray())
serialized.put(event.optString("content", ""))
// JSONObject escapes forward slashes (/ -> \/), but NIP-01 event ID hashing
// requires unescaped slashes. Replace them before hashing.
val serializedStr = serialized.toString().replace("\\/", "/")
bytesToHex(sha256(serializedStr.toByteArray(StandardCharsets.UTF_8)))
} catch (_: Exception) {
""
}
}
private fun deriveXOnlyPubkey(secretHex: String): String {
val secret = hexToBytes(secretHex)
if (secret.size != 32 || !SECP.secKeyVerify(secret)) return ""
val pubkey65 = try { SECP.pubkeyCreate(secret) } catch (_: Exception) { return "" }
if (pubkey65.size != 65) return ""
return bytesToHex(Arrays.copyOfRange(pubkey65, 1, 33))
}
private fun schnorrSign(secretHex: String, messageHex: String): String {
val sk = hexToBytes(secretHex)
val msg = hexToBytes(messageHex)
if (sk.size != 32 || msg.size != 32 || !SECP.secKeyVerify(sk)) return ""
val aux = ByteArray(32).also { SecureRandom().nextBytes(it) }
val sig = try { SECP.signSchnorr(msg, sk, aux) } catch (_: Exception) { return "" }
if (sig.size != 64) return ""
return bytesToHex(sig)
}
private fun sha256(input: ByteArray): ByteArray =
try { MessageDigest.getInstance("SHA-256").digest(input) } catch (_: Exception) { ByteArray(32) }
private fun hexToBytes(hex: String?): ByteArray {
var s = hex?.trim()?.lowercase() ?: ""
if (s.startsWith("0x")) s = s.substring(2)
if (s.length % 2 == 1) s = "0$s"
val bytes = ByteArray(s.length / 2)
var i = 0
while (i < s.length) {
val hi = Character.digit(s[i], 16)
val lo = Character.digit(s[i + 1], 16)
if (hi < 0 || lo < 0) return ByteArray(0)
bytes[i / 2] = ((hi shl 4) + lo).toByte()
i += 2
}
return bytes
}
private fun bytesToHex(bytes: ByteArray): String {
val hex = "0123456789abcdef".toCharArray()
val chars = CharArray(bytes.size * 2)
for (i in bytes.indices) {
val v = bytes[i].toInt() and 0xFF
chars[i * 2] = hex[v ushr 4]
chars[i * 2 + 1] = hex[v and 0x0F]
}
return String(chars)
}
// ---- NIP-44 encryption (v2: ECDH + HKDF + ChaCha20 + HMAC-SHA256) ----
private fun nip44ConversationKey(clientSecret: String, theirPubkey: String): ByteArray {
val sk = hexToBytes(clientSecret)
val pk = hexToBytes("02$theirPubkey")
if (sk.size != 32 || pk.size != 33) return ByteArray(0)
val shared = try { SECP.pubKeyTweakMul(pk, sk) } catch (_: Exception) { return ByteArray(0) }
if (shared.size != 65) return ByteArray(0)
val sharedX = Arrays.copyOfRange(shared, 1, 33)
return hkdfExtract(sharedX, "nip44-v2".toByteArray(StandardCharsets.UTF_8))
}
private fun hkdfExtract(ikm: ByteArray, salt: ByteArray): ByteArray {
val mac = javax.crypto.Mac.getInstance("HmacSHA256")
mac.init(javax.crypto.spec.SecretKeySpec(salt, "HmacSHA256"))
return mac.doFinal(ikm)
}
private fun hkdfExpand(prk: ByteArray, info: ByteArray, length: Int): ByteArray {
val mac = javax.crypto.Mac.getInstance("HmacSHA256")
val result = ByteArray(length)
var prev = ByteArray(0)
var offset = 0
var counter = 1
while (offset < length) {
mac.init(javax.crypto.spec.SecretKeySpec(prk, "HmacSHA256"))
mac.update(prev)
mac.update(info)
mac.update(counter.toByte())
prev = mac.doFinal()
val toCopy = minOf(prev.size, length - offset)
System.arraycopy(prev, 0, result, offset, toCopy)
offset += toCopy
counter++
}
return result
}
private fun hmacSha256(key: ByteArray, vararg parts: ByteArray): ByteArray {
val mac = javax.crypto.Mac.getInstance("HmacSHA256")
mac.init(javax.crypto.spec.SecretKeySpec(key, "HmacSHA256"))
for (part in parts) mac.update(part)
return mac.doFinal()
}
// ChaCha20 block function per RFC 8439
private fun chacha20Block(key: ByteArray, counter: Int, nonce: ByteArray): ByteArray {
fun Int.rotl(n: Int) = (this shl n) or (this ushr (32 - n))
val state = IntArray(16)
state[0] = 0x61707865; state[1] = 0x3320646e; state[2] = 0x79622d32; state[3] = 0x6b206574
for (i in 0..7) state[4 + i] = (key[i*4].toInt() and 0xFF) or
((key[i*4+1].toInt() and 0xFF) shl 8) or
((key[i*4+2].toInt() and 0xFF) shl 16) or
((key[i*4+3].toInt() and 0xFF) shl 24)
state[12] = counter
for (i in 0..2) state[13 + i] = (nonce[i*4].toInt() and 0xFF) or
((nonce[i*4+1].toInt() and 0xFF) shl 8) or
((nonce[i*4+2].toInt() and 0xFF) shl 16) or
((nonce[i*4+3].toInt() and 0xFF) shl 24)
val working = state.copyOf()
repeat(10) {
fun quarterRound(a: Int, b: Int, c: Int, d: Int) {
working[a] += working[b]; working[d] = (working[d] xor working[a]).rotl(16)
working[c] += working[d]; working[b] = (working[b] xor working[c]).rotl(12)
working[a] += working[b]; working[d] = (working[d] xor working[a]).rotl(8)
working[c] += working[d]; working[b] = (working[b] xor working[c]).rotl(7)
}
quarterRound(0,4,8,12); quarterRound(1,5,9,13); quarterRound(2,6,10,14); quarterRound(3,7,11,15)
quarterRound(0,5,10,15); quarterRound(1,6,11,12); quarterRound(2,7,8,13); quarterRound(3,4,9,14)
}
val out = ByteArray(64)
for (i in 0..15) {
val v = working[i] + state[i]
out[i*4] = v.toByte()
out[i*4+1] = (v ushr 8).toByte()
out[i*4+2] = (v ushr 16).toByte()
out[i*4+3] = (v ushr 24).toByte()
}
return out
}
private fun chacha20(key: ByteArray, nonce: ByteArray, data: ByteArray): ByteArray {
val out = ByteArray(data.size)
var counter = 0
var offset = 0
while (offset < data.size) {
val block = chacha20Block(key, counter, nonce)
val len = minOf(64, data.size - offset)
for (i in 0 until len) out[offset + i] = (data[offset + i].toInt() xor block[i].toInt()).toByte()
offset += len
counter++
}
return out
}
private fun nip44CalcPaddedLen(len: Int): Int {
if (len <= 32) return 32
val nextPower = 1 shl (Math.floor(Math.log((len - 1).toDouble()) / Math.log(2.0)).toInt() + 1)
val chunk = if (nextPower <= 256) 32 else nextPower / 8
return chunk * ((len - 1) / chunk + 1)
}
private fun nip44Pad(plaintext: String): ByteArray {
val unpadded = plaintext.toByteArray(StandardCharsets.UTF_8)
val len = unpadded.size
val padded = ByteArray(2 + nip44CalcPaddedLen(len))
padded[0] = (len ushr 8).toByte()
padded[1] = len.toByte()
System.arraycopy(unpadded, 0, padded, 2, len)
return padded
}
private fun nip44Unpad(padded: ByteArray): String {
val len = ((padded[0].toInt() and 0xFF) shl 8) or (padded[1].toInt() and 0xFF)
if (len == 0 || len > padded.size - 2) return ""
return String(padded, 2, len, StandardCharsets.UTF_8)
}
private fun encryptNip44(plaintext: String, conversationKey: ByteArray): String {
return try {
val nonce = ByteArray(32).also { SecureRandom().nextBytes(it) }
val keys = hkdfExpand(conversationKey, nonce, 76)
val chachaKey = keys.sliceArray(0 until 32)
val chachaNonce = keys.sliceArray(32 until 44)
val hmacKey = keys.sliceArray(44 until 76)
val padded = nip44Pad(plaintext)
val ciphertext = chacha20(chachaKey, chachaNonce, padded)
val mac = hmacSha256(hmacKey, nonce, ciphertext)
val payload = ByteArray(1 + 32 + ciphertext.size + 32)
payload[0] = 2
System.arraycopy(nonce, 0, payload, 1, 32)
System.arraycopy(ciphertext, 0, payload, 33, ciphertext.size)
System.arraycopy(mac, 0, payload, 33 + ciphertext.size, 32)
Base64.encodeToString(payload, Base64.NO_WRAP)
} catch (_: Exception) {
""
}
}
private fun decryptNip44(payload: String, conversationKey: ByteArray): String {
return try {
if (payload.isEmpty() || payload[0] == '#') return ""
val data = Base64.decode(payload, Base64.NO_WRAP)
if (data.size < 99 || data[0] != 2.toByte()) return ""
val nonce = data.sliceArray(1 until 33)
val ciphertext = data.sliceArray(33 until data.size - 32)
val mac = data.sliceArray(data.size - 32 until data.size)
val keys = hkdfExpand(conversationKey, nonce, 76)
val chachaKey = keys.sliceArray(0 until 32)
val chachaNonce = keys.sliceArray(32 until 44)
val hmacKey = keys.sliceArray(44 until 76)
val expectedMac = hmacSha256(hmacKey, nonce, ciphertext)
if (!expectedMac.contentEquals(mac)) return ""
val padded = chacha20(chachaKey, chachaNonce, ciphertext)
nip44Unpad(padded)
} catch (_: Exception) {
""
}
}
// ---- Signing ----
private fun signWithNip01Secret(secretHex: String, eventJson: String, expectedPubkey: String): String {
return try {
val secret = hexToBytes(secretHex)
if (secret.size != 32) return ""
val event = JSONObject(eventJson)
var pubkey = event.optString("pubkey", expectedPubkey)
if (pubkey.isEmpty()) pubkey = deriveXOnlyPubkey(secretHex)
if (pubkey.isEmpty()) return ""
event.put("pubkey", pubkey)
val id = computeEventId(event)
if (id.isEmpty()) return ""
val sig = schnorrSign(secretHex, id)
if (sig.isEmpty()) return ""
event.put("id", id)
event.put("sig", sig)
event.toString()
} catch (_: Exception) {
""
}
}
private fun signWithNip55ContentResolver(packageName: String, eventJson: String, pubkey: String): String {
val uri = Uri.parse("content://$packageName.SIGN_EVENT")
var cursor: Cursor? = null
return try {
cursor = applicationContext.contentResolver.query(uri, arrayOf(eventJson, "", pubkey), "1", null, null)
if (cursor == null || !cursor.moveToFirst()) return ""
val rejIdx = cursor.getColumnIndex("rejected")
if (rejIdx >= 0) {
val v = cursor.getString(rejIdx)
if (v == "1" || v.equals("true", ignoreCase = true)) return REJECTED
}
val eventIdx = cursor.getColumnIndex("event")
if (eventIdx >= 0) cursor.getString(eventIdx) ?: "" else ""
} catch (_: Exception) {
""
} finally {
cursor?.close()
}
}
// ---- Data types ----
private data class SessionInfo(
val method: String,
val pubkey: String,
val session: JSONObject,
)
private data class Subscription(
val relay: String,
val key: String,
val filters: JSONArray,
val ignore: JSONArray?,
)
private class RelayResult {
val events = mutableListOf<JSONObject>()
var lastCursor = 0L
}
// ---- Relay listener ----
private inner class RelayListener(
private val sub: Subscription,
private val since: Long,
private val sessionInfo: SessionInfo,
private val result: RelayResult,
private val latch: CountDownLatch,
private val pool: SocketPool,
) : WebSocketListener() {
private val subId = UUID.randomUUID().toString().replace("-", "")
private var done = false
private var authed = false
private var authEventId = ""
private var nip46InFlight = false
private var pendingDone = false
override fun onOpen(webSocket: WebSocket, response: Response) {
sendReq(webSocket)
}
private fun sendReq(webSocket: WebSocket) {
val req = JSONArray()
req.put("REQ")
req.put(subId)
for (i in 0 until sub.filters.length()) {
val filter = sub.filters.optJSONObject(i) ?: continue
val shaped = JSONObject(filter.toString())
if (since > 0) shaped.put("since", since + 1)
shaped.put("limit", 1)
req.put(shaped)
}
if (req.length() <= 2) { finish(); return }
send(webSocket, req.toString())
}
override fun onMessage(webSocket: WebSocket, text: String) {
try {
val message = JSONArray(text)
Log.d(TAG, "Received message from ${sub.relay}: $text")
when (message.optString(0, "")) {
"EVENT" -> {
val event = message.optJSONObject(2) ?: return
if (!matchesAnyFilter(sub.filters, event)) return
if (isIgnored(event)) return
result.events.add(event)
val createdAt = event.optLong("created_at", 0L)
if (createdAt > result.lastCursor) result.lastCursor = createdAt
}
"AUTH" -> {
// Only auth once per connection
if (!authed) {
authed = true
tryAuth(webSocket, message.optString(1, ""))
}
}
"OK" -> {
val okId = message.optString(1, "")
val accepted = message.optBoolean(2, false)
if (accepted && okId == authEventId) sendReq(webSocket)
}
"EOSE" -> {
send(webSocket, JSONArray().apply { put("CLOSE"); put(subId) }.toString())
finish()
}
}
} catch (_: Exception) {
finish()
}
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) = finish()
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) = finish()
private fun finish() {
if (done) return
if (nip46InFlight) { pendingDone = true; return }
done = true
latch.countDown()
}
private fun isIgnored(event: JSONObject): Boolean {
val ignore = sub.ignore ?: return false
for (i in 0 until ignore.length()) {
val filter = ignore.optJSONObject(i) ?: continue
if (matchesFilter(filter, event)) return true
}
return false
}
private fun matchesAnyFilter(filters: JSONArray, event: JSONObject): Boolean {
for (i in 0 until filters.length()) {
val filter = filters.optJSONObject(i) ?: continue
if (matchesFilter(filter, event)) return true
}
return false
}
// ---- NIP-42 auth ----
private fun tryAuth(webSocket: WebSocket, challenge: String) {
if (challenge.isEmpty()) return
when (sessionInfo.method) {
"nip01" -> tryNip01Auth(webSocket, challenge)
"nip55" -> tryNip55Auth(webSocket, challenge)
"nip46" -> tryNip46Auth(webSocket, challenge)
// Pomade background auth is not supported: properly delegating to the Pomade signer
// from a background worker is complex, usage is rare, and relays that require auth
// may still be readable without it.
}
}
private fun buildAuthEvent(challenge: String): JSONObject {
return JSONObject().apply {
put("kind", KIND_RELAY_AUTH)
put("pubkey", sessionInfo.pubkey)
put("created_at", System.currentTimeMillis() / 1000L)
put("content", "")
put("id", "")
put("sig", "")
put("tags", JSONArray().apply {
put(JSONArray().apply { put("relay"); put(sub.relay) })
put(JSONArray().apply { put("challenge"); put(challenge) })
})
}
}
private fun sendAuthMessage(webSocket: WebSocket, signedEventJson: String): Boolean {
if (signedEventJson.isEmpty() || signedEventJson == REJECTED) return false
return try {
val event = JSONObject(signedEventJson)
authEventId = event.optString("id", "")
send(webSocket, JSONArray().apply { put("AUTH"); put(event) }.toString())
} catch (_: Exception) {
false
}
}
private fun send(webSocket: WebSocket, message: String): Boolean {
Log.d(TAG, "Sending message to ${webSocket.request().url}: $message")
return webSocket.send(message)
}
private fun tryNip01Auth(webSocket: WebSocket, challenge: String): Boolean {
val secret = sessionInfo.session.optString("secret", "")
if (secret.isEmpty()) return false
val signed = signWithNip01Secret(secret, buildAuthEvent(challenge).toString(), sessionInfo.pubkey)
return sendAuthMessage(webSocket, signed)
}
private fun tryNip55Auth(webSocket: WebSocket, challenge: String): Boolean {
val signerPackage = sessionInfo.session.optString("signer", "")
if (signerPackage.isEmpty()) return false
val signed = signWithNip55ContentResolver(signerPackage, buildAuthEvent(challenge).toString(), sessionInfo.pubkey)
return sendAuthMessage(webSocket, signed)
}
private fun tryNip46Auth(webSocket: WebSocket, challenge: String): Boolean {
val handler = sessionInfo.session.optJSONObject("handler") ?: return false
val clientSecret = sessionInfo.session.optString("secret", "")
val signerPubkey = handler.optString("pubkey", "")
val relays = handler.optJSONArray("relays")
if (clientSecret.isEmpty() || signerPubkey.isEmpty() || relays == null || relays.length() == 0) return false
val clientPubkey = deriveXOnlyPubkey(clientSecret)
if (clientPubkey.isEmpty()) return false
val authEventJson = buildAuthEvent(challenge).toString()
nip46InFlight = true
var success = false
try {
for (i in 0 until relays.length()) {
val signerRelay = relays.optString(i, "").trim()
if (!signerRelay.startsWith("wss://") && !signerRelay.startsWith("ws://")) continue
if (tryNip46ViaRelay(webSocket, signerRelay, clientSecret, clientPubkey, signerPubkey, authEventJson)) { success = true; break }
}
} finally {
nip46InFlight = false
if (pendingDone) finish()
}
return success
}
private fun tryNip46ViaRelay(
relaySocket: WebSocket,
signerRelay: String,
clientSecret: String,
clientPubkey: String,
signerPubkey: String,
authEventJson: String,
): Boolean {
val localLatch = CountDownLatch(1)
val signedEvent = StringBuilder()
val requestId = UUID.randomUUID().toString().replace("-", "")
val signerSocket = pool.open(signerRelay, object : WebSocketListener() {
private var done = false
override fun onOpen(webSocket: WebSocket, response: Response) {
try {
val rpcEnvelope = JSONObject().apply {
put("kind", KIND_NIP46_RPC)
put("pubkey", clientPubkey)
put("created_at", System.currentTimeMillis() / 1000L)
put("content", encryptNip44(
JSONObject().apply {
put("id", requestId)
put("method", "sign_event")
put("params", JSONArray().apply { put(authEventJson) })
}.toString(),
nip44ConversationKey(clientSecret, signerPubkey),
))
put("id", "")
put("sig", "")
put("tags", JSONArray().apply { put(JSONArray().apply { put("p"); put(signerPubkey) }) })
}
val signedEnvelope = signWithNip01Secret(clientSecret, rpcEnvelope.toString(), clientPubkey)
if (signedEnvelope.isEmpty()) { finish(); return }
val sentAt = System.currentTimeMillis() / 1000L
send(webSocket, JSONArray().apply { put("EVENT"); put(JSONObject(signedEnvelope)) }.toString())
send(webSocket, JSONArray().apply {
put("REQ")
put(requestId)
put(JSONObject().apply {
put("#p", JSONArray().apply { put(clientPubkey) })
put("kinds", JSONArray().apply { put(KIND_NIP46_RPC) })
put("since", sentAt)
put("limit", 10)
})
}.toString())
} catch (_: Exception) {
finish()
}
}
override fun onMessage(webSocket: WebSocket, text: String) {
try {
val message = JSONArray(text)
val msgType = message.optString(0, "")
Log.d(TAG, "NIP-46 signer message: $msgType from ${webSocket.request().url}")
if (msgType != "EVENT") return
val event = message.optJSONObject(2) ?: return
val tags = event.optJSONArray("tags")
var hasP = false
if (tags != null) {
for (i in 0 until tags.length()) {
val tag = tags.optJSONArray(i) ?: continue
if (tag.optString(0, "") == "p" && tag.optString(1, "") == clientPubkey) { hasP = true; break }
}
}
if (!hasP) { Log.d(TAG, "NIP-46 event missing p tag for client"); return }
val decryptedContent = decryptNip44(event.optString("content", ""), nip44ConversationKey(clientSecret, signerPubkey))
Log.d(TAG, "NIP-46 decrypted response: $decryptedContent")
if (decryptedContent.isEmpty()) return
val payload = JSONObject(decryptedContent)
if (requestId == payload.optString("id", "")) {
val result = payload.optString("result", "")
if (result.isNotEmpty()) {
signedEvent.setLength(0)
signedEvent.append(result)
finish()
}
}
} catch (e: Exception) {
Log.e(TAG, "NIP-46 signer message error", e)
}
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) = finish()
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) = finish()
private fun finish() {
if (!done) { done = true; localLatch.countDown() }
}
})
try {
localLatch.await(5, TimeUnit.SECONDS)
} catch (_: InterruptedException) {
return false
}
if (signedEvent.isEmpty()) return false
val authEvent = JSONObject(signedEvent.toString())
authEventId = authEvent.optString("id", "")
val authMessage = JSONArray().apply { put("AUTH"); put(authEvent) }.toString()
Log.d(TAG, "NIP-46 sending AUTH to relay ${relaySocket.request().url}: $authMessage")
return try {
relaySocket.send(authMessage)
} catch (e: Exception) {
Log.e(TAG, "NIP-46 failed to send AUTH", e)
false
}
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

+4 -2
View File
@@ -1,14 +1,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '2.2.20'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.8.0'
classpath 'com.google.gms:google-services:4.4.2'
classpath 'com.android.tools.build:gradle:8.13.2'
classpath 'com.google.gms:google-services:4.4.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
+19 -10
View File
@@ -1,30 +1,39 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/android/capacitor')
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/android/capacitor')
include ':aparajita-capacitor-secure-storage'
project(':aparajita-capacitor-secure-storage').projectDir = new File('../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage/android')
include ':capacitor-community-safe-area'
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.4.3/node_modules/@capacitor-community/safe-area/android')
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area/android')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@7.1.0_@capacitor+core@7.4.3/node_modules/@capacitor/app/android')
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app/android')
include ':capacitor-clipboard'
project(':capacitor-clipboard').projectDir = new File('../node_modules/.pnpm/@capacitor+clipboard@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/clipboard/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.4.3/node_modules/@capacitor/filesystem/android')
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem/android')
include ':capacitor-keyboard'
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/keyboard/android')
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/keyboard/android')
include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.4.3/node_modules/@capacitor/preferences/android')
project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences/android')
include ':capacitor-push-notifications'
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/push-notifications/android')
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications/android')
include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/share/android')
include ':capawesome-capacitor-android-dark-mode-support'
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@7.0.0_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-android-dark-mode-support/android')
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-android-dark-mode-support/android')
include ':capawesome-capacitor-badge'
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-badge/android')
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge/android')
include ':nostr-signer-capacitor-plugin'
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.4.3/node_modules/nostr-signer-capacitor-plugin/android')
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin/android')
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+4 -5
View File
@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
+2 -2
View File
@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
+14 -14
View File
@@ -1,18 +1,18 @@
ext {
minSdkVersion = 23
compileSdkVersion = 35
targetSdkVersion = 35
androidxActivityVersion = '1.9.2'
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
//https://github.com/ionic-team/capacitor/issues/7866
// androidxAppCompatVersion = '1.7.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.15.0'
androidxFragmentVersion = '1.8.4'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.12.1'
// androidxAppCompatVersion = '1.7.1'
androidxAppCompatVersion = '1.7.1'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.17.0'
androidxFragmentVersion = '1.8.9'
coreSplashScreenVersion = '1.2.0'
androidxWebkitVersion = '1.14.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.2.1'
androidxEspressoCoreVersion = '3.6.1'
cordovaAndroidVersion = '10.1.1'
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
}
+6 -5
View File
@@ -2,8 +2,8 @@
temp_env=$(declare -p -x)
if [ -f .env.template ]; then
source .env.template
if [ -f .env ]; then
source .env
fi
# Avoid overwriting env vars provided directly
@@ -14,12 +14,13 @@ if [[ -z $VITE_BUILD_HASH ]]; then
export VITE_BUILD_HASH=$(git rev-parse --short HEAD)
fi
if [[ $VITE_PLATFORM_LOGO =~ ^https://* ]]; then
curl $VITE_PLATFORM_LOGO > static/logo.png
if [[ $VITE_PLATFORM_LOGO =~ ^https:// ]]; then
curl -fSL "$VITE_PLATFORM_LOGO" -o static/logo.png
export VITE_PLATFORM_LOGO=static/logo.png
fi
npx pwa-assets-generator
# Ensure generator uses local path (dotenv may have loaded URL from .env)
VITE_PLATFORM_LOGO="${VITE_PLATFORM_LOGO}" npx pwa-assets-generator
npx vite build
# Replace index.html variables with stuff from our env
+22 -16
View File
@@ -1,18 +1,24 @@
import type { CapacitorConfig } from '@capacitor/cli';
import type {CapacitorConfig} from "@capacitor/cli"
const config: CapacitorConfig = {
appId: 'social.flotilla',
appName: 'Flotilla',
webDir: 'build'
server: {
androidScheme: "https"
appId: "social.flotilla",
appName: "Flotilla",
webDir: "build",
ios: {
scheme: "Flotilla Chat",
},
android: {
adjustMarginsForEdgeToEdge: false,
adjustMarginsForEdgeToEdge: true,
},
plugins: {
CapacitorHttp: {
enabled: true,
},
SystemBars: {
insetsHandling: "enable",
},
SplashScreen: {
androidSplashResourceName: "splash"
androidSplashResourceName: "splash",
},
Keyboard: {
style: "DARK",
@@ -20,14 +26,14 @@ const config: CapacitorConfig = {
},
Badge: {
persist: true,
autoClear: true
autoClear: true,
},
},
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
// server: {
// url: "http://192.168.1.115:1847",
// cleartext: true
// },
};
server: {
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
// url: "http://192.168.1.17:1847",
// cleartext: true,
},
}
export default config;
export default config
+10 -8
View File
@@ -291,7 +291,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -342,7 +342,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
@@ -358,18 +358,19 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 25;
CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.5.3;
MARKETING_VERSION = 1.7.4;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -384,17 +385,18 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 25;
CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.5.3;
MARKETING_VERSION = 1.7.4;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

+10 -6
View File
@@ -20,8 +20,18 @@
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Flotilla uses the camera when you enable it in a voice room.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Flotilla uses the microphone when you enable it in a voice room.</string>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@@ -47,11 +57,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
</dict>
</plist>
+15 -12
View File
@@ -1,6 +1,6 @@
require_relative '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios/scripts/pods_helpers'
require_relative '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '14.0'
platform :ios, '15.0'
use_frameworks!
# workaround to avoid Xcode caching of Pods that requires
@@ -9,16 +9,19 @@ use_frameworks!
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@7.4.3_@capacitor+core@7.4.3/node_modules/@capacitor/ios'
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.4.3/node_modules/@capacitor-community/safe-area'
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.1.0_@capacitor+core@7.4.3/node_modules/@capacitor/app'
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@7.1.4_@capacitor+core@7.4.3/node_modules/@capacitor/filesystem'
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/keyboard'
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@7.0.2_@capacitor+core@7.4.3/node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.3_@capacitor+core@7.4.3/node_modules/@capacitor/push-notifications'
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.4.3/node_modules/@capawesome/capacitor-badge'
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.4.3/node_modules/nostr-signer-capacitor-plugin'
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios'
pod 'AparajitaCapacitorSecureStorage', :path => '../../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage'
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area'
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app'
pod 'CapacitorClipboard', :path => '../../node_modules/.pnpm/@capacitor+clipboard@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/clipboard'
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem'
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/keyboard'
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications'
pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/share'
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge'
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin'
end
target 'Flotilla Chat' do
Executable
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env node
import fs from 'fs'
import { execSync } from 'child_process'
const force = process.argv.includes('--force')
if (execSync('git status --porcelain', { encoding: 'utf8' }).trim() && !force) {
console.error('Error: Git working tree is dirty. Please commit or stash your changes first, or re-run with --force.')
process.exit(1)
}
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
pkg.pnpm.overrides = pkg.pnpm.overrides || {}
pkg.pnpm.overrides["@welshman/app"] = "link:../welshman/packages/app"
pkg.pnpm.overrides["@welshman/content"] = "link:../welshman/packages/content"
pkg.pnpm.overrides["@welshman/editor"] = "link:../welshman/packages/editor"
pkg.pnpm.overrides["@welshman/feeds"] = "link:../welshman/packages/feeds"
pkg.pnpm.overrides["@welshman/lib"] = "link:../welshman/packages/lib"
pkg.pnpm.overrides["@welshman/net"] = "link:../welshman/packages/net"
pkg.pnpm.overrides["@welshman/router"] = "link:../welshman/packages/router"
pkg.pnpm.overrides["@welshman/signer"] = "link:../welshman/packages/signer"
pkg.pnpm.overrides["@welshman/store"] = "link:../welshman/packages/store"
pkg.pnpm.overrides["@welshman/util"] = "link:../welshman/packages/util"
// pkg.pnpm.overrides["nostr-editor"] = "link:../nostr-editor"
// pkg.pnpm.overrides["@pomade/core"] = "link:../pomade/packages/core"
// pkg.pnpm.overrides["nostr-signer-capacitor-plugin"] = "link:../nostr-signer-capacitor-plugin"
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n')
execSync('pnpm i', { stdio: 'inherit' })
execSync('git checkout -f pnpm-lock.yaml', { stdio: 'inherit' })
execSync('git checkout -f package.json', { stdio: 'inherit' })
+66 -51
View File
@@ -1,86 +1,97 @@
{
"name": "flotilla",
"version": "1.5.3",
"version": "1.7.4",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "./build.sh",
"sourcemaps": "./build.sh && ./sourcemaps.sh",
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"tauri:info": "tauri info",
"tauri:icons": "tauri icon assets/logo.png --output src-tauri/icons",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check src && eslint src",
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte)$' | xargs -r prettier --write",
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write",
"format:all": "prettier --write src",
"prepare": "husky"
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@eslint/js": "^9.37.0",
"@sentry/cli": "^2.56.1",
"@sveltejs/kit": "^2.46.5",
"@eslint/js": "^9.39.2",
"@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@tailwindcss/postcss": "^4.2.2",
"@tauri-apps/cli": "^2.9.6",
"@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.21",
"autoprefixer": "^10.4.23",
"classnames": "^2.5.1",
"eslint": "^9.37.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^9.1.2",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.15.0",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.39.12",
"svelte-check": "^4.3.3",
"tailwindcss": "^3.4.18",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1",
"svelte": "^5.48.0",
"svelte-check": "^4.3.5",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.1",
"vite": "^5.4.20"
"typescript-eslint": "^8.53.1",
"vite": "^5.4.21"
},
"type": "module",
"dependencies": {
"@capacitor-community/safe-area": "7.0.0-alpha.1",
"@capacitor/android": "^7.4.3",
"@capacitor/app": "^7.1.0",
"@capacitor/cli": "^7.4.3",
"@capacitor/core": "^7.4.3",
"@capacitor/filesystem": "^7.1.4",
"@capacitor/ios": "^7.4.3",
"@capacitor/keyboard": "^7.0.3",
"@capacitor/preferences": "^7.0.2",
"@capacitor/push-notifications": "^7.0.3",
"@capawesome/capacitor-android-dark-mode-support": "^7.0.0",
"@capawesome/capacitor-badge": "^7.0.1",
"@aparajita/capacitor-secure-storage": "^8.0.0",
"@capacitor-community/safe-area": "^8.0.1",
"@capacitor/android": "^8.0.1",
"@capacitor/app": "^8.0.0",
"@capacitor/cli": "^8.0.1",
"@capacitor/clipboard": "^8.0.1",
"@capacitor/core": "^8.0.1",
"@capacitor/filesystem": "^8.1.0",
"@capacitor/ios": "^8.0.1",
"@capacitor/keyboard": "^8.0.0",
"@capacitor/preferences": "^8.0.0",
"@capacitor/push-notifications": "^8.0.0",
"@capacitor/share": "^8.0.1",
"@capawesome/capacitor-android-dark-mode-support": "^8.0.0",
"@capawesome/capacitor-badge": "^8.0.0",
"@getalby/lightning-tools": "^6.1.0",
"@getalby/sdk": "^5.1.2",
"@noble/curves": "^1.9.7",
"@pomade/core": "^0.2.3",
"@poppanator/sveltekit-svg": "^4.2.1",
"@sentry/browser": "^8.55.0",
"@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.26.3",
"@types/qrcode": "^1.5.5",
"@tiptap/core": "^2.27.2",
"@tiptap/pm": "^2.27.2",
"@types/qrcode": "^1.5.6",
"@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.6.8",
"@welshman/content": "^0.6.8",
"@welshman/editor": "^0.6.8",
"@welshman/feeds": "^0.6.8",
"@welshman/lib": "^0.6.8",
"@welshman/net": "^0.6.8",
"@welshman/router": "^0.6.8",
"@welshman/signer": "^0.6.8",
"@welshman/store": "^0.6.8",
"@welshman/util": "^0.6.8",
"compressorjs": "^1.2.1",
"daisyui": "^4.12.24",
"date-picker-svelte": "^2.16.0",
"@welshman/app": "^0.8.13",
"@welshman/content": "^0.8.13",
"@welshman/editor": "^0.8.13",
"@welshman/feeds": "^0.8.13",
"@welshman/lib": "^0.8.13",
"@welshman/net": "^0.8.13",
"@welshman/router": "^0.8.13",
"@welshman/signer": "^0.8.13",
"@welshman/store": "^0.8.13",
"@welshman/util": "^0.8.13",
"compressorjs-next": "^1.1.2",
"daisyui": "^5.5.19",
"date-picker-svelte": "^2.17.0",
"dotenv": "^16.6.1",
"emoji-picker-element": "^1.27.0",
"emoji-picker-element": "^1.28.1",
"fuse.js": "^7.1.0",
"husky": "^9.1.7",
"idb": "^8.0.3",
"nostr-signer-capacitor-plugin": "^0.0.4",
"nostr-tools": "^2.14.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"livekit-client": "^2.17.2",
"nostr-signer-capacitor-plugin": "github:coracle-social/nostr-signer-capacitor-plugin#main",
"nostr-tools": "^2.19.4",
"prettier-plugin-tailwindcss": "^0.7.2",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.4",
"throttle-debounce": "^5.0.2",
@@ -88,11 +99,15 @@
},
"pnpm": {
"ignoredBuiltDependencies": [
"@sentry/cli",
"esbuild"
],
"onlyBuiltDependencies": [
"sharp"
]
}
"sharp",
"nostr-signer-capacitor-plugin"
],
"overrides": {
"sharp": "0.35.0-rc.0"
}
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
}
+2853 -2527
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -1,6 +1,5 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
"@tailwindcss/postcss": {},
},
}
+1 -1
View File
@@ -1,8 +1,8 @@
import dotenv from "dotenv"
import {defineConfig, minimalPreset as preset} from "@vite-pwa/assets-generator/config"
dotenv.config({path: ".env.local"})
dotenv.config({path: ".env"})
dotenv.config({path: ".env.template"})
export default defineConfig({
preset,
+2
View File
@@ -0,0 +1,2 @@
[toolchain]
channel = "1.92.0"
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash
hash=$(find build -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum | awk '{print $1}')
sentry-cli \
--url https://glitchtip.coracle.social \
--auth-token $GLITCHTIP_AUTH_TOKEN \
--api-key $VITE_GLITCHTIP_API_KEY \
sourcemaps \
--org coracle \
--project flotilla \
--release $hash \
upload \
--url-prefix /_app/immutable/ \
build/_app/immutable
+4784
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "flotilla"
version = "0.1.0"
edition = "2021"
[lib]
name = "flotilla_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.5.3", features = [] }
[dependencies]
tauri = { version = "2.9.5", features = [] }
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default desktop capability for the main window",
"windows": ["main"],
"permissions": ["core:default"]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

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