Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dbe9c0ebb | |||
| 45df132dc6 | |||
| c42a285f0b | |||
| 1e3211ae74 | |||
| ec507b05d6 | |||
| 339bb1afac | |||
| c441012e02 | |||
| 0d61278c56 | |||
| ffd06ab561 | |||
| eb8dd330b6 | |||
| 6267e52bdf | |||
| ab21008f34 | |||
| 0998639d59 | |||
| eccde07d06 | |||
| 770cdc5f13 | |||
| 6bafb62414 | |||
| 6ce0fbbbe6 | |||
| 8fe42e6f22 | |||
| 47a6209730 | |||
| 24d3f867f8 | |||
| 9db60374e4 | |||
| 8ef4b21dab | |||
| 8f56812dd1 | |||
| 3833cb093d | |||
| 94db65b85e | |||
| 6f731e48d2 | |||
| 99fe0e543c | |||
| c6b0799b2a | |||
| 861f2286db | |||
| 9af3e3b2e9 | |||
| 341c1b45b2 | |||
| 89f5d8cdf5 | |||
| ca3270437d | |||
| bbbc6f7363 | |||
| 8a0abacf6f | |||
| 976ccdabd4 | |||
| 99b26680b6 | |||
| c5be477855 | |||
| 32c1501e9c | |||
| 463837e7d4 | |||
| d74f142cdd | |||
| 53954aae89 | |||
| 24aa62a503 | |||
| 2618bb9c63 | |||
| 32a31045ef | |||
| 56edad77a8 | |||
| fdb604e350 | |||
| 3c66dfd83c | |||
| 81633b0a1e | |||
| 4a967de184 | |||
| 59961cbdb5 |
@@ -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 }}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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'
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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 = "";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,10 +18,9 @@
|
|||||||
"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",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -31,15 +28,15 @@
|
|||||||
"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 +57,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.15",
|
||||||
"@welshman/content": "^0.8.13",
|
"@welshman/content": "^0.8.15",
|
||||||
"@welshman/editor": "^0.8.13",
|
"@welshman/editor": "^0.8.15",
|
||||||
"@welshman/feeds": "^0.8.13",
|
"@welshman/feeds": "^0.8.15",
|
||||||
"@welshman/lib": "^0.8.13",
|
"@welshman/lib": "^0.8.15",
|
||||||
"@welshman/net": "^0.8.13",
|
"@welshman/net": "^0.8.15",
|
||||||
"@welshman/router": "^0.8.13",
|
"@welshman/router": "^0.8.15",
|
||||||
"@welshman/signer": "^0.8.13",
|
"@welshman/signer": "^0.8.15",
|
||||||
"@welshman/store": "^0.8.13",
|
"@welshman/store": "^0.8.15",
|
||||||
"@welshman/util": "^0.8.13",
|
"@welshman/util": "^0.8.15",
|
||||||
|
"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",
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
[toolchain]
|
|
||||||
channel = "1.92.0"
|
|
||||||
@@ -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}`)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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"]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
fn main() {
|
|
||||||
tauri_build::build()
|
|
||||||
}
|
|
||||||
@@ -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"]
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
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>
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
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>
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 668 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 926 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
@@ -1,2 +0,0 @@
|
|||||||
[toolchain]
|
|
||||||
channel = "1.92.0"
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}" />
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -6,14 +6,16 @@ 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"
|
||||||
@@ -22,10 +24,10 @@ import {AbortError, 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
|
||||||
@@ -173,6 +178,7 @@ const setUpMicrophone = async (
|
|||||||
|
|
||||||
const onRoomDisconnected = (reason?: DisconnectReason) => {
|
const onRoomDisconnected = (reason?: DisconnectReason) => {
|
||||||
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 +190,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 +220,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +279,11 @@ export const joinVoiceRoom = async (
|
|||||||
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([
|
||||||
@@ -287,19 +298,19 @@ export const joinVoiceRoom = async (
|
|||||||
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)
|
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
|
||||||
|
|
||||||
|
voiceMicMuted.set(muted)
|
||||||
currentVoiceSession.set({
|
currentVoiceSession.set({
|
||||||
url,
|
url,
|
||||||
h,
|
h,
|
||||||
room: liveKitRoom,
|
room: liveKitRoom,
|
||||||
muted,
|
|
||||||
cameraOn: false,
|
cameraOn: false,
|
||||||
screenShareOn: false,
|
screenShareOn: false,
|
||||||
})
|
})
|
||||||
@@ -339,11 +350,12 @@ export const leaveVoiceRoom = async () => {
|
|||||||
|
|
||||||
voiceState.set(VoiceState.Disconnected)
|
voiceState.set(VoiceState.Disconnected)
|
||||||
videoPrimaryTileKey.set(undefined)
|
videoPrimaryTileKey.set(undefined)
|
||||||
|
voiceMicMuted.set(true)
|
||||||
currentVoiceSession.set(undefined)
|
currentVoiceSession.set(undefined)
|
||||||
resetVideoCallLayout()
|
resetVideoCallLayout()
|
||||||
session.room.disconnect()
|
session.room.disconnect()
|
||||||
speakingParticipants.set([])
|
speakingParticipants.set([])
|
||||||
participantPubkeyMap.set(new Map())
|
participantMediaState.set(new Map())
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rejoinVoiceRoom = async (): Promise<void> => {
|
export const rejoinVoiceRoom = async (): Promise<void> => {
|
||||||
@@ -356,18 +368,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">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<Icon icon={ClockCircle} size={4} />
|
<Icon icon={ClockCircle} size={4} />
|
||||||
<span class="hidden sm:block">{formatTimestampAsDate(start)}</span>
|
{formatTimestampAsDate(start)}
|
||||||
|
</div>
|
||||||
{formatTimestampAsTime(start)} — {isSingleDay
|
{formatTimestampAsTime(start)} — {isSingleDay
|
||||||
? formatTimestampAsTime(end)
|
? formatTimestampAsTime(end)
|
||||||
: formatTimestamp(end)}
|
: formatTimestamp(end)}
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
next: () => void
|
next: () => void
|
||||||
|
step?: number
|
||||||
|
totalSteps?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const {next}: Props = $props()
|
const {next, step, totalSteps}: Props = $props()
|
||||||
|
|
||||||
const secret = getKey<string>("signup.secret")!
|
const secret = getKey<string>("signup.secret")!
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<KeyDownload {secret} {next} />
|
<KeyDownload {secret} {next} {step} {totalSteps} />
|
||||||
|
|||||||
@@ -5,15 +5,16 @@
|
|||||||
import {getKey, setKey} from "@lib/implicit"
|
import {getKey, setKey} from "@lib/implicit"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
|
||||||
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
import ProfileEditForm from "@app/components/ProfileEditForm.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 profile = getKey<Profile>("signup.profile")!
|
const profile = getKey<Profile>("signup.profile")!
|
||||||
|
|
||||||
@@ -27,8 +28,6 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
|
||||||
<ModalBody>
|
|
||||||
<ProfileEditForm isSignup {initialValues} {onsubmit}>
|
<ProfileEditForm isSignup {initialValues} {onsubmit}>
|
||||||
{#snippet footer()}
|
{#snippet footer()}
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
@@ -40,6 +39,9 @@
|
|||||||
<Icon icon={AltArrowRight} />
|
<Icon icon={AltArrowRight} />
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
{#snippet progressBar()}
|
||||||
|
{#if step && totalSteps}
|
||||||
|
<ProgressBar current={step} total={totalSteps} />
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
</ProfileEditForm>
|
</ProfileEditForm>
|
||||||
</ModalBody>
|
|
||||||
</Modal>
|
|
||||||
|
|||||||