Compare commits

...

37 Commits

Author SHA1 Message Date
mplorentz a42ba5446a 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-03 19:52:22 +00:00
Jon Staab ccfe1bded5 Bring back some notification badges 2026-02-27 12:25:16 -08:00
Jon Staab dfedf4e879 Make sync logic more robust 2026-02-26 14:51:31 -08:00
Jon Staab 0682c404f2 Bump welshman 2026-02-26 13:43:31 -08:00
Jon Staab 80ece70450 Support unban/unallow 2026-02-26 13:35:13 -08:00
Jon Staab f6245c712d Fix WalletPay 2026-02-26 13:06:34 -08:00
Jon Staab e0c5f0d4f1 Bump welshman 2026-02-25 15:50:14 -08:00
Jon Staab b616e2ea33 Blobify images so users can open them easier 2026-02-25 14:52:05 -08:00
Jon Staab 6fb6995103 Add manual invoice payment 2026-02-25 14:25:59 -08:00
Jon Staab 47bc0c2382 Update link_deps 2026-02-25 13:14:19 -08:00
Jon Staab 59a919d888 Fix enter selecting an option when there is no term. Closes #84 2026-02-25 10:01:08 -08:00
triesap 17d673c288 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-02-25 00:23:16 +00:00
triesap 985fd46243 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-02-25 00:09:50 +00:00
triesap 6d99e296e4 Show wallet status when wallet is unreachable 2026-02-24 15:45:29 -08:00
Jon Staab 21c34efb6a Remove capacitor plugin from overrides 2026-02-24 10:45:43 -08:00
Jon Staab 9c2f923c26 Bump welshman and pomade 2026-02-23 15:38:45 -08:00
Jon Staab 52d2d70838 Update nostr signer capacitor plugin 2026-02-23 15:15:59 -08:00
Jon Staab edd8824c5e Fix safe area inset for modal footer 2026-02-23 14:40:00 -08:00
Jon Staab 4f12ad9533 Bump nip55 signer 2026-02-21 11:18:38 -08:00
Jon Staab 2a5850e67f Update pomade version 2026-02-20 14:24:15 -08:00
Jon Staab 15341edece Fix svgs with 302 redirects on safari 2026-02-19 12:59:03 -08:00
Jon Staab 30f8b4160e Fix mask-repeat property 2026-02-19 12:26:18 -08:00
Jon Staab 937ca5ecf6 Refine space join dialogs and discover page 2026-02-19 11:20:46 -08:00
mplorentz ba1757d4f1 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-02-19 18:31:54 +00:00
Jon Staab 5a2b5f43b8 Add content-visibility class 2026-02-18 16:37:55 -08:00
Jon Staab 2f487705c3 Get rid of ChatEnable, automatically enable unwrapping when the user first visits the dms page. Closes #72 2026-02-18 16:02:49 -08:00
Jon Staab 558d59ce88 Enable auth for relays we're publishing to 2026-02-18 15:54:14 -08:00
triesap 1030edd322 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-02-18 23:03:08 +00:00
Jon Staab 981c8fd706 re-order some menu items 2026-02-18 14:50:39 -08:00
Jon Staab 45ade602b5 Fix iOS zoom bug 2026-02-18 12:49:20 -08:00
Jon Staab ef8a8682cd Reset to old home page 2026-02-18 11:02:39 -08:00
Jon Staab 112ac4b6d5 Continue working on feed page 2026-02-18 11:01:28 -08:00
Jon Staab 3a26d2cb0b Add better muting, add EventReducer 2026-02-18 10:22:23 -08:00
mplorentz a678bf42f1 Add back button to settings menu 2026-02-18 17:06:07 +00:00
Jon Staab dc314a1d1b Work on feed page 2026-02-17 17:37:19 -08:00
Jon Staab 3af56f6bb1 Prevent error loop on images 2026-02-17 13:45:00 -08:00
triesap a996664e6c 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-02-17 20:39:08 +00:00
136 changed files with 7251 additions and 1148 deletions
+1
View File
@@ -1,5 +1,6 @@
--ignore-dir=.svelte-kit --ignore-dir=.svelte-kit
--ignore-dir=android --ignore-dir=android
--ignore-dir=target
--ignore-dir=build --ignore-dir=build
--ignore-dir=ios/DerivedData --ignore-dir=ios/DerivedData
--ignore-dir=ios/App/App/public --ignore-dir=ios/App/App/public
+8
View File
@@ -2,3 +2,11 @@ node_modules
android android
ios ios
build build
# Git
.git
.gitignore
# Env files (keep .env for build; exclude local overrides)
.env.local
.env.*.local
+1
View File
@@ -1,4 +1,5 @@
src/assets src/assets
target
build build
.idea .idea
.gradle .gradle
+4
View File
@@ -27,6 +27,10 @@ node_modules/
build/ build/
.svelte-kit/ .svelte-kit/
# Rust/Tauri
*target/
src-tauri/binaries/
# iOS # iOS
ios/App/App/public ios/App/App/public
ios/DerivedData ios/DerivedData
+9
View File
@@ -170,6 +170,15 @@ src/
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates - 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. - 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 ## Common Tasks
### Adding a New Component ### Adding a New Component
+17 -10
View File
@@ -1,24 +1,31 @@
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
# Install pnpm
RUN npm install -g pnpm@latest RUN npm install -g pnpm@latest
# Set working directory
WORKDIR /app WORKDIR /app
# Copy package files
COPY package.json pnpm-lock.yaml ./ COPY package.json pnpm-lock.yaml ./
# Install dependencies
RUN pnpm i RUN pnpm i
# Copy the rest of the application # Copy everything (including .env when present) - build.sh will source it
COPY . . COPY . .
# Build the application ARG VITE_BUILD_HASH
ENV VITE_BUILD_HASH=${VITE_BUILD_HASH}
ENV NODE_OPTIONS=--max_old_space_size=16384 ENV NODE_OPTIONS=--max_old_space_size=16384
RUN pnpm run build RUN pnpm run build
# Default to serving the build directory # Stage 2: Runtime
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", "build"]
+1 -1
View File
@@ -11,7 +11,7 @@ You can also optionally create an `.env` file and populate it with the following
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust - `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_URL` - The url where the app will be hosted
- `VITE_PLATFORM_NAME` - The name of the app - `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_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_ACCENT` - A hex color for the app's accent color
- `VITE_PLATFORM_DESCRIPTION` - A description of the app - `VITE_PLATFORM_DESCRIPTION` - A description of the app
+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') 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' 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 if [ -f .env.template ]; then
source .env.template source .env.template
fi fi
if [ -f .env ]; then
source .env
fi
# Avoid overwriting env vars provided directly # Avoid overwriting env vars provided directly
# https://stackoverflow.com/a/69127685/1467342 # 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) export VITE_BUILD_HASH=$(git rev-parse --short HEAD)
fi fi
if [[ $VITE_PLATFORM_LOGO =~ ^https://* ]]; then if [[ $VITE_PLATFORM_LOGO =~ ^https:// ]]; then
curl $VITE_PLATFORM_LOGO > static/logo.png curl -fSL "$VITE_PLATFORM_LOGO" -o static/logo.png
export VITE_PLATFORM_LOGO=static/logo.png export VITE_PLATFORM_LOGO=static/logo.png
fi 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 npx vite build
# Replace index.html variables with stuff from our env # Replace index.html variables with stuff from our env
+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 '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 '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 '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 end
target 'Flotilla Chat' do target 'Flotilla Chat' do
+6 -3
View File
@@ -1,11 +1,12 @@
#!/usr/bin/env node #!/usr/bin/env node
import fs from 'fs' import fs from 'fs'
import path from 'path'
import { execSync } from 'child_process' import { execSync } from 'child_process'
if (execSync('git status --porcelain', { encoding: 'utf8' }).trim()) { const force = process.argv.includes('--force')
console.error('Error: Git working tree is dirty. Please commit or stash your changes first.')
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) 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/signer"] = "link:../welshman/packages/signer"
pkg.pnpm.overrides["@welshman/store"] = "link:../welshman/packages/store" pkg.pnpm.overrides["@welshman/store"] = "link:../welshman/packages/store"
pkg.pnpm.overrides["@welshman/util"] = "link:../welshman/packages/util" 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["@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') fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n')
+20 -14
View File
@@ -6,8 +6,12 @@
"dev": "vite dev", "dev": "vite dev",
"build": "./build.sh", "build": "./build.sh",
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner", "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": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "./check.sh",
"lint": "prettier --check src && eslint src", "lint": "prettier --check src && eslint src",
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | 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", "format:all": "prettier --write src",
@@ -18,6 +22,7 @@
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@sveltejs/kit": "^2.50.1", "@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^4.0.4", "@sveltejs/vite-plugin-svelte": "^4.0.4",
"@tauri-apps/cli": "^2.9.6",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.23", "autoprefixer": "^10.4.23",
"classnames": "^2.5.1", "classnames": "^2.5.1",
@@ -52,7 +57,7 @@
"@getalby/lightning-tools": "^6.1.0", "@getalby/lightning-tools": "^6.1.0",
"@getalby/sdk": "^5.1.2", "@getalby/sdk": "^5.1.2",
"@noble/curves": "^1.9.7", "@noble/curves": "^1.9.7",
"@pomade/core": "^0.0.12", "@pomade/core": "^0.1.1",
"@poppanator/sveltekit-svg": "^4.2.1", "@poppanator/sveltekit-svg": "^4.2.1",
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.27.2", "@tiptap/core": "^2.27.2",
@@ -60,16 +65,16 @@
"@types/throttle-debounce": "^5.0.2", "@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.8", "@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.8.4", "@welshman/app": "^0.8.7",
"@welshman/content": "^0.8.4", "@welshman/content": "^0.8.7",
"@welshman/editor": "^0.8.4", "@welshman/editor": "^0.8.7",
"@welshman/feeds": "^0.8.4", "@welshman/feeds": "^0.8.7",
"@welshman/lib": "^0.8.4", "@welshman/lib": "^0.8.7",
"@welshman/net": "^0.8.4", "@welshman/net": "^0.8.7",
"@welshman/router": "^0.8.4", "@welshman/router": "^0.8.7",
"@welshman/signer": "^0.8.4", "@welshman/signer": "^0.8.7",
"@welshman/store": "^0.8.4", "@welshman/store": "^0.8.7",
"@welshman/util": "^0.8.4", "@welshman/util": "^0.8.7",
"compressorjs-next": "^1.1.2", "compressorjs-next": "^1.1.2",
"daisyui": "^4.12.24", "daisyui": "^4.12.24",
"date-picker-svelte": "^2.17.0", "date-picker-svelte": "^2.17.0",
@@ -78,7 +83,7 @@
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"idb": "^8.0.3", "idb": "^8.0.3",
"nostr-signer-capacitor-plugin": "^0.0.4", "nostr-signer-capacitor-plugin": "github:coracle-social/nostr-signer-capacitor-plugin#main",
"nostr-tools": "^2.19.4", "nostr-tools": "^2.19.4",
"prettier-plugin-tailwindcss": "^0.6.14", "prettier-plugin-tailwindcss": "^0.6.14",
"qr-scanner": "^1.4.2", "qr-scanner": "^1.4.2",
@@ -91,7 +96,8 @@
"esbuild" "esbuild"
], ],
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"sharp" "sharp",
"nostr-signer-capacitor-plugin"
], ],
"overrides": { "overrides": {
"sharp": "0.35.0-rc.0" "sharp": "0.35.0-rc.0"
+689 -486
View File
File diff suppressed because it is too large Load Diff
+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"
]
}
}
+8 -2
View File
@@ -274,7 +274,7 @@
.input-editor, .input-editor,
.chat-editor, .chat-editor,
.note-editor { .note-editor {
@apply -m-1 min-h-12 p-1 text-sm; @apply -m-1 p-1;
} }
.tiptap { .tiptap {
@@ -300,7 +300,7 @@
} }
.tiptap { .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 { .tiptap p.is-editor-empty:first-child::before {
@@ -425,3 +425,9 @@ body.keyboard-open .hide-on-keyboard {
.chat__scroll-down { .chat__scroll-down {
@apply pb-sai fixed bottom-28 right-4 z-feature md:bottom-16; @apply pb-sai fixed bottom-28 right-4 z-feature md:bottom-16;
} }
/* content visibility */
.cv {
content-visibility: auto;
}
+1 -1
View File
@@ -19,7 +19,7 @@
</script> </script>
<Link <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))}> href={makeCalendarPath(url, getAddress(event))}>
<CalendarEventHeader {event} /> <CalendarEventHeader {event} />
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"> <div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
-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>
+11 -1
View File
@@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import {uniq} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" 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 {pubkey} from "@welshman/app"
import Pen2 from "@assets/icons/pen-2.svg?dataurl" import Pen2 from "@assets/icons/pen-2.svg?dataurl"
import {normalizeTopic} from "@lib/util"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -27,6 +29,7 @@
const {url, event, showRoom, showActivity}: Props = $props() const {url, event, showRoom, showActivity}: Props = $props()
const h = getTagValue("h", event.tags) const h = getTagValue("h", event.tags)
const topics = getTagValues("t", event.tags)
const path = makeClassifiedPath(url, getAddress(event)) const path = makeClassifiedPath(url, getAddress(event))
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
@@ -45,6 +48,13 @@
Posted in #<RoomName {h} {url} /> Posted in #<RoomName {h} {url} />
</Link> </Link>
{/if} {/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" /> <ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
<ThunkStatusOrDeleted {event}> <ThunkStatusOrDeleted {event}>
<ClassifiedStatus {event} /> <ClassifiedStatus {event} />
+2 -1
View File
@@ -18,7 +18,8 @@
const {d, title, status} = fromPairs(event.tags) const {d, title, status} = fromPairs(event.tags)
const [_, price = 0, currency = "SAT"] = getTag("price", event.tags) || [] const [_, price = 0, currency = "SAT"] = getTag("price", event.tags) || []
const images = getTagValues("image", 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> </script>
<ClassifiedForm {url} {initialValues}> <ClassifiedForm {url} {initialValues}>
+17 -1
View File
@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {randomId} from "@welshman/lib" import {removeUndefined, randomId, uniq} from "@welshman/lib"
import {makeEvent, CLASSIFIED} from "@welshman/util" import {makeEvent, CLASSIFIED} from "@welshman/util"
import {publishThunk} from "@welshman/app" import {publishThunk} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import {normalizeTopic} from "@lib/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
@@ -14,6 +15,7 @@
import ModalBody from "@lib/components/ModalBody.svelte" import ModalBody from "@lib/components/ModalBody.svelte"
import ImagesInput from "@lib/components/ImagesInput.svelte" import ImagesInput from "@lib/components/ImagesInput.svelte"
import CurrencyInput from "@app/components/CurrencyInput.svelte" import CurrencyInput from "@app/components/CurrencyInput.svelte"
import TopicMultiSelect from "@app/components/TopicMultiSelect.svelte"
import EditorContent from "@app/editor/EditorContent.svelte" import EditorContent from "@app/editor/EditorContent.svelte"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state" import {PROTECTED} from "@app/core/state"
@@ -32,6 +34,7 @@
currency?: string currency?: string
images?: string[] images?: string[]
status?: string status?: string
topics?: string[]
} }
} }
@@ -71,6 +74,10 @@
...ed.storage.nostr.getEditorTags(), ...ed.storage.nostr.getEditorTags(),
] ]
for (const topic of topics) {
tags.push(["t", topic])
}
if (await shouldProtect) { if (await shouldProtect) {
tags.push(PROTECTED) tags.push(PROTECTED)
} }
@@ -118,6 +125,7 @@
let price = $state(Number(initialValues?.price || 0)) let price = $state(Number(initialValues?.price || 0))
let currency = $state(initialValues?.currency || "SAT") let currency = $state(initialValues?.currency || "SAT")
let images = $state<(string | File)[]>(initialValues?.images || []) let images = $state<(string | File)[]>(initialValues?.images || [])
let topics = $state(uniq(removeUndefined((initialValues?.topics || []).map(normalizeTopic))))
</script> </script>
<Modal tag="form" onsubmit={preventDefault(submit)}> <Modal tag="form" onsubmit={preventDefault(submit)}>
@@ -150,6 +158,14 @@
</div> </div>
{/snippet} {/snippet}
</Field> </Field>
<Field>
{#snippet label()}
<p>Topics</p>
{/snippet}
{#snippet input()}
<TopicMultiSelect bind:value={topics} />
{/snippet}
</Field>
<Field> <Field>
{#snippet label()} {#snippet label()}
<p>Price*</p> <p>Price*</p>
+1 -1
View File
@@ -25,7 +25,7 @@
</script> </script>
<Link <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))}> href={makeClassifiedPath(url, getAddress(event))}>
{#if title} {#if title}
<div class="flex w-full items-center justify-between gap-2"> <div class="flex w-full items-center justify-between gap-2">
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {onMount, onDestroy} from "svelte" import {onMount, onDestroy} from "svelte"
import {displayUrl} from "@welshman/lib" import {displayUrl, once} from "@welshman/lib"
import { import {
getTags, getTags,
getBlob, getBlob,
@@ -26,8 +26,24 @@
const key = getTagValue("decryption-key", meta) const key = getTagValue("decryption-key", meta)
const nonce = getTagValue("decryption-nonce", meta) const nonce = getTagValue("decryption-nonce", meta)
const algorithm = getTagValue("encryption-algorithm", 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 the image failed to load, try authenticating
if (hash && $signer) { if (hash && $signer) {
const server = new URL(url).origin const server = new URL(url).origin
@@ -36,14 +52,15 @@
const res = await getBlob(server, hash, {authEvent}) const res = await getBlob(server, hash, {authEvent})
if (res.status === 200) { if (res.status === 200) {
src = URL.createObjectURL(await res.blob()) const blob = await res.blob()
setBlobSrc(blob, blob.type || undefined)
} else { } else {
hasError = true hasError = true
} }
} else { } else {
hasError = true hasError = true
} }
} })
let hasError = $state(false) let hasError = $state(false)
let src = $state("") let src = $state("")
@@ -57,7 +74,7 @@
const ciphertext = new Uint8Array(await response.arrayBuffer()) const ciphertext = new Uint8Array(await response.arrayBuffer())
const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm}) const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm})
src = URL.createObjectURL(new Blob([new Uint8Array(decryptedData)])) setBlobSrc(new Uint8Array(decryptedData), mime)
} }
} else { } else {
src = url src = url
@@ -65,7 +82,7 @@
}) })
onDestroy(() => { onDestroy(() => {
URL.revokeObjectURL(src) revokeSrc()
}) })
</script> </script>
+4
View File
@@ -96,6 +96,10 @@
params={{ params={{
trigger: "manual", trigger: "manual",
interactive: true, interactive: true,
placement: "bottom",
getReferenceClientRect: () => wrapper!.getBoundingClientRect(), getReferenceClientRect: () => wrapper!.getBoundingClientRect(),
onShow: (instance: Instance) => {
instance.popper.style.width = `${wrapper!.getBoundingClientRect().width + 8}px`
},
}} /> }} />
</button> </button>
+1 -1
View File
@@ -26,7 +26,7 @@
<Icon icon={Reply} /> <Icon icon={Reply} />
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span> <span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
</div> </div>
<div class="btn btn-neutral btn-xs relative hidden rounded-full sm:flex"> <div class="btn btn-neutral btn-xs relative rounded-full">
{#if gt(lastActive, $checked)} {#if gt(lastActive, $checked)}
<div class="h-2 w-2 rounded-full bg-primary"></div> <div class="h-2 w-2 rounded-full bg-primary"></div>
{/if} {/if}
+3 -1
View File
@@ -20,7 +20,9 @@
const h = getTagValue("h", event.tags) const h = getTagValue("h", event.tags)
</script> </script>
<Link class="col-2 card2 bg-alt w-full cursor-pointer shadow-md" href={makeGoalPath(url, event.id)}> <Link
class="cv col-2 card2 bg-alt w-full cursor-pointer shadow-md"
href={makeGoalPath(url, event.id)}>
<p class="text-2xl">{event.content}</p> <p class="text-2xl">{event.content}</p>
<Content <Content
event={{content: summary, tags: event.tags}} event={{content: summary, tags: event.tags}}
+2 -2
View File
@@ -51,7 +51,7 @@
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: `Failed to recover: ${request.messages[0]?.payload.message.toLowerCase()}`, message: `Failed to recover: ${request.messages[0]?.res?.message.toLowerCase()}`,
}) })
} }
@@ -62,7 +62,7 @@
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: `Failed to recover: ${result.messages[0]?.payload.message.toLowerCase()}`, message: `Failed to recover: ${result.messages[0]?.res?.message.toLowerCase()}`,
}) })
} }
-15
View File
@@ -1,15 +0,0 @@
<script lang="ts">
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
type Props = {
urls: string[]
}
const {urls}: Props = $props()
</script>
<div class="column menu gap-2">
{#each urls as url (url)}
<MenuSpacesItem {url} />
{/each}
</div>
+6 -1
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {Capacitor} from "@capacitor/core" import {Capacitor} from "@capacitor/core"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl" import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import Server from "@assets/icons/server.svg?dataurl" import Server from "@assets/icons/server.svg?dataurl"
import Moon from "@assets/icons/moon.svg?dataurl" import Moon from "@assets/icons/moon.svg?dataurl"
@@ -19,8 +20,8 @@
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {theme} from "@app/util/theme" import {theme} from "@app/util/theme"
const back = () => history.back()
const logout = () => pushModal(LogOut) const logout = () => pushModal(LogOut)
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark") const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
</script> </script>
@@ -123,6 +124,10 @@
<Button onclick={logout} class="btn btn-neutral"> <Button onclick={logout} class="btn btn-neutral">
<Icon icon={Exit} /> Log Out <Icon icon={Exit} /> Log Out
</Button> </Button>
<Button class="btn btn-link w-full md:hidden" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
</div> </div>
</ModalBody> </ModalBody>
</Modal> </Modal>
+1 -1
View File
@@ -13,7 +13,7 @@
</script> </script>
<Link replaceState href={path}> <Link replaceState href={path}>
<CardButton class="btn-neutral shadow-md bg-alt"> <CardButton class="btn-neutral shadow-md bg-alt rounded-box border-none">
{#snippet icon()} {#snippet icon()}
<RelayIcon {url} size={12} class="rounded-full" /> <RelayIcon {url} size={12} class="rounded-full" />
{/snippet} {/snippet}
+2 -3
View File
@@ -2,15 +2,14 @@
import cx from "classnames" import cx from "classnames"
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {formatTimestamp} from "@welshman/lib" import {formatTimestamp} from "@welshman/lib"
import {getListTags, getPubkeyTagValues} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {userMuteList} from "@welshman/app"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Profile from "@app/components/Profile.svelte" import Profile from "@app/components/Profile.svelte"
import ProfileName from "@app/components/ProfileName.svelte" import ProfileName from "@app/components/ProfileName.svelte"
import {goToEvent} from "@app/util/routes" import {goToEvent} from "@app/util/routes"
import {isEventMuted} from "@app/core/state"
const { const {
event, event,
@@ -32,7 +31,7 @@
muted = false muted = false
} }
let muted = $state(getPubkeyTagValues(getListTags($userMuteList)).includes(event.pubkey)) let muted = $state($isEventMuted(event))
</script> </script>
<div class="flex flex-col gap-2 shadow-md {restProps.class}"> <div class="flex flex-col gap-2 shadow-md {restProps.class}">
+1 -1
View File
@@ -38,7 +38,7 @@
}) })
</script> </script>
<NoteCard {event} {url} class="card2 bg-alt"> <NoteCard {event} {url} class="cv card2 bg-alt">
<NoteContent {event} expandMode="inline" /> <NoteContent {event} expandMode="inline" />
<div class="flex w-full justify-between gap-2"> <div class="flex w-full justify-between gap-2">
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-right"> <ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-right">
+15 -24
View File
@@ -17,35 +17,28 @@
if (!isPomadeSession($session)) return if (!isPomadeSession($session)) return
const client = new Client($session.clientOptions) const client = new Client($session.clientOptions)
const result = await client.listSessions()
const pubkey = await client.getPubkey()
try { if (result.ok) {
const result = await client.listSessions() // Group sessions by client pubkey and collect peers
const pubkey = await client.getPubkey() const sessionMap = new Map<string, SessionWithPeers>()
if (result.ok) { for (const message of result.messages) {
// Group sessions by client pubkey and collect peers if (!message.res?.items) continue
const sessionMap = new Map<string, SessionWithPeers>()
for (const message of result.messages) { for (const item of message.res.items) {
if (!message?.payload.items) continue const existing = sessionMap.get(item.client)
const peer = message.event.pubkey if (existing) {
existing.peers.push(message.url)
for (const item of message.payload.items) { } else if (item.client !== pubkey) {
const existing = sessionMap.get(item.client) sessionMap.set(item.client, {...item, peers: [message.url]})
if (existing) {
existing.peers.push(peer)
} else if (item.client !== pubkey) {
sessionMap.set(item.client, {...item, peers: [peer]})
}
} }
} }
sessions = Array.from(sessionMap.values())
} }
} finally {
client.stop() sessions = Array.from(sessionMap.values())
} }
} }
@@ -71,8 +64,6 @@
message: "Failed to delete session", message: "Failed to delete session",
}) })
} }
client.stop()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
pushToast({ pushToast({
+11 -19
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {goto} from "$app/navigation"
import {splitAt} from "@welshman/lib" import {splitAt} from "@welshman/lib"
import {userProfile, shouldUnwrap} from "@welshman/app" import {userProfile} from "@welshman/app"
import Widget from "@assets/icons/widget.svg?dataurl" import Widget from "@assets/icons/widget.svg?dataurl"
import Compass from "@assets/icons/compass.svg?dataurl" import Compass from "@assets/icons/compass.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
@@ -14,13 +13,12 @@
import ImageIcon from "@lib/components/ImageIcon.svelte" import ImageIcon from "@lib/components/ImageIcon.svelte"
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte"
import MenuOtherSpaces from "@app/components/MenuOtherSpaces.svelte"
import MenuSettings from "@app/components/MenuSettings.svelte" import MenuSettings from "@app/components/MenuSettings.svelte"
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte" import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
import {userSpaceUrls, PLATFORM_RELAYS, PLATFORM_LOGO} from "@app/core/state" import {userSpaceUrls, PLATFORM_RELAYS, PLATFORM_LOGO} from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {notifications} from "@app/util/notifications" import {notifications} from "@app/util/notifications"
import {goToLastChat} from "@app/util/routes"
type Props = { type Props = {
children?: Snippet children?: Snippet
@@ -28,12 +26,8 @@
const {children}: Props = $props() const {children}: Props = $props()
const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls})
const showSettingsMenu = () => pushModal(MenuSettings) const showSettingsMenu = () => pushModal(MenuSettings)
const openChat = () => ($shouldUnwrap ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
let windowHeight = $state(0) let windowHeight = $state(0)
const itemHeight = 56 const itemHeight = 56
@@ -60,15 +54,13 @@
{#each primarySpaceUrls as url (url)} {#each primarySpaceUrls as url (url)}
<PrimaryNavItemSpace {url} /> <PrimaryNavItemSpace {url} />
{/each} {/each}
{#if secondarySpaceUrls.length > 0} <PrimaryNavItem
<PrimaryNavItem href="/spaces"
title="Other Spaces" title="All Spaces"
class="tooltip-right" class="tooltip-right"
onclick={showOtherSpacesMenu} notification={otherSpaceNotifications}>
notification={otherSpaceNotifications}> <ImageIcon alt="All Spaces" src={Widget} size={8} />
<ImageIcon alt="Other Spaces" src={Widget} size={8} /> </PrimaryNavItem>
</PrimaryNavItem>
{/if}
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right"> <PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
<ImageIcon alt="Add a Space" src={Compass} size={8} /> <ImageIcon alt="Add a Space" src={Compass} size={8} />
</PrimaryNavItem> </PrimaryNavItem>
@@ -91,7 +83,7 @@
</PrimaryNavItem> </PrimaryNavItem>
<PrimaryNavItem <PrimaryNavItem
title="Messages" title="Messages"
onclick={openChat} onclick={goToLastChat}
class="tooltip-right" class="tooltip-right"
notification={$notifications.has("/chat")}> notification={$notifications.has("/chat")}>
<ImageIcon alt="Messages" src={Letter} size={8} /> <ImageIcon alt="Messages" src={Letter} size={8} />
@@ -118,7 +110,7 @@
</PrimaryNavItem> </PrimaryNavItem>
<PrimaryNavItem <PrimaryNavItem
title="Messages" title="Messages"
onclick={openChat} onclick={goToLastChat}
notification={$notifications.has("/chat")}> notification={$notifications.has("/chat")}>
<ImageIcon alt="Messages" src={Letter} size={8} /> <ImageIcon alt="Messages" src={Letter} size={8} />
</PrimaryNavItem> </PrimaryNavItem>
+1 -3
View File
@@ -4,7 +4,6 @@
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {ManagementMethod} from "@welshman/util" import {ManagementMethod} from "@welshman/util"
import { import {
shouldUnwrap,
manageRelay, manageRelay,
deriveProfile, deriveProfile,
displayProfileByPubkey, displayProfileByPubkey,
@@ -30,7 +29,6 @@
import ProfileInfo from "@app/components/ProfileInfo.svelte" import ProfileInfo from "@app/components/ProfileInfo.svelte"
import EventInfo from "@app/components/EventInfo.svelte" import EventInfo from "@app/components/EventInfo.svelte"
import ProfileBadges from "@app/components/ProfileBadges.svelte" import ProfileBadges from "@app/components/ProfileBadges.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte"
import {pubkeyLink, deriveUserIsSpaceAdmin, deriveSpaceBannedPubkeyItems} from "@app/core/state" import {pubkeyLink, deriveUserIsSpaceAdmin, deriveSpaceBannedPubkeyItems} from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
@@ -57,7 +55,7 @@
const showInfo = () => pushModal(EventInfo, {url, event: $profile!.event}) const showInfo = () => pushModal(EventInfo, {url, event: $profile!.event})
const openChat = () => ($shouldUnwrap ? goto(chatPath) : pushModal(ChatEnable, {next: chatPath})) const openChat = () => goto(chatPath)
const toggleMenu = (pubkey: string) => { const toggleMenu = (pubkey: string) => {
showMenu = !showMenu showMenu = !showMenu
+7 -4
View File
@@ -2,6 +2,8 @@
import type {Profile} from "@welshman/util" import type {Profile} from "@welshman/util"
import {getTag, makeProfile} from "@welshman/util" import {getTag, makeProfile} from "@welshman/util"
import {pubkey, profilesByPubkey} from "@welshman/app" import {pubkey, profilesByPubkey} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileEditForm from "@app/components/ProfileEditForm.svelte" import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
import {clearModals} from "@app/util/modal" import {clearModals} from "@app/util/modal"
@@ -24,9 +26,10 @@
<ProfileEditForm {initialValues} {onsubmit}> <ProfileEditForm {initialValues} {onsubmit}>
{#snippet footer()} {#snippet footer()}
<div class="mt-4 flex flex-row items-center justify-between gap-4"> <Button class="btn btn-link" onclick={back}>
<Button class="btn btn-neutral" onclick={back}>Discard Changes</Button> <Icon icon={AltArrowLeft} />
<Button type="submit" class="btn btn-primary">Save Changes</Button> Go Back
</div> </Button>
<Button type="submit" class="btn btn-primary">Save Changes</Button>
{/snippet} {/snippet}
</ProfileEditForm> </ProfileEditForm>
+4 -1
View File
@@ -10,6 +10,7 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Modal from "@lib/components/Modal.svelte" import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte" import ModalBody from "@lib/components/ModalBody.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import InputProfilePicture from "@app/components/InputProfilePicture.svelte" import InputProfilePicture from "@app/components/InputProfilePicture.svelte"
import InfoHandle from "@app/components/InfoHandle.svelte" import InfoHandle from "@app/components/InfoHandle.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
@@ -123,5 +124,7 @@
</FieldInline> </FieldInline>
{/if} {/if}
</ModalBody> </ModalBody>
{@render footer()} <ModalFooter>
{@render footer()}
</ModalFooter>
</Modal> </Modal>
+8 -5
View File
@@ -62,7 +62,7 @@
} }
} }
let input: Element | undefined = $state() let label: Element | undefined = $state()
let popover: Instance | undefined = $state() let popover: Instance | undefined = $state()
let instance: any = $state() let instance: any = $state()
@@ -92,7 +92,7 @@
</div> </div>
{/each} {/each}
</div> </div>
<label class="input input-bordered flex w-full items-center gap-2" bind:this={input}> <label class="input input-bordered flex w-full items-center gap-2" bind:this={label}>
<Icon icon={Magnifier} /> <Icon icon={Magnifier} />
<!-- svelte-ignore a11y_autofocus --> <!-- svelte-ignore a11y_autofocus -->
<input <input
@@ -114,12 +114,15 @@
select: selectPubkey, select: selectPubkey,
component: ProfileSuggestion, component: ProfileSuggestion,
class: "rounded-box", class: "rounded-box",
style: `left: 4px; width: ${input?.clientWidth + 12}px`, style: `left: 4px; width: ${label?.clientWidth + 12}px`,
}} }}
params={{ params={{
trigger: "manual", trigger: "manual",
interactive: true, interactive: true,
maxWidth: "none", placement: "bottom",
getReferenceClientRect: () => input!.getBoundingClientRect(), getReferenceClientRect: () => label!.getBoundingClientRect(),
onShow: (instance: Instance) => {
instance.popper.style.width = `${label!.getBoundingClientRect().width + 8}px`
},
}} /> }} />
</div> </div>
+1 -1
View File
@@ -25,7 +25,7 @@
const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url)) const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url))
</script> </script>
<Button class="card2 bg-alt shadow-md" onclick={onClick}> <Button class="cv card2 bg-alt shadow-md" onclick={onClick}>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="flex items-center gap-2 text-sm"> <div class="flex items-center gap-2 text-sm">
{#if h} {#if h}
+6 -6
View File
@@ -152,18 +152,18 @@
transition:fly transition:fly
class="bg-alt menu absolute right-0 z-popover w-48 gap-1 rounded-box p-2 shadow-md"> class="bg-alt menu absolute right-0 z-popover w-48 gap-1 rounded-box p-2 shadow-md">
{#if $userIsAdmin} {#if $userIsAdmin}
<li>
<Button class="text-error" onclick={startDelete}>
<Icon icon={TrashBin2} />
Delete Room
</Button>
</li>
<li> <li>
<Button onclick={startEdit}> <Button onclick={startEdit}>
<Icon icon={Pen} /> <Icon icon={Pen} />
Edit Room Edit Room
</Button> </Button>
</li> </li>
<li>
<Button class="text-error" onclick={startDelete}>
<Icon icon={TrashBin2} />
Delete Room
</Button>
</li>
{:else if $membershipStatus === MembershipStatus.Initial} {:else if $membershipStatus === MembershipStatus.Initial}
<li> <li>
<Button disabled={loading} onclick={join}> <Button disabled={loading} onclick={join}>
+2 -6
View File
@@ -39,8 +39,6 @@
loading = true loading = true
let client: Client | undefined = undefined
try { try {
const secret = getKey<string>("signup.secret")! const secret = getKey<string>("signup.secret")!
const {clientOptions, ...registerRes} = await Client.register(2, 3, secret) const {clientOptions, ...registerRes} = await Client.register(2, 3, secret)
@@ -52,12 +50,11 @@
}) })
} }
client = new Client(clientOptions) const client = new Client(clientOptions)
const setupRes = await client.setupRecovery(email, password) const setupRes = await client.setupRecovery(email, password)
if (!setupRes.ok) { if (!setupRes.ok) {
const message = setupRes.messages[0]?.payload.message || "Please try again." const message = setupRes.messages[0]?.res?.message || "Please try again."
return pushToast({ return pushToast({
theme: "error", theme: "error",
@@ -86,7 +83,6 @@
message: "Failed to register! Please try again.", message: "Failed to register! Please try again.",
}) })
} finally { } finally {
client?.stop()
loading = false loading = false
} }
} }

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