Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a42ba5446a | |||
| ccfe1bded5 | |||
| dfedf4e879 | |||
| 0682c404f2 | |||
| 80ece70450 | |||
| f6245c712d | |||
| e0c5f0d4f1 | |||
| b616e2ea33 | |||
| 6fb6995103 | |||
| 47bc0c2382 | |||
| 59a919d888 | |||
| 17d673c288 | |||
| 985fd46243 | |||
| 6d99e296e4 | |||
| 21c34efb6a | |||
| 9c2f923c26 | |||
| 52d2d70838 | |||
| edd8824c5e | |||
| 4f12ad9533 | |||
| 2a5850e67f | |||
| 15341edece | |||
| 30f8b4160e | |||
| 937ca5ecf6 | |||
| ba1757d4f1 | |||
| 5a2b5f43b8 | |||
| 2f487705c3 | |||
| 558d59ce88 | |||
| 1030edd322 | |||
| 981c8fd706 | |||
| 45ade602b5 | |||
| ef8a8682cd | |||
| 112ac4b6d5 | |||
| 3a26d2cb0b | |||
| a678bf42f1 | |||
| dc314a1d1b | |||
| 3af56f6bb1 | |||
| a996664e6c | |||
| 6e865fef06 | |||
| 588bd0f341 | |||
| 69f6abf4b6 | |||
| c8eb4ac31a | |||
| e3e69390ce | |||
| d0b34dfdf8 | |||
| bcdb3dc351 | |||
| a7b0031b8d | |||
| 2c05bc6961 | |||
| c2d0ec92bf | |||
| 407b4dce94 | |||
| 796157384f | |||
| 3446977df6 | |||
| f8016aba99 | |||
| 56d8527ed9 | |||
| 302788bcba | |||
| db075e602a | |||
| 67011d4740 | |||
| a35d867b34 | |||
| 23b59e54d7 | |||
| da2665d2bc |
@@ -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
|
||||||
|
|||||||
@@ -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,4 +1,5 @@
|
|||||||
src/assets
|
src/assets
|
||||||
|
target
|
||||||
build
|
build
|
||||||
.idea
|
.idea
|
||||||
.gradle
|
.gradle
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ src/
|
|||||||
- Derive all other data inside the component from identifiers
|
- Derive all other data inside the component from identifiers
|
||||||
- Example: Don't pass `members` prop, derive it from `h` inside component
|
- Example: Don't pass `members` prop, derive it from `h` inside component
|
||||||
|
|
||||||
**Code Style:**
|
**CRITICAL Code Style Guidelines:**
|
||||||
|
|
||||||
- **No `null`** - only use `undefined`
|
- **No `null`** - only use `undefined`
|
||||||
- Svelte 5 runes (`$state`, `$derived`, `$effect`) only in UI components
|
- 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 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
|
- 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
|
- 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
|
## Common Tasks
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -20,7 +20,7 @@ If you're deploying a custom version of flotilla, be sure to remove the `plausib
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See [./CONTRIBUTING.md](CONTRIBUTING.md).
|
See [CONTRIBUTING.md](AGENTS.md).
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -358,7 +358,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 30;
|
CURRENT_PROJECT_VERSION = 31;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
@@ -385,7 +385,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 30;
|
CURRENT_PROJECT_VERSION = 31;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||
@@ -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,17 +65,17 @@
|
|||||||
"@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": "^1.2.1",
|
"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",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.92.0"
|
||||||
@@ -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"]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
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>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
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>
|
||||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 668 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 926 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.92.0"
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
@@ -402,6 +402,10 @@ progress[value]::-webkit-progress-value {
|
|||||||
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
|
@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 */
|
/* Keyboard open state adjustments */
|
||||||
|
|
||||||
body.keyboard-open .cb {
|
body.keyboard-open .cb {
|
||||||
@@ -419,5 +423,11 @@ body.keyboard-open .hide-on-keyboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat__scroll-down {
|
.chat__scroll-down {
|
||||||
@apply 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
|
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 {preventDefault, stopPropagation} from "@lib/html"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||||
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
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"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
const {value, event} = $props()
|
const {value, event} = $props()
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
let hideImage = $state(false)
|
let hideImage = $state(false)
|
||||||
|
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
|
const fileType = getTagValue("file-type", event.tags) || ""
|
||||||
const [href, external] = call(() => {
|
const [href, external] = call(() => {
|
||||||
if (isRelayUrl(url)) return [makeSpacePath(url), false]
|
if (isRelayUrl(url)) return [makeSpacePath(url), false]
|
||||||
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
||||||
@@ -40,11 +41,11 @@
|
|||||||
|
|
||||||
<Link {external} {href} class="my-2 block">
|
<Link {external} {href} class="my-2 block">
|
||||||
<div class="overflow-hidden rounded-box">
|
<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">
|
<video controls src={url} class="max-h-96 rounded-box object-contain object-center">
|
||||||
<track kind="captions" />
|
<track kind="captions" />
|
||||||
</video>
|
</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))}>
|
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
|
||||||
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
|
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {call, displayUrl} from "@welshman/lib"
|
import {call, displayUrl} from "@welshman/lib"
|
||||||
import {isRelayUrl} from "@welshman/util"
|
import {isRelayUrl, getTagValue} from "@welshman/util"
|
||||||
import {preventDefault, stopPropagation} from "@lib/html"
|
import {preventDefault, stopPropagation} from "@lib/html"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
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"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
const {value, event} = $props()
|
const {value, event} = $props()
|
||||||
|
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
|
const fileType = getTagValue("file-type", event.tags) || ""
|
||||||
const [href, external] = call(() => {
|
const [href, external] = call(() => {
|
||||||
if (isRelayUrl(url)) return [makeSpacePath(url), false]
|
if (isRelayUrl(url)) return [makeSpacePath(url), false]
|
||||||
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_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})
|
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
|
||||||
</script>
|
</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 -->
|
<!-- Use a real link so people can copy the href -->
|
||||||
<a
|
<a
|
||||||
href={url}
|
href={url}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}}
|
||||||
|
|||||||
@@ -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()}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
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"
|
||||||
@@ -18,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>
|
||||||
|
|
||||||
@@ -52,19 +54,21 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</CardButton>
|
</CardButton>
|
||||||
</Link>
|
</Link>
|
||||||
<Link replaceState href="/settings/wallet">
|
{#if Capacitor.getPlatform() !== "ios"}
|
||||||
<CardButton class="btn-neutral">
|
<Link replaceState href="/settings/wallet">
|
||||||
{#snippet icon()}
|
<CardButton class="btn-neutral">
|
||||||
<div><Icon icon={Wallet} size={7} /></div>
|
{#snippet icon()}
|
||||||
{/snippet}
|
<div><Icon icon={Wallet} size={7} /></div>
|
||||||
{#snippet title()}
|
{/snippet}
|
||||||
<div>Wallet</div>
|
{#snippet title()}
|
||||||
{/snippet}
|
<div>Wallet</div>
|
||||||
{#snippet info()}
|
{/snippet}
|
||||||
<div>Connect a bitcoin wallet for sending social tips</div>
|
{#snippet info()}
|
||||||
{/snippet}
|
<div>Connect a bitcoin wallet for sending social tips</div>
|
||||||
</CardButton>
|
{/snippet}
|
||||||
</Link>
|
</CardButton>
|
||||||
|
</Link>
|
||||||
|
{/if}
|
||||||
<Link replaceState href="/settings/relays">
|
<Link replaceState href="/settings/relays">
|
||||||
<CardButton class="btn-neutral">
|
<CardButton class="btn-neutral">
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
@@ -120,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>
|
||||||
|
|||||||
@@ -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,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}">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
|
import {Router} from "@welshman/router"
|
||||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||||
@@ -11,31 +12,33 @@
|
|||||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
children?: Snippet
|
children?: Snippet
|
||||||
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, event, children}: Props = $props()
|
const {url, event, children}: Props = $props()
|
||||||
|
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const relays = url ? [url] : Router.get().Event(event).getUrls()
|
||||||
|
|
||||||
|
const shouldProtect = url ? canEnforceNip70(url) : Promise.resolve(false)
|
||||||
|
|
||||||
const deleteReaction = async (event: TrustedEvent) =>
|
const deleteReaction = async (event: TrustedEvent) =>
|
||||||
publishDelete({relays: [url], event, protect: await shouldProtect})
|
publishDelete({relays, event, protect: await shouldProtect})
|
||||||
|
|
||||||
const createReaction = async (template: EventContent) =>
|
const createReaction = async (template: EventContent) =>
|
||||||
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
publishReaction({...template, event, relays, protect: await shouldProtect})
|
||||||
|
|
||||||
const onEmoji = async (emoji: NativeEmoji) =>
|
const onEmoji = async (emoji: NativeEmoji) =>
|
||||||
publishReaction({
|
publishReaction({
|
||||||
event,
|
event,
|
||||||
|
relays,
|
||||||
content: emoji.unicode,
|
content: emoji.unicode,
|
||||||
relays: [url],
|
|
||||||
protect: await shouldProtect,
|
protect: await shouldProtect,
|
||||||
})
|
})
|
||||||
</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">
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -15,6 +14,7 @@
|
|||||||
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
|
import Restart from "@assets/icons/restart.svg?dataurl"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
@@ -29,8 +29,7 @@
|
|||||||
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} 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"
|
||||||
import {makeChatPath} from "@app/util/routes"
|
import {makeChatPath} from "@app/util/routes"
|
||||||
@@ -46,13 +45,17 @@
|
|||||||
|
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
|
||||||
|
const bannedPubkeys = url ? deriveSpaceBannedPubkeyItems(url) : undefined
|
||||||
|
|
||||||
|
const isBanned = $derived($bannedPubkeys?.some(item => item.pubkey === pubkey) ?? false)
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const chatPath = makeChatPath([pubkey])
|
const chatPath = makeChatPath([pubkey])
|
||||||
|
|
||||||
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
|
||||||
@@ -81,6 +84,20 @@
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const restoreMember = async () => {
|
||||||
|
const {error} = await manageRelay(url!, {
|
||||||
|
method: ManagementMethod.AllowPubkey,
|
||||||
|
params: [pubkey],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "User has successfully been restored!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let showMenu = $state(false)
|
let showMenu = $state(false)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -112,12 +129,21 @@
|
|||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $userIsAdmin}
|
{#if $userIsAdmin}
|
||||||
<li>
|
{#if isBanned}
|
||||||
<Button class="text-error" onclick={banMember}>
|
<li>
|
||||||
<Icon icon={MinusCircle} />
|
<Button onclick={restoreMember}>
|
||||||
Ban User
|
<Icon icon={Restart} />
|
||||||
</Button>
|
Restore User
|
||||||
</li>
|
</Button>
|
||||||
|
</li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={banMember}>
|
||||||
|
<Icon icon={MinusCircle} />
|
||||||
|
Ban User
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||