Compare commits

..

3 Commits

Author SHA1 Message Date
sixside 66f2b70030 Fix "Membship" typo 2026-02-18 11:41:57 +00:00
Jon Staab 445ed27eb8 Add rewrite to dockerfile
Docker / build-and-push-image (push) Successful in 14m14s
2026-02-17 12:01:12 -08:00
Jon Staab 21f3970ca8 Use explicit image name in workflow file
Docker / build-and-push-image (push) Has been cancelled
2026-02-17 11:48:52 -08:00
182 changed files with 1596 additions and 8984 deletions
-1
View File
@@ -1,6 +1,5 @@
--ignore-dir=.svelte-kit --ignore-dir=.svelte-kit
--ignore-dir=android --ignore-dir=android
--ignore-dir=target
--ignore-dir=build --ignore-dir=build
--ignore-dir=ios/DerivedData --ignore-dir=ios/DerivedData
--ignore-dir=ios/App/App/public --ignore-dir=ios/App/App/public
-8
View File
@@ -2,11 +2,3 @@ node_modules
android android
ios ios
build build
# Git
.git
.gitignore
# Env files (keep .env for build; exclude local overrides)
.env.local
.env.*.local
-1
View File
@@ -1,5 +1,4 @@
src/assets src/assets
target
build build
.idea .idea
.gradle .gradle
+1 -1
View File
@@ -6,7 +6,7 @@ on:
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: coracle-social/flotilla
jobs: jobs:
build-and-push-image: build-and-push-image:
-5
View File
@@ -1,6 +1,5 @@
# Env # Env
.env .env
.env.local
# Vite # Vite
vite.config.js.timestamp-* vite.config.js.timestamp-*
@@ -28,10 +27,6 @@ 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
+1 -11
View File
@@ -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
**CRITICAL Code Style Guidelines:** **Code Style:**
- **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,16 +168,6 @@ 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
-26
View File
@@ -1,31 +1,5 @@
# Changelog # Changelog
# 1.6.5
* Attempt to fix permission grant for notifications
* Make sync logic more robust
* Add unban/unallow support
* Improve support for downloading/opening protected images
* Add manual send/receive to wallet
* Show wallet status when wallet is unreachable
* Update nostr signer capacitor plugin
* Fix some safe area insets
* Update NIP 55 signer plugin (fixes Primal login)
* Refine space join dialogs and discover page
* Reopen the last DM that was open when navigating back to chat
* Get rid of ChatEnable interstitial
* Enable auth for relays we're publishing to
* Drag and drop space icons
* Add better muting support
* Add back button to settings menu
* Add page titles
* Improve scroll to event behavior
* Add in-memory search to rooms
* Fix editing messages with html tags
* Fix DM media detection
* Clean up reporting dialogs
* Improve room detail
# 1.6.4 # 1.6.4
* Clean up modal design * Clean up modal design
+10 -17
View File
@@ -1,31 +1,24 @@
# Stage 1: Build FROM node:20-slim
# 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 everything (including .env when present) - build.sh will source it # Copy the rest of the application
COPY . . COPY . .
ARG VITE_BUILD_HASH # Build the application
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
# Stage 2: Runtime # Default to serving the build directory
FROM node:20-alpine CMD ["npx", "serve", "-s", "build"]
WORKDIR /app
# Copy only the built output - no source, no .env, no dev deps
COPY --from=builder /app/build ./build
CMD ["npx", "serve", "build"]
+2 -2
View File
@@ -11,7 +11,7 @@ You can also optionally create an `.env` file and populate it with the following
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust - `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
- `VITE_PLATFORM_URL` - The url where the app will be hosted - `VITE_PLATFORM_URL` - The url where the app will be hosted
- `VITE_PLATFORM_NAME` - The name of the app - `VITE_PLATFORM_NAME` - The name of the app
- `VITE_PLATFORM_LOGO` - A logo url for the app. Can be a local path or https link. Must be a PNG file. - `VITE_PLATFORM_LOGO` - A logo url for the app
- `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](AGENTS.md). See [./CONTRIBUTING.md](CONTRIBUTING.md).
## Deployment ## Deployment
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "social.flotilla" applicationId "social.flotilla"
minSdk rootProject.ext.minSdkVersion minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion targetSdk rootProject.ext.targetSdkVersion
versionCode 41 versionCode 40
versionName "1.6.5" versionName "1.6.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+1 -1
View File
@@ -27,4 +27,4 @@ include ':capawesome-capacitor-badge'
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge/android') project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge/android')
include ':nostr-signer-capacitor-plugin' include ':nostr-signer-capacitor-plugin'
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin/android') 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')
+3 -7
View File
@@ -5,9 +5,6 @@ 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.local ]; then
source .env.local
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
@@ -17,13 +14,12 @@ 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 -fSL "$VITE_PLATFORM_LOGO" -o static/logo.png curl $VITE_PLATFORM_LOGO > static/logo.png
export VITE_PLATFORM_LOGO=static/logo.png export VITE_PLATFORM_LOGO=static/logo.png
fi fi
# Ensure generator uses local path (dotenv may have loaded URL from .env) npx pwa-assets-generator
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
+4 -4
View File
@@ -358,14 +358,14 @@
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 = 32; CURRENT_PROJECT_VERSION = 30;
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";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.6.5; MARKETING_VERSION = 1.6.4;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -385,14 +385,14 @@
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 = 32; CURRENT_PROJECT_VERSION = 30;
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";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.6.5; MARKETING_VERSION = 1.6.4;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
+1 -1
View File
@@ -18,7 +18,7 @@ def capacitor_pods
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences' pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications' pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications'
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge' pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge'
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin' pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@8.0.1/node_modules/nostr-signer-capacitor-plugin'
end end
target 'Flotilla Chat' do target 'Flotilla Chat' do
+3 -6
View File
@@ -1,12 +1,11 @@
#!/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'
const force = process.argv.includes('--force') if (execSync('git status --porcelain', { encoding: 'utf8' }).trim()) {
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)
} }
@@ -23,9 +22,7 @@ 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')
+15 -21
View File
@@ -1,15 +1,11 @@
{ {
"name": "flotilla", "name": "flotilla",
"version": "1.6.5", "version": "1.6.4",
"private": true, "private": true,
"scripts": { "scripts": {
"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": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check src && eslint src", "lint": "prettier --check src && eslint src",
@@ -22,7 +18,6 @@
"@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",
@@ -57,7 +52,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.2.1", "@pomade/core": "^0.0.12",
"@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",
@@ -65,17 +60,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.8", "@welshman/app": "^0.8.4",
"@welshman/content": "^0.8.8", "@welshman/content": "^0.8.4",
"@welshman/editor": "^0.8.8", "@welshman/editor": "^0.8.4",
"@welshman/feeds": "^0.8.8", "@welshman/feeds": "^0.8.4",
"@welshman/lib": "^0.8.8", "@welshman/lib": "^0.8.4",
"@welshman/net": "^0.8.8", "@welshman/net": "^0.8.4",
"@welshman/router": "^0.8.8", "@welshman/router": "^0.8.4",
"@welshman/signer": "^0.8.8", "@welshman/signer": "^0.8.4",
"@welshman/store": "^0.8.8", "@welshman/store": "^0.8.4",
"@welshman/util": "^0.8.8", "@welshman/util": "^0.8.4",
"compressorjs-next": "^1.1.2", "compressorjs": "^1.2.1",
"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",
@@ -83,7 +78,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": "github:coracle-social/nostr-signer-capacitor-plugin#main", "nostr-signer-capacitor-plugin": "^0.0.4",
"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",
@@ -96,8 +91,7 @@
"esbuild" "esbuild"
], ],
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"sharp", "sharp"
"nostr-signer-capacitor-plugin"
], ],
"overrides": { "overrides": {
"sharp": "0.35.0-rc.0" "sharp": "0.35.0-rc.0"
+518 -933
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
import dotenv from "dotenv" import dotenv from "dotenv"
import {defineConfig, minimalPreset as preset} from "@vite-pwa/assets-generator/config" import {defineConfig, minimalPreset as preset} from "@vite-pwa/assets-generator/config"
dotenv.config({path: ".env.local"}) dotenv.config({path: ".env"})
dotenv.config({path: ".env.template"}) dotenv.config({path: ".env.template"})
export default defineConfig({ export default defineConfig({
-2
View File
@@ -1,2 +0,0 @@
[toolchain]
channel = "1.92.0"
-4784
View File
File diff suppressed because it is too large Load Diff
-18
View File
@@ -1,18 +0,0 @@
[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
@@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}
-7
View File
@@ -1,7 +0,0 @@
{
"$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.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,5 +0,0 @@
<?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.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

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

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

-2
View File
@@ -1,2 +0,0 @@
[toolchain]
channel = "1.92.0"
-6
View File
@@ -1,6 +0,0 @@
#[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
@@ -1,6 +0,0 @@
// 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
@@ -1,37 +0,0 @@
{
"$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"
]
}
}
+3 -13
View File
@@ -274,7 +274,7 @@
.input-editor, .input-editor,
.chat-editor, .chat-editor,
.note-editor { .note-editor {
@apply -m-1 p-1; @apply -m-1 min-h-12 p-1 text-sm;
} }
.tiptap { .tiptap {
@@ -300,7 +300,7 @@
} }
.tiptap { .tiptap {
@apply max-h-[350px] min-h-10 overflow-y-auto p-2 px-4; @apply max-h-[350px] overflow-y-auto p-2 px-4;
} }
.tiptap p.is-editor-empty:first-child::before { .tiptap p.is-editor-empty:first-child::before {
@@ -402,10 +402,6 @@ 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 {
@@ -423,11 +419,5 @@ body.keyboard-open .hide-on-keyboard {
} }
.chat__scroll-down { .chat__scroll-down {
@apply pb-sai fixed bottom-28 right-4 z-feature md:bottom-16; @apply fixed bottom-28 right-4 z-feature md:bottom-16;
}
/* content visibility */
.cv {
content-visibility: auto;
} }
+1 -1
View File
@@ -19,7 +19,7 @@
</script> </script>
<Link <Link
class="cv col-3 card2 bg-alt w-full cursor-pointer shadow-md" class="col-3 card2 bg-alt w-full cursor-pointer shadow-md"
href={makeCalendarPath(url, getAddress(event))}> href={makeCalendarPath(url, getAddress(event))}>
<CalendarEventHeader {event} /> <CalendarEventHeader {event} />
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"> <div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
+65
View File
@@ -0,0 +1,65 @@
<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 -11
View File
@@ -1,10 +1,8 @@
<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, getTagValues, getAddress} from "@welshman/util" import {getTagValue, 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"
@@ -29,7 +27,6 @@
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)
@@ -48,13 +45,6 @@
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} />
+1 -2
View File
@@ -18,8 +18,7 @@
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 topics = getTagValues("t", event.tags) const initialValues = {d, title, status, content, price: Number(price), currency, images}
const initialValues = {d, title, status, content, price: Number(price), currency, images, topics}
</script> </script>
<ClassifiedForm {url} {initialValues}> <ClassifiedForm {url} {initialValues}>
+1 -17
View File
@@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {removeUndefined, randomId, uniq} from "@welshman/lib" import {randomId} 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"
@@ -15,7 +14,6 @@
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"
@@ -34,7 +32,6 @@
currency?: string currency?: string
images?: string[] images?: string[]
status?: string status?: string
topics?: string[]
} }
} }
@@ -74,10 +71,6 @@
...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)
} }
@@ -125,7 +118,6 @@
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)}>
@@ -158,14 +150,6 @@
</div> </div>
{/snippet} {/snippet}
</Field> </Field>
<Field>
{#snippet label()}
<p>Topics</p>
{/snippet}
{#snippet input()}
<TopicMultiSelect bind:value={topics} />
{/snippet}
</Field>
<Field> <Field>
{#snippet label()} {#snippet label()}
<p>Price*</p> <p>Price*</p>
+1 -1
View File
@@ -25,7 +25,7 @@
</script> </script>
<Link <Link
class="cv col-2 card2 bg-alt w-full cursor-pointer shadow-xl" class="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">
+4 -5
View File
@@ -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, getTagValue} from "@welshman/util" import {isRelayUrl} 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, IMAGE_CONTENT_TYPES, VIDEO_CONTENT_TYPES} from "@app/core/state" import {dufflepud, PLATFORM_URL} 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,7 +14,6 @@
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]
@@ -41,11 +40,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)$/) || VIDEO_CONTENT_TYPES.includes(fileType)} {#if url.match(/\.(mov|webm|mp4)$/)}
<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)$/) || IMAGE_CONTENT_TYPES.includes(fileType)} {:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
<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, once} from "@welshman/lib" import {displayUrl} from "@welshman/lib"
import { import {
getTags, getTags,
getBlob, getBlob,
@@ -26,24 +26,8 @@
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 revokeSrc = () => { const onError = async () => {
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
@@ -52,15 +36,14 @@
const res = await getBlob(server, hash, {authEvent}) const res = await getBlob(server, hash, {authEvent})
if (res.status === 200) { if (res.status === 200) {
const blob = await res.blob() src = URL.createObjectURL(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("")
@@ -74,7 +57,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})
setBlobSrc(new Uint8Array(decryptedData), mime) src = URL.createObjectURL(new Blob([new Uint8Array(decryptedData)]))
} }
} else { } else {
src = url src = url
@@ -82,7 +65,7 @@
}) })
onDestroy(() => { onDestroy(() => {
revokeSrc() URL.revokeObjectURL(src)
}) })
</script> </script>
+3 -4
View File
@@ -1,19 +1,18 @@
<script lang="ts"> <script lang="ts">
import {call, displayUrl} from "@welshman/lib" import {call, displayUrl} from "@welshman/lib"
import {isRelayUrl, getTagValue} from "@welshman/util" import {isRelayUrl} 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, IMAGE_CONTENT_TYPES} from "@app/core/state" import {PLATFORM_URL} 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]
@@ -24,7 +23,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)$/) || IMAGE_CONTENT_TYPES.includes(fileType)} {#if url.match(/\.(jpe?g|png|gif|webp)$/)}
<!-- 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}
-4
View File
@@ -96,10 +96,6 @@
params={{ params={{
trigger: "manual", trigger: "manual",
interactive: true, interactive: true,
placement: "bottom",
getReferenceClientRect: () => wrapper!.getBoundingClientRect(), getReferenceClientRect: () => wrapper!.getBoundingClientRect(),
onShow: (instance: Instance) => {
instance.popper.style.width = `${wrapper!.getBoundingClientRect().width + 8}px`
},
}} /> }} />
</button> </button>
+1 -1
View File
@@ -26,7 +26,7 @@
<Icon icon={Reply} /> <Icon icon={Reply} />
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span> <span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
</div> </div>
<div class="btn btn-neutral btn-xs relative rounded-full"> <div class="btn btn-neutral btn-xs relative hidden rounded-full sm:flex">
{#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}
+1 -3
View File
@@ -20,9 +20,7 @@
const h = getTagValue("h", event.tags) const h = getTagValue("h", event.tags)
</script> </script>
<Link <Link class="col-2 card2 bg-alt w-full cursor-pointer shadow-md" href={makeGoalPath(url, event.id)}>
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}}
+21 -6
View File
@@ -15,10 +15,10 @@
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import StringMultiInput from "@lib/components/StringMultiInput.svelte"
import KeyDownload from "@app/components/KeyDownload.svelte" import KeyDownload from "@app/components/KeyDownload.svelte"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {pushModal, clearModals} from "@app/util/modal" import {pushModal, clearModals} from "@app/util/modal"
import {POMADE_SIGNERS} from "@app/core/state"
type Props = { type Props = {
peersByPrefix: Map<string, string> peersByPrefix: Map<string, string>
@@ -32,6 +32,18 @@
} = $session as SessionPomade } = $session as SessionPomade
const confirmRecovery = async () => { const confirmRecovery = async () => {
const otps = input
.split(/\n/)
.map(x => x.trim())
.filter(x => x.match(/^[0-9]{8}$/))
if (otps.length < 2) {
return pushToast({
theme: "error",
message: "Failed to recover, not enough valid recovery codes were provided.",
})
}
const request = await Client.recoverWithChallenge(email, peersByPrefix, otps) const request = await Client.recoverWithChallenge(email, peersByPrefix, otps)
if (!request.ok) { if (!request.ok) {
@@ -39,7 +51,7 @@
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: `Failed to recover: ${request.messages[0]?.res?.message.toLowerCase()}`, message: `Failed to recover: ${request.messages[0]?.payload.message.toLowerCase()}`,
}) })
} }
@@ -50,7 +62,7 @@
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: `Failed to recover: ${result.messages[0]?.res?.message.toLowerCase()}`, message: `Failed to recover: ${result.messages[0]?.payload.message.toLowerCase()}`,
}) })
} }
@@ -70,7 +82,7 @@
const back = () => history.back() const back = () => history.back()
let loading = $state(false) let loading = $state(false)
let otps = $state<string[]>([]) let input = $state("")
</script> </script>
<Modal tag="form" onsubmit={preventDefault(submit)}> <Modal tag="form" onsubmit={preventDefault(submit)}>
@@ -84,14 +96,17 @@
For security reasons, you may receive three or more emails with recovery codes in them. Please For security reasons, you may receive three or more emails with recovery codes in them. Please
paste <strong>all</strong> recovery codes into the text box below, on separate lines. paste <strong>all</strong> recovery codes into the text box below, on separate lines.
</p> </p>
<StringMultiInput bind:value={otps} placeholder="Enter your recovery codes..." /> <textarea
rows={POMADE_SIGNERS.length + 1}
class="textarea textarea-bordered leading-4"
bind:value={input}></textarea>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading || otps.length < 2}> <Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Confirm recovery</Spinner> <Spinner {loading}>Confirm recovery</Spinner>
<Icon icon={AltArrowRight} /> <Icon icon={AltArrowRight} />
</Button> </Button>
+1
View File
@@ -34,6 +34,7 @@
const onSuccess = async (session: Session) => { const onSuccess = async (session: Session) => {
addSession(session) addSession(session)
pushToast({message: "Successfully logged in!"})
setChecked("*") setChecked("*")
clearModals() clearModals()
} }
+15 -21
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {uniq} from "@welshman/lib"
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {loginWithPomade} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -17,8 +17,6 @@
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import LogInOTP from "@app/components/LogInOTP.svelte" import LogInOTP from "@app/components/LogInOTP.svelte"
import LogInSelect from "@app/components/LogInSelect.svelte"
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
import {pushModal, clearModals} from "@app/util/modal" import {pushModal, clearModals} from "@app/util/modal"
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
@@ -39,7 +37,7 @@
try { try {
const {ok, options, messages, clientSecret} = await Client.loginWithPassword(email, password) const {ok, options, messages, clientSecret} = await Client.loginWithPassword(email, password)
if (!ok || options.length === 0) { if (!ok) {
console.error(messages) console.error(messages)
return pushToast({ return pushToast({
@@ -48,25 +46,21 @@
}) })
} }
if (uniq(options.map(o => o.pubkey)).length > 1) { const [client, peers] = options[0]!
pushModal(LogInSelect, {email, options, clientSecret}) const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
if (res.ok && clientOptions) {
loginWithPomade(clientOptions.group.group_pk.slice(2), email, clientOptions)
pushToast({message: "Successfully logged in!"})
setChecked("*")
clearModals()
} else { } else {
const {client, peers} = options[0] console.error(res.messages)
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
if (res.ok && clientOptions) { pushToast({
loginWithPomade(clientOptions, email) theme: "error",
deleteDeactivatedPomadeSessions() message: "Sorry, we were unable to log you in.",
setChecked("*") })
clearModals()
} else {
console.error(res.messages)
pushToast({
theme: "error",
message: "Sorry, we were unable to log you in.",
})
}
} }
} finally { } finally {
loading = false loading = false
+1
View File
@@ -57,6 +57,7 @@
} }
loginWithNip01(secret) loginWithNip01(secret)
pushToast({message: "Successfully logged in!"})
setChecked("*") setChecked("*")
clearModals() clearModals()
} catch (e) { } catch (e) {
+36 -27
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {uniq} from "@welshman/lib"
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {loginWithPomade} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -13,12 +13,10 @@
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import StringMultiInput from "@lib/components/StringMultiInput.svelte" import {clearModals} from "@app/util/modal"
import LogInSelect from "@app/components/LogInSelect.svelte"
import {pushToast} from "@app/util/toast"
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
import {pushModal, clearModals} from "@app/util/modal" import {pushToast} from "@app/util/toast"
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade" import {POMADE_SIGNERS} from "@app/core/state"
type Props = { type Props = {
email: string email: string
@@ -30,6 +28,18 @@
const back = () => history.back() const back = () => history.back()
const onSubmit = async () => { const onSubmit = async () => {
const otps = input
.split(/\n/)
.map(x => x.trim())
.filter(x => x.match(/^[0-9]{8}$/))
if (otps.length < 2) {
return pushToast({
theme: "error",
message: "Failed to recover, not enough valid recovery codes were provided.",
})
}
loading = true loading = true
try { try {
@@ -39,7 +49,7 @@
otps, otps,
) )
if (!ok || options.length === 0) { if (!ok) {
console.error(messages) console.error(messages)
return pushToast({ return pushToast({
@@ -48,32 +58,28 @@
}) })
} }
if (uniq(options.map(o => o.pubkey)).length > 1) { const [client, peers] = options[0]!
pushModal(LogInSelect, {email, options, clientSecret}) const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
if (res.ok && clientOptions) {
loginWithPomade(clientOptions.group.group_pk.slice(2), email, clientOptions)
pushToast({message: "Successfully logged in!"})
setChecked("*")
clearModals()
} else { } else {
const {client, peers} = options[0] console.error(res.messages)
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
if (res.ok && clientOptions) { pushToast({
loginWithPomade(clientOptions, email) theme: "error",
deleteDeactivatedPomadeSessions() message: "Sorry, we were unable to log you in.",
setChecked("*") })
clearModals()
} else {
console.error(res.messages)
pushToast({
theme: "error",
message: "Sorry, we were unable to log you in.",
})
}
} }
} finally { } finally {
loading = false loading = false
} }
} }
let otps = $state<string[]>([]) let input = $state("")
let loading = $state(false) let loading = $state(false)
</script> </script>
@@ -88,14 +94,17 @@
For security reasons, you may receive three or more emails with login codes in them. Please For security reasons, you may receive three or more emails with login codes in them. Please
paste <strong>all</strong> login codes into the text box below, on separate lines. paste <strong>all</strong> login codes into the text box below, on separate lines.
</p> </p>
<StringMultiInput bind:value={otps} placeholder="Enter your login codes..." /> <textarea
rows={POMADE_SIGNERS.length + 1}
class="textarea textarea-bordered leading-4"
bind:value={input}></textarea>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back} disabled={loading}> <Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading || otps.length < 3}> <Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Log In</Spinner> <Spinner {loading}>Log In</Spinner>
<Icon icon={AltArrowRight} /> <Icon icon={AltArrowRight} />
</Button> </Button>
-83
View File
@@ -1,83 +0,0 @@
<script lang="ts">
import type {AccountOption} from "@pomade/core"
import {Client} from "@pomade/core"
import {uniqBy} from "@welshman/lib"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.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 Profile from "@app/components/Profile.svelte"
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
import {setChecked} from "@app/util/notifications"
import {clearModals} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
interface Props {
email: string
options: AccountOption[]
clientSecret: string
}
const {email, options, clientSecret}: Props = $props()
let loading = $state(false)
const back = () => history.back()
const selectAccount = async ({client, peers}: AccountOption) => {
loading = true
try {
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
if (res.ok && clientOptions) {
loginWithPomade(clientOptions, email)
deleteDeactivatedPomadeSessions()
setChecked("*")
clearModals()
} else {
console.error(res.messages)
pushToast({
theme: "error",
message: "Sorry, we were unable to log you in.",
})
}
} finally {
loading = false
}
}
</script>
<Modal>
<ModalBody>
<ModalHeader>
<ModalTitle>Select Account</ModalTitle>
<ModalSubtitle
>Multiple accounts are associated with {email}. Please select one to continue.</ModalSubtitle>
</ModalHeader>
<div class="flex flex-col gap-2">
{#each uniqBy(o => o.pubkey, options) as option (option.pubkey)}
<Button
onclick={() => selectAccount(option)}
disabled={loading}
class="card2 bg-alt flex w-full items-center p-3 text-left">
<Profile pubkey={option.pubkey} />
</Button>
{/each}
</div>
</ModalBody>
<ModalFooter>
<Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Spinner {loading} />
</ModalFooter>
</Modal>
+9 -2
View File
@@ -9,7 +9,8 @@
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import {logout} from "@app/util/logout" import {Push} from "@app/util/notifications"
import {kv, db} from "@app/core/storage"
const back = () => history.back() const back = () => history.back()
@@ -17,7 +18,13 @@
loading = true loading = true
try { try {
await logout() await Push.disable()
await kv.clear()
await db.clear()
localStorage.clear()
window.location.href = "/"
} catch (e) { } catch (e) {
console.error(e) console.error(e)
loading = false loading = false
+15
View File
@@ -0,0 +1,15 @@
<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>

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