Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a915eb71c |
@@ -4,6 +4,7 @@ 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,17 +1,12 @@
|
|||||||
name: Container Image Build and Publish
|
name: Docker
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: gitea.coracle.social
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: coracle/flotilla
|
IMAGE_NAME: coracle-social/flotilla
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push-image:
|
build-and-push-image:
|
||||||
@@ -28,8 +23,8 @@ jobs:
|
|||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: hodlbod
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
password: ${{ secrets.PACKAGE_TOKEN }}
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
@@ -37,7 +32,6 @@ 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
|
||||||
@@ -51,7 +45,6 @@ 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,10 +27,13 @@ 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,31 +1,5 @@
|
|||||||
# 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,31 +1,32 @@
|
|||||||
# Build and run the Flotilla web server.
|
# Stage 1: Build
|
||||||
#
|
# Uses .env from build context for config (logo, branding, etc.)
|
||||||
# docker build -t flotilla .
|
# Optional: docker build --build-arg VITE_BUILD_HASH=$(git rev-parse --short HEAD) -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.
|
|
||||||
|
|
||||||
# https://pnpm.io/docker#example-3-build-on-cicd
|
FROM node:20-bookworm AS builder
|
||||||
FROM node:24-slim AS builder
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
RUN apt-get update && apt-get install -y --no-install-recommends curl
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
|
||||||
RUN corepack enable
|
RUN npm install -g pnpm@latest
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_OPTIONS=--max_old_space_size=16384
|
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml ./
|
||||||
RUN pnpm i --frozen-lockfile
|
RUN pnpm i
|
||||||
COPY . .
|
|
||||||
ARG VITE_BUILD_HASH
|
# Copy everything (including .env when present) - build.sh will source it
|
||||||
RUN pnpm run build
|
COPY . .
|
||||||
RUN pnpm run build:server
|
|
||||||
|
ARG VITE_BUILD_HASH
|
||||||
|
ENV VITE_BUILD_HASH=${VITE_BUILD_HASH}
|
||||||
|
|
||||||
|
ENV NODE_OPTIONS=--max_old_space_size=16384
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
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 --from=builder /app/build-server/server.js /app/server.js
|
# Copy only the built output - no source, no .env, no dev deps
|
||||||
EXPOSE 3000
|
COPY --from=builder /app/build ./build
|
||||||
USER node
|
|
||||||
CMD ["node", "server.js"]
|
CMD ["npx", "serve", "-s", "build"]
|
||||||
|
|||||||
@@ -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
|
||||||
pnpm run start
|
npx serve -s build
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you prefer to use a container:
|
Or, if you prefer to use a container:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run -d -p 3000:3000 gitea.coracle.social/coracle/flotilla:latest
|
podman run -d -p 3000:3000 ghcr.io/coracle-social/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
|
||||||
docker run -v ./mount:/app/mount gitea.coracle.social/coracle/flotilla:latest bash -c 'cp -r build/* mount'
|
podman run -v ./mount:/app/mount ghcr.io/coracle-social/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 47
|
versionCode 46
|
||||||
versionName "1.8.0"
|
versionName "1.7.4"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -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_a3c0fb15d5bfa83f24d0070ca2583fc9/node_modules/nostr-signer-capacitor-plugin/android')
|
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin/android')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 48;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -131,9 +131,8 @@
|
|||||||
504EC2FC1FED79650016851F /* Project object */ = {
|
504EC2FC1FED79650016851F /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
|
||||||
LastSwiftUpdateCheck = 920;
|
LastSwiftUpdateCheck = 920;
|
||||||
LastUpgradeCheck = 2630;
|
LastUpgradeCheck = 920;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
504EC3031FED79650016851F = {
|
504EC3031FED79650016851F = {
|
||||||
CreatedOnToolsVersion = 9.2;
|
CreatedOnToolsVersion = 9.2;
|
||||||
@@ -258,7 +257,6 @@
|
|||||||
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;
|
||||||
@@ -266,10 +264,8 @@
|
|||||||
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;
|
||||||
@@ -279,10 +275,8 @@
|
|||||||
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;
|
||||||
@@ -301,7 +295,6 @@
|
|||||||
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";
|
||||||
};
|
};
|
||||||
@@ -321,7 +314,6 @@
|
|||||||
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;
|
||||||
@@ -329,10 +321,8 @@
|
|||||||
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;
|
||||||
@@ -342,10 +332,8 @@
|
|||||||
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;
|
||||||
@@ -357,9 +345,7 @@
|
|||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -372,16 +358,14 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 38;
|
CURRENT_PROJECT_VERSION = 37;
|
||||||
|
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 = (
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
"$(inherited)",
|
MARKETING_VERSION = 1.7.4;
|
||||||
"@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)";
|
||||||
@@ -401,16 +385,14 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 38;
|
CURRENT_PROJECT_VERSION = 37;
|
||||||
|
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 = (
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
"$(inherited)",
|
MARKETING_VERSION = 1.7.4;
|
||||||
"@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_a3c0fb15d5bfa83f24d0070ca2583fc9/node_modules/nostr-signer-capacitor-plugin'
|
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin'
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'Flotilla Chat' do
|
target 'Flotilla Chat' do
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "1.8.0",
|
"version": "1.7.4",
|
||||||
"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",
|
||||||
@@ -18,26 +20,26 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@sveltejs/kit": "^2.61.1",
|
"@sveltejs/kit": "^2.50.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
||||||
"@tailwindcss/postcss": "^4.2.2",
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
|
"@tauri-apps/cli": "^2.9.6",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/node": "^25.9.1",
|
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^9.1.2",
|
"eslint-config-prettier": "^9.1.2",
|
||||||
"eslint-plugin-svelte": "^2.46.1",
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"prettier-plugin-svelte": "^3.4.1",
|
"prettier-plugin-svelte": "^3.4.1",
|
||||||
"svelte": "^5.55.9",
|
"svelte": "^5.48.0",
|
||||||
"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": "^6.4.2"
|
"vite": "^5.4.21"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -58,28 +60,26 @@
|
|||||||
"@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.5",
|
"@pomade/core": "^0.2.3",
|
||||||
"@poppanator/sveltekit-svg": "^7.0.0",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@tiptap/core": "^2.27.2",
|
"@tiptap/core": "^2.27.2",
|
||||||
"@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": "^1.0.2",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^1.1.0",
|
"@vite-pwa/sveltekit": "^0.6.8",
|
||||||
"@welshman/app": "^0.8.16",
|
"@welshman/app": "^0.8.13",
|
||||||
"@welshman/content": "^0.8.16",
|
"@welshman/content": "^0.8.13",
|
||||||
"@welshman/editor": "^0.8.16",
|
"@welshman/editor": "^0.8.13",
|
||||||
"@welshman/feeds": "^0.8.16",
|
"@welshman/feeds": "^0.8.13",
|
||||||
"@welshman/lib": "^0.8.16",
|
"@welshman/lib": "^0.8.13",
|
||||||
"@welshman/net": "^0.8.16",
|
"@welshman/net": "^0.8.13",
|
||||||
"@welshman/router": "^0.8.16",
|
"@welshman/router": "^0.8.13",
|
||||||
"@welshman/signer": "^0.8.16",
|
"@welshman/signer": "^0.8.13",
|
||||||
"@welshman/store": "^0.8.16",
|
"@welshman/store": "^0.8.13",
|
||||||
"@welshman/util": "^0.8.16",
|
"@welshman/util": "^0.8.13",
|
||||||
"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",
|
||||||
@@ -87,7 +87,6 @@
|
|||||||
"emoji-picker-element": "^1.28.1",
|
"emoji-picker-element": "^1.28.1",
|
||||||
"emoji-picker-element-data": "^1.8.0",
|
"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",
|
||||||
@@ -99,5 +98,17 @@
|
|||||||
"throttle-debounce": "^5.0.2",
|
"throttle-debounce": "^5.0.2",
|
||||||
"tippy.js": "^6.3.7"
|
"tippy.js": "^6.3.7"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.4.0"
|
"pnpm": {
|
||||||
|
"ignoredBuiltDependencies": [
|
||||||
|
"esbuild"
|
||||||
|
],
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"sharp",
|
||||||
|
"nostr-signer-capacitor-plugin"
|
||||||
|
],
|
||||||
|
"overrides": {
|
||||||
|
"sharp": "0.35.0-rc.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
allowBuilds:
|
|
||||||
nostr-signer-capacitor-plugin: true
|
|
||||||
cbor-extract: false
|
|
||||||
esbuild: false
|
|
||||||
sharp: true
|
|
||||||
minimumReleaseAgeExclude:
|
|
||||||
- '@pomade/core'
|
|
||||||
- '@welshman/app'
|
|
||||||
- '@welshman/content'
|
|
||||||
- '@welshman/editor'
|
|
||||||
- '@welshman/feeds'
|
|
||||||
- '@welshman/lib'
|
|
||||||
- '@welshman/net'
|
|
||||||
- '@welshman/router'
|
|
||||||
- '@welshman/signer'
|
|
||||||
- '@welshman/store'
|
|
||||||
- '@welshman/util'
|
|
||||||
overrides:
|
|
||||||
sharp: 0.35.0-rc.0
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.92.0"
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
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}`)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "flotilla"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "flotilla_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2.5.3", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2.9.5", features = [] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["custom-protocol"]
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default desktop capability for the main window",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": ["core:default"]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 668 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 926 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.92.0"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
flotilla_lib::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
|
"productName": "Flotilla",
|
||||||
|
"mainBinaryName": "flotilla",
|
||||||
|
"identifier": "social.flotilla.app",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
"beforeBuildCommand": "pnpm build",
|
||||||
|
"devUrl": "http://localhost:1847",
|
||||||
|
"frontendDist": "../build"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"security": {
|
||||||
|
"capabilities": ["default"]
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"label": "main",
|
||||||
|
"title": "Flotilla",
|
||||||
|
"width": 1240,
|
||||||
|
"height": 775,
|
||||||
|
"resizable": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": false,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -235,7 +235,6 @@
|
|||||||
|
|
||||||
: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));
|
||||||
@@ -333,7 +332,7 @@
|
|||||||
|
|
||||||
.input-editor .tiptap {
|
.input-editor .tiptap {
|
||||||
--tiptap-object-bg: var(--color-base-200);
|
--tiptap-object-bg: var(--color-base-200);
|
||||||
@apply input block h-auto p-[.65rem];
|
@apply input h-auto p-[.65rem];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* link-content, based on tiptap */
|
/* link-content, based on tiptap */
|
||||||
@@ -417,24 +416,12 @@ 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,18 +2,15 @@
|
|||||||
<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 property="og:url" content="{URL}" />
|
<meta name="og:url" content="{URL}" />
|
||||||
<meta property="og:type" content="website" />
|
<meta name="og:type" content="website" />
|
||||||
<meta property="og:title" content="{NAME}" />
|
<meta name="og:title" content="{NAME}" />
|
||||||
<meta property="og:description" content="{DESCRIPTION}" />
|
<meta name="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,13 +6,11 @@ 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}
|
||||||
@@ -29,6 +27,8 @@ 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,21 +41,6 @@ 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,28 +6,26 @@ 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, not, nthEq, reject, removeUndefined, uniqBy} from "@welshman/lib"
|
import {map, removeUndefined, uniqBy} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
||||||
import {signer} from "@welshman/app"
|
import {signer} from "@welshman/app"
|
||||||
import {getLivekitEndpoint} from "$lib/livekit"
|
import {getLivekitEndpoint} from "$lib/livekit"
|
||||||
import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util"
|
import {AbortError, whenAborted, whenTimeout} from "$lib/util"
|
||||||
import {
|
import {
|
||||||
currentVoiceRoom,
|
currentVoiceRoom,
|
||||||
currentVoiceSession,
|
currentVoiceSession,
|
||||||
voiceMicMuted,
|
|
||||||
participantFromLiveKitIdentity,
|
participantFromLiveKitIdentity,
|
||||||
participantKey,
|
participantKey,
|
||||||
participantMediaState,
|
participantPubkeyMap,
|
||||||
|
pubkeyFromLiveKitIdentity,
|
||||||
speakingParticipants,
|
speakingParticipants,
|
||||||
VoiceState,
|
VoiceState,
|
||||||
type VoiceParticipant,
|
type VoiceParticipant,
|
||||||
@@ -77,23 +75,20 @@ export const switchVoiceActiveDevice = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteParticipant = (identity: string) => {
|
const addParticipant = (identity: string) => {
|
||||||
participantMediaState.update(m => new Map(reject(nthEq(0, identity), [...m])))
|
participantPubkeyMap.update(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(participant.identity, state)
|
next.set(identity, pubkeyFromLiveKitIdentity(identity) ?? "")
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onParticipantMediaChanged = (_publication: TrackPublication, participant: Participant) => {
|
const deleteParticipant = (identity: string) => {
|
||||||
syncParticipantMedia(participant)
|
participantPubkeyMap.update(m => {
|
||||||
|
const next = new Map(m)
|
||||||
|
next.delete(identity)
|
||||||
|
return next
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchLivekitToken = async (
|
const fetchLivekitToken = async (
|
||||||
@@ -129,15 +124,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(
|
||||||
[
|
[
|
||||||
participantMediaState,
|
participantPubkeyMap,
|
||||||
currentVoiceRoom,
|
currentVoiceRoom,
|
||||||
deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
|
deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
|
||||||
],
|
],
|
||||||
([$participantMediaState, $currentVoiceRoom, $publishedParticipantList]) => {
|
([$participantPubkeyMap, $currentVoiceRoom, $publishedParticipantList]) => {
|
||||||
const inCall = $participantMediaState.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
|
const inCall = $participantPubkeyMap.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
|
||||||
|
|
||||||
if (inCall) {
|
if (inCall) {
|
||||||
const participants = [...$participantMediaState.keys()].map(participantFromLiveKitIdentity)
|
const participants = [...$participantPubkeyMap.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
|
||||||
@@ -157,8 +152,6 @@ const setUpMicrophone = async (
|
|||||||
startMuted: boolean,
|
startMuted: boolean,
|
||||||
preferredMicId: string | undefined,
|
preferredMicId: string | undefined,
|
||||||
participant: LocalParticipant,
|
participant: LocalParticipant,
|
||||||
signal?: AbortSignal,
|
|
||||||
settleSignal?: AbortSignal,
|
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
if (startMuted) {
|
if (startMuted) {
|
||||||
return true
|
return true
|
||||||
@@ -170,36 +163,16 @@ const setUpMicrophone = async (
|
|||||||
capture = {deviceId: preferredMicId}
|
capture = {deviceId: preferredMicId}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await Promise.race([
|
await participant.setMicrophoneEnabled(true, capture)
|
||||||
participant.setMicrophoneEnabled(true, capture),
|
|
||||||
whenTimeout(15_000, {message: "Microphone access timed out.", signal: settleSignal}),
|
|
||||||
whenAborted(signal),
|
|
||||||
])
|
|
||||||
muted = false
|
muted = false
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Timeout or microphone rejection: join muted, the call is still usable. A
|
pushToast({theme: "error", message: "Could not access microphone"})
|
||||||
// genuine abort is surfaced to the caller so it can tear down the room.
|
|
||||||
if (e instanceof AbortError) throw e
|
|
||||||
if (!(e instanceof TimeoutError)) {
|
|
||||||
pushToast({theme: "error", message: "Could not access microphone"})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return muted
|
return muted
|
||||||
}
|
}
|
||||||
|
|
||||||
// The room whose events are allowed to mutate shared state. Abandoned rooms
|
const onRoomDisconnected = (reason?: DisconnectReason) => {
|
||||||
// (after switching calls or an engine reconnect give-up) must not clobber it.
|
|
||||||
let activeRoom: LiveKitRoom | undefined
|
|
||||||
|
|
||||||
const makeOnRoomDisconnected = (room: LiveKitRoom) => (reason?: DisconnectReason) => {
|
|
||||||
// Ignore disconnects from rooms that are no longer the active session.
|
|
||||||
if (room !== activeRoom) return
|
|
||||||
|
|
||||||
activeRoom = undefined
|
|
||||||
room.removeAllListeners()
|
|
||||||
|
|
||||||
videoPrimaryTileKey.set(undefined)
|
videoPrimaryTileKey.set(undefined)
|
||||||
voiceMicMuted.set(true)
|
|
||||||
currentVoiceSession.set(undefined)
|
currentVoiceSession.set(undefined)
|
||||||
resetVideoCallLayout()
|
resetVideoCallLayout()
|
||||||
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
|
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
|
||||||
@@ -211,7 +184,7 @@ const makeOnRoomDisconnected = (room: LiveKitRoom) => (reason?: DisconnectReason
|
|||||||
pushToast({theme: "error", message})
|
pushToast({theme: "error", message})
|
||||||
}
|
}
|
||||||
speakingParticipants.set([])
|
speakingParticipants.set([])
|
||||||
participantMediaState.set(new Map())
|
participantPubkeyMap.set(new Map())
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTrackSubscribed = (track: Track) => {
|
const onTrackSubscribed = (track: Track) => {
|
||||||
@@ -241,8 +214,8 @@ const playJoinSound = () => {
|
|||||||
audio.play().catch(() => {})
|
audio.play().catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onParticipantConnected = (participant: Participant) => {
|
const onParticipantConnected = (participant: {identity: string}) => {
|
||||||
syncParticipantMedia(participant)
|
addParticipant(participant.identity)
|
||||||
playJoinSound()
|
playJoinSound()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,6 +248,9 @@ export const joinVoiceRoom = async (
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
cancelJoinVoiceRoom()
|
cancelJoinVoiceRoom()
|
||||||
|
|
||||||
|
const session = get(currentVoiceSession)
|
||||||
|
if (session) await leaveVoiceRoom()
|
||||||
|
|
||||||
currentVoiceRoom.set(get(deriveRoom(url, h)))
|
currentVoiceRoom.set(get(deriveRoom(url, h)))
|
||||||
voiceState.set(VoiceState.Joining)
|
voiceState.set(VoiceState.Joining)
|
||||||
|
|
||||||
@@ -283,101 +259,47 @@ export const joinVoiceRoom = async (
|
|||||||
const signal = controller.signal
|
const signal = controller.signal
|
||||||
const isActive = () => joinAbortController === controller
|
const isActive = () => joinAbortController === controller
|
||||||
|
|
||||||
// Self-cleaning controller: aborted in finally so whenTimeout/whenAborted
|
|
||||||
// helpers clear their timers/listeners once the races below have settled.
|
|
||||||
const settle = new AbortController()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Tear down any existing session before joining. Bound it so a slow leave
|
const {server_url, participant_token} = await fetchLivekitToken(url, h, signal)
|
||||||
// (camera/screenshare renegotiation can take ~15s) cannot block this join.
|
|
||||||
if (get(currentVoiceSession)) {
|
|
||||||
await Promise.race([
|
|
||||||
leaveVoiceRoom(),
|
|
||||||
whenTimeout(15_000, {message: "Leaving previous call timed out.", signal: settle.signal}),
|
|
||||||
whenAborted(signal),
|
|
||||||
]).catch(e => {
|
|
||||||
if (e instanceof AbortError) throw e
|
|
||||||
})
|
|
||||||
|
|
||||||
// leaveVoiceRoom flips voiceState to Disconnected; re-assert Joining.
|
|
||||||
voiceState.set(VoiceState.Joining)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signal.aborted) throw new AbortError()
|
|
||||||
|
|
||||||
const {server_url, participant_token} = await Promise.race([
|
|
||||||
fetchLivekitToken(url, h, signal),
|
|
||||||
whenTimeout(15_000, {
|
|
||||||
message: "Connection timed out. Please check your network and try again.",
|
|
||||||
signal: settle.signal,
|
|
||||||
}),
|
|
||||||
whenAborted(signal),
|
|
||||||
])
|
|
||||||
|
|
||||||
if (signal.aborted) throw new AbortError()
|
if (signal.aborted) throw new AbortError()
|
||||||
|
|
||||||
const liveKitRoom = new LiveKitRoom({adaptiveStream: true, dynacast: true})
|
const liveKitRoom = new LiveKitRoom({adaptiveStream: true, dynacast: true})
|
||||||
activeRoom = liveKitRoom
|
|
||||||
|
|
||||||
liveKitRoom.on(RoomEvent.Disconnected, makeOnRoomDisconnected(liveKitRoom))
|
liveKitRoom.on(RoomEvent.Disconnected, onRoomDisconnected)
|
||||||
liveKitRoom.on(RoomEvent.ParticipantConnected, onParticipantConnected)
|
liveKitRoom.on(RoomEvent.ParticipantConnected, onParticipantConnected)
|
||||||
liveKitRoom.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected)
|
liveKitRoom.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected)
|
||||||
liveKitRoom.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
|
liveKitRoom.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
|
||||||
liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
||||||
liveKitRoom.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
|
liveKitRoom.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
|
||||||
liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
|
liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
|
||||||
liveKitRoom.on(RoomEvent.TrackMuted, onParticipantMediaChanged)
|
|
||||||
liveKitRoom.on(RoomEvent.TrackUnmuted, onParticipantMediaChanged)
|
|
||||||
liveKitRoom.on(RoomEvent.TrackPublished, onParticipantMediaChanged)
|
|
||||||
liveKitRoom.on(RoomEvent.TrackUnpublished, onParticipantMediaChanged)
|
|
||||||
liveKitRoom.on(RoomEvent.LocalTrackPublished, onParticipantMediaChanged)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
liveKitRoom.connect(server_url, participant_token, {maxRetries: 0}),
|
liveKitRoom.connect(server_url, participant_token, {maxRetries: 0}),
|
||||||
whenTimeout(15_000, {
|
whenTimeout(15_000, {
|
||||||
message: "Connection timed out. Please check your network and try again.",
|
message: "Connection timed out. Please check your network and try again.",
|
||||||
signal: settle.signal,
|
|
||||||
}),
|
}),
|
||||||
whenAborted(signal),
|
whenAborted(signal),
|
||||||
])
|
])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (activeRoom === liveKitRoom) activeRoom = undefined
|
|
||||||
liveKitRoom.removeAllListeners()
|
|
||||||
liveKitRoom.disconnect()
|
liveKitRoom.disconnect()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
participantMediaState.set(new Map())
|
participantPubkeyMap.set(new Map())
|
||||||
syncParticipantMedia(liveKitRoom.localParticipant)
|
addParticipant(liveKitRoom.localParticipant.identity)
|
||||||
for (const p of liveKitRoom.remoteParticipants.values()) {
|
for (const p of liveKitRoom.remoteParticipants.values()) {
|
||||||
syncParticipantMedia(p)
|
addParticipant(p.identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bounded against timeout/abort inside setUpMicrophone: a stuck permission
|
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
|
||||||
// prompt resolves to muted rather than hanging the join forever.
|
|
||||||
const muted = await setUpMicrophone(
|
|
||||||
startMuted,
|
|
||||||
preferredMicId,
|
|
||||||
liveKitRoom.localParticipant,
|
|
||||||
signal,
|
|
||||||
settle.signal,
|
|
||||||
)
|
|
||||||
|
|
||||||
// A cancel during the mic step must tear down the connected room rather
|
|
||||||
// than leaking it.
|
|
||||||
if (signal.aborted) {
|
|
||||||
if (activeRoom === liveKitRoom) activeRoom = undefined
|
|
||||||
liveKitRoom.removeAllListeners()
|
|
||||||
liveKitRoom.disconnect()
|
|
||||||
throw new AbortError()
|
|
||||||
}
|
|
||||||
|
|
||||||
voiceMicMuted.set(muted)
|
|
||||||
currentVoiceSession.set({
|
currentVoiceSession.set({
|
||||||
url,
|
url,
|
||||||
h,
|
h,
|
||||||
room: liveKitRoom,
|
room: liveKitRoom,
|
||||||
|
muted,
|
||||||
cameraOn: false,
|
cameraOn: false,
|
||||||
screenShareOn: false,
|
screenShareOn: false,
|
||||||
})
|
})
|
||||||
@@ -388,7 +310,6 @@ export const joinVoiceRoom = async (
|
|||||||
if (e instanceof AbortError) return
|
if (e instanceof AbortError) return
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
settle.abort()
|
|
||||||
if (isActive()) joinAbortController = undefined
|
if (isActive()) joinAbortController = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,23 +337,13 @@ export const leaveVoiceRoom = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always tear down this room's connection and listeners.
|
voiceState.set(VoiceState.Disconnected)
|
||||||
if (activeRoom === session.room) activeRoom = undefined
|
videoPrimaryTileKey.set(undefined)
|
||||||
session.room.removeAllListeners()
|
currentVoiceSession.set(undefined)
|
||||||
|
resetVideoCallLayout()
|
||||||
session.room.disconnect()
|
session.room.disconnect()
|
||||||
|
speakingParticipants.set([])
|
||||||
// Only reset shared UI state if this session is still current. A slow leave
|
participantPubkeyMap.set(new Map())
|
||||||
// that was superseded by a new join (bounded by a timeout in joinVoiceRoom)
|
|
||||||
// must not clobber the freshly-joined session when it finally completes.
|
|
||||||
if (get(currentVoiceSession) === session) {
|
|
||||||
voiceState.set(VoiceState.Disconnected)
|
|
||||||
videoPrimaryTileKey.set(undefined)
|
|
||||||
voiceMicMuted.set(true)
|
|
||||||
currentVoiceSession.set(undefined)
|
|
||||||
resetVideoCallLayout()
|
|
||||||
speakingParticipants.set([])
|
|
||||||
participantMediaState.set(new Map())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rejoinVoiceRoom = async (): Promise<void> => {
|
export const rejoinVoiceRoom = async (): Promise<void> => {
|
||||||
@@ -445,17 +356,18 @@ export const toggleMute = async () => {
|
|||||||
const session = get(currentVoiceSession)
|
const session = get(currentVoiceSession)
|
||||||
if (!session) return
|
if (!session) return
|
||||||
|
|
||||||
voiceMicMuted.update(not)
|
const muted = !session.muted
|
||||||
if (get(voiceMicMuted)) {
|
if (muted) {
|
||||||
// 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,17 +19,15 @@
|
|||||||
const end = $derived(parseInt(meta.end))
|
const end = $derived(parseInt(meta.end))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col justify-between gap-1">
|
<div class="flex grow flex-wrap justify-between gap-2">
|
||||||
<p class="text-lg">{meta.title || meta.name}</p>
|
<p class="text-xl">{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 flex-wrap gap-2 text-xs">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<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, makeChatId} from "@app/core/state"
|
import {userSettingsValues, deriveChat} 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,9 +66,8 @@
|
|||||||
|
|
||||||
const {pubkeys, info}: Props = $props()
|
const {pubkeys, info}: Props = $props()
|
||||||
|
|
||||||
const chatId = makeChatId(pubkeys)
|
const chat = deriveChat(pubkeys)
|
||||||
const chat = deriveChat(chatId)
|
const draftKey = new DraftKey<{content?: string | object}>(`dm:${$chat?.id}`)
|
||||||
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,7 +6,6 @@
|
|||||||
truncate,
|
truncate,
|
||||||
renderAsHtml,
|
renderAsHtml,
|
||||||
isText,
|
isText,
|
||||||
isEmail,
|
|
||||||
isEmoji,
|
isEmoji,
|
||||||
isTopic,
|
isTopic,
|
||||||
isCode,
|
isCode,
|
||||||
@@ -27,7 +26,6 @@
|
|||||||
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"
|
||||||
@@ -161,8 +159,6 @@
|
|||||||
<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}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<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,7 +7,6 @@
|
|||||||
renderAsHtml,
|
renderAsHtml,
|
||||||
isText,
|
isText,
|
||||||
isEmoji,
|
isEmoji,
|
||||||
isEmail,
|
|
||||||
isTopic,
|
isTopic,
|
||||||
isCode,
|
isCode,
|
||||||
isCashu,
|
isCashu,
|
||||||
@@ -25,7 +24,6 @@
|
|||||||
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"
|
||||||
@@ -111,8 +109,6 @@
|
|||||||
<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}
|
||||||
|
|||||||
@@ -13,16 +13,13 @@
|
|||||||
|
|
||||||
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(path)}>
|
notification={$notifications.has(makeSpacePath(url))}>
|
||||||
<RelayIcon {url} size={10} class="rounded-full" />
|
<RelayIcon {url} size={10} class="rounded-full" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
|
|||||||
@@ -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-2 sm:gap-4">
|
<div class="relative flex 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} size={10} />
|
<RelayIcon {url} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $rooms.includes(url)}
|
{#if $rooms.includes(url)}
|
||||||
@@ -36,11 +36,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<RelayName {url} class="ellipsize whitespace-nowrap text-lg sm:text-xl" />
|
<h2 class="ellipsize whitespace-nowrap text-xl">
|
||||||
<p class="text-xs sm:text-sm opacity-75">{url}</p>
|
<RelayName {url} />
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm opacity-75">{url}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RelayDescription {url} class="text-sm sm:text-md" />
|
<RelayDescription {url} />
|
||||||
</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,6 +68,8 @@
|
|||||||
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()
|
||||||
|
|||||||
@@ -122,7 +122,6 @@
|
|||||||
repository.removeEvent(thunk.event.id)
|
repository.removeEvent(thunk.event.id)
|
||||||
pushToast({theme: "error", message})
|
pushToast({theme: "error", message})
|
||||||
} else {
|
} else {
|
||||||
await removeRoomMembership(url, h)
|
|
||||||
goto(makeSpacePath(url))
|
goto(makeSpacePath(url))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
|
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
|
||||||
<RoomItemContent {url} event={$innerEvent ?? event} />
|
<RoomItemContent {url} event={$innerEvent ?? event} />
|
||||||
{#if thunk}
|
{#if thunk}
|
||||||
<ThunkFailure showToastOnRetry {thunk} class="mt-1 flex justify-end" />
|
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
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"
|
||||||
@@ -90,9 +91,11 @@
|
|||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<h1 class="heading">Join {PLATFORM_NAME}</h1>
|
<h1 class="heading">Sign up with Nostr</h1>
|
||||||
<p class="m-auto max-w-sm text-center">
|
<p class="m-auto max-w-sm text-center">
|
||||||
Censorship resistant digital spaces for communities. Meet new people, own your identity.
|
{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>.
|
||||||
</p>
|
</p>
|
||||||
{#if hasPomade}
|
{#if hasPomade}
|
||||||
<Button onclick={flows.email.start} class="btn btn-primary">
|
<Button onclick={flows.email.start} class="btn btn-primary">
|
||||||
|
|||||||
@@ -7,19 +7,17 @@
|
|||||||
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 PageBar from "@lib/components/PageBar.svelte"
|
import PageBar from "@lib/components/PageBar.svelte"
|
||||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
|
||||||
import {decodeRelay} from "@app/core/state"
|
import {decodeRelay} from "@app/core/state"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
back?: () => unknown
|
back?: () => unknown
|
||||||
leading?: Snippet
|
|
||||||
title?: Snippet
|
title?: Snippet
|
||||||
action?: Snippet
|
action?: Snippet
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const {back = () => goto(makeSpacePath(url)), leading, title, action, ...props}: Props = $props()
|
const {back = () => goto(makeSpacePath(url)), title, action, ...props}: Props = $props()
|
||||||
|
|
||||||
const url = decodeRelay($page.params.relay!)
|
const url = decodeRelay($page.params.relay!)
|
||||||
</script>
|
</script>
|
||||||
@@ -32,10 +30,6 @@
|
|||||||
<div class="ellipsize whitespace-nowrap flex grow items-center justify-between gap-4">
|
<div class="ellipsize whitespace-nowrap flex grow items-center justify-between gap-4">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<RelayIcon {url} size={5} class="rounded-full md:hidden" />
|
|
||||||
<div class="hidden md:contents">
|
|
||||||
{@render leading?.()}
|
|
||||||
</div>
|
|
||||||
{@render title?.()}
|
{@render title?.()}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-primary md:hidden">
|
<div class="text-xs text-primary md:hidden">
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte"
|
import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
import {Push} from "@app/util/notifications"
|
||||||
import {relaysMostlyRestricted, notificationSettings, parseInviteLink} from "@app/core/state"
|
import {relaysMostlyRestricted, notificationSettings, parseInviteLink} from "@app/core/state"
|
||||||
import {Push} from "@app/util/push"
|
|
||||||
import {
|
import {
|
||||||
attemptRelayAccess,
|
attemptRelayAccess,
|
||||||
addSpaceMembership,
|
addSpaceMembership,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
import {Push} from "@app/util/push"
|
import {Push} from "@app/util/notifications"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util"
|
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util"
|
||||||
import {deriveRelay, createSearch, pubkey} from "@welshman/app"
|
import {deriveRelay, deriveRelayDisplay, createSearch, pubkey} from "@welshman/app"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
const relay = deriveRelay(url)
|
const relay = deriveRelay(url)
|
||||||
|
const display = deriveRelayDisplay(url)
|
||||||
const chatPath = makeSpacePath(url, "chat")
|
const chatPath = makeSpacePath(url, "chat")
|
||||||
const goalsPath = makeSpacePath(url, "goals")
|
const goalsPath = makeSpacePath(url, "goals")
|
||||||
const threadsPath = makeSpacePath(url, "threads")
|
const threadsPath = makeSpacePath(url, "threads")
|
||||||
@@ -143,7 +144,9 @@
|
|||||||
class="relative flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
class="relative flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
||||||
onclick={openMenu}>
|
onclick={openMenu}>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<strong class="flex items-center gap-1 relative">
|
<strong
|
||||||
|
class="flex items-center gap-1 relative tooltip tooltip-right"
|
||||||
|
data-tip={$display}>
|
||||||
<RelayName {url} class="ellipsize" />
|
<RelayName {url} class="ellipsize" />
|
||||||
<div
|
<div
|
||||||
class="absolute -right-3 top-0 h-2 w-2 rounded-full bg-primary transition-all opacity-0"
|
class="absolute -right-3 top-0 h-2 w-2 rounded-full bg-primary transition-all opacity-0"
|
||||||
|
|||||||
@@ -26,22 +26,22 @@
|
|||||||
{@const {pubkey, software, version, supported_nips, limitation} = $relay}
|
{@const {pubkey, software, version, supported_nips, limitation} = $relay}
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
{#if pubkey}
|
{#if pubkey}
|
||||||
<div class="badge badge-neutral text-wrap h-auto">
|
<div class="badge badge-neutral">
|
||||||
<span class="ellipsize">Administrator: <ProfileLink unstyled {pubkey} /></span>
|
<span class="ellipsize">Administrator: <ProfileLink unstyled {pubkey} /></span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $relay?.contact}
|
{#if $relay?.contact}
|
||||||
<div class="badge badge-neutral text-wrap h-auto">
|
<div class="badge badge-neutral">
|
||||||
<span class="ellipsize">Contact: {$relay.contact}</span>
|
<span class="ellipsize">Contact: {$relay.contact}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if software}
|
{#if software}
|
||||||
<div class="badge badge-neutral text-wrap h-auto">
|
<div class="badge badge-neutral">
|
||||||
<span class="ellipsize">Software: {software}</span>
|
<span class="ellipsize">Software: {software}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if version}
|
{#if version}
|
||||||
<div class="badge badge-neutral text-wrap h-auto">
|
<div class="badge badge-neutral">
|
||||||
<span class="ellipsize">Version: {version}</span>
|
<span class="ellipsize">Version: {version}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if limitation?.min_pow_difficulty}
|
{#if limitation?.min_pow_difficulty}
|
||||||
<p class="badge badge-warning text-wrap h-auto">
|
<p class="badge badge-warning">
|
||||||
<span class="ellipsize">Min PoW: {limitation?.min_pow_difficulty}</span>
|
<span class="ellipsize">Min PoW: {limitation?.min_pow_difficulty}</span>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {stopPropagation} from "svelte/legacy"
|
import {stopPropagation} from "svelte/legacy"
|
||||||
import {noop} from "@welshman/lib"
|
import {noop} from "@welshman/lib"
|
||||||
import type {AbstractThunk} from "@welshman/app"
|
import type {AbstractThunk} from "@welshman/app"
|
||||||
import {flattenThunks, getFailedThunkUrls, publishThunk, thunkIsComplete} from "@welshman/app"
|
import {retryThunk, thunkIsComplete, getFailedThunkUrls} from "@welshman/app"
|
||||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
@@ -16,45 +16,40 @@
|
|||||||
class?: string
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {thunk, showToastOnRetry, ...restProps}: Props = $props()
|
let {thunk, showToastOnRetry, ...restProps}: Props = $props()
|
||||||
|
|
||||||
const showFailure = $derived(thunkIsComplete($thunk) && getFailedThunkUrls($thunk).length > 0)
|
const retry = () => {
|
||||||
|
thunk = retryThunk(thunk)
|
||||||
|
|
||||||
const retry = (url: string) => {
|
if (showToastOnRetry) {
|
||||||
for (const child of flattenThunks([thunk])) {
|
pushToast({
|
||||||
if (!child.options.relays.includes(url)) {
|
timeout: 30_000,
|
||||||
continue
|
children: {
|
||||||
}
|
component: ThunkToast,
|
||||||
|
props: {thunk},
|
||||||
const retried = publishThunk({...child.options, relays: [url]})
|
},
|
||||||
|
})
|
||||||
if (showToastOnRetry) {
|
|
||||||
pushToast({
|
|
||||||
timeout: 30_000,
|
|
||||||
children: {
|
|
||||||
component: ThunkToast,
|
|
||||||
props: {thunk: retried},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const failedUrls = $derived(getFailedThunkUrls($thunk))
|
||||||
|
const showFailure = $derived(thunkIsComplete($thunk) && failedUrls.length > 0)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showFailure}
|
{#if showFailure}
|
||||||
|
{@const url = failedUrls[0]}
|
||||||
|
{@const {status, detail: message} = $thunk.results[url]}
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-end px-1 text-xs {restProps.class}"
|
class="flex w-full justify-end px-1 text-xs {restProps.class}"
|
||||||
onclick={stopPropagation(noop)}>
|
onclick={stopPropagation(noop)}>
|
||||||
<Tippy
|
<Tippy
|
||||||
class="flex items-center"
|
class="flex items-center"
|
||||||
component={ThunkStatusDetail}
|
component={ThunkStatusDetail}
|
||||||
props={{thunk, retry}}
|
props={{url, message, status, retry}}
|
||||||
params={{interactive: true, maxWidth: "none"}}>
|
params={{interactive: true}}>
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
<span class="flex cursor-pointer items-center gap-1 opacity-75">
|
<span class="flex cursor-pointer items-center gap-1 text-error">
|
||||||
<Icon icon={Danger} class="text-error" size={3} />
|
<Icon icon={Danger} size={3} />
|
||||||
<span>Failed to send!</span>
|
<span>Failed to send!</span>
|
||||||
</span>
|
</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -6,18 +6,17 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
thunk: AbstractThunk
|
thunk: AbstractThunk
|
||||||
showToastOnRetry?: boolean
|
|
||||||
class?: string
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {thunk, showToastOnRetry, ...restProps}: Props = $props()
|
const {thunk, ...restProps}: Props = $props()
|
||||||
|
|
||||||
const showFailure = $derived(thunkIsComplete($thunk) && getFailedThunkUrls($thunk).length > 0)
|
const showFailure = $derived(thunkIsComplete($thunk) && getFailedThunkUrls($thunk).length > 0)
|
||||||
const showPending = $derived(!thunkIsComplete($thunk))
|
const showPending = $derived(!thunkIsComplete($thunk))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showFailure}
|
{#if showFailure}
|
||||||
<ThunkFailure class={restProps.class} {thunk} {showToastOnRetry} />
|
<ThunkFailure class={restProps.class} {thunk} />
|
||||||
{:else if showPending}
|
{:else if showPending}
|
||||||
<ThunkPending class={restProps.class} {thunk} />
|
<ThunkPending class={restProps.class} {thunk} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,69 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {stopPropagation} from "svelte/legacy"
|
|
||||||
import type {AbstractThunk} from "@welshman/app"
|
|
||||||
import {getFailedThunkUrls, getThunkUrlsWithStatus} from "@welshman/app"
|
|
||||||
import {PublishStatus} from "@welshman/net"
|
import {PublishStatus} from "@welshman/net"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
|
||||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import {addPeriod} from "@lib/util"
|
import {addPeriod} from "@lib/util"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
thunk: AbstractThunk
|
url: string
|
||||||
retry: (url: string) => void
|
status: string
|
||||||
|
message: string
|
||||||
|
retry: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {thunk, retry}: Props = $props()
|
let {url, status, message = $bindable(), retry}: Props = $props()
|
||||||
|
|
||||||
const successUrls = $derived(getThunkUrlsWithStatus(PublishStatus.Success, $thunk))
|
$effect(() => {
|
||||||
const failedUrls = $derived(getFailedThunkUrls($thunk))
|
if (!message && status === PublishStatus.Timeout) {
|
||||||
const total = $derived(successUrls.length + failedUrls.length)
|
message = "request timed out"
|
||||||
const isPartial = $derived(successUrls.length > 0 && failedUrls.length > 0)
|
|
||||||
|
|
||||||
const title = $derived(
|
|
||||||
isPartial ? `Partial delivery ${successUrls.length}/${total} relays` : "Failed to send!",
|
|
||||||
)
|
|
||||||
|
|
||||||
const relayMessage = (status: PublishStatus | undefined, detail: string | undefined) => {
|
|
||||||
if (detail) {
|
|
||||||
return detail
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === PublishStatus.Timeout) {
|
if (!message) {
|
||||||
return "request timed out"
|
message = "no details recieved"
|
||||||
}
|
}
|
||||||
|
})
|
||||||
return "no details received"
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card2 bg-alt flex min-w-72 max-w-sm flex-col gap-3 px-4 py-3 shadow-lg">
|
<div class="card2 bg-alt col-2 shadow-lg">
|
||||||
<span class="flex items-center gap-2 text-sm font-medium">
|
<p>
|
||||||
<Icon icon={Danger} class="text-error" size={4} />
|
Failed to publish to {displayRelayUrl(url)}: {addPeriod(message)}
|
||||||
{title}
|
</p>
|
||||||
</span>
|
<Button class="link" onclick={retry}>Retry</Button>
|
||||||
<div class="divider my-0"></div>
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
{#each successUrls as url (url)}
|
|
||||||
<div class="flex items-start gap-2 text-sm">
|
|
||||||
<Icon icon={CheckCircle} class="mt-0.5 shrink-0 text-success" size={4} />
|
|
||||||
<span>{displayRelayUrl(url)}</span>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{#each failedUrls as url (url)}
|
|
||||||
{@const {detail, status} = $thunk.results[url] || {}}
|
|
||||||
<div class="grid grid-cols-[1rem_1fr_auto] items-start gap-x-3 gap-y-1 text-sm">
|
|
||||||
<Icon icon={Danger} class="mt-0.5 text-error" size={4} />
|
|
||||||
<div class="min-w-0">
|
|
||||||
<p class="break-all">{displayRelayUrl(url)}</p>
|
|
||||||
<p class="text-xs opacity-60">{addPeriod(relayMessage(status, detail))}</p>
|
|
||||||
</div>
|
|
||||||
<Button class="link shrink-0 px-1" onclick={stopPropagation(() => retry(url))}>
|
|
||||||
Retry
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!containerEl) return
|
if (!containerEl) return
|
||||||
containerEl.addEventListener("touchmove", onTouchMove, {passive: false})
|
containerEl.addEventListener("touchmove", onTouchMove, {passive: false})
|
||||||
return () => containerEl?.removeEventListener("touchmove", onTouchMove)
|
return () => containerEl!.removeEventListener("touchmove", onTouchMove)
|
||||||
})
|
})
|
||||||
|
|
||||||
const onActionClick = () => {
|
const onActionClick = () => {
|
||||||
@@ -71,7 +71,6 @@
|
|||||||
|
|
||||||
{#if $toast}
|
{#if $toast}
|
||||||
{@const theme = $toast.theme || "info"}
|
{@const theme = $toast.theme || "info"}
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
||||||
<div
|
<div
|
||||||
bind:this={containerEl}
|
bind:this={containerEl}
|
||||||
transition:fly={{y: -20}}
|
transition:fly={{y: -20}}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import VideoCallTile from "@app/components/VideoCallTile.svelte"
|
import VideoCallTile from "@app/components/VideoCallTile.svelte"
|
||||||
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
||||||
import VoiceParticipantMediaBadges from "@app/components/VoiceParticipantMediaBadges.svelte"
|
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import {
|
import {
|
||||||
VideoCallLayout,
|
VideoCallLayout,
|
||||||
@@ -19,12 +18,7 @@
|
|||||||
ViewportSize,
|
ViewportSize,
|
||||||
videoPrimaryTileKey,
|
videoPrimaryTileKey,
|
||||||
} from "@app/call/video"
|
} from "@app/call/video"
|
||||||
import {
|
import {currentVoiceSession, currentVoiceRoom, pubkeyFromLiveKitIdentity} from "@app/call/stores"
|
||||||
currentVoiceSession,
|
|
||||||
currentVoiceRoom,
|
|
||||||
mediaStateByIdentity,
|
|
||||||
pubkeyFromLiveKitIdentity,
|
|
||||||
} from "@app/call/stores"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layout: VideoCallLayout
|
layout: VideoCallLayout
|
||||||
@@ -127,25 +121,6 @@
|
|||||||
source: Track.Source.ScreenShare,
|
source: Track.Source.ScreenShare,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!videoTiles.some(t => t.identity === rp.identity)) {
|
|
||||||
videoTiles.push({
|
|
||||||
identity: rp.identity,
|
|
||||||
isLocal: false,
|
|
||||||
trackSid: `avatar-${rp.identity}`,
|
|
||||||
track: undefined,
|
|
||||||
source: Track.Source.Camera,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!videoTiles.some(t => t.identity === user.identity)) {
|
|
||||||
videoTiles.push({
|
|
||||||
identity: user.identity,
|
|
||||||
isLocal: true,
|
|
||||||
trackSid: "local-avatar",
|
|
||||||
track: undefined,
|
|
||||||
source: Track.Source.Camera,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return videoTiles
|
return videoTiles
|
||||||
@@ -169,9 +144,6 @@
|
|||||||
|
|
||||||
const useSpotlightLayout = $derived(primaryTile !== undefined)
|
const useSpotlightLayout = $derived(primaryTile !== undefined)
|
||||||
const useMultiGrid = $derived(!useSpotlightLayout && videoTiles.length > 2)
|
const useMultiGrid = $derived(!useSpotlightLayout && videoTiles.length > 2)
|
||||||
const multiGridClass = $derived(
|
|
||||||
layout === VideoCallLayout.Split ? "grid-cols-1" : "grid-cols-1 sm:grid-cols-2",
|
|
||||||
)
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const k = $videoPrimaryTileKey
|
const k = $videoPrimaryTileKey
|
||||||
@@ -212,7 +184,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet videoTile(tile: VideoTileData, layout: TileLayout)}
|
{#snippet videoTile(tile: VideoTileData, layout: TileLayout)}
|
||||||
{@const media = $mediaStateByIdentity(tile.identity)}
|
|
||||||
<div
|
<div
|
||||||
class={cx(
|
class={cx(
|
||||||
"relative isolate overflow-hidden rounded-box shadow-sm",
|
"relative isolate overflow-hidden rounded-box shadow-sm",
|
||||||
@@ -232,15 +203,6 @@
|
|||||||
<ProfileCircle pubkey={pubkeyFromLiveKitIdentity(tile.identity)} {url} size={14} />
|
<ProfileCircle pubkey={pubkeyFromLiveKitIdentity(tile.identity)} {url} size={14} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if tile.track}
|
|
||||||
<div class="pointer-events-none absolute left-1 top-1 z-10">
|
|
||||||
<VoiceParticipantMediaBadges
|
|
||||||
muted={media.muted}
|
|
||||||
cameraOn={media.cameraOn}
|
|
||||||
showCamera={tile.source === Track.Source.Camera}
|
|
||||||
size={3} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<span
|
<span
|
||||||
class="pointer-events-none absolute bottom-1 left-1 max-w-[calc(100%-0.5rem)] truncate rounded bg-base-100/80 px-1.5 py-0.5 text-xs">
|
class="pointer-events-none absolute bottom-1 left-1 max-w-[calc(100%-0.5rem)] truncate rounded bg-base-100/80 px-1.5 py-0.5 text-xs">
|
||||||
{labelFor(tile.identity, tile.source)}{tile.isLocal ? " (you)" : ""}
|
{labelFor(tile.identity, tile.source)}{tile.isLocal ? " (you)" : ""}
|
||||||
@@ -251,8 +213,8 @@
|
|||||||
data-tip={pinned ? "Exit spotlight" : "Spotlight"}
|
data-tip={pinned ? "Exit spotlight" : "Spotlight"}
|
||||||
aria-pressed={pinned}
|
aria-pressed={pinned}
|
||||||
class={cx(
|
class={cx(
|
||||||
"absolute right-1 top-1 z-20 btn btn-xs btn-square",
|
"absolute right-1 top-1 z-20 btn btn-xs btn-square btn-ghost",
|
||||||
pinned ? "btn-primary" : "btn-ghost bg-base-100",
|
pinned ? "btn-active bg-primary/25 text-primary" : "bg-base-100/70",
|
||||||
)}
|
)}
|
||||||
onclick={spotlightHandlerFor(tileKey(tile))}>
|
onclick={spotlightHandlerFor(tileKey(tile))}>
|
||||||
<Icon icon={Pin} size={3} />
|
<Icon icon={Pin} size={3} />
|
||||||
@@ -276,7 +238,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if useMultiGrid}
|
{:else if useMultiGrid}
|
||||||
<div class={cx("grid min-h-0 flex-1 content-start gap-2 overflow-y-auto", multiGridClass)}>
|
<div
|
||||||
|
class="grid min-h-0 flex-1 grid-cols-1 content-start gap-2 overflow-y-auto sm:grid-cols-2">
|
||||||
{#each videoTiles as tile (tileKey(tile))}
|
{#each videoTiles as tile (tileKey(tile))}
|
||||||
{@render videoTile(tile, "default")}
|
{@render videoTile(tile, "default")}
|
||||||
{/each}
|
{/each}
|
||||||
@@ -291,10 +254,8 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="flex min-h-[12rem] flex-1 flex-col items-center justify-center gap-2 rounded-box bg-base-200/50 p-4 text-center text-sm opacity-80">
|
class="flex min-h-[12rem] flex-1 flex-col items-center justify-center gap-2 rounded-box bg-base-200/50 p-4 text-center text-sm opacity-80">
|
||||||
<p>No one is sharing video yet.</p>
|
<p>No camera or screen share yet.</p>
|
||||||
<p class="text-xs">
|
<p class="text-xs">Use the camera or screen share control to share video.</p>
|
||||||
Participants appear here when they turn on their camera or share their screen.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||