Compare commits

...

59 Commits

Author SHA1 Message Date
mplorentz 3dde0253e6 Fix issue where docker build would rebuild when app did not change 2026-02-27 12:44:34 -05:00
mplorentz eca8981542 Fix pnpm-lock 2026-02-27 12:44:12 -05: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: #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: #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: #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: #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: #62
Co-authored-by: triesap <tyson@radroots.org>
Co-committed-by: triesap <tyson@radroots.org>
2026-02-17 20:39:08 +00:00
Jon Staab 6e865fef06 tweak how at works 2026-02-17 10:45:50 -08:00
Jon Staab 588bd0f341 Fix scroll to event behavior 2026-02-17 10:45:50 -08:00
Jon Staab 69f6abf4b6 Pin scroll position to at'd event until user scrolls 2026-02-17 10:45:50 -08:00
Jon Staab c8eb4ac31a Simplify goToEvent 2026-02-17 10:45:50 -08:00
Jon Staab e3e69390ce Add forward scrolling to makeMakeFeed 2026-02-17 10:45:49 -08:00
Jon Staab d0b34dfdf8 Make createScroller honor reverse param 2026-02-17 10:45:49 -08:00
Jon Staab bcdb3dc351 Fix duplicate ids in chat 2026-02-17 10:45:49 -08:00
Jon Staab a7b0031b8d Use compressorjs-next 2026-02-17 10:45:49 -08:00
Jon Staab 2c05bc6961 Refactor SpaceSearch into its own component 2026-02-17 10:45:49 -08:00
Ben c2d0ec92bf Space search 2026-02-17 10:45:49 -08:00
Jon Staab 407b4dce94 Fix editing messages with html tags 2026-02-17 10:45:49 -08:00
Jon Staab 796157384f Fix DM media detection 2026-02-17 10:45:49 -08:00
Jon Staab 3446977df6 Make hover target for menu button more reasonable 2026-02-17 10:45:49 -08:00
Jon Staab f8016aba99 Watch tracker in feed utils 2026-02-17 10:45:49 -08:00
Jon Staab 56d8527ed9 Revert makeFeed changes 2026-02-17 10:45:49 -08:00
Jon Staab 302788bcba Clean up report item design, bad/restore user actions, space description input, add feed to home page 2026-02-17 10:45:49 -08:00
Jon Staab db075e602a Tweak wallet page 2026-02-17 10:45:49 -08:00
Jon Staab 67011d4740 Fix makeFeed (maybe) 2026-02-17 10:45:49 -08:00
Jon Staab a35d867b34 Tweak room detail 2026-02-17 10:45:49 -08:00
Jon Staab 23b59e54d7 Fix scroll to bottom button safe insets 2026-02-17 10:45:49 -08:00
Jon Staab da2665d2bc Disable wallet on ios 2026-02-17 10:45:49 -08:00
Jon Staab 919fe29ffb Update docker image creation for gitea ci
Docker / build-and-push-image (push) Failing after 20m22s
2026-02-17 10:45:39 -08:00
154 changed files with 7930 additions and 1358 deletions
+1
View File
@@ -1,5 +1,6 @@
--ignore-dir=.svelte-kit
--ignore-dir=android
--ignore-dir=target
--ignore-dir=build
--ignore-dir=ios/DerivedData
--ignore-dir=ios/App/App/public
+9
View File
@@ -2,3 +2,12 @@ node_modules
android
ios
build
# Git
.git
.gitignore
# Env files (use build args instead; .env.template is copied for defaults)
.env
.env.local
.env.*.local
+1
View File
@@ -1,4 +1,5 @@
src/assets
target
build
.idea
.gradle
@@ -2,7 +2,7 @@ name: Docker
on:
push:
branches: ['master']
branches: [master]
env:
REGISTRY: ghcr.io
@@ -14,8 +14,6 @@ jobs:
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
@@ -25,8 +23,8 @@ jobs:
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
@@ -50,10 +48,3 @@ jobs:
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
+4
View File
@@ -27,6 +27,10 @@ node_modules/
build/
.svelte-kit/
# Rust/Tauri
*target/
src-tauri/binaries/
# iOS
ios/App/App/public
ios/DerivedData
+11 -1
View File
@@ -157,7 +157,7 @@ src/
- Derive all other data inside the component from identifiers
- Example: Don't pass `members` prop, derive it from `h` inside component
**Code Style:**
**CRITICAL Code Style Guidelines:**
- **No `null`** - only use `undefined`
- Svelte 5 runes (`$state`, `$derived`, `$effect`) only in UI components
@@ -168,6 +168,16 @@ src/
- When dynamically building classes, use `cx` from `classnames` rather than embedded ternaries or svelte 4's old `class:` syntax.
- When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
**Human-First Simplicity (Jon Staab Style):**
- Prefer direct, readable code over layered abstractions.
- Do not add indirection (extra helpers, wrappers, stores, or derived state) unless it removes real repeated complexity.
- Reuse existing Welshman and Flotilla primitives before introducing new utilities or dependencies.
- Favor linear control flow and explicit naming over clever patterns.
- Remove defensive checks that do not apply in this runtime model.
- When two approaches work, pick the one that feels more human and easier to maintain.
## Common Tasks
+6
View File
@@ -1,3 +1,6 @@
# Pass the build hash as an argument:
# docker build --build-arg VITE_BUILD_HASH=$(git rev-parse --short HEAD) -t flotilla .
FROM node:20-slim
# Install pnpm
@@ -15,6 +18,9 @@ RUN pnpm i
# Copy the rest of the application
COPY . .
ARG VITE_BUILD_HASH
ENV VITE_BUILD_HASH=${VITE_BUILD_HASH}
# Build the application
ENV NODE_OPTIONS=--max_old_space_size=16384
RUN pnpm run build
+1 -1
View File
@@ -20,7 +20,7 @@ If you're deploying a custom version of flotilla, be sure to remove the `plausib
## Development
See [./CONTRIBUTING.md](CONTRIBUTING.md).
See [CONTRIBUTING.md](AGENTS.md).
## Deployment
+1 -1
View File
@@ -27,4 +27,4 @@ include ':capawesome-capacitor-badge'
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge/android')
include ':nostr-signer-capacitor-plugin'
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@8.0.1/node_modules/nostr-signer-capacitor-plugin/android')
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin/android')
+2 -2
View File
@@ -358,7 +358,7 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30;
CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
@@ -385,7 +385,7 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30;
CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
+1 -1
View File
@@ -18,7 +18,7 @@ def capacitor_pods
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications'
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge'
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@8.0.1/node_modules/nostr-signer-capacitor-plugin'
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin'
end
target 'Flotilla Chat' do
+6 -3
View File
@@ -1,11 +1,12 @@
#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
import { execSync } from 'child_process'
if (execSync('git status --porcelain', { encoding: 'utf8' }).trim()) {
console.error('Error: Git working tree is dirty. Please commit or stash your changes first.')
const force = process.argv.includes('--force')
if (execSync('git status --porcelain', { encoding: 'utf8' }).trim() && !force) {
console.error('Error: Git working tree is dirty. Please commit or stash your changes first, or re-run with --force.')
process.exit(1)
}
@@ -22,7 +23,9 @@ pkg.pnpm.overrides["@welshman/router"] = "link:../welshman/packages/router"
pkg.pnpm.overrides["@welshman/signer"] = "link:../welshman/packages/signer"
pkg.pnpm.overrides["@welshman/store"] = "link:../welshman/packages/store"
pkg.pnpm.overrides["@welshman/util"] = "link:../welshman/packages/util"
// pkg.pnpm.overrides["nostr-editor"] = "link:../nostr-editor"
// pkg.pnpm.overrides["@pomade/core"] = "link:../pomade/packages/core"
// pkg.pnpm.overrides["nostr-signer-capacitor-plugin"] = "link:../nostr-signer-capacitor-plugin"
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n')
+21 -15
View File
@@ -6,8 +6,12 @@
"dev": "vite dev",
"build": "./build.sh",
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"tauri:info": "tauri info",
"tauri:icons": "tauri icon assets/logo.png --output src-tauri/icons",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"check:watch": "./check.sh",
"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:all": "prettier --write src",
@@ -18,6 +22,7 @@
"@eslint/js": "^9.39.2",
"@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@tauri-apps/cli": "^2.9.6",
"@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.23",
"classnames": "^2.5.1",
@@ -52,7 +57,7 @@
"@getalby/lightning-tools": "^6.1.0",
"@getalby/sdk": "^5.1.2",
"@noble/curves": "^1.9.7",
"@pomade/core": "^0.0.12",
"@pomade/core": "^0.1.1",
"@poppanator/sveltekit-svg": "^4.2.1",
"@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.27.2",
@@ -60,17 +65,17 @@
"@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.8.4",
"@welshman/content": "^0.8.4",
"@welshman/editor": "^0.8.4",
"@welshman/feeds": "^0.8.4",
"@welshman/lib": "^0.8.4",
"@welshman/net": "^0.8.4",
"@welshman/router": "^0.8.4",
"@welshman/signer": "^0.8.4",
"@welshman/store": "^0.8.4",
"@welshman/util": "^0.8.4",
"compressorjs": "^1.2.1",
"@welshman/app": "^0.8.7",
"@welshman/content": "^0.8.7",
"@welshman/editor": "^0.8.7",
"@welshman/feeds": "^0.8.7",
"@welshman/lib": "^0.8.7",
"@welshman/net": "^0.8.7",
"@welshman/router": "^0.8.7",
"@welshman/signer": "^0.8.7",
"@welshman/store": "^0.8.7",
"@welshman/util": "^0.8.7",
"compressorjs-next": "^1.1.2",
"daisyui": "^4.12.24",
"date-picker-svelte": "^2.17.0",
"dotenv": "^16.6.1",
@@ -78,7 +83,7 @@
"fuse.js": "^7.1.0",
"husky": "^9.1.7",
"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",
"prettier-plugin-tailwindcss": "^0.6.14",
"qr-scanner": "^1.4.2",
@@ -91,7 +96,8 @@
"esbuild"
],
"onlyBuiltDependencies": [
"sharp"
"sharp",
"nostr-signer-capacitor-plugin"
],
"overrides": {
"sharp": "0.35.0-rc.0"
+695 -506
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"
]
}
}
+13 -3
View File
@@ -274,7 +274,7 @@
.input-editor,
.chat-editor,
.note-editor {
@apply -m-1 min-h-12 p-1 text-sm;
@apply -m-1 p-1;
}
.tiptap {
@@ -300,7 +300,7 @@
}
.tiptap {
@apply max-h-[350px] overflow-y-auto p-2 px-4;
@apply max-h-[350px] min-h-10 overflow-y-auto p-2 px-4;
}
.tiptap p.is-editor-empty:first-child::before {
@@ -402,6 +402,10 @@ progress[value]::-webkit-progress-value {
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
}
.ct {
@apply top-[calc(var(--sait)+5rem)] md:top-[calc(var(--sait)+3rem)];
}
/* Keyboard open state adjustments */
body.keyboard-open .cb {
@@ -419,5 +423,11 @@ body.keyboard-open .hide-on-keyboard {
}
.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;
}
+1 -1
View File
@@ -19,7 +19,7 @@
</script>
<Link
class="col-3 card2 bg-alt w-full cursor-pointer shadow-md"
class="cv col-3 card2 bg-alt w-full cursor-pointer shadow-md"
href={makeCalendarPath(url, getAddress(event))}>
<CalendarEventHeader {event} />
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
-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">
import {uniq} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue, getAddress} from "@welshman/util"
import {getTagValue, getTagValues, getAddress} from "@welshman/util"
import {pubkey} from "@welshman/app"
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
import {normalizeTopic} from "@lib/util"
import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
@@ -27,6 +29,7 @@
const {url, event, showRoom, showActivity}: Props = $props()
const h = getTagValue("h", event.tags)
const topics = getTagValues("t", event.tags)
const path = makeClassifiedPath(url, getAddress(event))
const shouldProtect = canEnforceNip70(url)
@@ -45,6 +48,13 @@
Posted in #<RoomName {h} {url} />
</Link>
{/if}
<div class="flex min-w-0 flex-wrap gap-2">
{#each uniq(topics) as topic (topic)}
<button type="button" class="btn btn-xs rounded-full font-normal">
#{normalizeTopic(topic)}
</button>
{/each}
</div>
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
<ThunkStatusOrDeleted {event}>
<ClassifiedStatus {event} />
+2 -1
View File
@@ -18,7 +18,8 @@
const {d, title, status} = fromPairs(event.tags)
const [_, price = 0, currency = "SAT"] = getTag("price", event.tags) || []
const images = getTagValues("image", event.tags)
const initialValues = {d, title, status, content, price: Number(price), currency, images}
const topics = getTagValues("t", event.tags)
const initialValues = {d, title, status, content, price: Number(price), currency, images, topics}
</script>
<ClassifiedForm {url} {initialValues}>
+17 -1
View File
@@ -1,9 +1,10 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {randomId} from "@welshman/lib"
import {removeUndefined, randomId, uniq} from "@welshman/lib"
import {makeEvent, CLASSIFIED} from "@welshman/util"
import {publishThunk} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html"
import {normalizeTopic} from "@lib/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte"
@@ -14,6 +15,7 @@
import ModalBody from "@lib/components/ModalBody.svelte"
import ImagesInput from "@lib/components/ImagesInput.svelte"
import CurrencyInput from "@app/components/CurrencyInput.svelte"
import TopicMultiSelect from "@app/components/TopicMultiSelect.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state"
@@ -32,6 +34,7 @@
currency?: string
images?: string[]
status?: string
topics?: string[]
}
}
@@ -71,6 +74,10 @@
...ed.storage.nostr.getEditorTags(),
]
for (const topic of topics) {
tags.push(["t", topic])
}
if (await shouldProtect) {
tags.push(PROTECTED)
}
@@ -118,6 +125,7 @@
let price = $state(Number(initialValues?.price || 0))
let currency = $state(initialValues?.currency || "SAT")
let images = $state<(string | File)[]>(initialValues?.images || [])
let topics = $state(uniq(removeUndefined((initialValues?.topics || []).map(normalizeTopic))))
</script>
<Modal tag="form" onsubmit={preventDefault(submit)}>
@@ -150,6 +158,14 @@
</div>
{/snippet}
</Field>
<Field>
{#snippet label()}
<p>Topics</p>
{/snippet}
{#snippet input()}
<TopicMultiSelect bind:value={topics} />
{/snippet}
</Field>
<Field>
{#snippet label()}
<p>Price*</p>
+1 -1
View File
@@ -25,7 +25,7 @@
</script>
<Link
class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
class="cv col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
href={makeClassifiedPath(url, getAddress(event))}>
{#if title}
<div class="flex w-full items-center justify-between gap-2">
+5 -4
View File
@@ -1,12 +1,12 @@
<script lang="ts">
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
import {isRelayUrl} from "@welshman/util"
import {isRelayUrl, getTagValue} from "@welshman/util"
import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
import {pushModal} from "@app/util/modal"
import {dufflepud, PLATFORM_URL} from "@app/core/state"
import {dufflepud, PLATFORM_URL, IMAGE_CONTENT_TYPES, VIDEO_CONTENT_TYPES} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
const {value, event} = $props()
@@ -14,6 +14,7 @@
let hideImage = $state(false)
const url = value.url.toString()
const fileType = getTagValue("file-type", event.tags) || ""
const [href, external] = call(() => {
if (isRelayUrl(url)) return [makeSpacePath(url), false]
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
@@ -40,11 +41,11 @@
<Link {external} {href} class="my-2 block">
<div class="overflow-hidden rounded-box">
{#if url.match(/\.(mov|webm|mp4)$/)}
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}
<video controls src={url} class="max-h-96 rounded-box object-contain object-center">
<track kind="captions" />
</video>
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
{:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
</button>
@@ -1,6 +1,6 @@
<script lang="ts">
import {onMount, onDestroy} from "svelte"
import {displayUrl} from "@welshman/lib"
import {displayUrl, once} from "@welshman/lib"
import {
getTags,
getBlob,
@@ -26,8 +26,24 @@
const key = getTagValue("decryption-key", meta)
const nonce = getTagValue("decryption-nonce", meta)
const algorithm = getTagValue("encryption-algorithm", meta)
const mime = getTagValue("m", meta)
const fileName =
getTagValue("filename", meta) ||
getTagValue("name", meta) ||
decodeURIComponent(new URL(url).pathname.split("/").filter(Boolean).at(-1) || "image")
const onError = async () => {
const revokeSrc = () => {
if (src.startsWith("blob:")) {
URL.revokeObjectURL(src)
}
}
const setBlobSrc = (data: Blob | Uint8Array<ArrayBuffer>, type?: string) => {
revokeSrc()
src = URL.createObjectURL(new File([data], fileName, type ? {type} : undefined))
}
const onError = once(async () => {
// If the image failed to load, try authenticating
if (hash && $signer) {
const server = new URL(url).origin
@@ -36,14 +52,15 @@
const res = await getBlob(server, hash, {authEvent})
if (res.status === 200) {
src = URL.createObjectURL(await res.blob())
const blob = await res.blob()
setBlobSrc(blob, blob.type || undefined)
} else {
hasError = true
}
} else {
hasError = true
}
}
})
let hasError = $state(false)
let src = $state("")
@@ -57,7 +74,7 @@
const ciphertext = new Uint8Array(await response.arrayBuffer())
const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm})
src = URL.createObjectURL(new Blob([new Uint8Array(decryptedData)]))
setBlobSrc(new Uint8Array(decryptedData), mime)
}
} else {
src = url
@@ -65,7 +82,7 @@
})
onDestroy(() => {
URL.revokeObjectURL(src)
revokeSrc()
})
</script>
+4 -3
View File
@@ -1,18 +1,19 @@
<script lang="ts">
import {call, displayUrl} from "@welshman/lib"
import {isRelayUrl} from "@welshman/util"
import {isRelayUrl, getTagValue} from "@welshman/util"
import {preventDefault, stopPropagation} from "@lib/html"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import {pushModal} from "@app/util/modal"
import {PLATFORM_URL} from "@app/core/state"
import {PLATFORM_URL, IMAGE_CONTENT_TYPES} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
const {value, event} = $props()
const url = value.url.toString()
const fileType = getTagValue("file-type", event.tags) || ""
const [href, external] = call(() => {
if (isRelayUrl(url)) return [makeSpacePath(url), false]
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
@@ -23,7 +24,7 @@
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
</script>
{#if url.match(/\.(jpe?g|png|gif|webp)$/)}
{#if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
<!-- Use a real link so people can copy the href -->
<a
href={url}
+4
View File
@@ -96,6 +96,10 @@
params={{
trigger: "manual",
interactive: true,
placement: "bottom",
getReferenceClientRect: () => wrapper!.getBoundingClientRect(),
onShow: (instance: Instance) => {
instance.popper.style.width = `${wrapper!.getBoundingClientRect().width + 8}px`
},
}} />
</button>
+1 -1
View File
@@ -26,7 +26,7 @@
<Icon icon={Reply} />
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
</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)}
<div class="h-2 w-2 rounded-full bg-primary"></div>
{/if}
+3 -1
View File
@@ -20,7 +20,9 @@
const h = getTagValue("h", event.tags)
</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>
<Content
event={{content: summary, tags: event.tags}}
+2 -2
View File
@@ -51,7 +51,7 @@
return pushToast({
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({
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>
+22 -14
View File
@@ -1,4 +1,6 @@
<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 Server from "@assets/icons/server.svg?dataurl"
import Moon from "@assets/icons/moon.svg?dataurl"
@@ -18,8 +20,8 @@
import {pushModal} from "@app/util/modal"
import {theme} from "@app/util/theme"
const back = () => history.back()
const logout = () => pushModal(LogOut)
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
</script>
@@ -52,19 +54,21 @@
{/snippet}
</CardButton>
</Link>
<Link replaceState href="/settings/wallet">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Wallet} size={7} /></div>
{/snippet}
{#snippet title()}
<div>Wallet</div>
{/snippet}
{#snippet info()}
<div>Connect a bitcoin wallet for sending social tips</div>
{/snippet}
</CardButton>
</Link>
{#if Capacitor.getPlatform() !== "ios"}
<Link replaceState href="/settings/wallet">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Wallet} size={7} /></div>
{/snippet}
{#snippet title()}
<div>Wallet</div>
{/snippet}
{#snippet info()}
<div>Connect a bitcoin wallet for sending social tips</div>
{/snippet}
</CardButton>
</Link>
{/if}
<Link replaceState href="/settings/relays">
<CardButton class="btn-neutral">
{#snippet icon()}
@@ -120,6 +124,10 @@
<Button onclick={logout} class="btn btn-neutral">
<Icon icon={Exit} /> Log Out
</Button>
<Button class="btn btn-link w-full md:hidden" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
</div>
</ModalBody>
</Modal>
+1 -1
View File
@@ -13,7 +13,7 @@
</script>
<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()}
<RelayIcon {url} size={12} class="rounded-full" />
{/snippet}
+2 -3
View File
@@ -2,15 +2,14 @@
import cx from "classnames"
import type {Snippet} from "svelte"
import {formatTimestamp} from "@welshman/lib"
import {getListTags, getPubkeyTagValues} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {userMuteList} from "@welshman/app"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Profile from "@app/components/Profile.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import {goToEvent} from "@app/util/routes"
import {isEventMuted} from "@app/core/state"
const {
event,
@@ -32,7 +31,7 @@
muted = false
}
let muted = $state(getPubkeyTagValues(getListTags($userMuteList)).includes(event.pubkey))
let muted = $state($isEventMuted(event))
</script>
<div class="flex flex-col gap-2 shadow-md {restProps.class}">
+9 -6
View File
@@ -2,6 +2,7 @@
import type {Snippet} from "svelte"
import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {Router} from "@welshman/router"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import EmojiButton from "@lib/components/EmojiButton.svelte"
@@ -11,31 +12,33 @@
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
type Props = {
url: string
event: TrustedEvent
children?: Snippet
url?: string
}
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) =>
publishDelete({relays: [url], event, protect: await shouldProtect})
publishDelete({relays, event, protect: await shouldProtect})
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) =>
publishReaction({
event,
relays,
content: emoji.unicode,
relays: [url],
protect: await shouldProtect,
})
</script>
<NoteCard {event} {url} class="card2 bg-alt">
<NoteCard {event} {url} class="cv card2 bg-alt">
<NoteContent {event} expandMode="inline" />
<div class="flex w-full justify-between gap-2">
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-right">
+15 -24
View File
@@ -17,35 +17,28 @@
if (!isPomadeSession($session)) return
const client = new Client($session.clientOptions)
const result = await client.listSessions()
const pubkey = await client.getPubkey()
try {
const result = await client.listSessions()
const pubkey = await client.getPubkey()
if (result.ok) {
// Group sessions by client pubkey and collect peers
const sessionMap = new Map<string, SessionWithPeers>()
if (result.ok) {
// Group sessions by client pubkey and collect peers
const sessionMap = new Map<string, SessionWithPeers>()
for (const message of result.messages) {
if (!message.res?.items) continue
for (const message of result.messages) {
if (!message?.payload.items) continue
for (const item of message.res.items) {
const existing = sessionMap.get(item.client)
const peer = message.event.pubkey
for (const item of message.payload.items) {
const existing = sessionMap.get(item.client)
if (existing) {
existing.peers.push(peer)
} else if (item.client !== pubkey) {
sessionMap.set(item.client, {...item, peers: [peer]})
}
if (existing) {
existing.peers.push(message.url)
} else if (item.client !== pubkey) {
sessionMap.set(item.client, {...item, peers: [message.url]})
}
}
sessions = Array.from(sessionMap.values())
}
} finally {
client.stop()
sessions = Array.from(sessionMap.values())
}
}
@@ -71,8 +64,6 @@
message: "Failed to delete session",
})
}
client.stop()
} catch (e) {
console.error(e)
pushToast({
+11 -19
View File
@@ -1,8 +1,7 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {goto} from "$app/navigation"
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 Compass from "@assets/icons/compass.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
@@ -14,13 +13,12 @@
import ImageIcon from "@lib/components/ImageIcon.svelte"
import Divider from "@lib/components/Divider.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 PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
import {userSpaceUrls, PLATFORM_RELAYS, PLATFORM_LOGO} from "@app/core/state"
import {pushModal} from "@app/util/modal"
import {notifications} from "@app/util/notifications"
import {goToLastChat} from "@app/util/routes"
type Props = {
children?: Snippet
@@ -28,12 +26,8 @@
const {children}: Props = $props()
const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls})
const showSettingsMenu = () => pushModal(MenuSettings)
const openChat = () => ($shouldUnwrap ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
let windowHeight = $state(0)
const itemHeight = 56
@@ -60,15 +54,13 @@
{#each primarySpaceUrls as url (url)}
<PrimaryNavItemSpace {url} />
{/each}
{#if secondarySpaceUrls.length > 0}
<PrimaryNavItem
title="Other Spaces"
class="tooltip-right"
onclick={showOtherSpacesMenu}
notification={otherSpaceNotifications}>
<ImageIcon alt="Other Spaces" src={Widget} size={8} />
</PrimaryNavItem>
{/if}
<PrimaryNavItem
href="/spaces"
title="All Spaces"
class="tooltip-right"
notification={otherSpaceNotifications}>
<ImageIcon alt="All Spaces" src={Widget} size={8} />
</PrimaryNavItem>
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
<ImageIcon alt="Add a Space" src={Compass} size={8} />
</PrimaryNavItem>
@@ -91,7 +83,7 @@
</PrimaryNavItem>
<PrimaryNavItem
title="Messages"
onclick={openChat}
onclick={goToLastChat}
class="tooltip-right"
notification={$notifications.has("/chat")}>
<ImageIcon alt="Messages" src={Letter} size={8} />
@@ -118,7 +110,7 @@
</PrimaryNavItem>
<PrimaryNavItem
title="Messages"
onclick={openChat}
onclick={goToLastChat}
notification={$notifications.has("/chat")}>
<ImageIcon alt="Messages" src={Letter} size={8} />
</PrimaryNavItem>
+36 -10
View File
@@ -4,7 +4,6 @@
import {removeUndefined} from "@welshman/lib"
import {ManagementMethod} from "@welshman/util"
import {
shouldUnwrap,
manageRelay,
deriveProfile,
displayProfileByPubkey,
@@ -15,6 +14,7 @@
import Letter from "@assets/icons/letter-opened.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.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 Icon from "@lib/components/Icon.svelte"
import ImageIcon from "@lib/components/ImageIcon.svelte"
@@ -29,8 +29,7 @@
import ProfileInfo from "@app/components/ProfileInfo.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import ProfileBadges from "@app/components/ProfileBadges.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte"
import {pubkeyLink, deriveUserIsSpaceAdmin} from "@app/core/state"
import {pubkeyLink, deriveUserIsSpaceAdmin, deriveSpaceBannedPubkeyItems} from "@app/core/state"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
import {makeChatPath} from "@app/util/routes"
@@ -46,13 +45,17 @@
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 chatPath = makeChatPath([pubkey])
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) => {
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)
onMount(() => {
@@ -112,12 +129,21 @@
</li>
{/if}
{#if $userIsAdmin}
<li>
<Button class="text-error" onclick={banMember}>
<Icon icon={MinusCircle} />
Ban User
</Button>
</li>
{#if isBanned}
<li>
<Button onclick={restoreMember}>
<Icon icon={Restart} />
Restore User
</Button>
</li>
{:else}
<li>
<Button class="text-error" onclick={banMember}>
<Icon icon={MinusCircle} />
Ban User
</Button>
</li>
{/if}
{/if}
</ul>
</Popover>
+7 -4
View File
@@ -2,6 +2,8 @@
import type {Profile} from "@welshman/util"
import {getTag, makeProfile} from "@welshman/util"
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 ProfileEditForm from "@app/components/ProfileEditForm.svelte"
import {clearModals} from "@app/util/modal"
@@ -24,9 +26,10 @@
<ProfileEditForm {initialValues} {onsubmit}>
{#snippet footer()}
<div class="mt-4 flex flex-row items-center justify-between gap-4">
<Button class="btn btn-neutral" onclick={back}>Discard Changes</Button>
<Button type="submit" class="btn btn-primary">Save Changes</Button>
</div>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go Back
</Button>
<Button type="submit" class="btn btn-primary">Save Changes</Button>
{/snippet}
</ProfileEditForm>
+4 -1
View File
@@ -10,6 +10,7 @@
import Button from "@lib/components/Button.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import InputProfilePicture from "@app/components/InputProfilePicture.svelte"
import InfoHandle from "@app/components/InfoHandle.svelte"
import {pushModal} from "@app/util/modal"
@@ -123,5 +124,7 @@
</FieldInline>
{/if}
</ModalBody>
{@render footer()}
<ModalFooter>
{@render footer()}
</ModalFooter>
</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 instance: any = $state()
@@ -92,7 +92,7 @@
</div>
{/each}
</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} />
<!-- svelte-ignore a11y_autofocus -->
<input
@@ -114,12 +114,15 @@
select: selectPubkey,
component: ProfileSuggestion,
class: "rounded-box",
style: `left: 4px; width: ${input?.clientWidth + 12}px`,
style: `left: 4px; width: ${label?.clientWidth + 12}px`,
}}
params={{
trigger: "manual",
interactive: true,
maxWidth: "none",
getReferenceClientRect: () => input!.getBoundingClientRect(),
placement: "bottom",
getReferenceClientRect: () => label!.getBoundingClientRect(),
onShow: (instance: Instance) => {
instance.popper.style.width = `${label!.getBoundingClientRect().width + 8}px`
},
}} />
</div>

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