Compare commits

...

52 Commits

Author SHA1 Message Date
userAdityaa b42ba377e5 fix: deleted rooms persisting in navigation 2026-05-26 13:17:17 +05:30
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
Docker / build-and-push-image (push) Successful in 11m19s
2026-05-12 08:46:50 -07:00
Jon Staab 0998639d59 Push to gitea package registry
Docker / build-and-push-image (push) Successful in 11m31s
2026-05-11 13:49:37 -07:00
Jon Staab eccde07d06 Fix dockerfile again
Docker / build-and-push-image (push) Successful in 12m2s
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
Docker / build-and-push-image (push) Successful in 12m9s
2026-05-11 12:28:42 -07:00
Jon Staab 6ce0fbbbe6 Make recommended ios changes
Docker / build-and-push-image (push) Failing after 56s
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
140 changed files with 1952 additions and 5920 deletions
-1
View File
@@ -4,7 +4,6 @@ ios
build
# Git
.git
.gitignore
# Env files (keep .env for build; exclude local overrides)
@@ -1,12 +1,17 @@
name: Docker
name: Container Image Build and Publish
on:
push:
branches: [master]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: coracle-social/flotilla
REGISTRY: gitea.coracle.social
IMAGE_NAME: coracle/flotilla
jobs:
build-and-push-image:
@@ -23,8 +28,8 @@ jobs:
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
username: hodlbod
password: ${{ secrets.PACKAGE_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
@@ -32,6 +37,7 @@ jobs:
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
- name: Set up Docker Buildx
@@ -45,6 +51,7 @@ jobs:
with:
context: .
push: true
target: production
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+1 -4
View File
@@ -27,13 +27,10 @@ android/app/src/main/assets/public/
node_modules/
.pnpm-store/
build/
build-server/
.svelte-kit/
.next/
# Rust/Tauri
*target/
src-tauri/binaries/
# iOS
ios/App/App/public
ios/DerivedData
+26
View File
@@ -1,5 +1,31 @@
# 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
* Fix safe area inset for FAB
+24 -25
View File
@@ -1,32 +1,31 @@
# Stage 1: Build
# Uses .env from build context for config (logo, branding, etc.)
# Optional: docker build --build-arg VITE_BUILD_HASH=$(git rev-parse --short HEAD) -t flotilla .
# Build and run the Flotilla web server.
#
# 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
RUN apt-get update && apt-get install -y --no-install-recommends curl
RUN npm install -g pnpm@latest
# https://pnpm.io/docker#example-3-build-on-cicd
FROM node:24-slim AS builder
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
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
COPY package.json pnpm-lock.yaml ./
RUN pnpm i --frozen-lockfile
COPY . .
ARG VITE_BUILD_HASH
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
# Copy only the built output - no source, no .env, no dev deps
COPY --from=builder /app/build ./build
CMD ["npx", "serve", "-s", "build"]
COPY --from=builder /app/build /app/build
COPY --from=builder /app/build-server/server.js /app/server.js
EXPOSE 3000
USER node
CMD ["node", "server.js"]
+3 -3
View File
@@ -31,18 +31,18 @@ To run your own Flotilla, it's as simple as:
```sh
pnpm install
pnpm run build
npx serve -s build
pnpm run start
```
Or, if you prefer to use a container:
```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:
```sh
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"
minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion
versionCode 46
versionName "1.7.4"
versionCode 47
versionName "1.8.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// 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')
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;
classes = {
};
objectVersion = 48;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -131,8 +131,9 @@
504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 920;
LastUpgradeCheck = 920;
LastUpgradeCheck = 2630;
TargetAttributes = {
504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2;
@@ -257,6 +258,7 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -264,8 +266,10 @@
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_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_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -275,8 +279,10 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = S26U9DYW3A;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@@ -295,6 +301,7 @@
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
@@ -314,6 +321,7 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -321,8 +329,10 @@
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_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_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -332,8 +342,10 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = S26U9DYW3A;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -345,7 +357,9 @@
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
@@ -358,14 +372,16 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = S26U9DYW3A;
CURRENT_PROJECT_VERSION = 38;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.7.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.8.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -385,14 +401,16 @@
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = S26U9DYW3A;
CURRENT_PROJECT_VERSION = 38;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.7.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.8.0;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
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 '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 '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
target 'Flotilla Chat' do
+25 -24
View File
@@ -1,15 +1,13 @@
{
"name": "flotilla",
"version": "1.7.4",
"version": "1.8.0",
"private": true,
"scripts": {
"dev": "vite dev",
"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",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"tauri:info": "tauri info",
"tauri:icons": "tauri icon assets/logo.png --output src-tauri/icons",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check src && eslint src",
@@ -20,10 +18,9 @@
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@eslint/js": "^9.39.2",
"@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@sveltejs/kit": "^2.61.1",
"@sveltejs/vite-plugin-svelte": "^5.1.1",
"@tailwindcss/postcss": "^4.2.2",
"@tauri-apps/cli": "^2.9.6",
"@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.23",
"classnames": "^2.5.1",
@@ -31,15 +28,15 @@
"eslint-config-prettier": "^9.1.2",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.15.0",
"postcss": "^8.5.6",
"postcss": "^8.5.15",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1",
"svelte": "^5.48.0",
"svelte": "^5.55.9",
"svelte-check": "^4.3.5",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.1",
"vite": "^5.4.21"
"vite": "^6.4.2"
},
"type": "module",
"dependencies": {
@@ -60,32 +57,36 @@
"@capawesome/capacitor-badge": "^8.0.0",
"@getalby/lightning-tools": "^6.1.0",
"@getalby/sdk": "^5.1.2",
"@hono/node-server": "^2.0.0",
"@noble/curves": "^1.9.7",
"@pomade/core": "^0.2.3",
"@poppanator/sveltekit-svg": "^4.2.1",
"@poppanator/sveltekit-svg": "^7.0.0",
"@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.27.2",
"@tiptap/pm": "^2.27.2",
"@types/qrcode": "^1.5.6",
"@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.8.13",
"@welshman/content": "^0.8.13",
"@welshman/editor": "^0.8.13",
"@welshman/feeds": "^0.8.13",
"@welshman/lib": "^0.8.13",
"@welshman/net": "^0.8.13",
"@welshman/router": "^0.8.13",
"@welshman/signer": "^0.8.13",
"@welshman/store": "^0.8.13",
"@welshman/util": "^0.8.13",
"@vite-pwa/assets-generator": "^1.0.2",
"@vite-pwa/sveltekit": "^1.1.0",
"@welshman/app": "^0.8.15",
"@welshman/content": "^0.8.15",
"@welshman/editor": "^0.8.15",
"@welshman/feeds": "^0.8.15",
"@welshman/lib": "^0.8.15",
"@welshman/net": "^0.8.15",
"@welshman/router": "^0.8.15",
"@welshman/signer": "^0.8.15",
"@welshman/store": "^0.8.15",
"@welshman/util": "^0.8.15",
"cheerio": "^1.2.0",
"compressorjs-next": "^1.1.2",
"daisyui": "^5.5.19",
"date-picker-svelte": "^2.17.0",
"dotenv": "^16.6.1",
"emoji-picker-element": "^1.28.1",
"emoji-picker-element-data": "^1.8.0",
"fuse.js": "^7.1.0",
"hono": "^4.12.23",
"husky": "^9.1.7",
"idb": "^8.0.3",
"livekit-client": "^2.17.2",
+656 -563
View File
File diff suppressed because it is too large Load Diff
-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 {
font-family: Lato;
text-size-adjust: 100%;
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
@@ -332,7 +333,7 @@
.input-editor .tiptap {
--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 */
@@ -416,12 +417,24 @@ progress[value]::-webkit-progress-value {
@apply md:left-[calc(18.5rem+var(--sail))];
}
.left-content-full {
@apply md:left-[calc(3.5rem+var(--sail))];
}
/* Keyboard open state adjustments */
body.keyboard-open {
--saib: 0px;
}
body.keyboard-open .hide-on-keyboard {
display: none;
}
body.keyboard-open .chat__compose {
margin-bottom: 0;
}
/* chat view */
.chat__compose {
+7 -4
View File
@@ -2,15 +2,18 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{NAME}</title>
<link rel="canonical" href="{URL}" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />
<meta name="theme-color" content="{ACCENT}" />
<meta name="description" content="{DESCRIPTION}" />
<meta name="og:url" content="{URL}" />
<meta name="og:type" content="website" />
<meta name="og:title" content="{NAME}" />
<meta name="og:description" content="{DESCRIPTION}" />
<meta property="og:url" content="{URL}" />
<meta property="og:type" content="website" />
<meta property="og:title" content="{NAME}" />
<meta property="og:description" content="{DESCRIPTION}" />
<meta property="og:image" content="" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="{URL}" />
<meta name="twitter:title" content="{NAME}" />
+18 -3
View File
@@ -6,11 +6,13 @@ export type VoiceSession = {
url: string
h: string
room: LiveKitRoom
muted: boolean
cameraOn: 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 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 participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
export const pubkeyFromLiveKitIdentity = (identity: string): string | 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 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(
speakingParticipants,
$participants => (p: VoiceParticipant) =>
+39 -28
View File
@@ -6,14 +6,16 @@ import {
DisconnectReason,
LocalParticipant,
LocalTrackPublication,
Participant,
Room as LiveKitRoom,
RoomEvent,
Track,
TrackPublication,
supportsAudioOutputSelection,
type AudioCaptureOptions,
} from "livekit-client"
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 {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
import {signer} from "@welshman/app"
@@ -22,10 +24,10 @@ import {AbortError, whenAborted, whenTimeout} from "$lib/util"
import {
currentVoiceRoom,
currentVoiceSession,
voiceMicMuted,
participantFromLiveKitIdentity,
participantKey,
participantPubkeyMap,
pubkeyFromLiveKitIdentity,
participantMediaState,
speakingParticipants,
VoiceState,
type VoiceParticipant,
@@ -75,20 +77,23 @@ export const switchVoiceActiveDevice = async (
}
}
const addParticipant = (identity: string) => {
participantPubkeyMap.update(m => {
const deleteParticipant = (identity: string) => {
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)
next.set(identity, pubkeyFromLiveKitIdentity(identity) ?? "")
next.set(participant.identity, state)
return next
})
}
const deleteParticipant = (identity: string) => {
participantPubkeyMap.update(m => {
const next = new Map(m)
next.delete(identity)
return next
})
const onParticipantMediaChanged = (_publication: TrackPublication, participant: Participant) => {
syncParticipantMedia(participant)
}
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.
derived(
[
participantPubkeyMap,
participantMediaState,
currentVoiceRoom,
deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
],
([$participantPubkeyMap, $currentVoiceRoom, $publishedParticipantList]) => {
const inCall = $participantPubkeyMap.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
([$participantMediaState, $currentVoiceRoom, $publishedParticipantList]) => {
const inCall = $participantMediaState.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
if (inCall) {
const participants = [...$participantPubkeyMap.keys()].map(participantFromLiveKitIdentity)
const participants = [...$participantMediaState.keys()].map(participantFromLiveKitIdentity)
return uniqBy((p: VoiceParticipant) => participantKey(p), participants)
} else {
const latestEvent = $publishedParticipantList as TrustedEvent | undefined
@@ -173,6 +178,7 @@ const setUpMicrophone = async (
const onRoomDisconnected = (reason?: DisconnectReason) => {
videoPrimaryTileKey.set(undefined)
voiceMicMuted.set(true)
currentVoiceSession.set(undefined)
resetVideoCallLayout()
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
@@ -184,7 +190,7 @@ const onRoomDisconnected = (reason?: DisconnectReason) => {
pushToast({theme: "error", message})
}
speakingParticipants.set([])
participantPubkeyMap.set(new Map())
participantMediaState.set(new Map())
}
const onTrackSubscribed = (track: Track) => {
@@ -214,8 +220,8 @@ const playJoinSound = () => {
audio.play().catch(() => {})
}
const onParticipantConnected = (participant: {identity: string}) => {
addParticipant(participant.identity)
const onParticipantConnected = (participant: Participant) => {
syncParticipantMedia(participant)
playJoinSound()
}
@@ -273,6 +279,11 @@ export const joinVoiceRoom = async (
liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
liveKitRoom.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
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 {
await Promise.race([
@@ -287,19 +298,19 @@ export const joinVoiceRoom = async (
throw e
}
participantPubkeyMap.set(new Map())
addParticipant(liveKitRoom.localParticipant.identity)
participantMediaState.set(new Map())
syncParticipantMedia(liveKitRoom.localParticipant)
for (const p of liveKitRoom.remoteParticipants.values()) {
addParticipant(p.identity)
syncParticipantMedia(p)
}
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
voiceMicMuted.set(muted)
currentVoiceSession.set({
url,
h,
room: liveKitRoom,
muted,
cameraOn: false,
screenShareOn: false,
})
@@ -339,11 +350,12 @@ export const leaveVoiceRoom = async () => {
voiceState.set(VoiceState.Disconnected)
videoPrimaryTileKey.set(undefined)
voiceMicMuted.set(true)
currentVoiceSession.set(undefined)
resetVideoCallLayout()
session.room.disconnect()
speakingParticipants.set([])
participantPubkeyMap.set(new Map())
participantMediaState.set(new Map())
}
export const rejoinVoiceRoom = async (): Promise<void> => {
@@ -356,18 +368,17 @@ export const toggleMute = async () => {
const session = get(currentVoiceSession)
if (!session) return
const muted = !session.muted
if (muted) {
voiceMicMuted.update(not)
if (get(voiceMicMuted)) {
// Disable and re-enable microphone to trigger permission prompt
session.room.localParticipant.setMicrophoneEnabled(false)
currentVoiceSession.set({...session, muted})
return
}
try {
await session.room.localParticipant.setMicrophoneEnabled(true)
currentVoiceSession.set({...session, muted})
} catch (e) {
voiceMicMuted.set(true)
pushToast({theme: "error", message: "Could not access microphone"})
}
}
@@ -19,15 +19,17 @@
const end = $derived(parseInt(meta.end))
</script>
<div class="flex grow flex-wrap justify-between gap-2">
<p class="text-xl">{meta.title || meta.name}</p>
<div class="flex flex-col justify-between gap-1">
<p class="text-lg">{meta.title || meta.name}</p>
{#if !isNaN(start) && !isNaN(end)}
{@const startDateDisplay = formatTimestampAsDate(start)}
{@const endDateDisplay = formatTimestampAsDate(end)}
{@const isSingleDay = startDateDisplay === endDateDisplay}
<div class="flex items-center gap-2 text-sm">
<Icon icon={ClockCircle} size={4} />
<span class="hidden sm:block">{formatTimestampAsDate(start)}</span>
<div class="flex flex-wrap gap-2 text-xs">
<div class="flex items-center gap-2">
<Icon icon={ClockCircle} size={4} />
{formatTimestampAsDate(start)}
</div>
{formatTimestampAsTime(start)}{isSingleDay
? formatTimestampAsTime(end)
: formatTimestamp(end)}
+4 -3
View File
@@ -53,7 +53,7 @@
import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte"
import ChatComposeParent from "@app/components/ChatComposeParent.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 {DraftKey} from "@app/util/drafts"
import {makeDelete, prependParent} from "@app/core/commands"
@@ -66,8 +66,9 @@
const {pubkeys, info}: Props = $props()
const chat = deriveChat(pubkeys)
const draftKey = new DraftKey<{content?: string | object}>(`dm:${$chat?.id}`)
const chatId = makeChatId(pubkeys)
const chat = deriveChat(chatId)
const draftKey = new DraftKey<{content?: string | object}>(`dm:${chatId}`)
const others = remove($pubkey!, pubkeys)
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
+4
View File
@@ -6,6 +6,7 @@
truncate,
renderAsHtml,
isText,
isEmail,
isEmoji,
isTopic,
isCode,
@@ -26,6 +27,7 @@
import Button from "@lib/components/Button.svelte"
import ContentToken from "@app/components/ContentToken.svelte"
import ContentEmoji from "@app/components/ContentEmoji.svelte"
import ContentEmail from "@app/components/ContentEmail.svelte"
import ContentCode from "@app/components/ContentCode.svelte"
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
@@ -159,6 +161,8 @@
<ContentTopic value={parsed.value} />
{:else if isEmoji(parsed)}
<ContentEmoji value={parsed.value} />
{:else if isEmail(parsed)}
<ContentEmail value={parsed.value} />
{:else if isCode(parsed)}
<ContentCode
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,
isText,
isEmoji,
isEmail,
isTopic,
isCode,
isCashu,
@@ -24,6 +25,7 @@
import Button from "@lib/components/Button.svelte"
import ContentToken from "@app/components/ContentToken.svelte"
import ContentEmoji from "@app/components/ContentEmoji.svelte"
import ContentEmail from "@app/components/ContentEmail.svelte"
import ContentCode from "@app/components/ContentCode.svelte"
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
import ContentNewline from "@app/components/ContentNewline.svelte"
@@ -109,6 +111,8 @@
<ContentTopic value={parsed.value} />
{:else if isEmoji(parsed)}
<ContentEmoji value={parsed.value} />
{:else if isEmail(parsed)}
<ContentEmail value={parsed.value} />
{:else if isCode(parsed)}
<ContentCode
value={parsed.value}
+3 -2
View File
@@ -42,7 +42,7 @@
let popover: Instance | undefined = $state()
</script>
<Button class="join rounded-full">
<div class="join items-center rounded-full">
{#if ENABLE_ZAPS && !hideZap}
<ZapButton {url} {event} class="btn join-item btn-neutral btn-xs">
<Icon icon={Bolt} size={4} />
@@ -52,6 +52,7 @@
<Icon icon={SmileCircle} size={4} />
</EmojiButton>
<Tippy
class="flex"
bind:popover
component={EventMenu}
props={{url, noun, event, customActions, onClick: hidePopover}}
@@ -60,4 +61,4 @@
<Icon icon={MenuDots} size={4} />
</Button>
</Tippy>
</Button>
</div>
+7 -1
View File
@@ -15,6 +15,7 @@
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
import {pushToast} from "@app/util/toast"
import {PLATFORM_NAME} from "@app/core/state"
@@ -22,9 +23,11 @@
secret: string
next: () => unknown
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()
@@ -150,6 +153,9 @@
</Button>
</div>
</ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
+11 -3
View File
@@ -19,6 +19,7 @@
import LogInOTP from "@app/components/LogInOTP.svelte"
import LogInSelect from "@app/components/LogInSelect.svelte"
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 {setChecked} from "@app/util/notifications"
import {pushToast} from "@app/util/toast"
@@ -44,7 +45,7 @@
return pushToast({
theme: "error",
message: "Sorry, we were unable to log you in.",
message: getPomadeLoginFailureMessage(messages),
})
}
@@ -64,10 +65,17 @@
pushToast({
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 {
loading = false
}
@@ -90,7 +98,7 @@
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Letter} />
<input bind:value={email} />
<input type="email" bind:value={email} />
</label>
{/snippet}
</FieldInline>
+12 -2
View File
@@ -15,6 +15,7 @@
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import LogInOTPConfirm from "@app/components/LogInOTPConfirm.svelte"
import {POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
@@ -35,11 +36,20 @@
if (ok) {
pushModal(LogInOTPConfirm, {email, peersByPrefix})
} else {
console.error("Pomade challenge request failed during OTP login")
pushToast({
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 {
loading = false
}
@@ -61,7 +71,7 @@
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Letter} />
<input bind:value={email} />
<input type="email" bind:value={email} />
</label>
{/snippet}
</FieldInline>
+12 -4
View File
@@ -15,10 +15,11 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import StringMultiInput from "@lib/components/StringMultiInput.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 {setChecked} from "@app/util/notifications"
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 = {
email: string
@@ -44,7 +45,7 @@
return pushToast({
theme: "error",
message: "Sorry, we were unable to log you in.",
message: getPomadeLoginFailureMessage(messages),
})
}
@@ -64,10 +65,17 @@
pushToast({
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 {
loading = false
}
+9 -1
View File
@@ -14,6 +14,7 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import Profile from "@app/components/Profile.svelte"
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
import {setChecked} from "@app/util/notifications"
import {clearModals} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
@@ -46,9 +47,16 @@
pushToast({
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 {
loading = false
}
@@ -13,13 +13,16 @@
const onClick = () => goToSpace(url)
const path = makeSpacePath(url)
const display = $derived(deriveRelayDisplay(url))
</script>
<PrimaryNavItem
href={path}
onclick={onClick}
title={$display}
class="tooltip-right"
notification={$notifications.has(makeSpacePath(url))}>
notification={$notifications.has(path)}>
<RelayIcon {url} size={10} class="rounded-full" />
</PrimaryNavItem>
+5 -1
View File
@@ -23,9 +23,10 @@
onsubmit: (values: Values) => void
isSignup?: boolean
footer: Snippet
progressBar?: Snippet
}
const {initialValues, isSignup, onsubmit, footer}: Props = $props()
const {initialValues, isSignup, onsubmit, footer, progressBar}: Props = $props()
const values = $state(initialValues)
@@ -103,6 +104,9 @@
</Field>
{/if}
</ModalBody>
{#if progressBar}
{@render progressBar()}
{/if}
<ModalFooter>
{@render footer()}
</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-2">
<div class="relative flex gap-4">
<div class="relative flex gap-2 sm:gap-4">
<div class="relative">
<div class="avatar relative">
<div
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>
{#if $rooms.includes(url)}
@@ -36,13 +36,11 @@
{/if}
</div>
<div class="min-w-0">
<h2 class="ellipsize whitespace-nowrap text-xl">
<RelayName {url} />
</h2>
<p class="text-sm opacity-75">{url}</p>
<RelayName {url} class="ellipsize whitespace-nowrap text-lg sm:text-xl" />
<p class="text-xs sm:text-sm opacity-75">{url}</p>
</div>
</div>
<RelayDescription {url} />
<RelayDescription {url} class="text-sm sm:text-md" />
</div>
{#if !hideFavorites && $favorited.size > 0}
<div class="row-2 card2 card2-sm bg-alt">
-2
View File
@@ -68,8 +68,6 @@
const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = ed.storage.nostr.getEditorTags()
if (!content) return
onSubmit({content, tags})
draftKey?.clear()
+1
View File
@@ -122,6 +122,7 @@
repository.removeEvent(thunk.event.id)
pushToast({theme: "error", message})
} else {
await removeRoomMembership(url, h)
goto(makeSpacePath(url))
}
},
+10 -11
View File
@@ -11,7 +11,6 @@
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import LogIn from "@app/components/LogIn.svelte"
import InfoNostr from "@app/components/InfoNostr.svelte"
import SignUpKey from "@app/components/SignUpKey.svelte"
import SignUpEmail from "@app/components/SignUpEmail.svelte"
import SignUpProfile from "@app/components/SignUpProfile.svelte"
@@ -62,9 +61,10 @@
const flows = {
email: {
start: () => pushModal(SignUpEmail, {next: flows.email.profile}),
profile: () => pushModal(SignUpProfile, {next: flows.email.complete}),
complete: () => pushModal(SignUpComplete, {next: flows.email.finalize}),
start: () => pushModal(SignUpEmail, {next: flows.email.profile, step: 1, totalSteps: 3}),
profile: () => pushModal(SignUpProfile, {next: flows.email.complete, step: 2, totalSteps: 3}),
complete: () =>
pushModal(SignUpComplete, {next: flows.email.finalize, step: 3, totalSteps: 3}),
finalize: () => {
const email = getKey<string>("signup.email")!
const clientOptions = getKey<ClientOptions>("signup.clientOptions")!
@@ -74,9 +74,10 @@
},
},
nostr: {
start: () => pushModal(SignUpProfile, {next: flows.nostr.key}),
key: () => pushModal(SignUpKey, {next: flows.nostr.complete}),
complete: () => pushModal(SignUpComplete, {next: flows.nostr.finalize}),
start: () => pushModal(SignUpProfile, {next: flows.nostr.key, step: 1, totalSteps: 3}),
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}),
complete: () =>
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
finalize: () => {
const secret = getKey<string>("signup.secret")!
@@ -89,11 +90,9 @@
<Modal>
<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">
{PLATFORM_NAME} is built using the
<Button class="link" onclick={() => pushModal(InfoNostr)}>nostr protocol</Button>, which gives
users control over their digital identity using <strong>cryptographic key pairs</strong>.
Censorship resistant digital spaces for communities. Meet new people, own your identity.
</p>
{#if hasPomade}
<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 ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
type Props = {
next: () => void
step?: number
totalSteps?: number
}
const {next}: Props = $props()
const {next, step, totalSteps}: Props = $props()
const back = () => history.back()
</script>
@@ -33,6 +36,9 @@
on groups you've already joined. Click below to get started!
</p>
</ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
+12 -3
View File
@@ -18,14 +18,17 @@
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
import {pushToast, popToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal"
type Props = {
next: () => void
step?: number
totalSteps?: number
}
const {next}: Props = $props()
const {next, step, totalSteps}: Props = $props()
const back = () => history.back()
@@ -81,7 +84,7 @@
setKey("signup.clientOptions", clientOptions)
popToast(toastId)
pushModal(SignUpEmailConfirm, {next})
pushModal(SignUpEmailConfirm, {next, step, totalSteps})
} catch (e) {
console.error(e)
@@ -120,7 +123,7 @@
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Letter} />
<input bind:value={email} />
<input type="email" bind:value={email} />
</label>
{/snippet}
</FieldInline>
@@ -134,8 +137,14 @@
<input type="password" bind:value={password} />
</label>
{/snippet}
{#snippet info()}
Must be at least 12 characters long.
{/snippet}
</FieldInline>
</ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
+7 -1
View File
@@ -15,12 +15,15 @@
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
type Props = {
next: () => void
step?: number
totalSteps?: number
}
const {next}: Props = $props()
const {next, step, totalSteps}: Props = $props()
const email = getKey<string>("signup.email")
@@ -61,6 +64,9 @@
above.
</p>
</ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter>
<Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon={AltArrowLeft} />
+4 -2
View File
@@ -4,11 +4,13 @@
type Props = {
next: () => void
step?: number
totalSteps?: number
}
const {next}: Props = $props()
const {next, step, totalSteps}: Props = $props()
const secret = getKey<string>("signup.secret")!
</script>
<KeyDownload {secret} {next} />
<KeyDownload {secret} {next} {step} {totalSteps} />

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