Compare commits

...

56 Commits

Author SHA1 Message Date
Jon Staab ae071fefaa Fail more gracefully when svelte sneezes 2026-05-29 08:30:42 -07:00
userAdityaa 152d35f92a Fix deleted rooms persisting in navigation (#285)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-05-29 15:20:21 +00:00
userAdityaa 8dd278f47c fix: turn on notification defaults and prompt on first DM visit (#284)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-05-29 15:13:10 +00:00
Jon Staab 045d6983dc Fix some voice room bugs 2026-05-28 12:17:17 -07:00
Jon Staab 2f8861be62 Bump welshman, update pnpm config 2026-05-28 12:14:40 -07:00
userAdityaa 6dbe9c0ebb chore: show space relay icon in mobile topbar headers (#283)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-05-25 19:18:51 +00:00
Jon Staab 45df132dc6 Remove tauri, bump deps 2026-05-25 10:41:34 -07:00
Jon Staab c42a285f0b Tweak wording 2026-05-25 09:04:14 -07:00
userAdityaa 1e3211ae74 chore: uppdate signup modal copy to focus on joining Flotilla (#282)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-05-25 16:00:28 +00:00
npub15skvhry ec507b05d6 minimize container size and caching 2026-05-22 15:36:28 -07:00
Jon Staab 339bb1afac Tweak page bar style 2026-05-21 15:17:28 -07:00
userAdityaa c441012e02 fix(video): restyle spotlight pin button on video tiles (#281)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-05-21 20:59:25 +00:00
userAdityaa 0d61278c56 chore: show call participant mute and camera-off state (#279)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-05-21 20:58:53 +00:00
userAdityaa ffd06ab561 fix: video blink when toggling mic mute in calls (#277)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-05-20 16:44:38 +00:00
userAdityaa eb8dd330b6 fix(video): use single-column tile grid when chat is open (#278)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-05-20 16:42:16 +00:00
userAdityaa 6267e52bdf feat: show unread chat badges during video-only voice calls (#276)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-05-19 15:36:22 +00:00
Jon Staab ab21008f34 Add etag for immutable assets 2026-05-12 08:46:50 -07:00
Jon Staab 0998639d59 Push to gitea package registry 2026-05-11 13:49:37 -07:00
Jon Staab eccde07d06 Fix dockerfile again 2026-05-11 13:20:47 -07:00
Jon Staab 770cdc5f13 Reduce extra space on android when keyboard is open 2026-05-11 12:44:44 -07:00
Jon Staab 6bafb62414 Fix docker build 2026-05-11 12:28:42 -07:00
Jon Staab 6ce0fbbbe6 Make recommended ios changes 2026-05-11 10:02:14 -07:00
Jon Staab 8fe42e6f22 Update version 2026-05-11 09:54:22 -07:00
Jon Staab 47a6209730 Bump welshman 2026-05-11 09:20:17 -07:00
Jon Staab 24d3f867f8 Improve space search 2026-05-07 12:53:49 -07:00
Jon Staab 9db60374e4 Make sure to always show date on calendar events when embedded in chat 2026-05-06 17:24:29 -07:00
Jon Staab 8ef4b21dab Allow sharing something to chat without a message 2026-05-06 17:17:55 -07:00
Jon Staab 8f56812dd1 Fix undefined chat draft key 2026-05-06 16:56:31 -07:00
Jon Staab 3833cb093d Attempt to fix wrapping on relay summary on ios 2026-05-06 16:39:01 -07:00
Jon Staab 94db65b85e Bump welshman, add email rendering support 2026-05-06 13:47:30 -07:00
Jon Staab 6f731e48d2 Hide keyboard on app resume 2026-05-06 12:48:15 -07:00
Jon Staab 99fe0e543c Avoid capturing stale cleanup function in chat 2026-05-06 09:31:28 -07:00
Jon Staab c6b0799b2a Remove cv class from chat since new messages weren't rendering in Safari 2026-05-06 09:25:29 -07:00
Jon Staab 861f2286db Remove unnecessary tooltip, fix chat padding on mobile 2026-05-06 09:01:01 -07:00
Jon Staab 9af3e3b2e9 Fix relay badge overflow 2026-05-05 09:11:12 -07:00
Jon Staab 341c1b45b2 Stop publishing join requests every time we open a space 2026-05-05 09:09:28 -07:00
Jon Staab 89f5d8cdf5 Fix pasting into event summary 2026-05-04 16:15:21 -07:00
Jon Staab ca3270437d Highlight active space 2026-05-04 16:11:30 -07:00
Khushvendra bbbc6f7363 fix(metadata): add case-insensitive HTML title fallback parsing for invite links (#248)
Co-authored-by: 1amKhush <khushvendras99@gmail.com>
Co-committed-by: 1amKhush <khushvendras99@gmail.com>
2026-05-04 21:02:56 +00:00
Jon Staab 8a0abacf6f Fix padding on page content on small screens 2026-05-01 13:24:42 -07:00
Jon Staab 976ccdabd4 fix: include MESSAGE kind and local matches in space search 2026-04-28 14:44:16 -07:00
Jon Staab 99b26680b6 feat: rework hosting page to 2+1 architecture (#231) 2026-04-28 14:42:09 -07:00
Jon Staab c5be477855 fix: bundle emoji-picker data locally for Capacitor Android
The emoji grid wasn't rendering on Android because emoji-picker-element
defaults to fetching its data.json from jsdelivr, and CapacitorHttp's
patched fetch breaks the library's ETag-based revalidation flow. Bundle
emoji-picker-element-data via Vite's ?url import so the JSON ships as a
same-origin asset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:15:12 -07:00
DeveshSingh 32c1501e9c feat: add progress bar to signup flow (#234)
Co-authored-by: deveshanim3 <deveshsingh6986@gmail.com>
Co-committed-by: deveshanim3 <deveshsingh6986@gmail.com>
2026-04-23 15:35:59 +00:00
DeveshSingh 463837e7d4 fix: restore consistent input field sizing and alignment in FieldInline (#235) (#238)
Co-authored-by: deveshanim3 <deveshsingh6986@gmail.com>
Co-committed-by: deveshanim3 <deveshsingh6986@gmail.com>
2026-04-20 16:26:44 +00:00
Shreyas2004wagh d74f142cdd Fix relay auth privacy toggle (#240)
Co-authored-by: Shreyas2004wagh <shreyaswagh2004@gmail.com>
Co-committed-by: Shreyas2004wagh <shreyaswagh2004@gmail.com>
2026-04-20 16:25:52 +00:00
Jon Staab 53954aae89 classnames tweak 2026-04-17 16:06:00 -07:00
userAdityaa 24aa62a503 chore: carify Pomade login errors with actionable invalid vs network messaging (#233)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-17 23:00:03 +00:00
Jon Staab 2618bb9c63 Fix centered layout 2026-04-17 14:58:59 -07:00
Prat_09 32a31045ef fix: Improve toggle switch placement in settings screen (#208) (#232)
Co-authored-by: Pratyush Mohanty <prat_09@noreply.coracle.social>
Co-committed-by: Pratyush Mohanty <prat_09@noreply.coracle.social>
2026-04-17 21:58:52 +00:00
DeveshSingh 56edad77a8 fix: added logic for password requirements on signup (#230)
Co-authored-by: deveshanim3 <deveshsingh6986@gmail.com>
Co-committed-by: deveshanim3 <deveshsingh6986@gmail.com>
2026-04-17 19:43:27 +00:00
priyanshu_bharti fdb604e350 Use type=email for signup/login email inputs (#225) (#228)
Co-authored-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
2026-04-17 18:55:04 +00:00
DeveshSingh 3c66dfd83c fix/wrong-message-offline (#222)
Co-authored-by: deveshanim3 <deveshsingh6986@gmail.com>
Co-committed-by: deveshanim3 <deveshsingh6986@gmail.com>
2026-04-17 18:24:55 +00:00
userAdityaa 81633b0a1e fix: vertical alignment of emoji and overflow buttons in shared event action row (#219)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-17 15:22:40 +00:00
Khushvendra 4a967de184 fix(chat): suppress programmatic scroll while user is scrolling (#132) (#216)
Co-authored-by: 1amKhush <khushvendras99@gmail.com>
Co-committed-by: 1amKhush <khushvendras99@gmail.com>
2026-04-16 23:20:17 +00:00
DeveshSingh 59961cbdb5 fix: supported nip overflow in SpaceRelayStatus.svelte (#215)
Co-authored-by: deveshanim3 <deveshsingh6986@gmail.com>
Co-committed-by: deveshanim3 <deveshsingh6986@gmail.com>
2026-04-16 21:36:14 +00:00
148 changed files with 4061 additions and 8169 deletions
-1
View File
@@ -4,7 +4,6 @@ ios
build build
# Git # Git
.git
.gitignore .gitignore
# Env files (keep .env for build; exclude local overrides) # Env files (keep .env for build; exclude local overrides)
@@ -1,12 +1,17 @@
name: Docker name: Container Image Build and Publish
on: on:
push: push:
branches: [master] branches: [master]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env: env:
REGISTRY: ghcr.io REGISTRY: gitea.coracle.social
IMAGE_NAME: coracle-social/flotilla IMAGE_NAME: coracle/flotilla
jobs: jobs:
build-and-push-image: build-and-push-image:
@@ -23,8 +28,8 @@ jobs:
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }} username: hodlbod
password: ${{ secrets.REGISTRY_PASSWORD }} password: ${{ secrets.PACKAGE_TOKEN }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
@@ -32,6 +37,7 @@ jobs:
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: | tags: |
type=sha
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -45,6 +51,7 @@ jobs:
with: with:
context: . context: .
push: true push: true
target: production
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
+1 -4
View File
@@ -27,13 +27,10 @@ android/app/src/main/assets/public/
node_modules/ node_modules/
.pnpm-store/ .pnpm-store/
build/ build/
build-server/
.svelte-kit/ .svelte-kit/
.next/ .next/
# Rust/Tauri
*target/
src-tauri/binaries/
# iOS # iOS
ios/App/App/public ios/App/App/public
ios/DerivedData ios/DerivedData
+26
View File
@@ -1,5 +1,31 @@
# Changelog # Changelog
# 1.8.0
* Fix relay badge overflow
* Suppress programmatic scroll when user is scrolling
* Fix vertical alignment of emoji and overflow buttons in shared event action row
* Use type=email for signup/login email inputs, validate password
* Improve toggle switch placement on settings screens
* Fix relay auth privacy toggle
* Improve field layout
* Add progress bar to signup flow
* Bundle emojis properly
* Rework hosting page
* Fix padding on pages on small screens
* Add richer link preview support
* Fix pasting into event summary
* Publish fewer join/claim requests
* Fix new messages not rendering in safari
* Avoid capturing stale cleanup function in chat
* Hide keyboard on app resume
* Add email rendering support
* Fix bunker login
* Fix undefined chat draft key
* Allow sharing to chat without a message
* Make sure to show date on calendar events when embedded
* Improve space search
# 1.7.4 # 1.7.4
* Fix safe area inset for FAB * Fix safe area inset for FAB
+24 -25
View File
@@ -1,32 +1,31 @@
# Stage 1: Build # Build and run the Flotilla web server.
# 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 . # docker build -t flotilla .
# docker run -p 3000:3000 flotilla
#
# Pass --build-arg VITE_BUILD_HASH=$(git rev-parse --short HEAD) to stamp the build.
# A .env in the build context is picked up by build.sh for branding config.
FROM node:20-bookworm AS builder # https://pnpm.io/docker#example-3-build-on-cicd
FROM node:24-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends curl ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN npm install -g pnpm@latest RUN corepack enable
WORKDIR /app WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm i
# Copy everything (including .env when present) - build.sh will source it
COPY . .
ARG VITE_BUILD_HASH
ENV VITE_BUILD_HASH=${VITE_BUILD_HASH}
ENV NODE_OPTIONS=--max_old_space_size=16384 ENV NODE_OPTIONS=--max_old_space_size=16384
COPY package.json pnpm-lock.yaml ./
RUN pnpm i --frozen-lockfile
COPY . .
ARG VITE_BUILD_HASH
RUN pnpm run build RUN pnpm run build
RUN pnpm run build:server
FROM node:20-alpine FROM node:24-slim AS production
ENV NODE_ENV=production
WORKDIR /app WORKDIR /app
COPY --from=builder /app/build /app/build
# Copy only the built output - no source, no .env, no dev deps COPY --from=builder /app/build-server/server.js /app/server.js
COPY --from=builder /app/build ./build EXPOSE 3000
USER node
CMD ["npx", "serve", "-s", "build"] CMD ["node", "server.js"]
+3 -3
View File
@@ -31,18 +31,18 @@ To run your own Flotilla, it's as simple as:
```sh ```sh
pnpm install pnpm install
pnpm run build pnpm run build
npx serve -s build pnpm run start
``` ```
Or, if you prefer to use a container: Or, if you prefer to use a container:
```sh ```sh
podman run -d -p 3000:3000 ghcr.io/coracle-social/flotilla:latest docker run -d -p 3000:3000 gitea.coracle.social/coracle/flotilla:latest
``` ```
Alternatively, you can copy the build files into a directory of your choice and serve it yourself: Alternatively, you can copy the build files into a directory of your choice and serve it yourself:
```sh ```sh
mkdir ./mount mkdir ./mount
podman run -v ./mount:/app/mount ghcr.io/coracle-social/flotilla:latest bash -c 'cp -r build/* mount' docker run -v ./mount:/app/mount gitea.coracle.social/coracle/flotilla:latest bash -c 'cp -r build/* mount'
``` ```
+2 -2
View File
@@ -8,8 +8,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 46 versionCode 47
versionName "1.7.4" versionName "1.8.0"
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
@@ -36,4 +36,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@https+++codeload.github.com+coracle-social+nostr-signer-c_a3c0fb15d5bfa83f24d0070ca2583fc9/node_modules/nostr-signer-capacitor-plugin/android')
+29 -11
View File
@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 48; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -131,8 +131,9 @@
504EC2FC1FED79650016851F /* Project object */ = { 504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 920; LastSwiftUpdateCheck = 920;
LastUpgradeCheck = 920; LastUpgradeCheck = 2630;
TargetAttributes = { TargetAttributes = {
504EC3031FED79650016851F = { 504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2; CreatedOnToolsVersion = 9.2;
@@ -257,6 +258,7 @@
CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES; CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
@@ -264,8 +266,10 @@
CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -275,8 +279,10 @@
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = S26U9DYW3A;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO; GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
@@ -295,6 +301,7 @@
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
}; };
@@ -314,6 +321,7 @@
CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES; CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
@@ -321,8 +329,10 @@
CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -332,8 +342,10 @@
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = S26U9DYW3A;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -345,7 +357,9 @@
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
name = Release; name = Release;
@@ -358,14 +372,16 @@
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 = 37; CURRENT_PROJECT_VERSION = 38;
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 = (
MARKETING_VERSION = 1.7.4; "$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.8.0;
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 +401,16 @@
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 = 37; CURRENT_PROJECT_VERSION = 38;
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 = (
MARKETING_VERSION = 1.7.4; "$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.8.0;
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
@@ -21,7 +21,7 @@ def capacitor_pods
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 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/share' pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/share'
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@https+++codeload.github.com+coracle-social+nostr-signer-c_a3c0fb15d5bfa83f24d0070ca2583fc9/node_modules/nostr-signer-capacitor-plugin'
end end
target 'Flotilla Chat' do target 'Flotilla Chat' do
+27 -37
View File
@@ -1,15 +1,13 @@
{ {
"name": "flotilla", "name": "flotilla",
"version": "1.7.4", "version": "1.8.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "./build.sh", "build": "./build.sh",
"build:server": "vite build --config vite.config.server.ts",
"start": "node server.js",
"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",
@@ -20,26 +18,26 @@
"devDependencies": { "devDependencies": {
"@capacitor/assets": "^3.0.5", "@capacitor/assets": "^3.0.5",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@sveltejs/kit": "^2.50.1", "@sveltejs/kit": "^2.61.1",
"@sveltejs/vite-plugin-svelte": "^4.0.4", "@sveltejs/vite-plugin-svelte": "^5.1.1",
"@tailwindcss/postcss": "^4.2.2", "@tailwindcss/postcss": "^4.2.2",
"@tauri-apps/cli": "^2.9.6",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/node": "^25.9.1",
"autoprefixer": "^10.4.23", "autoprefixer": "^10.4.23",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-config-prettier": "^9.1.2", "eslint-config-prettier": "^9.1.2",
"eslint-plugin-svelte": "^2.46.1", "eslint-plugin-svelte": "^2.46.1",
"globals": "^15.15.0", "globals": "^15.15.0",
"postcss": "^8.5.6", "postcss": "^8.5.15",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1", "prettier-plugin-svelte": "^3.4.1",
"svelte": "^5.48.0", "svelte": "^5.55.9",
"svelte-check": "^4.3.5", "svelte-check": "^4.3.5",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.53.1", "typescript-eslint": "^8.53.1",
"vite": "^5.4.21" "vite": "^6.4.2"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
@@ -60,32 +58,36 @@
"@capawesome/capacitor-badge": "^8.0.0", "@capawesome/capacitor-badge": "^8.0.0",
"@getalby/lightning-tools": "^6.1.0", "@getalby/lightning-tools": "^6.1.0",
"@getalby/sdk": "^5.1.2", "@getalby/sdk": "^5.1.2",
"@hono/node-server": "^2.0.0",
"@noble/curves": "^1.9.7", "@noble/curves": "^1.9.7",
"@pomade/core": "^0.2.3", "@pomade/core": "^0.2.3",
"@poppanator/sveltekit-svg": "^4.2.1", "@poppanator/sveltekit-svg": "^7.0.0",
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.27.2", "@tiptap/core": "^2.27.2",
"@tiptap/pm": "^2.27.2", "@tiptap/pm": "^2.27.2",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@types/throttle-debounce": "^5.0.2", "@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/assets-generator": "^1.0.2",
"@vite-pwa/sveltekit": "^0.6.8", "@vite-pwa/sveltekit": "^1.1.0",
"@welshman/app": "^0.8.13", "@welshman/app": "^0.8.16",
"@welshman/content": "^0.8.13", "@welshman/content": "^0.8.16",
"@welshman/editor": "^0.8.13", "@welshman/editor": "^0.8.16",
"@welshman/feeds": "^0.8.13", "@welshman/feeds": "^0.8.16",
"@welshman/lib": "^0.8.13", "@welshman/lib": "^0.8.16",
"@welshman/net": "^0.8.13", "@welshman/net": "^0.8.16",
"@welshman/router": "^0.8.13", "@welshman/router": "^0.8.16",
"@welshman/signer": "^0.8.13", "@welshman/signer": "^0.8.16",
"@welshman/store": "^0.8.13", "@welshman/store": "^0.8.16",
"@welshman/util": "^0.8.13", "@welshman/util": "^0.8.16",
"cheerio": "^1.2.0",
"compressorjs-next": "^1.1.2", "compressorjs-next": "^1.1.2",
"daisyui": "^5.5.19", "daisyui": "^5.5.19",
"date-picker-svelte": "^2.17.0", "date-picker-svelte": "^2.17.0",
"dotenv": "^16.6.1", "dotenv": "^16.6.1",
"emoji-picker-element": "^1.28.1", "emoji-picker-element": "^1.28.1",
"emoji-picker-element-data": "^1.8.0",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"hono": "^4.12.23",
"husky": "^9.1.7", "husky": "^9.1.7",
"idb": "^8.0.3", "idb": "^8.0.3",
"livekit-client": "^2.17.2", "livekit-client": "^2.17.2",
@@ -97,17 +99,5 @@
"throttle-debounce": "^5.0.2", "throttle-debounce": "^5.0.2",
"tippy.js": "^6.3.7" "tippy.js": "^6.3.7"
}, },
"pnpm": { "packageManager": "pnpm@11.4.0"
"ignoredBuiltDependencies": [
"esbuild"
],
"onlyBuiltDependencies": [
"sharp",
"nostr-signer-capacitor-plugin"
],
"overrides": {
"sharp": "0.35.0-rc.0"
}
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
} }
+2611 -2772
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
allowBuilds:
nostr-signer-capacitor-plugin: true
cbor-extract: false
esbuild: false
sharp: true
minimumReleaseAgeExclude:
- '@welshman/app@0.8.16'
- '@welshman/content@0.8.16'
- '@welshman/editor@0.8.16'
- '@welshman/feeds@0.8.16'
- '@welshman/lib@0.8.16'
- '@welshman/net@0.8.16'
- '@welshman/router@0.8.16'
- '@welshman/signer@0.8.16'
- '@welshman/store@0.8.16'
- '@welshman/util@0.8.16'
overrides:
sharp: 0.35.0-rc.0
-2
View File
@@ -1,2 +0,0 @@
[toolchain]
channel = "1.92.0"
+288
View File
@@ -0,0 +1,288 @@
import path from "node:path"
import {promises as fs} from "node:fs"
import {fileURLToPath} from "node:url"
import "dotenv/config"
import {serve} from "@hono/node-server"
import {serveStatic} from "@hono/node-server/serve-static"
import {loadRelay} from "@welshman/app"
import {displayRelayUrl, normalizeRelayUrl} from "@welshman/util"
import {load} from "cheerio"
import {Hono} from "hono"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const BUILD_DIR = path.join(__dirname, "build")
const INDEX_PATH = path.join(BUILD_DIR, "index.html")
const PORT = parseInt(process.env.PORT || "", 10) || 3000
const HOST = process.env.HOST || "0.0.0.0"
let TEMPLATE_HTML = ""
try {
TEMPLATE_HTML = await fs.readFile(INDEX_PATH, "utf8")
} catch (error) {
console.error(`Unable to read ${INDEX_PATH}. Run "pnpm run build" first.`)
process.exit(1)
}
const PLATFORM_NAME = process.env.VITE_PLATFORM_NAME
const PLATFORM_DESCRIPTION = process.env.VITE_PLATFORM_DESCRIPTION
// Match client-side decode logic
const decodeRelay = url => {
try {
return normalizeRelayUrl(decodeURIComponent(url))
} catch {
return undefined
}
}
const requestUrlFromContext = context => {
const requestUrl = new URL(context.req.url)
const forwardedProto = context.req.header("x-forwarded-proto")?.split(",")[0]?.trim()
const forwardedHost = context.req.header("x-forwarded-host")?.split(",")[0]?.trim()
if (forwardedProto === "http" || forwardedProto === "https") {
requestUrl.protocol = `${forwardedProto}:`
}
if (forwardedHost) {
requestUrl.host = forwardedHost
}
return requestUrl
}
const fetchRelayMeta = async relayUrl => {
if (!relayUrl) return undefined
try {
return await loadRelay(normalizeRelayUrl(relayUrl))
} catch (err) {
console.error(`Failed to fetch relay metadata for ${relayUrl}:`, err)
return undefined
}
}
const buildDefaultImage = requestUrl => {
return new URL("/maskable-icon-512x512.png", requestUrl.origin).toString()
}
const getMetadataForInvite = async (url, match) => {
const relayParam = url.searchParams.get("r")
if (!relayParam) return undefined
const relayMetadata = await fetchRelayMeta(relayParam)
if (!relayMetadata) return undefined
const relayDisplay = displayRelayUrl(relayParam)
const spaceName = relayMetadata.name
const relayDescription = relayMetadata.description
const title = spaceName
? `Invite to ${spaceName} on ${PLATFORM_NAME}`
: `Invite to a Space on ${PLATFORM_NAME}`
const parts = []
if (spaceName) {
parts.push(`You are invited to join ${spaceName} on ${PLATFORM_NAME}.`)
} else {
parts.push(`You are invited to join a space on ${PLATFORM_NAME}.`)
}
if (relayDisplay) parts.push(`Relay: ${relayDisplay}.`)
if (relayDescription) parts.push(relayDescription)
else parts.push(PLATFORM_DESCRIPTION)
const description = parts.join(" ")
const image =
relayMetadata.icon ||
relayMetadata.picture ||
relayMetadata.image ||
buildDefaultImage(url)
return {
title,
description,
image,
url: url.toString(),
site: url.origin,
}
}
const getMetadataForSpace = async (url, match) => {
const relayParam = decodeRelay(match[1])
if (!relayParam) return undefined
const relayMetadata = await fetchRelayMeta(relayParam)
if (!relayMetadata) return undefined
const spaceName = relayMetadata.name || displayRelayUrl(relayParam)
return {
title: `${spaceName} on ${PLATFORM_NAME}`,
description: relayMetadata.description || PLATFORM_DESCRIPTION,
image:
relayMetadata.icon ||
relayMetadata.picture ||
relayMetadata.image ||
buildDefaultImage(url),
url: url.toString(),
site: url.origin,
}
}
const getMetadataForSpaceSection = async (url, match) => {
const spaceMeta = await getMetadataForSpace(url, match)
if (!spaceMeta) return undefined
const section = match[2]
const sectionName = section.charAt(0).toUpperCase() + section.slice(1)
spaceMeta.title = `${sectionName} on ${spaceMeta.title}`
return spaceMeta
}
const getMetadataForSpaceItem = async (url, match) => {
const spaceMeta = await getMetadataForSpace(url, match)
if (!spaceMeta) return undefined
const section = match[2]
let itemType = "Item"
if (section === "calendar") itemType = "Event"
if (section === "threads") itemType = "Thread"
if (section === "polls") itemType = "Poll"
if (section === "goals") itemType = "Goal"
if (section === "classifieds") itemType = "Listing"
spaceMeta.title = `${itemType} on ${spaceMeta.title}`
return spaceMeta
}
const getMetadataForRoom = async (url, match) => {
const spaceMeta = await getMetadataForSpace(url, match)
if (!spaceMeta) return undefined
// Room metadata requires fetching from Nostr, which can be added later.
spaceMeta.title = `Room on ${spaceMeta.title}`
return spaceMeta
}
const routes = [
[/^\/join\/?$/, getMetadataForInvite],
[/^\/spaces\/([^/]+)\/(calendar|chat|threads|polls|goals|classifieds|recent)\/?$/, getMetadataForSpaceSection],
[/^\/spaces\/([^/]+)\/(calendar|threads|polls|goals|classifieds)\/([^/]+)\/?$/, getMetadataForSpaceItem],
[/^\/spaces\/([^/]+)\/([^/]+)\/?$/, getMetadataForRoom],
[/^\/spaces\/([^/]+)\/?$/, getMetadataForSpace],
]
const getMetadataForRoute = async url => {
for (const [regex, getMetadata] of routes) {
const match = url.pathname.match(regex)
if (match) {
try {
return await getMetadata(url, match)
} catch (err) {
console.error(`Error generating metadata for route ${url.pathname}:`, err)
return undefined
}
}
}
return undefined
}
const injectMeta = metadata => {
const $ = load(TEMPLATE_HTML)
if (metadata.title) {
$("title").text(metadata.title)
$('meta[property="og:title"]').attr("content", metadata.title)
$('meta[name="twitter:title"]').attr("content", metadata.title)
}
if (metadata.description) {
$('meta[name="description"]').attr("content", metadata.description)
$('meta[property="og:description"]').attr("content", metadata.description)
$('meta[name="twitter:description"]').attr("content", metadata.description)
}
if (metadata.image) {
$('meta[property="og:image"]').attr("content", metadata.image)
$('meta[name="twitter:image"]').attr("content", metadata.image)
}
if (metadata.url) {
$('meta[property="og:url"]').attr("content", metadata.url)
$('meta[name="twitter:site"]').attr("content", metadata.site)
$('meta[name="twitter:url"]').attr("content", metadata.url)
$('link[rel="canonical"]').attr("href", metadata.url)
}
return $.html()
}
const app = new Hono()
// Only allow GET and HEAD requests
app.use("*", async (context, next) => {
const method = context.req.method
if (method !== "GET" && method !== "HEAD") {
return context.text("Method Not Allowed", 405, {Allow: "GET, HEAD"})
}
await next()
})
// Serve static assets with appropriate caching
app.use(
"*",
serveStatic({
root: BUILD_DIR,
onFound: (filePath, context) => {
const isImmutable = filePath.split(path.sep).join("/").includes("/_app/immutable/")
const cacheControl =
path.basename(filePath) === "index.html"
? "no-cache"
: isImmutable
? "public, max-age=31536000, immutable"
: "public, max-age=3600"
context.header("Cache-Control", cacheControl)
// Immutable assets are content-hashed by Vite, so the filename is itself a
// stable content identifier. Exposing it as an ETag lets clients that
// revalidate explicitly (e.g. emoji-picker-element checks its data source
// on every load) skip re-downloading large files when nothing changed.
if (isImmutable) {
context.header("ETag", `"${path.basename(filePath)}"`)
}
},
}),
)
// SPA fallback for routes that don't match static files
app.get("*", async context => {
const requestUrl = requestUrlFromContext(context)
// If the path has an extension, it's likely a missing static asset, not an SPA route
if (path.extname(requestUrl.pathname)) {
return context.text("Not found", 404)
}
const metadata = await getMetadataForRoute(requestUrl)
const html = metadata ? injectMeta(metadata) : TEMPLATE_HTML
return context.html(html, 200, {
"Cache-Control": metadata ? "no-store" : "no-cache",
})
})
serve(
{
fetch: app.fetch,
hostname: HOST,
port: PORT,
},
() => {
console.log(`Flotilla server running on http://${HOST}:${PORT}`)
},
)
-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"
]
}
}
+14 -1
View File
@@ -235,6 +235,7 @@
:root { :root {
font-family: Lato; font-family: Lato;
text-size-adjust: 100%;
--sait: var(--safe-area-inset-top, env(safe-area-inset-top)); --sait: var(--safe-area-inset-top, env(safe-area-inset-top));
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom)); --saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
--sail: var(--safe-area-inset-left, env(safe-area-inset-left)); --sail: var(--safe-area-inset-left, env(safe-area-inset-left));
@@ -332,7 +333,7 @@
.input-editor .tiptap { .input-editor .tiptap {
--tiptap-object-bg: var(--color-base-200); --tiptap-object-bg: var(--color-base-200);
@apply input h-auto p-[.65rem]; @apply input block h-auto p-[.65rem];
} }
/* link-content, based on tiptap */ /* link-content, based on tiptap */
@@ -416,12 +417,24 @@ progress[value]::-webkit-progress-value {
@apply md:left-[calc(18.5rem+var(--sail))]; @apply md:left-[calc(18.5rem+var(--sail))];
} }
.left-content-full {
@apply md:left-[calc(3.5rem+var(--sail))];
}
/* Keyboard open state adjustments */ /* Keyboard open state adjustments */
body.keyboard-open {
--saib: 0px;
}
body.keyboard-open .hide-on-keyboard { body.keyboard-open .hide-on-keyboard {
display: none; display: none;
} }
body.keyboard-open .chat__compose {
margin-bottom: 0;
}
/* chat view */ /* chat view */
.chat__compose { .chat__compose {
+7 -4
View File
@@ -2,15 +2,18 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>{NAME}</title>
<link rel="canonical" href="{URL}" />
<meta <meta
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" /> content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />
<meta name="theme-color" content="{ACCENT}" /> <meta name="theme-color" content="{ACCENT}" />
<meta name="description" content="{DESCRIPTION}" /> <meta name="description" content="{DESCRIPTION}" />
<meta name="og:url" content="{URL}" /> <meta property="og:url" content="{URL}" />
<meta name="og:type" content="website" /> <meta property="og:type" content="website" />
<meta name="og:title" content="{NAME}" /> <meta property="og:title" content="{NAME}" />
<meta name="og:description" content="{DESCRIPTION}" /> <meta property="og:description" content="{DESCRIPTION}" />
<meta property="og:image" content="" />
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="{URL}" /> <meta name="twitter:site" content="{URL}" />
<meta name="twitter:title" content="{NAME}" /> <meta name="twitter:title" content="{NAME}" />
+18 -3
View File
@@ -6,11 +6,13 @@ export type VoiceSession = {
url: string url: string
h: string h: string
room: LiveKitRoom room: LiveKitRoom
muted: boolean
cameraOn: boolean cameraOn: boolean
screenShareOn: boolean screenShareOn: boolean
} }
/** Mic mute state is separate so toggling it does not re-render video tiles. */
export const voiceMicMuted = writable(true)
export type Pubkey = string export type Pubkey = string
export type VoiceParticipant = {pubkey?: Pubkey; identity: string} export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
@@ -27,8 +29,6 @@ export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
export const currentVoiceRoom = writable<Room | undefined>(undefined) export const currentVoiceRoom = writable<Room | undefined>(undefined)
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined => export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined =>
/^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined /^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined
@@ -41,6 +41,21 @@ export const participantKey = (p: VoiceParticipant) => p.pubkey ?? p.identity
export const speakingParticipants = writable<VoiceParticipant[]>([]) export const speakingParticipants = writable<VoiceParticipant[]>([])
export const participantMediaState = writable(
new Map<string, {muted: boolean; cameraOn: boolean}>(),
)
export const mediaStateByIdentity = derived(
[participantMediaState, currentVoiceSession, voiceMicMuted],
([$media, $session, $micMuted]) =>
(identity: string) => {
if ($session?.room.localParticipant.identity === identity) {
return {muted: $micMuted, cameraOn: $session.cameraOn}
}
return $media.get(identity) ?? {muted: true, cameraOn: false}
},
)
export const isParticipantSpeaking = derived( export const isParticipantSpeaking = derived(
speakingParticipants, speakingParticipants,
$participants => (p: VoiceParticipant) => $participants => (p: VoiceParticipant) =>
+131 -43
View File
@@ -6,26 +6,28 @@ import {
DisconnectReason, DisconnectReason,
LocalParticipant, LocalParticipant,
LocalTrackPublication, LocalTrackPublication,
Participant,
Room as LiveKitRoom, Room as LiveKitRoom,
RoomEvent, RoomEvent,
Track, Track,
TrackPublication,
supportsAudioOutputSelection, supportsAudioOutputSelection,
type AudioCaptureOptions, type AudioCaptureOptions,
} from "livekit-client" } from "livekit-client"
import {derived, get} from "svelte/store" import {derived, get} from "svelte/store"
import {map, removeUndefined, uniqBy} from "@welshman/lib" import {map, not, nthEq, reject, removeUndefined, uniqBy} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util" import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
import {signer} from "@welshman/app" import {signer} from "@welshman/app"
import {getLivekitEndpoint} from "$lib/livekit" import {getLivekitEndpoint} from "$lib/livekit"
import {AbortError, whenAborted, whenTimeout} from "$lib/util" import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util"
import { import {
currentVoiceRoom, currentVoiceRoom,
currentVoiceSession, currentVoiceSession,
voiceMicMuted,
participantFromLiveKitIdentity, participantFromLiveKitIdentity,
participantKey, participantKey,
participantPubkeyMap, participantMediaState,
pubkeyFromLiveKitIdentity,
speakingParticipants, speakingParticipants,
VoiceState, VoiceState,
type VoiceParticipant, type VoiceParticipant,
@@ -75,20 +77,23 @@ export const switchVoiceActiveDevice = async (
} }
} }
const addParticipant = (identity: string) => { const deleteParticipant = (identity: string) => {
participantPubkeyMap.update(m => { participantMediaState.update(m => new Map(reject(nthEq(0, identity), [...m])))
}
const syncParticipantMedia = (participant: Participant) => {
const state = {muted: !participant.isMicrophoneEnabled, cameraOn: participant.isCameraEnabled}
participantMediaState.update(m => {
const prev = m.get(participant.identity)
if (prev?.muted === state.muted && prev?.cameraOn === state.cameraOn) return m
const next = new Map(m) const next = new Map(m)
next.set(identity, pubkeyFromLiveKitIdentity(identity) ?? "") next.set(participant.identity, state)
return next return next
}) })
} }
const deleteParticipant = (identity: string) => { const onParticipantMediaChanged = (_publication: TrackPublication, participant: Participant) => {
participantPubkeyMap.update(m => { syncParticipantMedia(participant)
const next = new Map(m)
next.delete(identity)
return next
})
} }
const fetchLivekitToken = async ( const fetchLivekitToken = async (
@@ -124,15 +129,15 @@ export const deriveVoiceParticipants = (url: string, h: string) =>
// We use the livekit identity list while in a call, and fall back to the list in kind 39004. // We use the livekit identity list while in a call, and fall back to the list in kind 39004.
derived( derived(
[ [
participantPubkeyMap, participantMediaState,
currentVoiceRoom, currentVoiceRoom,
deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]), deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
], ],
([$participantPubkeyMap, $currentVoiceRoom, $publishedParticipantList]) => { ([$participantMediaState, $currentVoiceRoom, $publishedParticipantList]) => {
const inCall = $participantPubkeyMap.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h) const inCall = $participantMediaState.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
if (inCall) { if (inCall) {
const participants = [...$participantPubkeyMap.keys()].map(participantFromLiveKitIdentity) const participants = [...$participantMediaState.keys()].map(participantFromLiveKitIdentity)
return uniqBy((p: VoiceParticipant) => participantKey(p), participants) return uniqBy((p: VoiceParticipant) => participantKey(p), participants)
} else { } else {
const latestEvent = $publishedParticipantList as TrustedEvent | undefined const latestEvent = $publishedParticipantList as TrustedEvent | undefined
@@ -152,6 +157,8 @@ const setUpMicrophone = async (
startMuted: boolean, startMuted: boolean,
preferredMicId: string | undefined, preferredMicId: string | undefined,
participant: LocalParticipant, participant: LocalParticipant,
signal?: AbortSignal,
settleSignal?: AbortSignal,
): Promise<boolean> => { ): Promise<boolean> => {
if (startMuted) { if (startMuted) {
return true return true
@@ -163,16 +170,36 @@ const setUpMicrophone = async (
capture = {deviceId: preferredMicId} capture = {deviceId: preferredMicId}
} }
try { try {
await participant.setMicrophoneEnabled(true, capture) await Promise.race([
participant.setMicrophoneEnabled(true, capture),
whenTimeout(15_000, {message: "Microphone access timed out.", signal: settleSignal}),
whenAborted(signal),
])
muted = false muted = false
} catch (e) { } catch (e) {
pushToast({theme: "error", message: "Could not access microphone"}) // Timeout or microphone rejection: join muted, the call is still usable. A
// genuine abort is surfaced to the caller so it can tear down the room.
if (e instanceof AbortError) throw e
if (!(e instanceof TimeoutError)) {
pushToast({theme: "error", message: "Could not access microphone"})
}
} }
return muted return muted
} }
const onRoomDisconnected = (reason?: DisconnectReason) => { // The room whose events are allowed to mutate shared state. Abandoned rooms
// (after switching calls or an engine reconnect give-up) must not clobber it.
let activeRoom: LiveKitRoom | undefined
const makeOnRoomDisconnected = (room: LiveKitRoom) => (reason?: DisconnectReason) => {
// Ignore disconnects from rooms that are no longer the active session.
if (room !== activeRoom) return
activeRoom = undefined
room.removeAllListeners()
videoPrimaryTileKey.set(undefined) videoPrimaryTileKey.set(undefined)
voiceMicMuted.set(true)
currentVoiceSession.set(undefined) currentVoiceSession.set(undefined)
resetVideoCallLayout() resetVideoCallLayout()
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) { if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
@@ -184,7 +211,7 @@ const onRoomDisconnected = (reason?: DisconnectReason) => {
pushToast({theme: "error", message}) pushToast({theme: "error", message})
} }
speakingParticipants.set([]) speakingParticipants.set([])
participantPubkeyMap.set(new Map()) participantMediaState.set(new Map())
} }
const onTrackSubscribed = (track: Track) => { const onTrackSubscribed = (track: Track) => {
@@ -214,8 +241,8 @@ const playJoinSound = () => {
audio.play().catch(() => {}) audio.play().catch(() => {})
} }
const onParticipantConnected = (participant: {identity: string}) => { const onParticipantConnected = (participant: Participant) => {
addParticipant(participant.identity) syncParticipantMedia(participant)
playJoinSound() playJoinSound()
} }
@@ -248,9 +275,6 @@ export const joinVoiceRoom = async (
): Promise<void> => { ): Promise<void> => {
cancelJoinVoiceRoom() cancelJoinVoiceRoom()
const session = get(currentVoiceSession)
if (session) await leaveVoiceRoom()
currentVoiceRoom.set(get(deriveRoom(url, h))) currentVoiceRoom.set(get(deriveRoom(url, h)))
voiceState.set(VoiceState.Joining) voiceState.set(VoiceState.Joining)
@@ -259,47 +283,101 @@ export const joinVoiceRoom = async (
const signal = controller.signal const signal = controller.signal
const isActive = () => joinAbortController === controller const isActive = () => joinAbortController === controller
// Self-cleaning controller: aborted in finally so whenTimeout/whenAborted
// helpers clear their timers/listeners once the races below have settled.
const settle = new AbortController()
try { try {
const {server_url, participant_token} = await fetchLivekitToken(url, h, signal) // Tear down any existing session before joining. Bound it so a slow leave
// (camera/screenshare renegotiation can take ~15s) cannot block this join.
if (get(currentVoiceSession)) {
await Promise.race([
leaveVoiceRoom(),
whenTimeout(15_000, {message: "Leaving previous call timed out.", signal: settle.signal}),
whenAborted(signal),
]).catch(e => {
if (e instanceof AbortError) throw e
})
// leaveVoiceRoom flips voiceState to Disconnected; re-assert Joining.
voiceState.set(VoiceState.Joining)
}
if (signal.aborted) throw new AbortError()
const {server_url, participant_token} = await Promise.race([
fetchLivekitToken(url, h, signal),
whenTimeout(15_000, {
message: "Connection timed out. Please check your network and try again.",
signal: settle.signal,
}),
whenAborted(signal),
])
if (signal.aborted) throw new AbortError() if (signal.aborted) throw new AbortError()
const liveKitRoom = new LiveKitRoom({adaptiveStream: true, dynacast: true}) const liveKitRoom = new LiveKitRoom({adaptiveStream: true, dynacast: true})
activeRoom = liveKitRoom
liveKitRoom.on(RoomEvent.Disconnected, onRoomDisconnected) liveKitRoom.on(RoomEvent.Disconnected, makeOnRoomDisconnected(liveKitRoom))
liveKitRoom.on(RoomEvent.ParticipantConnected, onParticipantConnected) liveKitRoom.on(RoomEvent.ParticipantConnected, onParticipantConnected)
liveKitRoom.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected) liveKitRoom.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected)
liveKitRoom.on(RoomEvent.TrackSubscribed, onTrackSubscribed) liveKitRoom.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed) liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
liveKitRoom.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished) liveKitRoom.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged) liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
liveKitRoom.on(RoomEvent.TrackMuted, onParticipantMediaChanged)
liveKitRoom.on(RoomEvent.TrackUnmuted, onParticipantMediaChanged)
liveKitRoom.on(RoomEvent.TrackPublished, onParticipantMediaChanged)
liveKitRoom.on(RoomEvent.TrackUnpublished, onParticipantMediaChanged)
liveKitRoom.on(RoomEvent.LocalTrackPublished, onParticipantMediaChanged)
try { try {
await Promise.race([ await Promise.race([
liveKitRoom.connect(server_url, participant_token, {maxRetries: 0}), liveKitRoom.connect(server_url, participant_token, {maxRetries: 0}),
whenTimeout(15_000, { whenTimeout(15_000, {
message: "Connection timed out. Please check your network and try again.", message: "Connection timed out. Please check your network and try again.",
signal: settle.signal,
}), }),
whenAborted(signal), whenAborted(signal),
]) ])
} catch (e) { } catch (e) {
if (activeRoom === liveKitRoom) activeRoom = undefined
liveKitRoom.removeAllListeners()
liveKitRoom.disconnect() liveKitRoom.disconnect()
throw e throw e
} }
participantPubkeyMap.set(new Map()) participantMediaState.set(new Map())
addParticipant(liveKitRoom.localParticipant.identity) syncParticipantMedia(liveKitRoom.localParticipant)
for (const p of liveKitRoom.remoteParticipants.values()) { for (const p of liveKitRoom.remoteParticipants.values()) {
addParticipant(p.identity) syncParticipantMedia(p)
} }
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant) // Bounded against timeout/abort inside setUpMicrophone: a stuck permission
// prompt resolves to muted rather than hanging the join forever.
const muted = await setUpMicrophone(
startMuted,
preferredMicId,
liveKitRoom.localParticipant,
signal,
settle.signal,
)
// A cancel during the mic step must tear down the connected room rather
// than leaking it.
if (signal.aborted) {
if (activeRoom === liveKitRoom) activeRoom = undefined
liveKitRoom.removeAllListeners()
liveKitRoom.disconnect()
throw new AbortError()
}
voiceMicMuted.set(muted)
currentVoiceSession.set({ currentVoiceSession.set({
url, url,
h, h,
room: liveKitRoom, room: liveKitRoom,
muted,
cameraOn: false, cameraOn: false,
screenShareOn: false, screenShareOn: false,
}) })
@@ -310,6 +388,7 @@ export const joinVoiceRoom = async (
if (e instanceof AbortError) return if (e instanceof AbortError) return
throw e throw e
} finally { } finally {
settle.abort()
if (isActive()) joinAbortController = undefined if (isActive()) joinAbortController = undefined
} }
} }
@@ -337,13 +416,23 @@ export const leaveVoiceRoom = async () => {
} }
} }
voiceState.set(VoiceState.Disconnected) // Always tear down this room's connection and listeners.
videoPrimaryTileKey.set(undefined) if (activeRoom === session.room) activeRoom = undefined
currentVoiceSession.set(undefined) session.room.removeAllListeners()
resetVideoCallLayout()
session.room.disconnect() session.room.disconnect()
speakingParticipants.set([])
participantPubkeyMap.set(new Map()) // Only reset shared UI state if this session is still current. A slow leave
// that was superseded by a new join (bounded by a timeout in joinVoiceRoom)
// must not clobber the freshly-joined session when it finally completes.
if (get(currentVoiceSession) === session) {
voiceState.set(VoiceState.Disconnected)
videoPrimaryTileKey.set(undefined)
voiceMicMuted.set(true)
currentVoiceSession.set(undefined)
resetVideoCallLayout()
speakingParticipants.set([])
participantMediaState.set(new Map())
}
} }
export const rejoinVoiceRoom = async (): Promise<void> => { export const rejoinVoiceRoom = async (): Promise<void> => {
@@ -356,18 +445,17 @@ export const toggleMute = async () => {
const session = get(currentVoiceSession) const session = get(currentVoiceSession)
if (!session) return if (!session) return
const muted = !session.muted voiceMicMuted.update(not)
if (muted) { if (get(voiceMicMuted)) {
// Disable and re-enable microphone to trigger permission prompt // Disable and re-enable microphone to trigger permission prompt
session.room.localParticipant.setMicrophoneEnabled(false) session.room.localParticipant.setMicrophoneEnabled(false)
currentVoiceSession.set({...session, muted})
return return
} }
try { try {
await session.room.localParticipant.setMicrophoneEnabled(true) await session.room.localParticipant.setMicrophoneEnabled(true)
currentVoiceSession.set({...session, muted})
} catch (e) { } catch (e) {
voiceMicMuted.set(true)
pushToast({theme: "error", message: "Could not access microphone"}) pushToast({theme: "error", message: "Could not access microphone"})
} }
} }
@@ -19,15 +19,17 @@
const end = $derived(parseInt(meta.end)) const end = $derived(parseInt(meta.end))
</script> </script>
<div class="flex grow flex-wrap justify-between gap-2"> <div class="flex flex-col justify-between gap-1">
<p class="text-xl">{meta.title || meta.name}</p> <p class="text-lg">{meta.title || meta.name}</p>
{#if !isNaN(start) && !isNaN(end)} {#if !isNaN(start) && !isNaN(end)}
{@const startDateDisplay = formatTimestampAsDate(start)} {@const startDateDisplay = formatTimestampAsDate(start)}
{@const endDateDisplay = formatTimestampAsDate(end)} {@const endDateDisplay = formatTimestampAsDate(end)}
{@const isSingleDay = startDateDisplay === endDateDisplay} {@const isSingleDay = startDateDisplay === endDateDisplay}
<div class="flex items-center gap-2 text-sm"> <div class="flex flex-wrap gap-2 text-xs">
<Icon icon={ClockCircle} size={4} /> <div class="flex items-center gap-2">
<span class="hidden sm:block">{formatTimestampAsDate(start)}</span> <Icon icon={ClockCircle} size={4} />
{formatTimestampAsDate(start)}
</div>
{formatTimestampAsTime(start)}{isSingleDay {formatTimestampAsTime(start)}{isSingleDay
? formatTimestampAsTime(end) ? formatTimestampAsTime(end)
: formatTimestamp(end)} : formatTimestamp(end)}
+4 -3
View File
@@ -53,7 +53,7 @@
import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte" import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte"
import ChatComposeParent from "@app/components/ChatComposeParent.svelte" import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte" import ThunkToast from "@app/components/ThunkToast.svelte"
import {userSettingsValues, deriveChat} from "@app/core/state" import {userSettingsValues, deriveChat, makeChatId} from "@app/core/state"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {DraftKey} from "@app/util/drafts" import {DraftKey} from "@app/util/drafts"
import {makeDelete, prependParent} from "@app/core/commands" import {makeDelete, prependParent} from "@app/core/commands"
@@ -66,8 +66,9 @@
const {pubkeys, info}: Props = $props() const {pubkeys, info}: Props = $props()
const chat = deriveChat(pubkeys) const chatId = makeChatId(pubkeys)
const draftKey = new DraftKey<{content?: string | object}>(`dm:${$chat?.id}`) const chat = deriveChat(chatId)
const draftKey = new DraftKey<{content?: string | object}>(`dm:${chatId}`)
const others = remove($pubkey!, pubkeys) const others = remove($pubkey!, pubkeys)
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayListsByPubkey.has(pk))) const missingRelayLists = $derived(others.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
+4
View File
@@ -6,6 +6,7 @@
truncate, truncate,
renderAsHtml, renderAsHtml,
isText, isText,
isEmail,
isEmoji, isEmoji,
isTopic, isTopic,
isCode, isCode,
@@ -26,6 +27,7 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ContentToken from "@app/components/ContentToken.svelte" import ContentToken from "@app/components/ContentToken.svelte"
import ContentEmoji from "@app/components/ContentEmoji.svelte" import ContentEmoji from "@app/components/ContentEmoji.svelte"
import ContentEmail from "@app/components/ContentEmail.svelte"
import ContentCode from "@app/components/ContentCode.svelte" import ContentCode from "@app/components/ContentCode.svelte"
import ContentLinkInline from "@app/components/ContentLinkInline.svelte" import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte" import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
@@ -159,6 +161,8 @@
<ContentTopic value={parsed.value} /> <ContentTopic value={parsed.value} />
{:else if isEmoji(parsed)} {:else if isEmoji(parsed)}
<ContentEmoji value={parsed.value} /> <ContentEmoji value={parsed.value} />
{:else if isEmail(parsed)}
<ContentEmail value={parsed.value} />
{:else if isCode(parsed)} {:else if isCode(parsed)}
<ContentCode <ContentCode
value={parsed.value} value={parsed.value}
+12
View File
@@ -0,0 +1,12 @@
<script lang="ts">
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
export let value: string
</script>
<Link external href="mailto:{value}">
<Icon icon={LinkRound} size={3} />
{value}
</Link>
+4
View File
@@ -7,6 +7,7 @@
renderAsHtml, renderAsHtml,
isText, isText,
isEmoji, isEmoji,
isEmail,
isTopic, isTopic,
isCode, isCode,
isCashu, isCashu,
@@ -24,6 +25,7 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ContentToken from "@app/components/ContentToken.svelte" import ContentToken from "@app/components/ContentToken.svelte"
import ContentEmoji from "@app/components/ContentEmoji.svelte" import ContentEmoji from "@app/components/ContentEmoji.svelte"
import ContentEmail from "@app/components/ContentEmail.svelte"
import ContentCode from "@app/components/ContentCode.svelte" import ContentCode from "@app/components/ContentCode.svelte"
import ContentLinkInline from "@app/components/ContentLinkInline.svelte" import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
import ContentNewline from "@app/components/ContentNewline.svelte" import ContentNewline from "@app/components/ContentNewline.svelte"
@@ -109,6 +111,8 @@
<ContentTopic value={parsed.value} /> <ContentTopic value={parsed.value} />
{:else if isEmoji(parsed)} {:else if isEmoji(parsed)}
<ContentEmoji value={parsed.value} /> <ContentEmoji value={parsed.value} />
{:else if isEmail(parsed)}
<ContentEmail value={parsed.value} />
{:else if isCode(parsed)} {:else if isCode(parsed)}
<ContentCode <ContentCode
value={parsed.value} value={parsed.value}
+3 -2
View File
@@ -42,7 +42,7 @@
let popover: Instance | undefined = $state() let popover: Instance | undefined = $state()
</script> </script>
<Button class="join rounded-full"> <div class="join items-center rounded-full">
{#if ENABLE_ZAPS && !hideZap} {#if ENABLE_ZAPS && !hideZap}
<ZapButton {url} {event} class="btn join-item btn-neutral btn-xs"> <ZapButton {url} {event} class="btn join-item btn-neutral btn-xs">
<Icon icon={Bolt} size={4} /> <Icon icon={Bolt} size={4} />
@@ -52,6 +52,7 @@
<Icon icon={SmileCircle} size={4} /> <Icon icon={SmileCircle} size={4} />
</EmojiButton> </EmojiButton>
<Tippy <Tippy
class="flex"
bind:popover bind:popover
component={EventMenu} component={EventMenu}
props={{url, noun, event, customActions, onClick: hidePopover}} props={{url, noun, event, customActions, onClick: hidePopover}}
@@ -60,4 +61,4 @@
<Icon icon={MenuDots} size={4} /> <Icon icon={MenuDots} size={4} />
</Button> </Button>
</Tippy> </Tippy>
</Button> </div>
+7 -1
View File
@@ -15,6 +15,7 @@
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 ProgressBar from "@app/components/ProgressBar.svelte"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {PLATFORM_NAME} from "@app/core/state" import {PLATFORM_NAME} from "@app/core/state"
@@ -22,9 +23,11 @@
secret: string secret: string
next: () => unknown next: () => unknown
submitText?: string submitText?: string
step?: number
totalSteps?: number
} }
const {secret, next, submitText = "Continue"}: Props = $props() const {secret, next, submitText = "Continue", step, totalSteps}: Props = $props()
const back = () => history.back() const back = () => history.back()
@@ -150,6 +153,9 @@
</Button> </Button>
</div> </div>
</ModalBody> </ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
+11 -3
View File
@@ -19,6 +19,7 @@
import LogInOTP from "@app/components/LogInOTP.svelte" import LogInOTP from "@app/components/LogInOTP.svelte"
import LogInSelect from "@app/components/LogInSelect.svelte" import LogInSelect from "@app/components/LogInSelect.svelte"
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade" import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
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"
@@ -44,7 +45,7 @@
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: "Sorry, we were unable to log you in.", message: getPomadeLoginFailureMessage(messages),
}) })
} }
@@ -64,10 +65,17 @@
pushToast({ pushToast({
theme: "error", theme: "error",
message: "Sorry, we were unable to log you in.", message: getPomadeLoginFailureMessage(res.messages),
}) })
} }
} }
} catch (error) {
console.error("Login error:", error)
pushToast({
theme: "error",
message: POMADE_NETWORK_ERROR_MESSAGE,
})
} finally { } finally {
loading = false loading = false
} }
@@ -90,7 +98,7 @@
{#snippet input()} {#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Letter} /> <Icon icon={Letter} />
<input bind:value={email} /> <input type="email" bind:value={email} />
</label> </label>
{/snippet} {/snippet}
</FieldInline> </FieldInline>
+12 -2
View File
@@ -15,6 +15,7 @@
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 LogInOTPConfirm from "@app/components/LogInOTPConfirm.svelte" import LogInOTPConfirm from "@app/components/LogInOTPConfirm.svelte"
import {POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
@@ -35,11 +36,20 @@
if (ok) { if (ok) {
pushModal(LogInOTPConfirm, {email, peersByPrefix}) pushModal(LogInOTPConfirm, {email, peersByPrefix})
} else { } else {
console.error("Pomade challenge request failed during OTP login")
pushToast({ pushToast({
theme: "error", theme: "error",
message: "Sorry, we were unable to request a login code.", message: POMADE_NETWORK_ERROR_MESSAGE,
}) })
} }
} catch (error) {
console.error(error)
pushToast({
theme: "error",
message: POMADE_NETWORK_ERROR_MESSAGE,
})
} finally { } finally {
loading = false loading = false
} }
@@ -61,7 +71,7 @@
{#snippet input()} {#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Letter} /> <Icon icon={Letter} />
<input bind:value={email} /> <input type="email" bind:value={email} />
</label> </label>
{/snippet} {/snippet}
</FieldInline> </FieldInline>
+12 -4
View File
@@ -15,10 +15,11 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import StringMultiInput from "@lib/components/StringMultiInput.svelte" import StringMultiInput from "@lib/components/StringMultiInput.svelte"
import LogInSelect from "@app/components/LogInSelect.svelte" import LogInSelect from "@app/components/LogInSelect.svelte"
import {pushToast} from "@app/util/toast"
import {setChecked} from "@app/util/notifications"
import {pushModal, clearModals} from "@app/util/modal" import {pushModal, clearModals} from "@app/util/modal"
import {setChecked} from "@app/util/notifications"
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade" import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
import {pushToast} from "@app/util/toast"
type Props = { type Props = {
email: string email: string
@@ -44,7 +45,7 @@
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: "Sorry, we were unable to log you in.", message: getPomadeLoginFailureMessage(messages),
}) })
} }
@@ -64,10 +65,17 @@
pushToast({ pushToast({
theme: "error", theme: "error",
message: "Sorry, we were unable to log you in.", message: getPomadeLoginFailureMessage(res.messages),
}) })
} }
} }
} catch (error) {
console.error("Login error:", error)
pushToast({
theme: "error",
message: POMADE_NETWORK_ERROR_MESSAGE,
})
} finally { } finally {
loading = false loading = false
} }
+9 -1
View File
@@ -14,6 +14,7 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import Profile from "@app/components/Profile.svelte" import Profile from "@app/components/Profile.svelte"
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade" import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
import {clearModals} from "@app/util/modal" import {clearModals} from "@app/util/modal"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
@@ -46,9 +47,16 @@
pushToast({ pushToast({
theme: "error", theme: "error",
message: "Sorry, we were unable to log you in.", message: getPomadeLoginFailureMessage(res.messages),
}) })
} }
} catch (error) {
console.error("Login error:", error)
pushToast({
theme: "error",
message: POMADE_NETWORK_ERROR_MESSAGE,
})
} finally { } finally {
loading = false loading = false
} }
@@ -13,13 +13,16 @@
const onClick = () => goToSpace(url) const onClick = () => goToSpace(url)
const path = makeSpacePath(url)
const display = $derived(deriveRelayDisplay(url)) const display = $derived(deriveRelayDisplay(url))
</script> </script>
<PrimaryNavItem <PrimaryNavItem
href={path}
onclick={onClick} onclick={onClick}
title={$display} title={$display}
class="tooltip-right" class="tooltip-right"
notification={$notifications.has(makeSpacePath(url))}> notification={$notifications.has(path)}>
<RelayIcon {url} size={10} class="rounded-full" /> <RelayIcon {url} size={10} class="rounded-full" />
</PrimaryNavItem> </PrimaryNavItem>
+5 -1
View File
@@ -23,9 +23,10 @@
onsubmit: (values: Values) => void onsubmit: (values: Values) => void
isSignup?: boolean isSignup?: boolean
footer: Snippet footer: Snippet
progressBar?: Snippet
} }
const {initialValues, isSignup, onsubmit, footer}: Props = $props() const {initialValues, isSignup, onsubmit, footer, progressBar}: Props = $props()
const values = $state(initialValues) const values = $state(initialValues)
@@ -103,6 +104,9 @@
</Field> </Field>
{/if} {/if}
</ModalBody> </ModalBody>
{#if progressBar}
{@render progressBar()}
{/if}
<ModalFooter> <ModalFooter>
{@render footer()} {@render footer()}
</ModalFooter> </ModalFooter>
+9
View File
@@ -0,0 +1,9 @@
<script lang="ts">
const {current, total}: {current: number; total: number} = $props()
</script>
<div class="flex w-full">
{#each Array(total) as _, i}
<div class="h-1 flex-1 transition-colors {i < current ? 'bg-primary' : 'bg-base-300'}"></div>
{/each}
</div>
+5 -7
View File
@@ -19,12 +19,12 @@
<div class="col-4 text-left"> <div class="col-4 text-left">
<div class="col-2"> <div class="col-2">
<div class="relative flex gap-4"> <div class="relative flex gap-2 sm:gap-4">
<div class="relative"> <div class="relative">
<div class="avatar relative"> <div class="avatar relative">
<div <div
class="center flex! h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300"> class="center flex! h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300">
<RelayIcon {url} /> <RelayIcon {url} size={10} />
</div> </div>
</div> </div>
{#if $rooms.includes(url)} {#if $rooms.includes(url)}
@@ -36,13 +36,11 @@
{/if} {/if}
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<h2 class="ellipsize whitespace-nowrap text-xl"> <RelayName {url} class="ellipsize whitespace-nowrap text-lg sm:text-xl" />
<RelayName {url} /> <p class="text-xs sm:text-sm opacity-75">{url}</p>
</h2>
<p class="text-sm opacity-75">{url}</p>
</div> </div>
</div> </div>
<RelayDescription {url} /> <RelayDescription {url} class="text-sm sm:text-md" />
</div> </div>
{#if !hideFavorites && $favorited.size > 0} {#if !hideFavorites && $favorited.size > 0}
<div class="row-2 card2 card2-sm bg-alt"> <div class="row-2 card2 card2-sm bg-alt">
-2
View File
@@ -68,8 +68,6 @@
const content = ed.getText({blockSeparator: "\n"}).trim() const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = ed.storage.nostr.getEditorTags() const tags = ed.storage.nostr.getEditorTags()
if (!content) return
onSubmit({content, tags}) onSubmit({content, tags})
draftKey?.clear() draftKey?.clear()
+1
View File
@@ -122,6 +122,7 @@
repository.removeEvent(thunk.event.id) repository.removeEvent(thunk.event.id)
pushToast({theme: "error", message}) pushToast({theme: "error", message})
} else { } else {
await removeRoomMembership(url, h)
goto(makeSpacePath(url)) goto(makeSpacePath(url))
} }
}, },
+10 -11
View File
@@ -11,7 +11,6 @@
import Modal from "@lib/components/Modal.svelte" import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte" import ModalBody from "@lib/components/ModalBody.svelte"
import LogIn from "@app/components/LogIn.svelte" import LogIn from "@app/components/LogIn.svelte"
import InfoNostr from "@app/components/InfoNostr.svelte"
import SignUpKey from "@app/components/SignUpKey.svelte" import SignUpKey from "@app/components/SignUpKey.svelte"
import SignUpEmail from "@app/components/SignUpEmail.svelte" import SignUpEmail from "@app/components/SignUpEmail.svelte"
import SignUpProfile from "@app/components/SignUpProfile.svelte" import SignUpProfile from "@app/components/SignUpProfile.svelte"
@@ -62,9 +61,10 @@
const flows = { const flows = {
email: { email: {
start: () => pushModal(SignUpEmail, {next: flows.email.profile}), start: () => pushModal(SignUpEmail, {next: flows.email.profile, step: 1, totalSteps: 3}),
profile: () => pushModal(SignUpProfile, {next: flows.email.complete}), profile: () => pushModal(SignUpProfile, {next: flows.email.complete, step: 2, totalSteps: 3}),
complete: () => pushModal(SignUpComplete, {next: flows.email.finalize}), complete: () =>
pushModal(SignUpComplete, {next: flows.email.finalize, step: 3, totalSteps: 3}),
finalize: () => { finalize: () => {
const email = getKey<string>("signup.email")! const email = getKey<string>("signup.email")!
const clientOptions = getKey<ClientOptions>("signup.clientOptions")! const clientOptions = getKey<ClientOptions>("signup.clientOptions")!
@@ -74,9 +74,10 @@
}, },
}, },
nostr: { nostr: {
start: () => pushModal(SignUpProfile, {next: flows.nostr.key}), start: () => pushModal(SignUpProfile, {next: flows.nostr.key, step: 1, totalSteps: 3}),
key: () => pushModal(SignUpKey, {next: flows.nostr.complete}), key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}),
complete: () => pushModal(SignUpComplete, {next: flows.nostr.finalize}), complete: () =>
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
finalize: () => { finalize: () => {
const secret = getKey<string>("signup.secret")! const secret = getKey<string>("signup.secret")!
@@ -89,11 +90,9 @@
<Modal> <Modal>
<ModalBody> <ModalBody>
<h1 class="heading">Sign up with Nostr</h1> <h1 class="heading">Join {PLATFORM_NAME}</h1>
<p class="m-auto max-w-sm text-center"> <p class="m-auto max-w-sm text-center">
{PLATFORM_NAME} is built using the Censorship resistant digital spaces for communities. Meet new people, own your identity.
<Button class="link" onclick={() => pushModal(InfoNostr)}>nostr protocol</Button>, which gives
users control over their digital identity using <strong>cryptographic key pairs</strong>.
</p> </p>
{#if hasPomade} {#if hasPomade}
<Button onclick={flows.email.start} class="btn btn-primary"> <Button onclick={flows.email.start} class="btn btn-primary">
+7 -1
View File
@@ -9,12 +9,15 @@
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 ProgressBar from "@app/components/ProgressBar.svelte"
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next}: Props = $props() const {next, step, totalSteps}: Props = $props()
const back = () => history.back() const back = () => history.back()
</script> </script>
@@ -33,6 +36,9 @@
on groups you've already joined. Click below to get started! on groups you've already joined. Click below to get started!
</p> </p>
</ModalBody> </ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
+12 -3
View File
@@ -18,14 +18,17 @@
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 SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte" import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
import {pushToast, popToast} from "@app/util/toast" import {pushToast, popToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next}: Props = $props() const {next, step, totalSteps}: Props = $props()
const back = () => history.back() const back = () => history.back()
@@ -81,7 +84,7 @@
setKey("signup.clientOptions", clientOptions) setKey("signup.clientOptions", clientOptions)
popToast(toastId) popToast(toastId)
pushModal(SignUpEmailConfirm, {next}) pushModal(SignUpEmailConfirm, {next, step, totalSteps})
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -120,7 +123,7 @@
{#snippet input()} {#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Letter} /> <Icon icon={Letter} />
<input bind:value={email} /> <input type="email" bind:value={email} />
</label> </label>
{/snippet} {/snippet}
</FieldInline> </FieldInline>
@@ -134,8 +137,14 @@
<input type="password" bind:value={password} /> <input type="password" bind:value={password} />
</label> </label>
{/snippet} {/snippet}
{#snippet info()}
Must be at least 12 characters long.
{/snippet}
</FieldInline> </FieldInline>
</ModalBody> </ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
+7 -1
View File
@@ -15,12 +15,15 @@
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 ProgressBar from "@app/components/ProgressBar.svelte"
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next}: Props = $props() const {next, step, totalSteps}: Props = $props()
const email = getKey<string>("signup.email") const email = getKey<string>("signup.email")
@@ -61,6 +64,9 @@
above. above.
</p> </p>
</ModalBody> </ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<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} />

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