Compare commits

...

161 Commits

Author SHA1 Message Date
mplorentz 9364b46e5d Clean up deriveProfile call in ProfileCircle 2026-03-16 20:36:59 +00:00
mplorentz af76726c88 Show VoiceWidget when disconnected but still viewing room page. 2026-03-16 20:36:59 +00:00
Jon Staab 33b9edb8b5 A little bit of cleanup 2026-03-16 20:36:59 +00:00
Jon Staab d410a6a043 Revert "Fix a docker rebuild issue (#88)"
This reverts commit bc145f4caf.
2026-03-16 20:36:59 +00:00
Jon Staab 2fa0f29fc6 Prevent shrinkage 2026-03-16 20:36:59 +00:00
Jon Staab c1c5d99a2a Slight style tweaks to room nav 2026-03-16 20:36:59 +00:00
Jon Staab 7b5e1eae20 Slight style tweaks to room nav 2026-03-16 20:36:59 +00:00
mplorentz 1792dd157d Remove info button from voice widgetn 2026-03-16 20:36:59 +00:00
mplorentz 2e1a0363bd Expect livekit identity not pubkey in 39004 2026-03-16 20:36:59 +00:00
mplorentz 61a3514cd2 Fix scrolling of space menu on mobile 2026-03-16 20:36:59 +00:00
mplorentz f89f1da947 Fix build on ios 2026-03-16 20:36:59 +00:00
mplorentz edc22bc88d Request microphone permission on ios 2026-03-16 20:36:59 +00:00
mplorentz 5135da826e Don't leave voice room on second click 2026-03-16 20:36:59 +00:00
mplorentz b340b9e76d Display voice widget in chat rooms on mobile 2026-03-16 20:36:59 +00:00
mplorentz 36eb8ace32 Fix voice widget layout on mobile 2026-03-16 20:36:59 +00:00
mplorentz 103a87e880 Fix voice room icon getting truncated in PageBar 2026-03-16 20:36:59 +00:00
mplorentz 2acf971a10 Integrate new PageBar behavior 2026-03-16 20:36:59 +00:00
mplorentz 1e42fd9b2d Switch to 39004 for room presence 2026-03-16 20:36:59 +00:00
mplorentz 87d509df06 Expect HTTP 204 for livekit support 2026-03-16 20:36:59 +00:00
mplorentz 588c4b9c3f Animate voice widget in and out 2026-03-16 20:36:59 +00:00
mplorentz 504764338c Add join and leave sounds 2026-03-16 20:36:59 +00:00
mplorentz 50199268f7 Remove no-text rooms, highlight active room, fix custom voice room icons 2026-03-16 20:36:59 +00:00
mplorentz 25228f785d Request microphone permissions when unmuting 2026-03-16 20:36:59 +00:00
mplorentz 697804c621 Show custom icon for voice rooms 2026-03-16 20:36:59 +00:00
mplorentz 298e419b02 Remove voice-only rooms from "Other rooms" 2026-03-16 20:36:59 +00:00
mplorentz 63d9adef19 Add lables on hover for voice room controls 2026-03-16 20:36:59 +00:00
mplorentz 697c116956 Use joinAbortController as isJoining 2026-03-16 20:36:59 +00:00
mplorentz 05de3dd7e0 Hide voice rooms on mobile 2026-03-16 20:36:59 +00:00
mplorentz 0b09b63d85 Move unfavorited voice rooms into a new section in the SpaceMenu 2026-03-16 20:36:59 +00:00
mplorentz c518926c2b Use if let 2026-03-16 20:36:59 +00:00
mplorentz a691f7b80a use new livekit welshman properties 2026-03-16 20:36:59 +00:00
mplorentz 84be1ae47b Allow joining without a microphone 2026-03-16 20:36:59 +00:00
mplorentz 89d11dccd3 Log join errors 2026-03-16 20:36:59 +00:00
mplorentz b1e52c29b4 include room participants inside VoiceRoomItem border 2026-03-16 20:36:59 +00:00
Jon Staab ea8a9652c9 Bump welshman 2026-03-16 20:36:59 +00:00
Jon Staab 3e14b31a09 Tweak voice room display 2026-03-16 20:36:59 +00:00
mplorentz c18154915d Don't show technical error message to the user 2026-03-16 20:36:59 +00:00
mplorentz 703d573f96 Revert changes to dockerfile 2026-03-16 20:36:59 +00:00
mplorentz 6fcda91764 Address remaining PR comments 2026-03-16 20:36:59 +00:00
mplorentz 79728f68a4 Use TrustedEvent 2026-03-16 20:36:59 +00:00
mplorentz 83133b50e9 Use bell icon for notifications 2026-03-16 20:36:59 +00:00
mplorentz f4e4ca1038 Address PR comments on RoomForm 2026-03-16 20:36:59 +00:00
mplorentz ea3be89341 Reorder imports 2026-03-16 20:36:59 +00:00
mplorentz e0d2988877 Add room info button to VoiceWidget 2026-03-16 20:36:59 +00:00
mplorentz fa976abe89 Fix muted icon 2026-03-16 20:36:59 +00:00
mplorentz 87b37bf0d8 Add a right around user avatar when speaking 2026-03-16 20:36:59 +00:00
mplorentz 6e72bc4b00 Move room type field up in the RoomForm 2026-03-16 20:36:59 +00:00
mplorentz 1b99ed6704 Disable rooms on mobile temporarily 2026-03-16 20:36:59 +00:00
mplorentz 6330150c66 Check if livekit is configured on the relay during room creation/edit 2026-03-16 20:36:59 +00:00
mplorentz 559df4b948 Add ability to joining a voice room while it's in progress 2026-03-16 20:36:59 +00:00
mplorentz 9bd57b0caa Add loading indicator while joining 2026-03-16 20:36:59 +00:00
mplorentz 652461fffc Add error toast on connection failure. 2026-03-16 20:36:59 +00:00
mplorentz de76cfd208 Make voice rooms in sidebar reactive 2026-03-16 20:36:59 +00:00
mplorentz 108a9fef06 Get rid of volume icon next to text rooms 2026-03-16 20:36:59 +00:00
mplorentz 140440ca4c Allow user to configure room for voice, text, or both. 2026-03-16 20:36:59 +00:00
mplorentz 9a2fcec3f6 Remove plan 2026-03-16 20:36:59 +00:00
mplorentz 52f2f31ce6 Move livekit auth to relay 2026-03-16 20:36:59 +00:00
mplorentz 3049efe889 Fix logo download during docker build 2026-03-16 20:36:59 +00:00
mplorentz df9f3a707a Source .env explicitly during build 2026-03-16 20:36:59 +00:00
mplorentz eeaad7b9b3 Fix issue where docker build would rebuild when app did not change 2026-03-16 20:36:59 +00:00
mplorentz 0cbcbfc47b Try just serving 404.html 2026-03-16 20:36:59 +00:00
mplorentz 73fb411122 Try rewrites to get SPA mode working 2026-03-16 20:36:59 +00:00
mplorentz 7d3176da78 Serve in SPA mode 2026-03-16 20:36:59 +00:00
mplorentz 0ab1cfadde Auto-play voice track when joining a voice room 2026-03-16 20:36:59 +00:00
mplorentz fa4afe0c01 Add android microphoen permissions 2026-03-16 20:36:59 +00:00
mplorentz ca259b119b ignore pnpm store 2026-03-16 20:36:59 +00:00
mplorentz 0daab7a46c WIP voice channels 2026-03-16 20:36:59 +00:00
mplorentz 9fe155c118 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: #88
Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social>
Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-03-16 20:36:59 +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
Docker / build-and-push-image (push) Successful in 15m7s
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: #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: #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: #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: #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: #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: #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
Docker / build-and-push-image (push) Successful in 14m14s
2026-02-17 12:01:12 -08:00
Jon Staab 21f3970ca8 Use explicit image name in workflow file
Docker / build-and-push-image (push) Has been cancelled
2026-02-17 11:48:52 -08:00
Jon Staab 919fe29ffb Update docker image creation for gitea ci
Docker / build-and-push-image (push) Failing after 20m22s
2026-02-17 10:45:39 -08:00
236 changed files with 11232 additions and 2693 deletions
+1
View File
@@ -1,5 +1,6 @@
--ignore-dir=.svelte-kit
--ignore-dir=android
--ignore-dir=target
--ignore-dir=build
--ignore-dir=ios/DerivedData
--ignore-dir=ios/App/App/public
+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
+2 -2
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_POMADE_SIGNERS=
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
@@ -15,7 +15,7 @@ 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_MESSAGING_RELAYS=auth.nostr1.com
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com,nostr-01.uid.ovh,relay.keychat.io,relay.0xchat.com
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
VITE_GLITCHTIP_API_KEY=
+1
View File
@@ -1,4 +1,5 @@
src/assets
target
build
.idea
.gradle
@@ -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
+6
View File
@@ -1,5 +1,6 @@
# Env
.env
.env.local
# Vite
vite.config.js.timestamp-*
@@ -24,9 +25,14 @@ android/app/src/main/assets/public/
# Web/JavaScript
node_modules/
.pnpm-store/
build/
.svelte-kit/
# Rust/Tauri
*target/
src-tauri/binaries/
# iOS
ios/App/App/public
ios/DerivedData
+11 -1
View File
@@ -157,7 +157,7 @@ src/
- Derive all other data inside the component from identifiers
- Example: Don't pass `members` prop, derive it from `h` inside component
**Code Style:**
**CRITICAL Code Style Guidelines:**
- **No `null`** - only use `undefined`
- Svelte 5 runes (`$state`, `$derived`, `$effect`) only in UI components
@@ -168,6 +168,16 @@ src/
- 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.
**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
+31
View File
@@ -1,5 +1,36 @@
# Changelog
# Current
* Enable email/password login
* Add up/edit to direct messages
# 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
+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"]
+4 -4
View File
@@ -6,12 +6,12 @@ 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
@@ -20,7 +20,7 @@ If you're deploying a custom version of flotilla, be sure to remove the `plausib
## Development
See [./CONTRIBUTING.md](CONTRIBUTING.md).
See [CONTRIBUTING.md](AGENTS.md).
## Deployment
@@ -29,7 +29,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:
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "social.flotilla"
minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion
versionCode 40
versionName "1.6.4"
versionCode 41
versionName "1.6.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+2
View File
@@ -42,4 +42,6 @@
<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" />
</manifest>
+1 -1
View File
@@ -27,4 +27,4 @@ include ':capawesome-capacitor-badge'
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@8.0.1/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')
+7 -3
View File
@@ -5,6 +5,9 @@ temp_env=$(declare -p -x)
if [ -f .env.template ]; then
source .env.template
fi
if [ -f .env.local ]; then
source .env.local
fi
# Avoid overwriting env vars provided directly
# https://stackoverflow.com/a/69127685/1467342
@@ -14,12 +17,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
+3
View File
@@ -4,6 +4,9 @@ const config: CapacitorConfig = {
appId: "social.flotilla",
appName: "Flotilla",
webDir: "build",
ios: {
scheme: "Flotilla Chat",
},
android: {
adjustMarginsForEdgeToEdge: true,
},
+4 -4
View File
@@ -358,14 +358,14 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30;
CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.6.4;
MARKETING_VERSION = 1.6.5;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -385,14 +385,14 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30;
CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.6.4;
MARKETING_VERSION = 1.6.5;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
+8 -6
View File
@@ -20,8 +20,16 @@
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>Flotilla uses the microphone for voice chat in rooms.</string>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@@ -47,11 +55,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
</dict>
</plist>
+1 -1
View File
@@ -18,7 +18,7 @@ def capacitor_pods
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 '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@0.0.4_@capacitor+core@8.0.1/node_modules/nostr-signer-capacitor-plugin'
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
+6 -3
View File
@@ -1,11 +1,12 @@
#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
import { execSync } from 'child_process'
if (execSync('git status --porcelain', { encoding: 'utf8' }).trim()) {
console.error('Error: Git working tree is dirty. Please commit or stash your changes first.')
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)
}
@@ -22,7 +23,9 @@ 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')
+22 -15
View File
@@ -1,11 +1,15 @@
{
"name": "flotilla",
"version": "1.6.4",
"version": "1.6.5",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "./build.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",
@@ -18,6 +22,7 @@
"@eslint/js": "^9.39.2",
"@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@tauri-apps/cli": "^2.9.6",
"@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.23",
"classnames": "^2.5.1",
@@ -52,7 +57,7 @@
"@getalby/lightning-tools": "^6.1.0",
"@getalby/sdk": "^5.1.2",
"@noble/curves": "^1.9.7",
"@pomade/core": "^0.0.12",
"@pomade/core": "^0.2.1",
"@poppanator/sveltekit-svg": "^4.2.1",
"@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.27.2",
@@ -60,17 +65,17 @@
"@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.8.4",
"@welshman/content": "^0.8.4",
"@welshman/editor": "^0.8.4",
"@welshman/feeds": "^0.8.4",
"@welshman/lib": "^0.8.4",
"@welshman/net": "^0.8.4",
"@welshman/router": "^0.8.4",
"@welshman/signer": "^0.8.4",
"@welshman/store": "^0.8.4",
"@welshman/util": "^0.8.4",
"compressorjs": "^1.2.1",
"@welshman/app": "^0.8.9",
"@welshman/content": "^0.8.9",
"@welshman/editor": "^0.8.9",
"@welshman/feeds": "^0.8.9",
"@welshman/lib": "^0.8.9",
"@welshman/net": "^0.8.9",
"@welshman/router": "^0.8.9",
"@welshman/signer": "^0.8.9",
"@welshman/store": "^0.8.9",
"@welshman/util": "^0.8.9",
"compressorjs-next": "^1.1.2",
"daisyui": "^4.12.24",
"date-picker-svelte": "^2.17.0",
"dotenv": "^16.6.1",
@@ -78,7 +83,8 @@
"fuse.js": "^7.1.0",
"husky": "^9.1.7",
"idb": "^8.0.3",
"nostr-signer-capacitor-plugin": "^0.0.4",
"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.6.14",
"qr-scanner": "^1.4.2",
@@ -91,7 +97,8 @@
"esbuild"
],
"onlyBuiltDependencies": [
"sharp"
"sharp",
"nostr-signer-capacitor-plugin"
],
"overrides": {
"sharp": "0.35.0-rc.0"
+1022 -519
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
import dotenv from "dotenv"
import {defineConfig, minimalPreset as preset} from "@vite-pwa/assets-generator/config"
dotenv.config({path: ".env"})
dotenv.config({path: ".env.local"})
dotenv.config({path: ".env.template"})
export default defineConfig({
+2
View File
@@ -0,0 +1,2 @@
[toolchain]
channel = "1.92.0"
+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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

+2
View File
@@ -0,0 +1,2 @@
[toolchain]
channel = "1.92.0"
+6
View File
@@ -0,0 +1,6 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
flotilla_lib::run();
}
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Flotilla",
"mainBinaryName": "flotilla",
"identifier": "social.flotilla.app",
"build": {
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build",
"devUrl": "http://localhost:1847",
"frontendDist": "../build"
},
"app": {
"security": {
"capabilities": ["default"]
},
"windows": [
{
"label": "main",
"title": "Flotilla",
"width": 1240,
"height": 775,
"resizable": true
}
]
},
"bundle": {
"active": false,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
+22 -4
View File
@@ -274,7 +274,7 @@
.input-editor,
.chat-editor,
.note-editor {
@apply -m-1 min-h-12 p-1 text-sm;
@apply -m-1 p-1;
}
.tiptap {
@@ -300,7 +300,7 @@
}
.tiptap {
@apply max-h-[350px] overflow-y-auto p-2 px-4;
@apply max-h-[350px] min-h-10 overflow-y-auto p-2 px-4;
}
.tiptap p.is-editor-empty:first-child::before {
@@ -402,6 +402,10 @@ progress[value]::-webkit-progress-value {
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
}
.ct {
@apply top-[calc(var(--sait)+5rem)] md:top-[calc(var(--sait)+3rem)];
}
/* Keyboard open state adjustments */
body.keyboard-open .cb {
@@ -418,6 +422,20 @@ body.keyboard-open .hide-on-keyboard {
@apply cb cw fixed z-compose;
}
.chat__scroll-down {
@apply fixed bottom-28 right-4 z-feature md:bottom-16;
.chat__compose-zone {
@apply cb cw fixed z-compose;
}
.chat__compose-zone .chat__compose-inner {
@apply min-w-0;
}
.chat__scroll-down {
@apply pb-sai fixed bottom-28 right-4 z-feature md:bottom-16;
}
/* content visibility */
.cv {
content-visibility: auto;
}
+2 -3
View File
@@ -1,12 +1,11 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {page} from "$app/stores"
import {pubkey} from "@welshman/app"
import Dialog from "@lib/components/Dialog.svelte"
import Landing from "@app/components/Landing.svelte"
import Toast from "@app/components/Toast.svelte"
import PrimaryNav from "@app/components/PrimaryNav.svelte"
import {modals} from "@app/util/modal"
import {modal} from "@app/util/modal"
interface Props {
children: Snippet
@@ -20,7 +19,7 @@
<PrimaryNav>
{@render children?.()}
</PrimaryNav>
{:else if !$modals[$page.url.hash.slice(1)]}
{:else if !$modal}
<Dialog children={{component: Landing, props: {}}} />
{/if}
</div>
+1 -1
View File
@@ -19,7 +19,7 @@
</script>
<Link
class="col-3 card2 bg-alt w-full cursor-pointer shadow-md"
class="cv col-3 card2 bg-alt w-full cursor-pointer shadow-md"
href={makeCalendarPath(url, getAddress(event))}>
<CalendarEventHeader {event} />
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
+152 -95
View File
@@ -2,9 +2,11 @@
import type {Snippet} from "svelte"
import {onMount} from "svelte"
import {
ago,
int,
ms,
partition,
ifLet,
spec,
nthEq,
nthNe,
@@ -46,11 +48,12 @@
import ChatMembers from "@app/components/ChatMembers.svelte"
import ChatMessage from "@app/components/ChatMessage.svelte"
import ChatCompose from "@app/components/ChatCompose.svelte"
import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte"
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte"
import {userSettingsValues, PLATFORM_NAME, deriveChat} from "@app/core/state"
import {pushModal} from "@app/util/modal"
import {prependParent} from "@app/core/commands"
import {makeDelete, prependParent} from "@app/core/commands"
import {pushToast} from "@app/util/toast"
type Props = {
@@ -78,73 +81,115 @@
parent = undefined
}
const onSubmit = async (params: EventContent) => {
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
// Remove p tags since they result in forking the conversation
params.tags = params.tags.filter(nthNe(0, "p"))
// Add our reply quote to content
params = prependParent(parent, params)
const [imetaTags, tags] = partition(nthEq(0, "imeta"), params.tags)
const imetas = getTags("imeta", imetaTags).map(tagsFromIMeta)
const templates: EventTemplate[] = []
const buffer = []
const addTemplate = (kind: number, content: string, tags: string[][]) => {
content = content.trim()
if (content) {
templates.push(makeEvent(kind, {content, tags: [...tags, ...ptags]}))
}
}
for (const p of parse(params)) {
const imeta = isLink(p)
? imetas.find(tags => tags.find(spec(["url", p.value.url.toString()])))
: undefined
if (isLink(p) && imeta) {
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
addTemplate(
DIRECT_MESSAGE_FILE,
p.value.url.toString(),
imeta.slice(1).filter(nthNe(0, "url")),
)
} else {
buffer.push(p.raw)
}
}
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
// Sleep 1 second between each one to make sure timestamps are distinct
const thunks = await Promise.all(
Array.from(enumerate(templates)).map(([i, event]) =>
sendWrapped({
event,
recipients: pubkeys,
delay: $userSettingsValues.send_delay + ms(i),
}),
),
)
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk: mergeThunks(thunks)},
},
})
clearParent()
const clearEventToEdit = () => {
eventToEdit = undefined
}
const onSubmit = async (params: EventContent) => {
try {
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
// Remove p tags since they result in forking the conversation
params.tags = params.tags.filter(nthNe(0, "p"))
// Add our reply quote to content
params = prependParent(parent, params)
if (eventToEdit) {
if (eventToEdit.content === params.content) {
return
}
await sendWrapped({
event: makeDelete({event: eventToEdit, protect: false}),
recipients: pubkeys,
})
}
const [imetaTags, tags] = partition(nthEq(0, "imeta"), params.tags)
const imetas = getTags("imeta", imetaTags).map(tagsFromIMeta)
const templates: EventTemplate[] = []
const buffer = []
const addTemplate = (kind: number, content: string, tags: string[][]) => {
content = content.trim()
if (content) {
templates.push(
makeEvent(kind, {
content,
tags: [...tags, ...ptags],
created_at: eventToEdit?.created_at,
}),
)
}
}
for (const p of parse(params)) {
const imeta = isLink(p)
? imetas.find(tags => tags.find(spec(["url", p.value.url.toString()])))
: undefined
if (isLink(p) && imeta) {
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
addTemplate(
DIRECT_MESSAGE_FILE,
p.value.url.toString(),
imeta.slice(1).filter(nthNe(0, "url")),
)
} else {
buffer.push(p.raw)
}
}
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
// Sleep 1 second between each one to make sure timestamps are distinct
const thunks = await Promise.all(
Array.from(enumerate(templates)).map(([i, event]) =>
sendWrapped({
event,
recipients: pubkeys,
delay: $userSettingsValues.send_delay + ms(i),
}),
),
)
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk: mergeThunks(thunks)},
},
})
} finally {
clearParent()
clearEventToEdit()
}
}
const onEscape = () => {
clearParent()
clearEventToEdit()
}
const canEditEvent = (event: TrustedEvent) =>
event.pubkey === $pubkey &&
event.kind === DIRECT_MESSAGE &&
event.created_at >= ago(500, MINUTE)
const onEditEvent = (event: TrustedEvent) => {
clearParent()
eventToEdit = event
}
const onEditPrevious = () => ifLet($chat?.messages.toReversed().find(canEditEvent), onEditEvent)
let loading = $state(true)
let compose: ChatCompose | undefined = $state()
let parent: TrustedEvent | undefined = $state()
let eventToEdit: TrustedEvent | undefined = $state()
let chatCompose: HTMLElement | undefined = $state()
let dynamicPadding: HTMLElement | undefined = $state()
@@ -204,36 +249,36 @@
</script>
<PageBar>
{#snippet title()}
<Button class="flex flex-col gap-1 sm:flex-row sm:gap-2" onclick={showMembers}>
{#if others.length === 0}
<div class="row-2">
<ProfileCircle pubkey={$pubkey!} size={5} />
<ProfileName pubkey={$pubkey!} />
</div>
{:else if others.length === 1}
<div class="row-2">
<ProfileCircle pubkey={others[0]} size={5} />
<ProfileName pubkey={others[0]} />
</div>
{:else}
<div class="flex items-center gap-2">
<ProfileCircles pubkeys={others} size={5} />
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
<div class="flex items-center justify-between gap-4">
<div class="ellipsize flex items-center gap-4 whitespace-nowrap">
<Button class="flex flex-col gap-1 sm:flex-row sm:gap-2" onclick={showMembers}>
{#if others.length === 0}
<div class="row-2">
<ProfileCircle pubkey={$pubkey!} size={5} />
<ProfileName pubkey={$pubkey!} />
</div>
{:else if others.length === 1}
<div class="row-2">
<ProfileCircle pubkey={others[0]} size={5} />
<ProfileName pubkey={others[0]} />
and
{#if others.length === 2}
<ProfileName pubkey={others[1]} />
{:else}
{others.length - 1}
{others.length > 2 ? "others" : "other"}
{/if}
</p>
</div>
{/if}
</Button>
{/snippet}
{#snippet action()}
</div>
{:else}
<div class="flex items-center gap-2">
<ProfileCircles pubkeys={others} size={5} />
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
<ProfileName pubkey={others[0]} />
and
{#if others.length === 2}
<ProfileName pubkey={others[1]} />
{:else}
{others.length - 1}
{others.length > 2 ? "others" : "other"}
{/if}
</p>
</div>
{/if}
</Button>
</div>
{#if remove($pubkey, missingRelayLists).length > 0}
{@const count = remove($pubkey, missingRelayLists).length}
{@const label = count > 1 ? "lists are" : "list is"}
@@ -244,7 +289,7 @@
{count}
</div>
{/if}
{/snippet}
</div>
</PageBar>
<PageContent class="flex flex-col-reverse gap-2 pt-4">
@@ -285,7 +330,9 @@
event={$state.snapshot(value as TrustedEvent)}
{pubkeys}
{showPubkey}
{replyTo} />
{replyTo}
canEdit={canEditEvent}
onEdit={onEditEvent} />
{/if}
{/each}
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
@@ -305,6 +352,16 @@
{#if parent}
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
{/if}
{#if eventToEdit}
<ChatComposeEdit clear={clearEventToEdit} />
{/if}
</div>
<ChatCompose bind:this={compose} {onSubmit} />
{#key eventToEdit}
<ChatCompose
bind:this={compose}
{onSubmit}
{onEscape}
{onEditPrevious}
content={eventToEdit?.content} />
{/key}
</div>
+29 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import {onDestroy, onMount} from "svelte"
import {writable} from "svelte/store"
import type {EventContent} from "@welshman/util"
import {isMobile, preventDefault} from "@lib/html"
@@ -10,10 +11,13 @@
import {makeEditor} from "@app/editor"
type Props = {
content?: string
onEscape?: () => void
onEditPrevious?: () => void
onSubmit: (event: EventContent) => void
}
const {onSubmit}: Props = $props()
const {content, onEscape, onEditPrevious, onSubmit}: Props = $props()
const autofocus = !isMobile
@@ -21,6 +25,19 @@
export const focus = () => editor.then(ed => ed.chain().focus().run())
export const canEnterEditPrevious = () =>
editor.then(ed => ed.getText({blockSeparator: "\n"}) === "")
const handleKeyDown = async (event: KeyboardEvent) => {
if (event.key === "Escape") {
onEscape?.()
}
if (event.key === "ArrowUp" && (await canEnterEditPrevious())) {
onEditPrevious?.()
}
}
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
const submit = async () => {
@@ -38,12 +55,23 @@
}
const editor = makeEditor({
content,
autofocus,
submit,
uploading,
aggressive: true,
encryptFiles: true,
})
onMount(async () => {
const ed = await editor
ed.view.dom.addEventListener("keydown", handleKeyDown)
})
onDestroy(async () => {
const ed = await editor
ed?.view?.dom.removeEventListener("keydown", handleKeyDown)
})
</script>
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import {slide} from "@lib/transition"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
const {
clear,
}: {
clear: () => void
} = $props()
</script>
<div
class="relative flex h-8 items-center justify-between border-l-2 border-solid border-primary bg-base-300 px-2 pr-7 text-xs"
transition:slide>
<p class="text-primary">Editing message</p>
<Button onclick={clear} class="flex items-center">
<Icon icon={CloseCircle} />
</Button>
</div>
-65
View File
@@ -1,65 +0,0 @@
<script lang="ts">
import {goto} from "$app/navigation"
import {preventDefault} from "@lib/html"
import {shouldUnwrap} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {PLATFORM_NAME} from "@app/core/state"
import {clearModals} from "@app/util/modal"
const {next} = $props()
const nextUrl = $state.snapshot(next)
let loading = $state(false)
const submit = async () => {
loading = true
try {
shouldUnwrap.set(true)
clearModals()
goto(nextUrl)
} finally {
loading = false
}
}
const back = () => history.back()
</script>
<Modal tag="form" onsubmit={preventDefault(submit)}>
<ModalBody>
<ModalHeader>
<ModalTitle>Enable Messages</ModalTitle>
<ModalSubtitle>Do you want to enable direct messages?</ModalSubtitle>
</ModalHeader>
<p>
By default, direct messages are disabled, since loading them requires
{PLATFORM_NAME} to download and decrypt a lot of data.
</p>
<p>
If you'd like to enable them, please make sure your signer is set up to to auto-approve
requests to decrypt data.
</p>
</ModalBody>
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Enable Messages</Spinner>
<Icon icon={AltArrowRight} />
</Button>
</ModalFooter>
</Modal>
+7 -4
View File
@@ -23,11 +23,13 @@
interface Props {
event: TrustedEvent
replyTo: (event: TrustedEvent) => void
canEdit?: (event: TrustedEvent) => boolean
onEdit?: (event: TrustedEvent) => void
pubkeys: string[]
showPubkey?: boolean
}
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props()
const isOwn = event.pubkey === $pubkey
const profileDisplay = deriveProfileDisplay(event.pubkey)
@@ -35,6 +37,7 @@
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
const reply = () => replyTo(event)
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined
const deleteReaction = (event: TrustedEvent) =>
sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys})
@@ -44,7 +47,7 @@
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply})
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply, edit})
const togglePopover = () => {
if (popoverIsVisible) {
@@ -71,7 +74,7 @@
<Tippy
bind:popover
component={ChatMessageMenu}
props={{event, pubkeys, popover, replyTo}}
props={{event, pubkeys, popover, replyTo, edit}}
params={{
interactive: true,
trigger: "manual",
@@ -93,7 +96,7 @@
{/if}
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
<TapTarget
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl min-w-[100px]"
onTap={showMobileMenu}>
{#if showPubkey}
<div class="flex items-center gap-2">
+8 -1
View File
@@ -4,12 +4,14 @@
import ChatMessageEmojiButton from "@app/components/ChatMessageEmojiButton.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import {pushModal} from "@app/util/modal"
import Pen from "@assets/icons/pen.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
const {event, pubkeys, popover, replyTo} = $props()
const {event, pubkeys, popover, replyTo, edit} = $props()
const reply = () => replyTo(event)
const onEdit = () => edit?.()
const showInfo = () => {
popover.hide()
@@ -24,6 +26,11 @@
<Icon size={4} icon={Reply} />
</Button>
{/if}
{#if edit}
<Button class="btn join-item btn-xs" onclick={onEdit}>
<Icon size={4} icon={Pen} />
</Button>
{/if}
<Button class="btn join-item btn-xs" onclick={showInfo}>
<Icon size={4} icon={Code2} />
</Button>
@@ -3,6 +3,7 @@
import type {TrustedEvent} from "@welshman/util"
import {sendWrapped} from "@welshman/app"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
@@ -20,9 +21,10 @@
pubkeys: string[]
event: TrustedEvent
reply: () => void
edit?: () => void
}
const {event, pubkeys, reply}: Props = $props()
const {event, pubkeys, reply, edit}: Props = $props()
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
history.back()
@@ -39,6 +41,11 @@
reply()
}
const sendEdit = () => {
history.back()
edit?.()
}
const copyText = () => {
history.back()
clip(event.content)
@@ -62,6 +69,12 @@
<Icon size={4} icon={Reply} />
Send Reply
</Button>
{#if edit}
<Button class="btn btn-neutral w-full" onclick={sendEdit}>
<Icon size={4} icon={Pen} />
Edit Message
</Button>
{/if}
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
<Icon size={4} icon={SmileCircle} />
Send Reaction
+11 -1
View File
@@ -1,8 +1,10 @@
<script lang="ts">
import {uniq} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue, getAddress} from "@welshman/util"
import {getTagValue, getTagValues, getAddress} from "@welshman/util"
import {pubkey} from "@welshman/app"
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
import {normalizeTopic} from "@lib/util"
import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
@@ -27,6 +29,7 @@
const {url, event, showRoom, showActivity}: Props = $props()
const h = getTagValue("h", event.tags)
const topics = getTagValues("t", event.tags)
const path = makeClassifiedPath(url, getAddress(event))
const shouldProtect = canEnforceNip70(url)
@@ -45,6 +48,13 @@
Posted in #<RoomName {h} {url} />
</Link>
{/if}
<div class="flex min-w-0 flex-wrap gap-2">
{#each uniq(topics) as topic (topic)}
<button type="button" class="btn btn-xs rounded-full font-normal">
#{normalizeTopic(topic)}
</button>
{/each}
</div>
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
<ThunkStatusOrDeleted {event}>
<ClassifiedStatus {event} />
+2 -1
View File
@@ -18,7 +18,8 @@
const {d, title, status} = fromPairs(event.tags)
const [_, price = 0, currency = "SAT"] = getTag("price", event.tags) || []
const images = getTagValues("image", event.tags)
const initialValues = {d, title, status, content, price: Number(price), currency, images}
const topics = getTagValues("t", event.tags)
const initialValues = {d, title, status, content, price: Number(price), currency, images, topics}
</script>
<ClassifiedForm {url} {initialValues}>
+17 -1
View File
@@ -1,9 +1,10 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {randomId} from "@welshman/lib"
import {removeUndefined, randomId, uniq} from "@welshman/lib"
import {makeEvent, CLASSIFIED} from "@welshman/util"
import {publishThunk} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html"
import {normalizeTopic} from "@lib/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte"
@@ -14,6 +15,7 @@
import ModalBody from "@lib/components/ModalBody.svelte"
import ImagesInput from "@lib/components/ImagesInput.svelte"
import CurrencyInput from "@app/components/CurrencyInput.svelte"
import TopicMultiSelect from "@app/components/TopicMultiSelect.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state"
@@ -32,6 +34,7 @@
currency?: string
images?: string[]
status?: string
topics?: string[]
}
}
@@ -71,6 +74,10 @@
...ed.storage.nostr.getEditorTags(),
]
for (const topic of topics) {
tags.push(["t", topic])
}
if (await shouldProtect) {
tags.push(PROTECTED)
}
@@ -118,6 +125,7 @@
let price = $state(Number(initialValues?.price || 0))
let currency = $state(initialValues?.currency || "SAT")
let images = $state<(string | File)[]>(initialValues?.images || [])
let topics = $state(uniq(removeUndefined((initialValues?.topics || []).map(normalizeTopic))))
</script>
<Modal tag="form" onsubmit={preventDefault(submit)}>
@@ -150,6 +158,14 @@
</div>
{/snippet}
</Field>
<Field>
{#snippet label()}
<p>Topics</p>
{/snippet}
{#snippet input()}
<TopicMultiSelect bind:value={topics} />
{/snippet}
</Field>
<Field>
{#snippet label()}
<p>Price*</p>
+1 -1
View File
@@ -25,7 +25,7 @@
</script>
<Link
class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
class="cv col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
href={makeClassifiedPath(url, getAddress(event))}>
{#if title}
<div class="flex w-full items-center justify-between gap-2">
+5 -4
View File
@@ -1,12 +1,12 @@
<script lang="ts">
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
import {isRelayUrl} from "@welshman/util"
import {isRelayUrl, getTagValue} from "@welshman/util"
import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
import {pushModal} from "@app/util/modal"
import {dufflepud, PLATFORM_URL} from "@app/core/state"
import {dufflepud, PLATFORM_URL, IMAGE_CONTENT_TYPES, VIDEO_CONTENT_TYPES} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
const {value, event} = $props()
@@ -14,6 +14,7 @@
let hideImage = $state(false)
const url = value.url.toString()
const fileType = getTagValue("file-type", event.tags) || ""
const [href, external] = call(() => {
if (isRelayUrl(url)) return [makeSpacePath(url), false]
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
@@ -40,11 +41,11 @@
<Link {external} {href} class="my-2 block">
<div class="overflow-hidden rounded-box">
{#if url.match(/\.(mov|webm|mp4)$/)}
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}
<video controls src={url} class="max-h-96 rounded-box object-contain object-center">
<track kind="captions" />
</video>
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
{:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
</button>
@@ -1,6 +1,6 @@
<script lang="ts">
import {onMount, onDestroy} from "svelte"
import {displayUrl} from "@welshman/lib"
import {displayUrl, once} from "@welshman/lib"
import {
getTags,
getBlob,
@@ -26,8 +26,24 @@
const key = getTagValue("decryption-key", meta)
const nonce = getTagValue("decryption-nonce", meta)
const algorithm = getTagValue("encryption-algorithm", meta)
const mime = getTagValue("m", meta)
const fileName =
getTagValue("filename", meta) ||
getTagValue("name", meta) ||
decodeURIComponent(new URL(url).pathname.split("/").filter(Boolean).at(-1) || "image")
const onError = async () => {
const revokeSrc = () => {
if (src.startsWith("blob:")) {
URL.revokeObjectURL(src)
}
}
const setBlobSrc = (data: Blob | Uint8Array<ArrayBuffer>, type?: string) => {
revokeSrc()
src = URL.createObjectURL(new File([data], fileName, type ? {type} : undefined))
}
const onError = once(async () => {
// If the image failed to load, try authenticating
if (hash && $signer) {
const server = new URL(url).origin
@@ -36,14 +52,15 @@
const res = await getBlob(server, hash, {authEvent})
if (res.status === 200) {
src = URL.createObjectURL(await res.blob())
const blob = await res.blob()
setBlobSrc(blob, blob.type || undefined)
} else {
hasError = true
}
} else {
hasError = true
}
}
})
let hasError = $state(false)
let src = $state("")
@@ -57,7 +74,7 @@
const ciphertext = new Uint8Array(await response.arrayBuffer())
const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm})
src = URL.createObjectURL(new Blob([new Uint8Array(decryptedData)]))
setBlobSrc(new Uint8Array(decryptedData), mime)
}
} else {
src = url
@@ -65,7 +82,7 @@
})
onDestroy(() => {
URL.revokeObjectURL(src)
revokeSrc()
})
</script>
+4 -3
View File
@@ -1,18 +1,19 @@
<script lang="ts">
import {call, displayUrl} from "@welshman/lib"
import {isRelayUrl} from "@welshman/util"
import {isRelayUrl, getTagValue} from "@welshman/util"
import {preventDefault, stopPropagation} from "@lib/html"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import {pushModal} from "@app/util/modal"
import {PLATFORM_URL} from "@app/core/state"
import {PLATFORM_URL, IMAGE_CONTENT_TYPES} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
const {value, event} = $props()
const url = value.url.toString()
const fileType = getTagValue("file-type", event.tags) || ""
const [href, external] = call(() => {
if (isRelayUrl(url)) return [makeSpacePath(url), false]
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
@@ -23,7 +24,7 @@
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
</script>
{#if url.match(/\.(jpe?g|png|gif|webp)$/)}
{#if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
<!-- Use a real link so people can copy the href -->
<a
href={url}

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