Compare commits

..

2 Commits

123 changed files with 1133 additions and 2639 deletions
+1
View File
@@ -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 -1
View File
@@ -19,6 +19,6 @@ VITE_DEFAULT_SEARCH_RELAYS=relay.ditto.pub,antiprimal.net,relay.vertexlab.io
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com,relay.keychat.io,relay.ditto.pub VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com,relay.keychat.io,relay.ditto.pub
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
VITE_THUMBNAIL_URL=https://vthumbs.coracle.social VITE_THUMBNAIL_URL=
VITE_GLITCHTIP_API_KEY= VITE_GLITCHTIP_API_KEY=
GLITCHTIP_AUTH_TOKEN= GLITCHTIP_AUTH_TOKEN=
+1 -2
View File
@@ -1,5 +1,4 @@
src/assets src/assets
.claude
target target
build build
.idea .idea
@@ -14,4 +13,4 @@ ios/App/Pods/
android/capacitor-cordova-android-plugins android/capacitor-cordova-android-plugins
android/app/src/androidTest android/app/src/androidTest
android/app/src/test android/app/src/test
node_modules
@@ -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 }}
-2
View File
@@ -27,7 +27,6 @@ android/app/src/main/assets/public/
node_modules/ node_modules/
.pnpm-store/ .pnpm-store/
build/ build/
build-server/
.svelte-kit/ .svelte-kit/
.next/ .next/
@@ -71,7 +70,6 @@ GoogleService-Info.plist
.roo .roo
.idea/ .idea/
.vscode/ .vscode/
.claude/
# OS generated # OS generated
.DS_Store .DS_Store
-57
View File
@@ -1,62 +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
* Fix safe area inset for FAB
# 1.7.3
* Add native share support for space invites
* Stop sending duplicate requests per room
* Add more robust thumbnail url generation
* Make space reordering discoverable with smoother drag animation
* Improve relay member list
* Add room mentions and clickable room/relay refs
* Support native clipboard image paste on mobile
* publish kind 9 quote after room content creation for cross-client interoperability
* Improve feed pagination logic and performance
* Support Aegis URL scheme for NIP-46 login
* Various UI and bug fixes
* Raise message size limit in chat
* Fix realtime updates for room members and admins
* Add video to calls
* Remove follow graph building
* Add start chat FAB
* Add drafts
* Redesign toast notifications
* Remove room/space leave indications
* Hide report badge for non-admin users
* Add polls
* Add search to recent activity page
* Fix notification badge on mobile nav
* Change audio devices in call
# 1.7.2 # 1.7.2
* Fix race condition in nip 46 * Fix race condition in nip 46
+26 -25
View File
@@ -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"]
+3 -3
View File
@@ -31,18 +31,18 @@ To run your own Flotilla, it's as simple as:
```sh ```sh
pnpm install pnpm install
pnpm run build pnpm run build
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'
``` ```
+2 -2
View File
@@ -8,8 +8,8 @@ android {
applicationId "social.flotilla" applicationId "social.flotilla"
minSdk rootProject.ext.minSdkVersion minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion targetSdk rootProject.ext.targetSdkVersion
versionCode 47 versionCode 44
versionName "1.8.0" versionName "1.7.2"
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.
-2
View File
@@ -12,12 +12,10 @@ dependencies {
implementation project(':aparajita-capacitor-secure-storage') implementation project(':aparajita-capacitor-secure-storage')
implementation project(':capacitor-community-safe-area') implementation project(':capacitor-community-safe-area')
implementation project(':capacitor-app') implementation project(':capacitor-app')
implementation project(':capacitor-clipboard')
implementation project(':capacitor-filesystem') implementation project(':capacitor-filesystem')
implementation project(':capacitor-keyboard') implementation project(':capacitor-keyboard')
implementation project(':capacitor-preferences') implementation project(':capacitor-preferences')
implementation project(':capacitor-push-notifications') implementation project(':capacitor-push-notifications')
implementation project(':capacitor-share')
implementation project(':capawesome-capacitor-android-dark-mode-support') implementation project(':capawesome-capacitor-android-dark-mode-support')
implementation project(':capawesome-capacitor-badge') implementation project(':capawesome-capacitor-badge')
implementation project(':nostr-signer-capacitor-plugin') implementation project(':nostr-signer-capacitor-plugin')
@@ -7,7 +7,6 @@ import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
import androidx.work.OutOfQuotaPolicy
import androidx.work.PeriodicWorkRequest import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager import androidx.work.WorkManager
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
@@ -77,7 +76,6 @@ class AndroidPushFallbackPlugin : Plugin() {
val immediate = OneTimeWorkRequest.Builder(AndroidPushFallbackWorker::class.java) val immediate = OneTimeWorkRequest.Builder(AndroidPushFallbackWorker::class.java)
.setConstraints(constraints) .setConstraints(constraints)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build() .build()
workManager.enqueueUniquePeriodicWork( workManager.enqueueUniquePeriodicWork(
@@ -43,7 +43,7 @@ class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Wo
private const val TAG = "PushFallback" private const val TAG = "PushFallback"
private const val CHANNEL_ID = "flotilla_fallback" private const val CHANNEL_ID = "flotilla_fallback"
private const val CURSOR_PREFIX = "androidPushFallback.cursor." private const val CURSOR_PREFIX = "androidPushFallback.cursor."
private const val SOCKET_TIMEOUT_SECONDS = 30L private const val SOCKET_TIMEOUT_SECONDS = 20L
private const val REJECTED = "__REJECTED__" private const val REJECTED = "__REJECTED__"
private const val KIND_RELAY_AUTH = 22242 private const val KIND_RELAY_AUTH = 22242
private const val KIND_NIP46_RPC = 24133 private const val KIND_NIP46_RPC = 24133
@@ -72,8 +72,6 @@ class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Wo
} }
override fun doWork(): Result { override fun doWork(): Result {
Log.i(TAG, "doWork() started")
if (isAppInForeground()) { if (isAppInForeground()) {
return Result.success() return Result.success()
} }
@@ -90,7 +88,7 @@ class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Wo
val activeSince = state.optLong("activeSince", 0L) val activeSince = state.optLong("activeSince", 0L)
val seen = mutableSetOf<String>() val seen = mutableSetOf<String>()
val newEvents = mutableListOf<Pair<String, JSONObject>>() var latestPair: Pair<String, JSONObject>? = null
for (sub in subscriptions) { for (sub in subscriptions) {
val cursorKey = CURSOR_PREFIX + sub.relay + ":" + sub.key val cursorKey = CURSOR_PREFIX + sub.relay + ":" + sub.key
@@ -104,19 +102,23 @@ class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Wo
for (event in result.events) { for (event in result.events) {
val id = event.optString("id", "") val id = event.optString("id", "")
if (id.isNotEmpty() && seen.add(id)) { if (id.isNotEmpty() && seen.add(id)) {
newEvents.add(Pair(sub.relay, event)) val createdAt = event.optLong("created_at", 0L)
if (latestPair == null || createdAt > latestPair!!.second.optLong("created_at", 0L)) {
latestPair = Pair(sub.relay, event)
}
} }
} }
} }
for ((relay, event) in newEvents) { if (latestPair != null) {
val (relay, event) = latestPair!!
postNotification(relay, event) postNotification(relay, event)
} }
return Result.success() return Result.success()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Worker failed", e) Log.e(TAG, "Worker failed", e)
return Result.retry() return Result.success()
} finally { } finally {
pool.closeAll() pool.closeAll()
client.dispatcher.executorService.shutdown() client.dispatcher.executorService.shutdown()
@@ -212,8 +214,7 @@ class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Wo
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.build() .build()
val notificationId = id.hashCode().let { if (it == 0) 1 else it } NotificationManagerCompat.from(context).notify(1, notification)
NotificationManagerCompat.from(context).notify(notificationId, notification)
} }
private fun matchesFilter(filter: JSONObject, event: JSONObject): Boolean { private fun matchesFilter(filter: JSONObject, event: JSONObject): Boolean {
-6
View File
@@ -11,9 +11,6 @@ project(':capacitor-community-safe-area').projectDir = new File('../node_modules
include ':capacitor-app' include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app/android') project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app/android')
include ':capacitor-clipboard'
project(':capacitor-clipboard').projectDir = new File('../node_modules/.pnpm/@capacitor+clipboard@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/clipboard/android')
include ':capacitor-filesystem' include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem/android') project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem/android')
@@ -26,9 +23,6 @@ project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@
include ':capacitor-push-notifications' include ':capacitor-push-notifications'
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications/android') project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications/android')
include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/share/android')
include ':capawesome-capacitor-android-dark-mode-support' include ':capawesome-capacitor-android-dark-mode-support'
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-android-dark-mode-support/android') project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-android-dark-mode-support/android')
+11 -29
View File
@@ -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 = 35;
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.2;
"@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 = 35;
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.2;
"@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 = "";
-2
View File
@@ -14,12 +14,10 @@ def capacitor_pods
pod 'AparajitaCapacitorSecureStorage', :path => '../../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage' pod 'AparajitaCapacitorSecureStorage', :path => '../../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage'
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area' pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area'
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app' pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app'
pod 'CapacitorClipboard', :path => '../../node_modules/.pnpm/@capacitor+clipboard@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/clipboard'
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem' pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem'
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/keyboard' pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/keyboard'
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences' pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications' pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications'
pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/share'
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge' pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge'
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin' pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin'
end end
+13 -23
View File
@@ -1,12 +1,10 @@
{ {
"name": "flotilla", "name": "flotilla",
"version": "1.8.0", "version": "1.7.2",
"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:dev": "tauri dev",
"tauri:build": "tauri build", "tauri:build": "tauri build",
@@ -50,48 +48,41 @@
"@capacitor/android": "^8.0.1", "@capacitor/android": "^8.0.1",
"@capacitor/app": "^8.0.0", "@capacitor/app": "^8.0.0",
"@capacitor/cli": "^8.0.1", "@capacitor/cli": "^8.0.1",
"@capacitor/clipboard": "^8.0.1",
"@capacitor/core": "^8.0.1", "@capacitor/core": "^8.0.1",
"@capacitor/filesystem": "^8.1.0", "@capacitor/filesystem": "^8.1.0",
"@capacitor/ios": "^8.0.1", "@capacitor/ios": "^8.0.1",
"@capacitor/keyboard": "^8.0.0", "@capacitor/keyboard": "^8.0.0",
"@capacitor/preferences": "^8.0.0", "@capacitor/preferences": "^8.0.0",
"@capacitor/push-notifications": "^8.0.0", "@capacitor/push-notifications": "^8.0.0",
"@capacitor/share": "^8.0.1",
"@capawesome/capacitor-android-dark-mode-support": "^8.0.0", "@capawesome/capacitor-android-dark-mode-support": "^8.0.0",
"@capawesome/capacitor-badge": "^8.0.0", "@capawesome/capacitor-badge": "^8.0.0",
"@getalby/lightning-tools": "^6.1.0", "@getalby/lightning-tools": "^6.1.0",
"@getalby/sdk": "^5.1.2", "@getalby/sdk": "^5.1.2",
"@hono/node-server": "^2.0.0",
"@noble/curves": "^1.9.7", "@noble/curves": "^1.9.7",
"@pomade/core": "^0.2.3", "@pomade/core": "^0.2.2",
"@poppanator/sveltekit-svg": "^4.2.1", "@poppanator/sveltekit-svg": "^4.2.1",
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@tiptap/core": "^2.27.2", "@tiptap/core": "^2.27.2",
"@tiptap/pm": "^2.27.2",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@types/throttle-debounce": "^5.0.2", "@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.8", "@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.8.15", "@welshman/app": "^0.8.12",
"@welshman/content": "^0.8.15", "@welshman/content": "^0.8.12",
"@welshman/editor": "^0.8.15", "@welshman/editor": "^0.8.12",
"@welshman/feeds": "^0.8.15", "@welshman/feeds": "^0.8.12",
"@welshman/lib": "^0.8.15", "@welshman/lib": "^0.8.12",
"@welshman/net": "^0.8.15", "@welshman/net": "^0.8.12",
"@welshman/router": "^0.8.15", "@welshman/router": "^0.8.12",
"@welshman/signer": "^0.8.15", "@welshman/signer": "^0.8.12",
"@welshman/store": "^0.8.15", "@welshman/store": "^0.8.12",
"@welshman/util": "^0.8.15", "@welshman/util": "^0.8.12",
"cheerio": "^1.2.0",
"compressorjs-next": "^1.1.2", "compressorjs-next": "^1.1.2",
"daisyui": "^5.5.19", "daisyui": "^5.5.19",
"date-picker-svelte": "^2.17.0", "date-picker-svelte": "^2.17.0",
"dotenv": "^16.6.1", "dotenv": "^16.6.1",
"emoji-picker-element": "^1.28.1", "emoji-picker-element": "^1.28.1",
"emoji-picker-element-data": "^1.8.0",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"hono": "^4.12.15",
"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",
@@ -114,6 +105,5 @@
"overrides": { "overrides": {
"sharp": "0.35.0-rc.0" "sharp": "0.35.0-rc.0"
} }
}, }
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
} }
+114 -290
View File
@@ -26,9 +26,6 @@ importers:
'@capacitor/cli': '@capacitor/cli':
specifier: ^8.0.1 specifier: ^8.0.1
version: 8.0.1 version: 8.0.1
'@capacitor/clipboard':
specifier: ^8.0.1
version: 8.0.1(@capacitor/core@8.0.1)
'@capacitor/core': '@capacitor/core':
specifier: ^8.0.1 specifier: ^8.0.1
version: 8.0.1 version: 8.0.1
@@ -47,9 +44,6 @@ importers:
'@capacitor/push-notifications': '@capacitor/push-notifications':
specifier: ^8.0.0 specifier: ^8.0.0
version: 8.0.0(@capacitor/core@8.0.1) version: 8.0.0(@capacitor/core@8.0.1)
'@capacitor/share':
specifier: ^8.0.1
version: 8.0.1(@capacitor/core@8.0.1)
'@capawesome/capacitor-android-dark-mode-support': '@capawesome/capacitor-android-dark-mode-support':
specifier: ^8.0.0 specifier: ^8.0.0
version: 8.0.0(@capacitor/core@8.0.1) version: 8.0.0(@capacitor/core@8.0.1)
@@ -62,15 +56,12 @@ importers:
'@getalby/sdk': '@getalby/sdk':
specifier: ^5.1.2 specifier: ^5.1.2
version: 5.1.2(typescript@5.9.3) version: 5.1.2(typescript@5.9.3)
'@hono/node-server':
specifier: ^2.0.0
version: 2.0.0(hono@4.12.15)
'@noble/curves': '@noble/curves':
specifier: ^1.9.7 specifier: ^1.9.7
version: 1.9.7 version: 1.9.7
'@pomade/core': '@pomade/core':
specifier: ^0.2.3 specifier: ^0.2.2
version: 0.2.3(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.15(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3)) version: 0.2.2(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))
'@poppanator/sveltekit-svg': '@poppanator/sveltekit-svg':
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.2.1(rollup@2.80.0)(svelte@5.48.0)(svgo@3.3.2)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0)) version: 4.2.1(rollup@2.80.0)(svelte@5.48.0)(svgo@3.3.2)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0))
@@ -80,9 +71,6 @@ importers:
'@tiptap/core': '@tiptap/core':
specifier: ^2.27.2 specifier: ^2.27.2
version: 2.27.2(@tiptap/pm@2.27.2) version: 2.27.2(@tiptap/pm@2.27.2)
'@tiptap/pm':
specifier: ^2.27.2
version: 2.27.2
'@types/qrcode': '@types/qrcode':
specifier: ^1.5.6 specifier: ^1.5.6
version: 1.5.6 version: 1.5.6
@@ -96,38 +84,35 @@ importers:
specifier: ^0.6.8 specifier: ^0.6.8
version: 0.6.8(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.48.0)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0)))(svelte@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0))(workbox-build@7.3.0)(workbox-window@7.3.0)) version: 0.6.8(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.48.0)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0)))(svelte@5.48.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0)))(@vite-pwa/assets-generator@0.2.6)(vite-plugin-pwa@0.21.2(@vite-pwa/assets-generator@0.2.6)(vite@5.4.21(@types/node@25.0.10)(lightningcss@1.32.0)(terser@5.46.0))(workbox-build@7.3.0)(workbox-window@7.3.0))
'@welshman/app': '@welshman/app':
specifier: ^0.8.15 specifier: ^0.8.12
version: 0.8.15(ff026297546a8274624eb18a0ea86191) version: 0.8.12(3074ef6691f94dc03952d8dbc98013a7)
'@welshman/content': '@welshman/content':
specifier: ^0.8.15 specifier: ^0.8.12
version: 0.8.15(nostr-tools@2.20.0(typescript@5.9.3)) version: 0.8.12(nostr-tools@2.20.0(typescript@5.9.3))
'@welshman/editor': '@welshman/editor':
specifier: ^0.8.15 specifier: ^0.8.12
version: 0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-editor@1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))))(nostr-tools@2.20.0(typescript@5.9.3)) version: 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-editor@1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))))(nostr-tools@2.20.0(typescript@5.9.3))
'@welshman/feeds': '@welshman/feeds':
specifier: ^0.8.15 specifier: ^0.8.12
version: 0.8.15(6e55dcd4e7516745e7b0228620d35545) version: 0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)
'@welshman/lib': '@welshman/lib':
specifier: ^0.8.15 specifier: ^0.8.12
version: 0.8.15 version: 0.8.12
'@welshman/net': '@welshman/net':
specifier: ^0.8.15 specifier: ^0.8.12
version: 0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3) version: 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
'@welshman/router': '@welshman/router':
specifier: ^0.8.15 specifier: ^0.8.12
version: 0.8.15(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3))) version: 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))
'@welshman/signer': '@welshman/signer':
specifier: ^0.8.15 specifier: ^0.8.12
version: 0.8.15(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)) version: 0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
'@welshman/store': '@welshman/store':
specifier: ^0.8.15 specifier: ^0.8.12
version: 0.8.15(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0) version: 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)
'@welshman/util': '@welshman/util':
specifier: ^0.8.15 specifier: ^0.8.12
version: 0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)) version: 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
cheerio:
specifier: ^1.2.0
version: 1.2.0
compressorjs-next: compressorjs-next:
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2 version: 1.1.2
@@ -143,15 +128,9 @@ importers:
emoji-picker-element: emoji-picker-element:
specifier: ^1.28.1 specifier: ^1.28.1
version: 1.28.1 version: 1.28.1
emoji-picker-element-data:
specifier: ^1.8.0
version: 1.8.0
fuse.js: fuse.js:
specifier: ^7.1.0 specifier: ^7.1.0
version: 7.1.0 version: 7.1.0
hono:
specifier: ^4.12.15
version: 4.12.15
husky: husky:
specifier: ^9.1.7 specifier: ^9.1.7
version: 9.1.7 version: 9.1.7
@@ -812,11 +791,6 @@ packages:
engines: {node: '>=22.0.0'} engines: {node: '>=22.0.0'}
hasBin: true hasBin: true
'@capacitor/clipboard@8.0.1':
resolution: {integrity: sha512-iOlbTi8MojKyLnYE+M27priXid7vHd0PlDwyHohPzkuQ8Rkp6q7ykwZmPEUD+OnU/Ink7Qw/pUOfKgraKmA6Eg==}
peerDependencies:
'@capacitor/core': '>=8.0.0'
'@capacitor/core@8.0.1': '@capacitor/core@8.0.1':
resolution: {integrity: sha512-5UqSWxGMp/B8KhYu7rAijqNtYslhcLh+TrbfU48PfdMDsPfaU/VY48sMNzC22xL8BmoFoql/3SKyP+pavTOvOA==} resolution: {integrity: sha512-5UqSWxGMp/B8KhYu7rAijqNtYslhcLh+TrbfU48PfdMDsPfaU/VY48sMNzC22xL8BmoFoql/3SKyP+pavTOvOA==}
@@ -853,11 +827,6 @@ packages:
peerDependencies: peerDependencies:
'@capacitor/core': '>=8.0.0' '@capacitor/core': '>=8.0.0'
'@capacitor/share@8.0.1':
resolution: {integrity: sha512-3cSBKBCJVon54rKDROP2rqGyeGks4pBh9TbaEk9S375Kbek/ZHe72N50zIa0Vn9Eac/SuhwgehO/mmA4CsUOiw==}
peerDependencies:
'@capacitor/core': '>=8.0.0'
'@capacitor/synapse@1.0.4': '@capacitor/synapse@1.0.4':
resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==} resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==}
@@ -1108,12 +1077,6 @@ packages:
resolution: {integrity: sha512-yUF9LhuvdIFOwjV1aG0ryzfwDiGBFk/CRLkRvrrM9dsE38SUjKsf1FDga5jxsKMu80nWcPZR9TiGGASWedoYPA==} resolution: {integrity: sha512-yUF9LhuvdIFOwjV1aG0ryzfwDiGBFk/CRLkRvrrM9dsE38SUjKsf1FDga5jxsKMu80nWcPZR9TiGGASWedoYPA==}
engines: {node: '>=14'} engines: {node: '>=14'}
'@hono/node-server@2.0.0':
resolution: {integrity: sha512-n3GfHwwCvHCkGmOwKfxUPOlbfzuO64Sbc5XC4NGPIXxkuOnJrdgExdRKmHfF924r914WRJPT397GdqLvdYTeyQ==}
engines: {node: '>=20'}
peerDependencies:
hono: ^4
'@humanfs/core@0.19.1': '@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'} engines: {node: '>=18.18.0'}
@@ -1464,9 +1427,9 @@ packages:
'@polka/url@1.0.0-next.29': '@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@pomade/core@0.2.3': '@pomade/core@0.2.2':
resolution: {integrity: sha512-36+abWfMH1Mif9FjBO7xICCkGZE4IqQpy7Csxlauyt0bhYQ9GsB07LqK5Qm3GgEHNwF9rFXdSZawkM+E6BeIfg==} resolution: {integrity: sha512-FoilLsO0gVjiKMW3LV63pmXU7x3gh8YVGVulyR6QJr4h47XrsBg8vPkZtKWr4+sH3sW31e2tNIPUb3ptiuhrMA==}
version: 0.2.3 version: 0.2.2
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
peerDependencies: peerDependencies:
'@frostr/bifrost': ^1.0.7 '@frostr/bifrost': ^1.0.7
@@ -2168,83 +2131,83 @@ packages:
'@vite-pwa/assets-generator': '@vite-pwa/assets-generator':
optional: true optional: true
'@welshman/app@0.8.15': '@welshman/app@0.8.12':
resolution: {integrity: sha512-GDo6w+UI/ldnh47c5IEDYWw8nbiyhnH4abJNy/q/jLBUwJ9SuiJ7GVVvhZ+t4XEo5NEMq+y4OLZs08+abf85MQ==} resolution: {integrity: sha512-kRp+AVzn4i3FvZmdlyMknFUAb/5SnUz9A/cFKkDqWHsd+N3PbNcL2ZOlV9v5NI77GtsDF2ez6PEQfsZxWvkS/g==}
peerDependencies: peerDependencies:
'@pomade/core': ^0.2.1 '@pomade/core': ^0.2.1
'@welshman/feeds': 0.8.15 '@welshman/feeds': 0.8.12
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/net': 0.8.15 '@welshman/net': 0.8.12
'@welshman/router': 0.8.15 '@welshman/router': 0.8.12
'@welshman/signer': 0.8.15 '@welshman/signer': 0.8.12
'@welshman/store': 0.8.15 '@welshman/store': 0.8.12
'@welshman/util': 0.8.15 '@welshman/util': 0.8.12
svelte: ^4.0.0 || ^5.0.0 svelte: ^4.0.0 || ^5.0.0
'@welshman/content@0.8.15': '@welshman/content@0.8.12':
resolution: {integrity: sha512-5qe+6Es1r62HkVdeHJPsWkOpLjhdxBTtw3d4+Or1JXl8BgpUE2JV7e+5HQQqnPRVHt3nt14YPt0oirar5p1Fvg==} resolution: {integrity: sha512-hviVTXdyGf04Xq7mGo/82fq6lnbyuUYOGPkf8pJqPkfGh0f3i9nKof6gkzPvjZeEYSczneI2GpLIxkaZ3w1/tw==}
peerDependencies: peerDependencies:
nostr-tools: ^2.19.4 nostr-tools: ^2.19.4
'@welshman/editor@0.8.15': '@welshman/editor@0.8.12':
resolution: {integrity: sha512-lqTLQGf54yPioBn2KQsF7F5ExWM6Co31wgGaUAhCSeUGiTzUQgMEut4/N8VB1rFZ0wqU6zyPG5jgeuhFhRJWSw==} resolution: {integrity: sha512-CEXszH6pfM1kZHU9WnB6Z97YlxEOtgao6R3hhnBL/kRXy2tUTLpmFWyMONg2vu8Uzxtwz665eZhucLsju60U6w==}
peerDependencies: peerDependencies:
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/util': 0.8.15 '@welshman/util': 0.8.12
nostr-editor: ^1.1.1 nostr-editor: ^1.1.1
nostr-tools: ^2.19.4 nostr-tools: ^2.19.4
'@welshman/feeds@0.8.15': '@welshman/feeds@0.8.12':
resolution: {integrity: sha512-xIQDKdV6uLxOz5qJUbc/2HC6qnikgH1GPoHQwBpwKH7Lga6a7IGLOR6kvghUaPpulKcuF4MxG9gmvEHqgsQkJw==} resolution: {integrity: sha512-Dp/063qdrbe096z6IpIneazsqscfzSwsg09ZNHB6c3OsVb6iLEXLe7mEpATGcRsZ8memuFNLVLIP51eVkBqPcQ==}
peerDependencies: peerDependencies:
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/net': 0.8.15 '@welshman/net': 0.8.12
'@welshman/router': 0.8.15 '@welshman/router': 0.8.12
'@welshman/signer': 0.8.15 '@welshman/signer': 0.8.12
'@welshman/util': 0.8.15 '@welshman/util': 0.8.12
'@welshman/lib@0.8.15': '@welshman/lib@0.8.12':
resolution: {integrity: sha512-d7o6WUSVYXOstpWTqOBDfkSyr3GOBm/UMbgFx3RXCxzib0cWm7z0w1oLWvy1N7fjHc/Jp65G2KRpT6//B9yAww==} resolution: {integrity: sha512-7Y1GjAcABquWF47A1Jni5JdP+k0GH2yRmEbVhIU+0R0TubCwPAKS38J2LTvtuE9CJMX6hPS9IKEZS6qTOAaVuw==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
'@welshman/net@0.8.15': '@welshman/net@0.8.12':
resolution: {integrity: sha512-AeJ/Vy7T6ruf1mjzzEUdH+aX5JriQKBzRn1zWZ4l8VEgxwc4w2bVte9a6aPnNJWc7JZT8ws8z+wOi4ECb6NPNA==} resolution: {integrity: sha512-Ba71jwb8BBwUfPPtWHKYLB0HqeDYK64oqwxfo5bYldtPfGYrlD+a7lHFSZvNyOhvyXygCaIMnaye1QxWAHP8ng==}
peerDependencies: peerDependencies:
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/util': 0.8.15 '@welshman/util': 0.8.12
'@welshman/router@0.8.15': '@welshman/router@0.8.12':
resolution: {integrity: sha512-3lxcCYMaPX0gFaoM1GjBRvXr4UrnPA3o/mBII2Zm3gJeFuXN3XG+REwIN6QNhvTB7syTCTwx+dRdHgvqHl9N6g==} resolution: {integrity: sha512-Rr7ryBNTvTvjoLsDRMKPuoNJbBv2MgyqN2338p4vVhPMK6MOGM3Nx1og0LpHDGiLlCFuPM8RpParpIWfNnWbKw==}
peerDependencies: peerDependencies:
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/net': 0.8.15 '@welshman/net': 0.8.12
'@welshman/util': 0.8.15 '@welshman/util': 0.8.12
'@welshman/signer@0.8.15': '@welshman/signer@0.8.12':
resolution: {integrity: sha512-Y96XZtsCHz8h7NK28sSi3CX+8lGG6WhLyVNyhlEhfypAxxx8Zpfr4GlSPApvp4tvm1//YfDtXHIIZTPXbmnqvA==} resolution: {integrity: sha512-eO4mw2QOR2d2oCS4zgptkCgjC1s8X+1vNnXfWDbtlEwhk7PD4ySSCkpNVMeElq+uluLOugmKvgDc4gfnIH3p2A==}
version: 0.8.15 version: 0.8.12
peerDependencies: peerDependencies:
'@noble/curves': ^1.9.7 '@noble/curves': ^1.9.7
'@noble/hashes': ^2.0.1 '@noble/hashes': ^2.0.1
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/net': 0.8.15 '@welshman/net': 0.8.12
'@welshman/util': 0.8.15 '@welshman/util': 0.8.12
nostr-signer-capacitor-plugin: '*' nostr-signer-capacitor-plugin: '*'
nostr-tools: ^2.19.4 nostr-tools: ^2.19.4
'@welshman/store@0.8.15': '@welshman/store@0.8.12':
resolution: {integrity: sha512-3rQVhAsQ1z5tcUzkJPkzVp3iBkMrUKVoBi07AYefqlhRoddhwB2pDBVhdZYoP2kl9wVPZlPV58vlD6BTo6TEwA==} resolution: {integrity: sha512-3IUzPRMMVF6Pcw3DaKkfJ4S6bYzVtk6Ze3FnaHs1xJIxkQj9GyBO9VreiovULPW0djsnrP5+1UEN4mKZsG3hXg==}
peerDependencies: peerDependencies:
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/net': 0.8.15 '@welshman/net': 0.8.12
'@welshman/util': 0.8.15 '@welshman/util': 0.8.12
svelte: ^4.0.0 || ^5.0.0 svelte: ^4.0.0 || ^5.0.0
'@welshman/util@0.8.15': '@welshman/util@0.8.12':
resolution: {integrity: sha512-zeNWMyOtIpOqj9/hBAT8qWvnp5w/IyrcT7CmDKLkWt6NU6ZoZ3pF5duTwtOYZqcftYJaHXgohOt0RsHVPR3M7w==} resolution: {integrity: sha512-lgftFt2moXZdN5fuL0RoAnAARV0n0d2+Q56gt7KrBSevjoCbtJgBVX5idvxL5PCEfh81veovJtty6eHxrhQv5A==}
peerDependencies: peerDependencies:
'@noble/curves': ^1.9.7 '@noble/curves': ^1.9.7
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
nostr-tools: ^2.19.4 nostr-tools: ^2.19.4
'@xml-tools/parser@1.0.11': '@xml-tools/parser@1.0.11':
@@ -2483,13 +2446,6 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
cheerio-select@2.1.0:
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
cheerio@1.2.0:
resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==}
engines: {node: '>=20.18.1'}
chevrotain@7.1.1: chevrotain@7.1.1:
resolution: {integrity: sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==} resolution: {integrity: sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==}
@@ -2849,18 +2805,12 @@ packages:
resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==} resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
engines: {node: '>= 0.4.0'} engines: {node: '>= 0.4.0'}
emoji-picker-element-data@1.8.0:
resolution: {integrity: sha512-VfRuRJNEDLS1JKlNS4olaqhjX5S1nnZ+ZHG73b/dV8QeZyi0yPruTPEE72EmF6XO3k/9hj3lybMIYMOYXb/57A==}
emoji-picker-element@1.28.1: emoji-picker-element@1.28.1:
resolution: {integrity: sha512-8c64IPish2PWoV9oYCo2pvuPHwIv+uK9bO0dfpPyMupDAvaWL9ZvYhWNTAR+2sx7BhfRjciImqP6CIUgNX+DMg==} resolution: {integrity: sha512-8c64IPish2PWoV9oYCo2pvuPHwIv+uK9bO0dfpPyMupDAvaWL9ZvYhWNTAR+2sx7BhfRjciImqP6CIUgNX+DMg==}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
encoding-sniffer@0.2.1:
resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==}
enhanced-resolve@5.20.1: enhanced-resolve@5.20.1:
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@@ -2872,14 +2822,6 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
env-paths@2.2.1: env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -3273,10 +3215,6 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true hasBin: true
hono@4.12.15:
resolution: {integrity: sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==}
engines: {node: '>=16.9.0'}
hosted-git-info@2.8.9: hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
@@ -3284,9 +3222,6 @@ packages:
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
engines: {node: '>=10'} engines: {node: '>=10'}
htmlparser2@10.1.0:
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
husky@9.1.7: husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -3295,10 +3230,6 @@ packages:
ico-endec@0.1.6: ico-endec@0.1.6:
resolution: {integrity: sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ==} resolution: {integrity: sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ==}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
idb@7.1.1: idb@7.1.1:
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
@@ -4065,15 +3996,6 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'} engines: {node: '>=8'}
parse5-htmlparser2-tree-adapter@7.1.0:
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
parse5-parser-stream@7.1.2:
resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==}
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
path-exists@3.0.0: path-exists@3.0.0:
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -4515,9 +4437,6 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sax@1.1.4: sax@1.1.4:
resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==} resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==}
@@ -4953,10 +4872,6 @@ packages:
undici-types@7.16.0: undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
undici@7.25.0:
resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==}
engines: {node: '>=20.18.1'}
unicode-canonical-property-names-ecmascript@2.0.1: unicode-canonical-property-names-ecmascript@2.0.1:
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -5075,15 +4990,6 @@ packages:
resolution: {integrity: sha512-5ZZY1+lGq8LEKuDlg9M2RPJHlH3R7OVwyHqMcUsLKCgd9Wvf+QrFTCItkXXYPmrJn8H6gRLXbSgxLLdexiqHxw==} resolution: {integrity: sha512-5ZZY1+lGq8LEKuDlg9M2RPJHlH3R7OVwyHqMcUsLKCgd9Wvf+QrFTCItkXXYPmrJn8H6gRLXbSgxLLdexiqHxw==}
engines: {node: '>=6.0.0', npm: '>=3.10.0'} engines: {node: '>=6.0.0', npm: '>=3.10.0'}
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
whatwg-url@5.0.0: whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@@ -6063,10 +5969,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@capacitor/clipboard@8.0.1(@capacitor/core@8.0.1)':
dependencies:
'@capacitor/core': 8.0.1
'@capacitor/core@8.0.1': '@capacitor/core@8.0.1':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -6104,10 +6006,6 @@ snapshots:
dependencies: dependencies:
'@capacitor/core': 8.0.1 '@capacitor/core': 8.0.1
'@capacitor/share@8.0.1(@capacitor/core@8.0.1)':
dependencies:
'@capacitor/core': 8.0.1
'@capacitor/synapse@1.0.4': {} '@capacitor/synapse@1.0.4': {}
'@capawesome/capacitor-android-dark-mode-support@8.0.0(@capacitor/core@8.0.1)': '@capawesome/capacitor-android-dark-mode-support@8.0.0(@capacitor/core@8.0.1)':
@@ -6303,10 +6201,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
'@hono/node-server@2.0.0(hono@4.12.15)':
dependencies:
hono: 4.12.15
'@humanfs/core@0.19.1': {} '@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7': '@humanfs/node@0.16.7':
@@ -6716,15 +6610,15 @@ snapshots:
'@polka/url@1.0.0-next.29': {} '@polka/url@1.0.0-next.29': {}
'@pomade/core@0.2.3(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.15(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))': '@pomade/core@0.2.2(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))':
dependencies: dependencies:
'@frostr/bifrost': 1.0.7(typescript@5.9.3) '@frostr/bifrost': 1.0.7(typescript@5.9.3)
'@noble/hashes': 2.0.1 '@noble/hashes': 2.0.1
'@peculiar/x509': 1.14.3 '@peculiar/x509': 1.14.3
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/net': 0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3) '@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
'@welshman/signer': 0.8.15(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)) '@welshman/signer': 0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
'@welshman/util': 0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)) '@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
cbor-x: 1.6.0 cbor-x: 1.6.0
hash-wasm: 4.12.0 hash-wasm: 4.12.0
nostr-tools: 2.20.0(typescript@5.9.3) nostr-tools: 2.20.0(typescript@5.9.3)
@@ -7382,26 +7276,26 @@ snapshots:
optionalDependencies: optionalDependencies:
'@vite-pwa/assets-generator': 0.2.6 '@vite-pwa/assets-generator': 0.2.6
'@welshman/app@0.8.15(ff026297546a8274624eb18a0ea86191)': '@welshman/app@0.8.12(3074ef6691f94dc03952d8dbc98013a7)':
dependencies: dependencies:
'@pomade/core': 0.2.3(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.15(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3)) '@pomade/core': 0.2.2(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-tools@2.20.0(typescript@5.9.3))
'@welshman/feeds': 0.8.15(6e55dcd4e7516745e7b0228620d35545) '@welshman/feeds': 0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/net': 0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3) '@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
'@welshman/router': 0.8.15(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3))) '@welshman/router': 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))
'@welshman/signer': 0.8.15(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)) '@welshman/signer': 0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
'@welshman/store': 0.8.15(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0) '@welshman/store': 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)
'@welshman/util': 0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)) '@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
fuse.js: 7.1.0 fuse.js: 7.1.0
svelte: 5.48.0 svelte: 5.48.0
throttle-debounce: 5.0.2 throttle-debounce: 5.0.2
'@welshman/content@0.8.15(nostr-tools@2.20.0(typescript@5.9.3))': '@welshman/content@0.8.12(nostr-tools@2.20.0(typescript@5.9.3))':
dependencies: dependencies:
'@braintree/sanitize-url': 7.1.1 '@braintree/sanitize-url': 7.1.1
nostr-tools: 2.20.0(typescript@5.9.3) nostr-tools: 2.20.0(typescript@5.9.3)
'@welshman/editor@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-editor@1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))))(nostr-tools@2.20.0(typescript@5.9.3))': '@welshman/editor@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-editor@1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))))(nostr-tools@2.20.0(typescript@5.9.3))':
dependencies: dependencies:
'@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2)
'@tiptap/extension-code': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) '@tiptap/extension-code': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
@@ -7416,64 +7310,64 @@ snapshots:
'@tiptap/extension-text': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) '@tiptap/extension-text': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
'@tiptap/pm': 2.27.2 '@tiptap/pm': 2.27.2
'@tiptap/suggestion': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) '@tiptap/suggestion': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/util': 0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)) '@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
nostr-editor: 1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))) nostr-editor: 1.1.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/extension-image@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))(@tiptap/extension-link@2.27.1(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(linkifyjs@4.3.2)(nostr-tools@2.20.0(typescript@5.9.3))(prosemirror-markdown@1.13.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.5)(tiptap-markdown@0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)))
nostr-tools: 2.20.0(typescript@5.9.3) nostr-tools: 2.20.0(typescript@5.9.3)
tippy.js: 6.3.7 tippy.js: 6.3.7
'@welshman/feeds@0.8.15(6e55dcd4e7516745e7b0228620d35545)': '@welshman/feeds@0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)':
dependencies: dependencies:
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/net': 0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3) '@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
'@welshman/router': 0.8.15(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3))) '@welshman/router': 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))
'@welshman/signer': 0.8.15(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3)) '@welshman/signer': 0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
'@welshman/util': 0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)) '@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
trava: 1.2.1 trava: 1.2.1
'@welshman/lib@0.8.15': '@welshman/lib@0.8.12':
dependencies: dependencies:
'@scure/base': 1.2.6 '@scure/base': 1.2.6
'@types/events': 3.0.3 '@types/events': 3.0.3
events: 3.3.0 events: 3.3.0
'@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)': '@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)':
dependencies: dependencies:
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/util': 0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)) '@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
events: 3.3.0 events: 3.3.0
isomorphic-ws: 5.0.0(ws@8.18.3) isomorphic-ws: 5.0.0(ws@8.18.3)
transitivePeerDependencies: transitivePeerDependencies:
- ws - ws
'@welshman/router@0.8.15(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))': '@welshman/router@0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))':
dependencies: dependencies:
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/net': 0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3) '@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
'@welshman/util': 0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)) '@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
'@welshman/signer@0.8.15(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))': '@welshman/signer@0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))':
dependencies: dependencies:
'@noble/curves': 1.9.7 '@noble/curves': 1.9.7
'@noble/hashes': 2.0.1 '@noble/hashes': 2.0.1
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/net': 0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3) '@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
'@welshman/util': 0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)) '@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
nostr-signer-capacitor-plugin: https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1) nostr-signer-capacitor-plugin: https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1)
nostr-tools: 2.20.0(typescript@5.9.3) nostr-tools: 2.20.0(typescript@5.9.3)
'@welshman/store@0.8.15(@welshman/lib@0.8.15)(@welshman/net@0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)': '@welshman/store@0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)':
dependencies: dependencies:
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
'@welshman/net': 0.8.15(@welshman/lib@0.8.15)(@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3) '@welshman/net': 0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3)
'@welshman/util': 0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3)) '@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
svelte: 5.48.0 svelte: 5.48.0
'@welshman/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(nostr-tools@2.20.0(typescript@5.9.3))': '@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))':
dependencies: dependencies:
'@noble/curves': 1.9.7 '@noble/curves': 1.9.7
'@types/ws': 8.18.1 '@types/ws': 8.18.1
'@welshman/lib': 0.8.15 '@welshman/lib': 0.8.12
js-base64: 3.7.8 js-base64: 3.7.8
nostr-tools: 2.20.0(typescript@5.9.3) nostr-tools: 2.20.0(typescript@5.9.3)
nostr-wasm: 0.1.0 nostr-wasm: 0.1.0
@@ -7721,29 +7615,6 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
cheerio-select@2.1.0:
dependencies:
boolbase: 1.0.0
css-select: 5.2.2
css-what: 6.2.2
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
cheerio@1.2.0:
dependencies:
cheerio-select: 2.1.0
dom-serializer: 2.0.0
domhandler: 5.0.3
domutils: 3.2.2
encoding-sniffer: 0.2.1
htmlparser2: 10.1.0
parse5: 7.3.0
parse5-htmlparser2-tree-adapter: 7.1.0
parse5-parser-stream: 7.1.2
undici: 7.25.0
whatwg-mimetype: 4.0.0
chevrotain@7.1.1: chevrotain@7.1.1:
dependencies: dependencies:
regexp-to-ast: 0.5.0 regexp-to-ast: 0.5.0
@@ -8130,17 +8001,10 @@ snapshots:
dependencies: dependencies:
sax: 1.1.4 sax: 1.1.4
emoji-picker-element-data@1.8.0: {}
emoji-picker-element@1.28.1: {} emoji-picker-element@1.28.1: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
encoding-sniffer@0.2.1:
dependencies:
iconv-lite: 0.6.3
whatwg-encoding: 3.1.1
enhanced-resolve@5.20.1: enhanced-resolve@5.20.1:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@@ -8150,10 +8014,6 @@ snapshots:
entities@4.5.0: {} entities@4.5.0: {}
entities@6.0.1: {}
entities@7.0.1: {}
env-paths@2.2.1: {} env-paths@2.2.1: {}
env-paths@3.0.0: {} env-paths@3.0.0: {}
@@ -8663,29 +8523,16 @@ snapshots:
he@1.2.0: {} he@1.2.0: {}
hono@4.12.15: {}
hosted-git-info@2.8.9: {} hosted-git-info@2.8.9: {}
hosted-git-info@4.1.0: hosted-git-info@4.1.0:
dependencies: dependencies:
lru-cache: 6.0.0 lru-cache: 6.0.0
htmlparser2@10.1.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
entities: 7.0.1
husky@9.1.7: {} husky@9.1.7: {}
ico-endec@0.1.6: {} ico-endec@0.1.6: {}
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
idb@7.1.1: {} idb@7.1.1: {}
idb@8.0.3: {} idb@8.0.3: {}
@@ -9392,19 +9239,6 @@ snapshots:
json-parse-even-better-errors: 2.3.1 json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4 lines-and-columns: 1.2.4
parse5-htmlparser2-tree-adapter@7.1.0:
dependencies:
domhandler: 5.0.3
parse5: 7.3.0
parse5-parser-stream@7.1.2:
dependencies:
parse5: 7.3.0
parse5@7.3.0:
dependencies:
entities: 6.0.1
path-exists@3.0.0: {} path-exists@3.0.0: {}
path-exists@4.0.0: {} path-exists@4.0.0: {}
@@ -9843,8 +9677,6 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
is-regex: 1.2.1 is-regex: 1.2.1
safer-buffer@2.1.2: {}
sax@1.1.4: {} sax@1.1.4: {}
sax@1.4.4: {} sax@1.4.4: {}
@@ -10370,8 +10202,6 @@ snapshots:
undici-types@7.16.0: {} undici-types@7.16.0: {}
undici@7.25.0: {}
unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-canonical-property-names-ecmascript@2.0.1: {}
unicode-match-property-ecmascript@2.0.0: unicode-match-property-ecmascript@2.0.0:
@@ -10452,12 +10282,6 @@ snapshots:
dependencies: dependencies:
sdp: 3.2.1 sdp: 3.2.1
whatwg-encoding@3.1.1:
dependencies:
iconv-lite: 0.6.3
whatwg-mimetype@4.0.0: {}
whatwg-url@5.0.0: whatwg-url@5.0.0:
dependencies: dependencies:
tr46: 0.0.3 tr46: 0.0.3
-288
View File
@@ -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}`)
},
)
+13 -26
View File
@@ -2,16 +2,6 @@
@config "../tailwind.config.js"; @config "../tailwind.config.js";
/* root */
:root {
font-family: Lato;
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
--sair: var(--safe-area-inset-right, env(safe-area-inset-right));
}
@utility pt-sai { @utility pt-sai {
padding-top: var(--sait); padding-top: var(--sait);
} }
@@ -32,6 +22,16 @@
@apply pl-sai pr-sai; @apply pl-sai pr-sai;
} }
/* root */
:root {
font-family: Lato;
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
--sair: var(--safe-area-inset-right, env(safe-area-inset-right));
}
@utility py-sai { @utility py-sai {
@apply pt-sai pb-sai; @apply pt-sai pb-sai;
} }
@@ -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));
@@ -328,12 +327,12 @@
.note-editor .tiptap { .note-editor .tiptap {
--tiptap-object-bg: var(--color-base-200); --tiptap-object-bg: var(--color-base-200);
@apply input rounded-box block h-auto min-h-32 w-full p-[.65rem] pb-6; @apply input rounded-box h-auto min-h-32 p-[.65rem] pb-6;
} }
.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,28 +416,16 @@ 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 {
@apply z-compose relative mb-14 shrink-0 md:mb-0; @apply relative z-compose mb-14 shrink-0 md:mb-0;
} }
.chat__compose .chat__compose-inner { .chat__compose .chat__compose-inner {
+4 -7
View File
@@ -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}" />
+3 -18
View File
@@ -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) =>
+29 -40
View File
@@ -6,16 +6,14 @@ 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"
@@ -24,10 +22,10 @@ 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
@@ -178,7 +173,6 @@ const setUpMicrophone = async (
const onRoomDisconnected = (reason?: DisconnectReason) => { const onRoomDisconnected = (reason?: DisconnectReason) => {
videoPrimaryTileKey.set(undefined) videoPrimaryTileKey.set(undefined)
voiceMicMuted.set(true)
currentVoiceSession.set(undefined) currentVoiceSession.set(undefined)
resetVideoCallLayout() resetVideoCallLayout()
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) { if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
@@ -190,7 +184,7 @@ const onRoomDisconnected = (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) => {
@@ -220,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()
} }
@@ -279,16 +273,11 @@ export const joinVoiceRoom = async (
liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed) liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
liveKitRoom.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished) liveKitRoom.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged) liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
liveKitRoom.on(RoomEvent.TrackMuted, onParticipantMediaChanged)
liveKitRoom.on(RoomEvent.TrackUnmuted, onParticipantMediaChanged)
liveKitRoom.on(RoomEvent.TrackPublished, onParticipantMediaChanged)
liveKitRoom.on(RoomEvent.TrackUnpublished, onParticipantMediaChanged)
liveKitRoom.on(RoomEvent.LocalTrackPublished, onParticipantMediaChanged)
try { try {
await Promise.race([ await Promise.race([
liveKitRoom.connect(server_url, participant_token, {maxRetries: 0}), liveKitRoom.connect(server_url, participant_token, {maxRetries: 0}),
whenTimeout(15_000, { whenTimeout(5_000, {
message: "Connection timed out. Please check your network and try again.", message: "Connection timed out. Please check your network and try again.",
}), }),
whenAborted(signal), whenAborted(signal),
@@ -298,19 +287,19 @@ export const joinVoiceRoom = async (
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)
} }
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant) const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
voiceMicMuted.set(muted)
currentVoiceSession.set({ currentVoiceSession.set({
url, url,
h, h,
room: liveKitRoom, room: liveKitRoom,
muted,
cameraOn: false, cameraOn: false,
screenShareOn: false, screenShareOn: false,
}) })
@@ -350,12 +339,11 @@ export const leaveVoiceRoom = async () => {
voiceState.set(VoiceState.Disconnected) voiceState.set(VoiceState.Disconnected)
videoPrimaryTileKey.set(undefined) videoPrimaryTileKey.set(undefined)
voiceMicMuted.set(true)
currentVoiceSession.set(undefined) currentVoiceSession.set(undefined)
resetVideoCallLayout() resetVideoCallLayout()
session.room.disconnect() session.room.disconnect()
speakingParticipants.set([]) speakingParticipants.set([])
participantMediaState.set(new Map()) participantPubkeyMap.set(new Map())
} }
export const rejoinVoiceRoom = async (): Promise<void> => { export const rejoinVoiceRoom = async (): Promise<void> => {
@@ -368,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} />
{formatTimestampAsDate(start)} <span class="hidden sm:block">{formatTimestampAsDate(start)}</span>
</div>
{formatTimestampAsTime(start)}{isSingleDay {formatTimestampAsTime(start)}{isSingleDay
? formatTimestampAsTime(end) ? formatTimestampAsTime(end)
: formatTimestamp(end)} : formatTimestamp(end)}
+4 -5
View File
@@ -53,7 +53,7 @@
import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte" import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte"
import ChatComposeParent from "@app/components/ChatComposeParent.svelte" import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte" import ThunkToast from "@app/components/ThunkToast.svelte"
import {userSettingsValues, deriveChat, 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)))
@@ -280,7 +279,7 @@
</div> </div>
</PageBar> </PageBar>
<PageContent class="flex flex-col-reverse gap-2 py-4"> <PageContent class="flex flex-col-reverse gap-2 pt-4">
{#if missingRelayLists.length > 0} {#if missingRelayLists.length > 0}
<div class="py-12"> <div class="py-12">
<div class="card2 col-2 m-auto max-w-md items-center text-center"> <div class="card2 col-2 m-auto max-w-md items-center text-center">
-4
View File
@@ -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}
-12
View File
@@ -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>
+4 -12
View File
@@ -5,32 +5,30 @@
import {preventDefault, stopPropagation} from "@lib/html" import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte" import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import ContentLinkUrl from "@app/components/ContentLinkUrl.svelte"
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte" import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import { import {
dufflepud, dufflepud,
IMAGE_CONTENT_TYPES,
PLATFORM_URL, PLATFORM_URL,
IMAGE_CONTENT_TYPES,
VIDEO_CONTENT_TYPES, VIDEO_CONTENT_TYPES,
THUMBNAIL_URL, THUMBNAIL_URL,
isRoomId,
} from "@app/core/state" } from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
const {value, event} = $props() const {value, event} = $props()
let hideImage = $state(false) let hideImage = $state(false)
const url = value.url.toString() const url = value.url.toString()
const isRoomOrRelay = isRoomId(url) || isRelayUrl(url) const fileType = getTagValue("file-type", event.tags) || ""
const [href, external] = call(() => { const [href, external] = call(() => {
if (isRelayUrl(url)) return [makeSpacePath(url), false]
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false] if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
return [url, true] return [url, true]
}) })
const fileType = getTagValue("file-type", event.tags) || ""
const getVideoPoster = (videoUrl: string): string | undefined => { const getVideoPoster = (videoUrl: string): string | undefined => {
if (Capacitor.getPlatform() === "android" && THUMBNAIL_URL) { if (Capacitor.getPlatform() === "android" && THUMBNAIL_URL) {
return `${THUMBNAIL_URL}/thumbnail?url=${encodeURIComponent(videoUrl)}` return `${THUMBNAIL_URL}/thumbnail?url=${encodeURIComponent(videoUrl)}`
@@ -56,11 +54,6 @@
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true}) const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
</script> </script>
{#if isRoomOrRelay}
<div>
<ContentLinkUrl {url} class="link-content whitespace-nowrap" />
</div>
{:else}
<Link {external} {href} class="my-2 block"> <Link {external} {href} class="my-2 block">
<div class="overflow-hidden rounded-box"> <div class="overflow-hidden rounded-box">
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)} {#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}
@@ -104,4 +97,3 @@
{/if} {/if}
</div> </div>
</Link> </Link>
{/if}
+15 -5
View File
@@ -1,18 +1,25 @@
<script lang="ts"> <script lang="ts">
import {displayUrl} from "@welshman/lib" import {call, displayUrl} from "@welshman/lib"
import {getTagValue} from "@welshman/util" import {isRelayUrl, getTagValue} from "@welshman/util"
import {preventDefault, stopPropagation} from "@lib/html" import {preventDefault, stopPropagation} from "@lib/html"
import LinkRound from "@assets/icons/link-round.svg?dataurl" import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte" import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import ContentLinkUrl from "@app/components/ContentLinkUrl.svelte"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {IMAGE_CONTENT_TYPES} from "@app/core/state" import {PLATFORM_URL, IMAGE_CONTENT_TYPES} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
const {value, event} = $props() const {value, event} = $props()
const url = value.url.toString() const url = value.url.toString()
const fileType = getTagValue("file-type", event.tags) || "" const fileType = getTagValue("file-type", event.tags) || ""
const [href, external] = call(() => {
if (isRelayUrl(url)) return [makeSpacePath(url), false]
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
return [url, true]
})
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true}) const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
</script> </script>
@@ -27,5 +34,8 @@
{displayUrl(url)} {displayUrl(url)}
</a> </a>
{:else} {:else}
<ContentLinkUrl {url} class="link-content whitespace-nowrap" /> <Link {external} {href} class="link-content whitespace-nowrap">
<Icon icon={LinkRound} size={3} class="inline-block" />
{displayUrl(url)}
</Link>
{/if} {/if}
-59
View File
@@ -1,59 +0,0 @@
<script lang="ts">
import {call, displayUrl} from "@welshman/lib"
import {displayRelayUrl, isRelayUrl, normalizeRelayUrl} from "@welshman/util"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import {PLATFORM_URL, displayRoom, isRoomId, splitRoomId} from "@app/core/state"
import {makeRoomPath, makeSpacePath} from "@app/util/routes"
const {
url,
class: className = "",
}: {
url: string
class?: string
} = $props()
const roomReference = call(() => {
if (!isRoomId(url)) {
return undefined
}
const [roomUrl, h] = splitRoomId(url)
if (!roomUrl || !h || !isRelayUrl(roomUrl)) {
return undefined
}
return {url: normalizeRelayUrl(roomUrl), h}
})
const relayReference = call(() => {
if (roomReference || !isRelayUrl(url)) {
return undefined
}
return normalizeRelayUrl(url)
})
const [href, external] = call(() => {
if (roomReference) return [makeRoomPath(roomReference.url, roomReference.h), false]
if (relayReference) return [makeSpacePath(relayReference), false]
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
return [url, true]
})
</script>
<Link {external} {href} class={className}>
{#if roomReference}
~<span class="text-primary">{displayRelayUrl(roomReference.url)}</span> /
{displayRoom(roomReference.url, roomReference.h)}
{:else if relayReference}
<span class="text-primary">{displayRelayUrl(relayReference)}</span>
{:else}
<Icon icon={LinkRound} size={3} class="inline-block" />
{displayUrl(url)}
{/if}
</Link>
-4
View File
@@ -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}
+1 -1
View File
@@ -49,7 +49,7 @@
<NoteContentMinimal trimParent {url} event={$quote} /> <NoteContentMinimal trimParent {url} event={$quote} />
</div> </div>
{:else} {:else}
<NoteCard noShadow event={$quote} {url} class="bg-alt rounded-box p-4"> <NoteCard event={$quote} {url} class="bg-alt rounded-box p-4">
<NoteContentMinimal {url} event={$quote} /> <NoteContentMinimal {url} event={$quote} />
</NoteCard> </NoteCard>
{/if} {/if}
+2 -3
View File
@@ -42,7 +42,7 @@
let popover: Instance | undefined = $state() let popover: Instance | undefined = $state()
</script> </script>
<div class="join items-center rounded-full"> <Button class="join rounded-full">
{#if ENABLE_ZAPS && !hideZap} {#if ENABLE_ZAPS && !hideZap}
<ZapButton {url} {event} class="btn join-item btn-neutral btn-xs"> <ZapButton {url} {event} class="btn join-item btn-neutral btn-xs">
<Icon icon={Bolt} size={4} /> <Icon icon={Bolt} size={4} />
@@ -52,7 +52,6 @@
<Icon icon={SmileCircle} size={4} /> <Icon icon={SmileCircle} size={4} />
</EmojiButton> </EmojiButton>
<Tippy <Tippy
class="flex"
bind:popover bind:popover
component={EventMenu} component={EventMenu}
props={{url, noun, event, customActions, onClick: hidePopover}} props={{url, noun, event, customActions, onClick: hidePopover}}
@@ -61,4 +60,4 @@
<Icon icon={MenuDots} size={4} /> <Icon icon={MenuDots} size={4} />
</Button> </Button>
</Tippy> </Tippy>
</div> </Button>
+2 -2
View File
@@ -68,7 +68,7 @@
}) })
const observer = new ResizeObserver(() => { const observer = new ResizeObserver(() => {
spacer!.style.minHeight = `${form!.offsetHeight + 60}px` spacer!.style.minHeight = `${form!.offsetHeight}px`
}) })
observer.observe(form!) observer.observe(form!)
@@ -84,7 +84,7 @@
in:fly in:fly
bind:this={form} bind:this={form}
onsubmit={preventDefault(submit)} onsubmit={preventDefault(submit)}
class="left-content bottom-sai right-sai fixed z-feature mb-14 md:mb-0 w-full md:w-auto pr-2"> class="left-content bottom-sai right-sai ml-2 pl-2 fixed z-feature">
<div class="card2 mx-2 my-2 bg-alt shadow-md"> <div class="card2 mx-2 my-2 bg-alt shadow-md">
<div class="relative"> <div class="relative">
<div class="note-editor grow overflow-hidden"> <div class="note-editor grow overflow-hidden">
+4 -4
View File
@@ -192,16 +192,16 @@
{/snippet} {/snippet}
{#snippet input()} {#snippet input()}
<div class="flex grow justify-end"> <div class="flex grow justify-end">
<label class="input input-bordered flex w-auto items-center gap-2"> <label class="input input-bordered flex items-center gap-2">
<Icon icon={Bolt} /> <Icon icon={Bolt} />
<input bind:value={amount} type="number" class="w-28 grow" /> <input bind:value={amount} type="number" class="w-28" />
<p class="shrink-0 opacity-50">sats</p> <p class="opacity-50">sats</p>
</label> </label>
</div> </div>
{/snippet} {/snippet}
</FieldInline> </FieldInline>
<input <input
class="range range-primary -mt-2 w-full" class="range range-primary -mt-2"
type="range" type="range"
min="1000" min="1000"
max="100000" max="100000"
+1 -7
View File
@@ -15,7 +15,6 @@
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {PLATFORM_NAME} from "@app/core/state" import {PLATFORM_NAME} from "@app/core/state"
@@ -23,11 +22,9 @@
secret: string secret: string
next: () => unknown next: () => unknown
submitText?: string submitText?: string
step?: number
totalSteps?: number
} }
const {secret, next, submitText = "Continue", step, totalSteps}: Props = $props() const {secret, next, submitText = "Continue"}: Props = $props()
const back = () => history.back() const back = () => history.back()
@@ -153,9 +150,6 @@
</Button> </Button>
</div> </div>
</ModalBody> </ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
-10
View File
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import {Capacitor} from "@capacitor/core"
import {onMount, onDestroy} from "svelte" import {onMount, onDestroy} from "svelte"
import type {Nip46ResponseWithResult} from "@welshman/signer" import type {Nip46ResponseWithResult} from "@welshman/signer"
import {Nip46Broker} from "@welshman/signer" import {Nip46Broker} from "@welshman/signer"
@@ -104,16 +103,10 @@
mode = "connect" mode = "connect"
} }
const openSigner = () => {
controller.launchSigner()
}
const selectBunker = () => { const selectBunker = () => {
mode = "bunker" mode = "bunker"
} }
const isIos = Capacitor.getPlatform() === "ios"
let mode: string = $state("bunker") let mode: string = $state("bunker")
$effect(() => { $effect(() => {
@@ -145,9 +138,6 @@
<BunkerUrl {controller} /> <BunkerUrl {controller} />
<Button class="btn {$bunker ? 'btn-neutral' : 'btn-primary'}" onclick={selectConnect} <Button class="btn {$bunker ? 'btn-neutral' : 'btn-primary'}" onclick={selectConnect}
>Log in with a QR code instead</Button> >Log in with a QR code instead</Button>
{#if isIos}
<Button class="btn btn-neutral" onclick={openSigner}>Open in Signer</Button>
{/if}
{/if} {/if}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
+3 -11
View File
@@ -19,7 +19,6 @@
import LogInOTP from "@app/components/LogInOTP.svelte" import LogInOTP from "@app/components/LogInOTP.svelte"
import LogInSelect from "@app/components/LogInSelect.svelte" import LogInSelect from "@app/components/LogInSelect.svelte"
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade" import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
import {pushModal, clearModals} from "@app/util/modal" import {pushModal, clearModals} from "@app/util/modal"
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
@@ -45,7 +44,7 @@
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: getPomadeLoginFailureMessage(messages), message: "Sorry, we were unable to log you in.",
}) })
} }
@@ -65,17 +64,10 @@
pushToast({ pushToast({
theme: "error", theme: "error",
message: getPomadeLoginFailureMessage(res.messages), message: "Sorry, we were unable to log you in.",
}) })
} }
} }
} catch (error) {
console.error("Login error:", error)
pushToast({
theme: "error",
message: POMADE_NETWORK_ERROR_MESSAGE,
})
} finally { } finally {
loading = false loading = false
} }
@@ -98,7 +90,7 @@
{#snippet input()} {#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Letter} /> <Icon icon={Letter} />
<input type="email" bind:value={email} /> <input bind:value={email} />
</label> </label>
{/snippet} {/snippet}
</FieldInline> </FieldInline>
+2 -12
View File
@@ -15,7 +15,6 @@
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import LogInOTPConfirm from "@app/components/LogInOTPConfirm.svelte" import LogInOTPConfirm from "@app/components/LogInOTPConfirm.svelte"
import {POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
@@ -36,20 +35,11 @@
if (ok) { if (ok) {
pushModal(LogInOTPConfirm, {email, peersByPrefix}) pushModal(LogInOTPConfirm, {email, peersByPrefix})
} else { } else {
console.error("Pomade challenge request failed during OTP login")
pushToast({ pushToast({
theme: "error", theme: "error",
message: POMADE_NETWORK_ERROR_MESSAGE, message: "Sorry, we were unable to request a login code.",
}) })
} }
} catch (error) {
console.error(error)
pushToast({
theme: "error",
message: POMADE_NETWORK_ERROR_MESSAGE,
})
} finally { } finally {
loading = false loading = false
} }
@@ -71,7 +61,7 @@
{#snippet input()} {#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Letter} /> <Icon icon={Letter} />
<input type="email" bind:value={email} /> <input bind:value={email} />
</label> </label>
{/snippet} {/snippet}
</FieldInline> </FieldInline>
+5 -13
View File
@@ -15,11 +15,10 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import StringMultiInput from "@lib/components/StringMultiInput.svelte" import StringMultiInput from "@lib/components/StringMultiInput.svelte"
import LogInSelect from "@app/components/LogInSelect.svelte" import LogInSelect from "@app/components/LogInSelect.svelte"
import {pushModal, clearModals} from "@app/util/modal"
import {setChecked} from "@app/util/notifications"
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {setChecked} from "@app/util/notifications"
import {pushModal, clearModals} from "@app/util/modal"
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
type Props = { type Props = {
email: string email: string
@@ -45,7 +44,7 @@
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: getPomadeLoginFailureMessage(messages), message: "Sorry, we were unable to log you in.",
}) })
} }
@@ -65,17 +64,10 @@
pushToast({ pushToast({
theme: "error", theme: "error",
message: getPomadeLoginFailureMessage(res.messages), message: "Sorry, we were unable to log you in.",
}) })
} }
} }
} catch (error) {
console.error("Login error:", error)
pushToast({
theme: "error",
message: POMADE_NETWORK_ERROR_MESSAGE,
})
} finally { } finally {
loading = false loading = false
} }
+1 -9
View File
@@ -14,7 +14,6 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import Profile from "@app/components/Profile.svelte" import Profile from "@app/components/Profile.svelte"
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade" import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
import {setChecked} from "@app/util/notifications" import {setChecked} from "@app/util/notifications"
import {clearModals} from "@app/util/modal" import {clearModals} from "@app/util/modal"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
@@ -47,16 +46,9 @@
pushToast({ pushToast({
theme: "error", theme: "error",
message: getPomadeLoginFailureMessage(res.messages), message: "Sorry, we were unable to log you in.",
}) })
} }
} catch (error) {
console.error("Login error:", error)
pushToast({
theme: "error",
message: POMADE_NETWORK_ERROR_MESSAGE,
})
} finally { } finally {
loading = false loading = false
} }
+1 -3
View File
@@ -16,7 +16,6 @@
children, children,
minimal = false, minimal = false,
hideProfile = false, hideProfile = false,
noShadow = false,
url, url,
...restProps ...restProps
}: { }: {
@@ -24,7 +23,6 @@
children: Snippet children: Snippet
minimal?: boolean minimal?: boolean
hideProfile?: boolean hideProfile?: boolean
noShadow?: boolean
url?: string url?: string
class?: string class?: string
} = $props() } = $props()
@@ -36,7 +34,7 @@
let muted = $state($isEventMuted(event)) let muted = $state($isEventMuted(event))
</script> </script>
<div class="flex flex-col gap-2 {restProps.class}" class:shadow-md={!noShadow}> <div class="flex flex-col gap-2 shadow-md {restProps.class}">
{#if muted} {#if muted}
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="row-2 relative"> <div class="row-2 relative">
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {ComponentProps} from "svelte" import type {ComponentProps} from "svelte"
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util" import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED} from "@welshman/util"
import {Poll} from "nostr-tools/kinds"
import NoteContentEventTime from "@app/components/NoteContentEventTime.svelte" import NoteContentEventTime from "@app/components/NoteContentEventTime.svelte"
import NoteContentThread from "@app/components/NoteContentThread.svelte" import NoteContentThread from "@app/components/NoteContentThread.svelte"
import NoteContentClassified from "@app/components/NoteContentClassified.svelte" import NoteContentClassified from "@app/components/NoteContentClassified.svelte"
@@ -20,7 +21,7 @@
<NoteContentClassified {...props} /> <NoteContentClassified {...props} />
{:else if props.event.kind === ZAP_GOAL} {:else if props.event.kind === ZAP_GOAL}
<NoteContentGoal {...props} /> <NoteContentGoal {...props} />
{:else if props.event.kind === POLL} {:else if props.event.kind === Poll}
<NoteContentPoll {...props} /> <NoteContentPoll {...props} />
{:else} {:else}
<Content {...props} /> <Content {...props} />
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {ComponentProps} from "svelte" import type {ComponentProps} from "svelte"
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util" import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED} from "@welshman/util"
import {Poll} from "nostr-tools/kinds"
import NoteContentMinimalEventTime from "@app/components/NoteContentMinimalEventTime.svelte" import NoteContentMinimalEventTime from "@app/components/NoteContentMinimalEventTime.svelte"
import NoteContentMinimalThread from "@app/components/NoteContentMinimalThread.svelte" import NoteContentMinimalThread from "@app/components/NoteContentMinimalThread.svelte"
import NoteContentMinimalClassified from "@app/components/NoteContentMinimalClassified.svelte" import NoteContentMinimalClassified from "@app/components/NoteContentMinimalClassified.svelte"
@@ -20,7 +21,7 @@
<NoteContentMinimalClassified {...props} /> <NoteContentMinimalClassified {...props} />
{:else if props.event.kind === ZAP_GOAL} {:else if props.event.kind === ZAP_GOAL}
<NoteContentMinimalGoal {...props} /> <NoteContentMinimalGoal {...props} />
{:else if props.event.kind === POLL} {:else if props.event.kind === Poll}
<NoteContentMinimalPoll {...props} /> <NoteContentMinimalPoll {...props} />
{:else} {:else}
<ContentMinimal {...props} /> <ContentMinimal {...props} />
@@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import type {ComponentProps} from "svelte" import type {ComponentProps} from "svelte"
import {derived} from "svelte/store" import {derived} from "svelte/store"
import {POLL_RESPONSE} from "@welshman/util" import {PollResponse} from "nostr-tools/kinds"
import ContentMinimal from "@app/components/ContentMinimal.svelte" import ContentMinimal from "@app/components/ContentMinimal.svelte"
import {deriveEvents} from "@app/core/state" import {deriveEvents} from "@app/core/state"
import {getPollResults} from "@app/util/polls" import {getPollResults} from "@app/util/polls"
const props: ComponentProps<typeof ContentMinimal> = $props() const props: ComponentProps<typeof ContentMinimal> = $props()
const responses = deriveEvents([{kinds: [POLL_RESPONSE], "#e": [props.event.id]}]) const responses = deriveEvents([{kinds: [PollResponse], "#e": [props.event.id]}])
const results = derived(responses, $responses => getPollResults(props.event, $responses)) const results = derived(responses, $responses => getPollResults(props.event, $responses))
</script> </script>
+2 -2
View File
@@ -2,7 +2,7 @@
import type {ComponentProps} from "svelte" import type {ComponentProps} from "svelte"
import {onMount} from "svelte" import {onMount} from "svelte"
import {request} from "@welshman/net" import {request} from "@welshman/net"
import {POLL_RESPONSE} from "@welshman/util" import {PollResponse} from "nostr-tools/kinds"
import PollVotes from "@app/components/PollVotes.svelte" import PollVotes from "@app/components/PollVotes.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
@@ -15,7 +15,7 @@
request({ request({
relays: [props.url], relays: [props.url],
filters: [{kinds: [POLL_RESPONSE], "#e": [props.event.id]}], filters: [{kinds: [PollResponse], "#e": [props.event.id]}],
}) })
}) })
</script> </script>
+3 -2
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib" import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib"
import {makeEvent, POLL} from "@welshman/util" import {makeEvent} from "@welshman/util"
import {publishThunk, waitForThunkError} from "@welshman/app" import {publishThunk, waitForThunkError} from "@welshman/app"
import {Poll} from "nostr-tools/kinds"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import HamburgerMenu from "@assets/icons/hamburger-menu.svg?dataurl" import HamburgerMenu from "@assets/icons/hamburger-menu.svg?dataurl"
@@ -144,7 +145,7 @@
const pollThunk = publishThunk({ const pollThunk = publishThunk({
relays: [url], relays: [url],
event: makeEvent(POLL, {content: title.trim(), tags}), event: makeEvent(Poll, {content: title.trim(), tags}),
}) })
const error = await waitForThunkError(pollThunk) const error = await waitForThunkError(pollThunk)
+2 -2
View File
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import {onDestroy} from "svelte" import {onDestroy} from "svelte"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {POLL_RESPONSE} from "@welshman/util"
import {pubkey, publishThunk, abortThunk} from "@welshman/app" import {pubkey, publishThunk, abortThunk} from "@welshman/app"
import {PollResponse} from "nostr-tools/kinds"
import {formatTimestampRelative} from "@welshman/lib" import {formatTimestampRelative} from "@welshman/lib"
import {deriveEvents} from "@app/core/state" import {deriveEvents} from "@app/core/state"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
@@ -24,7 +24,7 @@
const {url, event}: Props = $props() const {url, event}: Props = $props()
const responses = deriveEvents([{kinds: [POLL_RESPONSE], "#e": [event.id]}]) const responses = deriveEvents([{kinds: [PollResponse], "#e": [event.id]}])
const pollType = getPollType(event) const pollType = getPollType(event)
const options = getPollOptions(event) const options = getPollOptions(event)
@@ -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>
+12 -4
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {Profile} from "@welshman/util" import type {Profile} from "@welshman/util"
import {makeProfile} from "@welshman/util" import {getTag, makeProfile} from "@welshman/util"
import {pubkey, profilesByPubkey, waitForThunkError} from "@welshman/app" import {pubkey, profilesByPubkey, waitForThunkError} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import {errorMessage} from "@lib/util" import {errorMessage} from "@lib/util"
@@ -10,18 +10,26 @@
import ProfileEditForm from "@app/components/ProfileEditForm.svelte" import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
import {clearModals} from "@app/util/modal" import {clearModals} from "@app/util/modal"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state"
import {updateProfile} from "@app/core/commands" import {updateProfile} from "@app/core/commands"
const profile = $profilesByPubkey.get($pubkey!) || makeProfile() const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
const initialValues = {profile} const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || [])
const initialValues = {profile, shouldBroadcast}
const back = () => history.back() const back = () => history.back()
const onsubmit = async ({profile}: {profile: Profile}) => { const onsubmit = async ({
profile,
shouldBroadcast,
}: {
profile: Profile
shouldBroadcast: boolean
}) => {
loading = true loading = true
try { try {
const error = await waitForThunkError(updateProfile({profile})) const error = await waitForThunkError(updateProfile({profile, shouldBroadcast}))
if (error) { if (error) {
pushToast({ pushToast({
+23 -6
View File
@@ -6,6 +6,7 @@
import MapPoint from "@assets/icons/map-point.svg?dataurl" import MapPoint from "@assets/icons/map-point.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
import FieldInline from "@lib/components/FieldInline.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
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"
@@ -16,6 +17,7 @@
type Values = { type Values = {
profile: Profile profile: Profile
shouldBroadcast: boolean
} }
type Props = { type Props = {
@@ -23,10 +25,9 @@
onsubmit: (values: Values) => void onsubmit: (values: Values) => void
isSignup?: boolean isSignup?: boolean
footer: Snippet footer: Snippet
progressBar?: Snippet
} }
const {initialValues, isSignup, onsubmit, footer, progressBar}: Props = $props() const {initialValues, isSignup, onsubmit, footer}: Props = $props()
const values = $state(initialValues) const values = $state(initialValues)
@@ -76,7 +77,7 @@
{/snippet} {/snippet}
{#snippet input()} {#snippet input()}
<textarea <textarea
class="textarea textarea-bordered leading-4 w-full" class="textarea textarea-bordered leading-4"
rows="5" rows="5"
bind:value={values.profile.about}></textarea> bind:value={values.profile.about}></textarea>
{/snippet} {/snippet}
@@ -103,10 +104,26 @@
{/snippet} {/snippet}
</Field> </Field>
{/if} {/if}
</ModalBody> {#if !isSignup}
{#if progressBar} <FieldInline>
{@render progressBar()} {#snippet label()}
<p>Broadcast Profile</p>
{/snippet}
{#snippet input()}
<input
type="checkbox"
class="toggle toggle-primary"
bind:checked={values.shouldBroadcast} />
{/snippet}
{#snippet info()}
<p>
If enabled, changes will be published to the broader nostr network in addition to spaces
you are a member of.
</p>
{/snippet}
</FieldInline>
{/if} {/if}
</ModalBody>
<ModalFooter> <ModalFooter>
{@render footer()} {@render footer()}
</ModalFooter> </ModalFooter>
-9
View File
@@ -1,9 +0,0 @@
<script lang="ts">
const {current, total}: {current: number; total: number} = $props()
</script>
<div class="flex w-full">
{#each Array(total) as _, i}
<div class="h-1 flex-1 transition-colors {i < current ? 'bg-primary' : 'bg-base-300'}"></div>
{/each}
</div>
+3 -17
View File
@@ -33,7 +33,6 @@
url?: string url?: string
reactionClass?: string reactionClass?: string
noTooltip?: boolean noTooltip?: boolean
innerEvent?: TrustedEvent
children?: Snippet children?: Snippet
} }
@@ -44,36 +43,23 @@
url = "", url = "",
reactionClass = "", reactionClass = "",
noTooltip = false, noTooltip = false,
innerEvent = undefined,
children, children,
}: Props = $props() }: Props = $props()
const eventIds = innerEvent ? [event.id, innerEvent.id] : [event.id]
const reports = deriveArray( const reports = deriveArray(
deriveEventsById({repository, filters: [{kinds: [REPORT], "#e": [event.id]}]}), deriveEventsById({repository, filters: [{kinds: [REPORT], "#e": [event.id]}]}),
) )
const reactions = deriveArray( const reactions = deriveArray(
deriveEventsById({repository, filters: [{kinds: [REACTION], "#e": eventIds}]}), deriveEventsById({repository, filters: [{kinds: [REACTION], "#e": [event.id]}]}),
) )
const zaps = deriveArray( const zaps = deriveArray(
deriveItemsByKey<Zap>({ deriveItemsByKey<Zap>({
repository, repository,
getKey: zap => zap.response.id, getKey: zap => zap.response.id,
filters: [{kinds: [ZAP_RESPONSE], "#e": eventIds}], filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
eventToItem: (response: TrustedEvent) => { eventToItem: (response: TrustedEvent) => getValidZap(response, event),
const zap = getValidZap(response, event)
if (zap) {
return zap
}
if (innerEvent) {
return getValidZap(response, innerEvent)
}
},
}), }),
) )
+7 -5
View File
@@ -19,12 +19,12 @@
<div class="col-4 text-left"> <div class="col-4 text-left">
<div class="col-2"> <div class="col-2">
<div class="relative flex gap-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">
+2
View File
@@ -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()
+1 -5
View File
@@ -243,7 +243,7 @@
{/if} {/if}
</div> </div>
</div> </div>
{#if $members !== undefined && $members.length > 0} {#if $members.length > 0}
<div class="card2 card2-sm bg-alt flex items-center justify-between gap-4"> <div class="card2 card2-sm bg-alt flex items-center justify-between gap-4">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<span>Members:</span> <span>Members:</span>
@@ -251,10 +251,6 @@
</div> </div>
<Button class="btn btn-neutral btn-sm" onclick={showMembers}>View All</Button> <Button class="btn btn-neutral btn-sm" onclick={showMembers}>View All</Button>
</div> </div>
{:else if $members === undefined}
<div class="card2 card2-sm bg-base-200 flex items-center gap-4">
<span class="text-error">Member list not available from this relay</span>
</div>
{/if} {/if}
<div class="card2 card2-sm bg-alt col-4"> <div class="card2 card2-sm bg-alt col-4">
<strong class="text-lg">Room Settings</strong> <strong class="text-lg">Room Settings</strong>
+12 -27
View File
@@ -1,16 +1,8 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import {readable} from "svelte/store" import {hash, now, displayList, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
import {
hash,
gte,
now,
displayList,
formatTimestampAsTime,
formatTimestampAsDate,
} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {MESSAGE, COMMENT, getTag} from "@welshman/util" import {MESSAGE, COMMENT} from "@welshman/util"
import { import {
thunks, thunks,
pubkey, pubkey,
@@ -35,7 +27,7 @@
import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte" import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte" import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
import RoomItemContent from "@app/components/RoomItemContent.svelte" import RoomItemContent from "@app/components/RoomItemContent.svelte"
import {colors, ENABLE_ZAPS, deriveEventsForUrl, deriveEvent} from "@app/core/state" import {colors, ENABLE_ZAPS, deriveEventsForUrl} from "@app/core/state"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands" import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
import {getRoomItemPath} from "@app/util/routes" import {getRoomItemPath} from "@app/util/routes"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
@@ -46,6 +38,7 @@
replyTo?: (event: TrustedEvent) => void replyTo?: (event: TrustedEvent) => void
showPubkey?: boolean showPubkey?: boolean
addSpaceBelow?: boolean addSpaceBelow?: boolean
inert?: boolean
canEdit: (event: TrustedEvent) => boolean canEdit: (event: TrustedEvent) => boolean
onEdit: (event: TrustedEvent) => void onEdit: (event: TrustedEvent) => void
} }
@@ -56,6 +49,7 @@
replyTo = undefined, replyTo = undefined,
showPubkey = false, showPubkey = false,
addSpaceBelow = false, addSpaceBelow = false,
inert = false,
canEdit, canEdit,
onEdit, onEdit,
}: Props = $props() }: Props = $props()
@@ -66,15 +60,7 @@
const profileDisplay = deriveProfileDisplay(event.pubkey, [url]) const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id)) const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length] const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
const comments = deriveEventsForUrl(url, [{kinds: [COMMENT], "#e": [event.id]}])
const qTag = getTag("q", event.tags)
const isQuoteOnly = Boolean(
gte(qTag?.length, 2) && event.content.trim().match(/^nostr:n(event|addr)1\w+\s*$/),
)
const innerComments = isQuoteOnly
? deriveEventsForUrl(url, [{kinds: [COMMENT], "#e": [qTag![1]]}])
: readable([])
const innerEvent = isQuoteOnly ? deriveEvent(qTag![1], [url]) : readable(undefined)
const reply = () => replyTo!(event) const reply = () => replyTo!(event)
const edit = canEdit(event) ? () => onEdit(event) : undefined const edit = canEdit(event) ? () => onEdit(event) : undefined
@@ -92,7 +78,7 @@
<TapTarget <TapTarget
data-event={event.id} data-event={event.id}
{onTap} onTap={inert ? null : onTap}
class={cx( class={cx(
"group relative flex w-full cursor-default flex-col px-2 py-0.5 text-left hover:bg-base-100/50", "group relative flex w-full cursor-default flex-col px-2 py-0.5 text-left hover:bg-base-100/50",
{"mt-1.5": showPubkey, "mb-1.5": addSpaceBelow}, {"mt-1.5": showPubkey, "mb-1.5": addSpaceBelow},
@@ -125,7 +111,7 @@
</div> </div>
{/if} {/if}
<div class:mt-2={showPubkey && event.kind !== MESSAGE}> <div class:mt-2={showPubkey && event.kind !== MESSAGE}>
<RoomItemContent {url} event={$innerEvent ?? event} /> <RoomItemContent {url} {event} />
{#if thunk} {#if thunk}
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" /> <ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
{/if} {/if}
@@ -138,10 +124,9 @@
{event} {event}
{deleteReaction} {deleteReaction}
{createReaction} {createReaction}
reactionClass="tooltip-right" reactionClass="tooltip-right" />
innerEvent={$innerEvent} /> {#if path && $comments.length > 0}
{#if path && $innerComments.length > 0} {@const pubkeys = $comments.map(e => e.pubkey)}
{@const pubkeys = $innerComments.map(e => e.pubkey)}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)} {@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))} {@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
{@const tooltip = `${info} commented`} {@const tooltip = `${info} commented`}
@@ -153,7 +138,7 @@
"btn-primary": isOwn, "btn-primary": isOwn,
})}> })}>
<Icon icon={ReplyAlt} /> <Icon icon={ReplyAlt} />
<span>{$innerComments.length} comment{$innerComments.length === 1 ? "" : "s"}</span> <span>{$comments.length} comment{$comments.length === 1 ? "" : "s"}</span>
</Link> </Link>
</div> </div>
{/if} {/if}
+3 -4
View File
@@ -8,17 +8,16 @@
import {getRoomItemPath} from "@app/util/routes" import {getRoomItemPath} from "@app/util/routes"
const props: ComponentProps<typeof NoteContent> = $props() const props: ComponentProps<typeof NoteContent> = $props()
const path = getRoomItemPath(props.url!, props.event) const path = getRoomItemPath(props.url!, props.event)
const minLength = 5000
const maxLength = 5500
</script> </script>
<div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}> <div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}>
{#if path && !isMobile} {#if path && !isMobile}
<Link href={path}> <Link href={path}>
<NoteContent {...props} {minLength} {maxLength} /> <NoteContent {...props} />
</Link> </Link>
{:else} {:else}
<NoteContent {...props} {minLength} {maxLength} /> <NoteContent {...props} />
{/if} {/if}
</div> </div>
-10
View File
@@ -73,15 +73,6 @@
</ModalSubtitle> </ModalSubtitle>
</ModalHeader> </ModalHeader>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#if $members === undefined}
<div class="card2 bg-base-200 p-4">
<span class="text-error">Member list not available from this relay</span>
</div>
{:else if $members.length === 0}
<div class="card2 bg-base-200 p-4">
<span class="text-base-content/70">No members yet</span>
</div>
{:else}
{#each $members as pubkey (pubkey)} {#each $members as pubkey (pubkey)}
<div class="card2 bg-alt relative"> <div class="card2 bg-alt relative">
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
@@ -110,7 +101,6 @@
</div> </div>
</div> </div>
{/each} {/each}
{/if}
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
-5
View File
@@ -56,11 +56,6 @@
} }
const onSubmit = async () => { const onSubmit = async () => {
if (!$spaceMembers) {
addMembers()
return
}
const pubkeysSnapshot = $state.snapshot(pubkeys) const pubkeysSnapshot = $state.snapshot(pubkeys)
const nonSpaceMembers = pubkeysSnapshot.filter(pubkey => !$spaceMembers.includes(pubkey)) const nonSpaceMembers = pubkeysSnapshot.filter(pubkey => !$spaceMembers.includes(pubkey))
+1 -1
View File
@@ -12,7 +12,7 @@
</script> </script>
<div class="flex grow items-center justify-between gap-4 {props.class}"> <div class="flex grow items-center justify-between gap-4 {props.class}">
<div class="flex items-center gap-2"> <div class="flex items-center gap-3">
<RoomImage {url} {h} /> <RoomImage {url} {h} />
<div class="min-w-0 overflow-hidden text-ellipsis"> <div class="min-w-0 overflow-hidden text-ellipsis">
<RoomName {url} {h} /> <RoomName {url} {h} />
+6 -8
View File
@@ -62,10 +62,9 @@
const flows = { const flows = {
email: { email: {
start: () => pushModal(SignUpEmail, {next: flows.email.profile, step: 1, totalSteps: 3}), start: () => pushModal(SignUpEmail, {next: flows.email.profile}),
profile: () => pushModal(SignUpProfile, {next: flows.email.complete, step: 2, totalSteps: 3}), profile: () => pushModal(SignUpProfile, {next: flows.email.complete}),
complete: () => complete: () => pushModal(SignUpComplete, {next: flows.email.finalize}),
pushModal(SignUpComplete, {next: flows.email.finalize, step: 3, totalSteps: 3}),
finalize: () => { finalize: () => {
const email = getKey<string>("signup.email")! const email = getKey<string>("signup.email")!
const clientOptions = getKey<ClientOptions>("signup.clientOptions")! const clientOptions = getKey<ClientOptions>("signup.clientOptions")!
@@ -75,10 +74,9 @@
}, },
}, },
nostr: { nostr: {
start: () => pushModal(SignUpProfile, {next: flows.nostr.key, step: 1, totalSteps: 3}), start: () => pushModal(SignUpProfile, {next: flows.nostr.key}),
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}), key: () => pushModal(SignUpKey, {next: flows.nostr.complete}),
complete: () => complete: () => pushModal(SignUpComplete, {next: flows.nostr.finalize}),
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
finalize: () => { finalize: () => {
const secret = getKey<string>("signup.secret")! const secret = getKey<string>("signup.secret")!
+1 -7
View File
@@ -9,15 +9,12 @@
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next, step, totalSteps}: Props = $props() const {next}: Props = $props()
const back = () => history.back() const back = () => history.back()
</script> </script>
@@ -36,9 +33,6 @@
on groups you've already joined. Click below to get started! on groups you've already joined. Click below to get started!
</p> </p>
</ModalBody> </ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
+3 -12
View File
@@ -18,17 +18,14 @@
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte" import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
import {pushToast, popToast} from "@app/util/toast" import {pushToast, popToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next, step, totalSteps}: Props = $props() const {next}: Props = $props()
const back = () => history.back() const back = () => history.back()
@@ -84,7 +81,7 @@
setKey("signup.clientOptions", clientOptions) setKey("signup.clientOptions", clientOptions)
popToast(toastId) popToast(toastId)
pushModal(SignUpEmailConfirm, {next, step, totalSteps}) pushModal(SignUpEmailConfirm, {next})
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -123,7 +120,7 @@
{#snippet input()} {#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={Letter} /> <Icon icon={Letter} />
<input type="email" bind:value={email} /> <input bind:value={email} />
</label> </label>
{/snippet} {/snippet}
</FieldInline> </FieldInline>
@@ -137,14 +134,8 @@
<input type="password" bind:value={password} /> <input type="password" bind:value={password} />
</label> </label>
{/snippet} {/snippet}
{#snippet info()}
Must be at least 12 characters long.
{/snippet}
</FieldInline> </FieldInline>
</ModalBody> </ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
+1 -7
View File
@@ -15,15 +15,12 @@
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next, step, totalSteps}: Props = $props() const {next}: Props = $props()
const email = getKey<string>("signup.email") const email = getKey<string>("signup.email")
@@ -64,9 +61,6 @@
above. above.
</p> </p>
</ModalBody> </ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back} disabled={loading}> <Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
+2 -4
View File
@@ -4,13 +4,11 @@
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next, step, totalSteps}: Props = $props() const {next}: Props = $props()
const secret = getKey<string>("signup.secret")! const secret = getKey<string>("signup.secret")!
</script> </script>
<KeyDownload {secret} {next} {step} {totalSteps} /> <KeyDownload {secret} {next} />
+8 -10
View File
@@ -5,20 +5,19 @@
import {getKey, setKey} from "@lib/implicit" import {getKey, setKey} from "@lib/implicit"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ProfileEditForm from "@app/components/ProfileEditForm.svelte" import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next, step, totalSteps}: Props = $props() const {next}: Props = $props()
const profile = getKey<Profile>("signup.profile")! const profile = getKey<Profile>("signup.profile")!
const initialValues = {profile} const initialValues = {profile, shouldBroadcast: false}
const back = () => history.back() const back = () => history.back()
@@ -28,6 +27,8 @@
} }
</script> </script>
<Modal>
<ModalBody>
<ProfileEditForm isSignup {initialValues} {onsubmit}> <ProfileEditForm isSignup {initialValues} {onsubmit}>
{#snippet footer()} {#snippet footer()}
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
@@ -39,9 +40,6 @@
<Icon icon={AltArrowRight} /> <Icon icon={AltArrowRight} />
</Button> </Button>
{/snippet} {/snippet}
{#snippet progressBar()}
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
{/snippet}
</ProfileEditForm> </ProfileEditForm>
</ModalBody>
</Modal>
+9 -63
View File
@@ -3,9 +3,7 @@
import {sleep} from "@welshman/lib" import {sleep} from "@welshman/lib"
import {request} from "@welshman/net" import {request} from "@welshman/net"
import {displayRelayUrl, getTagValue, RELAY_INVITE} from "@welshman/util" import {displayRelayUrl, getTagValue, RELAY_INVITE} from "@welshman/util"
import {Share} from "@capacitor/share"
import LinkRound from "@assets/icons/link-round.svg?dataurl" import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Upload from "@assets/icons/upload.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl" import Copy from "@assets/icons/copy.svg?dataurl"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
@@ -25,72 +23,36 @@
const {url} = $props() const {url} = $props()
const authError = deriveRelayAuthError(url) const authError = deriveRelayAuthError(url)
let networkError = $state(false)
const isExplicitAuthError = $derived(
$authError &&
!(
$authError.toLowerCase().includes("failed") ||
$authError.toLowerCase().includes("timeout") ||
$authError.toLowerCase().includes("network")
),
)
const isGenericError = $derived(networkError || ($authError && !isExplicitAuthError))
const back = () => history.back() const back = () => history.back()
const copyInvite = () => clip(invite) const copyInvite = () => clip(invite)
const shareInvite = async () => {
if (!canShare) return
try {
await Share.share({url: invite})
} catch (e) {
console.error(e)
}
}
let canShare = $state(false)
let claim = $state("") let claim = $state("")
let loading = $state(true) let loading = $state(true)
let invite = $state("") let invite = $state("")
$effect(() => { $effect(() => {
const relay = displayRelayUrl(url) const relay = displayRelayUrl(url)
const params = new URLSearchParams({r: relay, c: claim}).toString() const params = new URLSearchParams({r: relay, c: claim}).toString()
invite = PLATFORM_URL + "/join?" + params invite = PLATFORM_URL + "/join?" + params
}) })
onMount(async () => { onMount(async () => {
try {
const {value} = await Share.canShare()
canShare = value
} catch {
canShare = false
}
try {
const [[event]] = await Promise.all([ const [[event]] = await Promise.all([
request({ request({
relays: [url], relays: [url],
autoClose: true, autoClose: true,
signal: AbortSignal.timeout(10000), signal: AbortSignal.timeout(3000),
filters: [{kinds: [RELAY_INVITE]}], filters: [{kinds: [RELAY_INVITE]}],
}), }),
sleep(2000), sleep(2000),
]) ])
claim = getTagValue("claim", event?.tags || []) || "" claim = getTagValue("claim", event?.tags || []) || ""
} catch (err) {
claim = ""
if (
(err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) ||
!navigator.onLine
) {
networkError = true
}
} finally {
loading = false loading = false
}
}) })
</script> </script>
@@ -108,36 +70,20 @@
<p class="center"> <p class="center">
<Spinner {loading}>Requesting an invite link...</Spinner> <Spinner {loading}>Requesting an invite link...</Spinner>
</p> </p>
{:else if isGenericError} {:else if $authError}
<p class="center text-center">
Unable to reach the relay. Please check your connection and try again.
</p>
{:else if isExplicitAuthError}
<p class="center">Oops! It looks like you're not a member of this relay.</p> <p class="center">Oops! It looks like you're not a member of this relay.</p>
{:else} {:else}
<div class="flex flex-col items-center gap-6"> <div class="flex flex-col items-center gap-6">
<div class="w-48">
<QRCode code={invite} /> <QRCode code={invite} />
</div>
<Field> <Field>
{#snippet input()} {#snippet input()}
<div class="flex w-full gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
{#if canShare} <Icon icon={LinkRound} />
<Button <input bind:value={invite} class="grow" type="text" />
class="input input-bordered flex shrink-0 w-12 items-center justify-center p-0" <Button onclick={copyInvite}>
onclick={shareInvite}>
<Icon icon={Upload} />
</Button>
{/if}
<label class="input input-bordered flex min-w-0 flex-1 items-center gap-2">
<Icon icon={LinkRound} class="shrink-0" />
<input bind:value={invite} class="min-w-0 flex-1 truncate" type="text" readonly />
<Button class="shrink-0" onclick={copyInvite}>
<Icon icon={Copy} /> <Icon icon={Copy} />
</Button> </Button>
</label> </label>
</div>
{/snippet} {/snippet}
{#snippet info()} {#snippet info()}
<p> <p>
+1 -13
View File
@@ -112,15 +112,6 @@
{/if} {/if}
{/if} {/if}
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#if $members === undefined}
<div class="card2 bg-base-200 p-4">
<span class="text-error">Member list not available from this space</span>
</div>
{:else if $members.length === 0}
<div class="card2 bg-base-200 p-4">
<span class="text-base-content/70">No members yet</span>
</div>
{:else}
{#each $members as pubkey (pubkey)} {#each $members as pubkey (pubkey)}
<div class="card2 card2-sm bg-alt relative"> <div class="card2 card2-sm bg-alt relative">
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
@@ -129,9 +120,7 @@
</div> </div>
{#if canBan || canUnallow} {#if canBan || canUnallow}
<div class="relative"> <div class="relative">
<Button <Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
class="btn btn-circle btn-ghost btn-sm"
onclick={() => toggleMenu(pubkey)}>
<Icon icon={MenuDots} /> <Icon icon={MenuDots} />
</Button> </Button>
{#if menuPubkey === pubkey} {#if menuPubkey === pubkey}
@@ -163,7 +152,6 @@
</div> </div>
</div> </div>
{/each} {/each}
{/if}
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
+8 -8
View File
@@ -1,7 +1,8 @@
<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} from "@welshman/util"
import {deriveRelay, createSearch, pubkey} from "@welshman/app" import {Poll} from "nostr-tools/kinds"
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 +66,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 +145,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"
@@ -177,11 +181,7 @@
<li> <li>
<Button onclick={showMembers}> <Button onclick={showMembers}>
<Icon icon={UserRounded} /> <Icon icon={UserRounded} />
{#if $members === undefined}
View Members
{:else}
View Members ({$members.length}) View Members ({$members.length})
{/if}
</Button> </Button>
</li> </li>
{#if $userIsAdmin} {#if $userIsAdmin}
@@ -263,7 +263,7 @@
<Icon icon={CalendarMinimalistic} /> Calendar <Icon icon={CalendarMinimalistic} /> Calendar
</SecondaryNavItem> </SecondaryNavItem>
{/if} {/if}
{#if $spaceKinds.has(POLL)} {#if $spaceKinds.has(Poll)}
<SecondaryNavItem href={pollsPath}> <SecondaryNavItem href={pollsPath}>
<Icon icon={Revote} /> Polls <Icon icon={Revote} /> Polls
</SecondaryNavItem> </SecondaryNavItem>
+6 -6
View File
@@ -26,27 +26,27 @@
{@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}
{#if Array.isArray(supported_nips)} {#if Array.isArray(supported_nips)}
<p class="badge badge-neutral text-wrap h-auto"> <p class="badge badge-neutral">
<span class="ellipsize">Supported NIPs: {supported_nips.join(", ")}</span> <span class="ellipsize">Supported NIPs: {supported_nips.join(", ")}</span>
</p> </p>
{/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}
+7 -16
View File
@@ -2,10 +2,9 @@
import {tick} from "svelte" import {tick} from "svelte"
import {debounce} from "throttle-debounce" import {debounce} from "throttle-debounce"
import {request} from "@welshman/net" import {request} from "@welshman/net"
import {repository, tracker} from "@welshman/app" import {formatTimestampAsDate, groupBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
import {formatTimestampAsDate, groupBy, uniqBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
import type {TrustedEvent, Filter} from "@welshman/util" import type {TrustedEvent, Filter} from "@welshman/util"
import {MESSAGE, sortEventsDesc} from "@welshman/util" import {sortEventsDesc} from "@welshman/util"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
@@ -54,11 +53,8 @@
const getFilter = (searchTerm: string): Filter => const getFilter = (searchTerm: string): Filter =>
h h
? {kinds: [MESSAGE, ...CONTENT_KINDS], "#h": [h], search: searchTerm} ? {kinds: CONTENT_KINDS, "#h": [h], search: searchTerm}
: {kinds: [MESSAGE, ...CONTENT_KINDS], search: searchTerm} : {kinds: CONTENT_KINDS, search: searchTerm}
const getLocalResults = (filter: Filter) =>
repository.query([filter]).filter(event => tracker.getRelays(event.id).has(url))
const search = debounce(300, async (searchTerm: string) => { const search = debounce(300, async (searchTerm: string) => {
controller?.abort() controller?.abort()
@@ -72,23 +68,18 @@
controller = new AbortController() controller = new AbortController()
loading = true loading = true
const filter = getFilter(searchTerm.trim())
const localResults = getLocalResults(filter)
results = sortEventsDesc(localResults)
try { try {
const events = await request({ const events = await request({
relays: getRelayUrls(), relays: getRelayUrls(),
autoClose: true, autoClose: true,
signal: controller.signal, signal: controller.signal,
filters: [filter], filters: [getFilter(searchTerm.trim())],
}) })
results = sortEventsDesc(uniqBy((e: TrustedEvent) => e.id, [...events, ...localResults])) results = sortEventsDesc(events)
} catch (error) { } catch (error) {
if (!(error instanceof DOMException && error.name === "AbortError")) { if (!(error instanceof DOMException && error.name === "AbortError")) {
results = sortEventsDesc(localResults) results = []
} }
} finally { } finally {
loading = false loading = false
+7 -46
View File
@@ -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}
@@ -1,34 +0,0 @@
<script lang="ts">
import cx from "classnames"
import MicrophoneOff from "@assets/icons/microphone-off.svg?dataurl"
import VideocameraOff from "@assets/icons/videocamera-off.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
type Props = {
muted: boolean
cameraOn: boolean
showCamera?: boolean
size?: number
class?: string
}
const {muted, cameraOn, showCamera = true, size = 3, class: className = ""}: Props = $props()
const badgeClass =
"inline-flex size-4 shrink-0 items-center justify-center rounded bg-base-100/80 p-0.5 text-error"
</script>
{#if muted || (showCamera && !cameraOn)}
<div class={cx("flex items-center gap-1", className)}>
{#if muted}
<span class={badgeClass} aria-label="Muted">
<Icon icon={MicrophoneOff} {size} />
</span>
{/if}
{#if showCamera && !cameraOn}
<span class={badgeClass} aria-label="Camera off">
<Icon icon={VideocameraOff} {size} />
</span>
{/if}
</div>
{/if}
+1 -11
View File
@@ -9,13 +9,11 @@
import {makeRoomPath} from "@app/util/routes" import {makeRoomPath} from "@app/util/routes"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte" import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
import VoiceParticipantMediaBadges from "@app/components/VoiceParticipantMediaBadges.svelte"
import {makeRoomId} from "@app/core/state" import {makeRoomId} from "@app/core/state"
import { import {
VoiceState, VoiceState,
currentVoiceRoom, currentVoiceRoom,
isParticipantSpeaking, isParticipantSpeaking,
mediaStateByIdentity,
participantKey, participantKey,
voiceState, voiceState,
type VoiceParticipant, type VoiceParticipant,
@@ -85,17 +83,9 @@
)}> )}>
<ProfileCircle pubkey={p.pubkey} size={5} class="h-5 w-5" /> <ProfileCircle pubkey={p.pubkey} size={5} class="h-5 w-5" />
</div> </div>
<span class="ellipsize min-w-0 flex-1 text-xs opacity-70"> <span class="ellipsize text-xs opacity-70">
{p.pubkey ? displayProfileByPubkey(p.pubkey) : "Unknown"} {p.pubkey ? displayProfileByPubkey(p.pubkey) : "Unknown"}
</span> </span>
{#if isActive}
{@const media = $mediaStateByIdentity(p.identity)}
<VoiceParticipantMediaBadges
muted={media.muted}
cameraOn={media.cameraOn}
size={3}
class="shrink-0" />
{/if}
</div> </div>
{/each} {/each}
{/if} {/if}
+9 -15
View File
@@ -6,7 +6,7 @@
import cx from "classnames" import cx from "classnames"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import Microphone from "@assets/icons/microphone.svg?dataurl" import Microphone from "@assets/icons/microphone.svg?dataurl"
import VideocameraOff from "@assets/icons/videocamera-off.svg?dataurl" import Videocamera from "@assets/icons/videocamera.svg?dataurl"
import VideocameraRecord from "@assets/icons/videocamera-record.svg?dataurl" import VideocameraRecord from "@assets/icons/videocamera-record.svg?dataurl"
import Monitor from "@assets/icons/monitor.svg?dataurl" import Monitor from "@assets/icons/monitor.svg?dataurl"
import PhoneRounded from "@assets/icons/phone-rounded.svg?dataurl" import PhoneRounded from "@assets/icons/phone-rounded.svg?dataurl"
@@ -41,8 +41,8 @@
VoiceState, VoiceState,
currentVoiceSession, currentVoiceSession,
currentVoiceRoom, currentVoiceRoom,
voiceMicMuted,
voiceState, voiceState,
isLocalSpeaking,
} from "@app/call/stores" } from "@app/call/stores"
import {cancelJoinVoiceRoom, leaveVoiceRoom, toggleMute} from "@app/call/voice" import {cancelJoinVoiceRoom, leaveVoiceRoom, toggleMute} from "@app/call/voice"
@@ -183,16 +183,18 @@
</Button> </Button>
{:else if $voiceState === VoiceState.Connected && $currentVoiceSession} {:else if $voiceState === VoiceState.Connected && $currentVoiceSession}
<Button <Button
data-tip={$voiceMicMuted ? "Unmute" : "Mute"} data-tip={$currentVoiceSession.muted ? "Unmute" : "Mute"}
class={cx( class={cx(
mediaToggleClass, mediaToggleClass,
"overflow-visible", "overflow-visible",
$voiceMicMuted && "text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100", !$currentVoiceSession.muted && $isLocalSpeaking && "text-primary",
$currentVoiceSession.muted &&
"text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
)} )}
onclick={toggleMute}> onclick={toggleMute}>
<span class="relative inline-flex items-center justify-center overflow-visible"> <span class="relative inline-flex items-center justify-center overflow-visible">
<Icon icon={Microphone} size={4} /> <Icon icon={Microphone} size={4} />
{#if $voiceMicMuted} {#if $currentVoiceSession.muted}
<span <span
class="pointer-events-none absolute inset-0 flex items-center justify-center overflow-visible" class="pointer-events-none absolute inset-0 flex items-center justify-center overflow-visible"
aria-hidden="true"> aria-hidden="true">
@@ -205,17 +207,9 @@
</Button> </Button>
<Button <Button
data-tip={$currentVoiceSession.cameraOn ? "Turn off camera" : "Turn on camera"} data-tip={$currentVoiceSession.cameraOn ? "Turn off camera" : "Turn on camera"}
class={cx( class={cx(mediaToggleClass, $currentVoiceSession.cameraOn && "text-primary")}
mediaToggleClass,
"overflow-visible",
$currentVoiceSession.cameraOn && "text-primary",
!$currentVoiceSession.cameraOn &&
"text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
)}
onclick={toggleCamera}> onclick={toggleCamera}>
<Icon <Icon icon={$currentVoiceSession.cameraOn ? VideocameraRecord : Videocamera} size={4} />
icon={$currentVoiceSession.cameraOn ? VideocameraRecord : VideocameraOff}
size={4} />
</Button> </Button>
{#if !Capacitor.isNativePlatform()} {#if !Capacitor.isNativePlatform()}
<Button <Button
+43 -13
View File
@@ -18,6 +18,7 @@ import {
import {Nip01Signer} from "@welshman/signer" import {Nip01Signer} from "@welshman/signer"
import type {UploadTask} from "@welshman/editor" import type {UploadTask} from "@welshman/editor"
import type {TrustedEvent, EventContent, Profile, PublishedRoomMeta} from "@welshman/util" import type {TrustedEvent, EventContent, Profile, PublishedRoomMeta} from "@welshman/util"
import {PollResponse} from "nostr-tools/kinds"
import { import {
DELETE, DELETE,
REPORT, REPORT,
@@ -32,7 +33,6 @@ import {
ROOMS, ROOMS,
COMMENT, COMMENT,
APP_DATA, APP_DATA,
POLL_RESPONSE,
isSignedEvent, isSignedEvent,
makeEvent, makeEvent,
normalizeRelayUrl, normalizeRelayUrl,
@@ -53,6 +53,7 @@ import {
isPublishedProfile, isPublishedProfile,
editProfile, editProfile,
createProfile, createProfile,
uniqTags,
ManagementMethod, ManagementMethod,
} from "@welshman/util" } from "@welshman/util"
import {Pool, AuthStatus, SocketStatus} from "@welshman/net" import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
@@ -73,6 +74,7 @@ import {
waitForThunkError, waitForThunkError,
getPubkeyRelays, getPubkeyRelays,
userBlossomServerList, userBlossomServerList,
getThunkError,
addRoomMember, addRoomMember,
manageRelay, manageRelay,
getRelay, getRelay,
@@ -83,7 +85,6 @@ import {
SETTINGS, SETTINGS,
PROTECTED, PROTECTED,
INDEXER_RELAYS, INDEXER_RELAYS,
DEFAULT_RELAYS,
DEFAULT_BLOSSOM_SERVERS, DEFAULT_BLOSSOM_SERVERS,
userSpaceUrls, userSpaceUrls,
userSettingsValues, userSettingsValues,
@@ -263,12 +264,16 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
return stripPrefix(error) return stripPrefix(error)
} }
export const deriveRelayAuthError = (url: string) => { export const deriveRelayAuthError = (url: string, claim = "") => {
// Kick off the auth process
Pool.get().get(url).auth.attemptAuth(sign) Pool.get().get(url).auth.attemptAuth(sign)
// Attempt to join the relay
const thunk = publishJoinRequest({url, claim})
return derived( return derived(
[relaysMostlyRestricted, deriveSocket(url)], [thunk, relaysMostlyRestricted, deriveSocket(url)],
([$relaysMostlyRestricted, $socket]) => { ([$thunk, $relaysMostlyRestricted, $socket]) => {
if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) { if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) {
return stripPrefix($socket.auth.details) return stripPrefix($socket.auth.details)
} }
@@ -276,6 +281,16 @@ export const deriveRelayAuthError = (url: string) => {
if ($relaysMostlyRestricted[url]) { if ($relaysMostlyRestricted[url]) {
return stripPrefix($relaysMostlyRestricted[url]) return stripPrefix($relaysMostlyRestricted[url])
} }
const error = getThunkError($thunk)
if (error) {
const isEmptyInvite = !claim && error.includes("invite code")
if (!shouldIgnoreError(error) && !isEmptyInvite) {
return stripPrefix(error) || "join request rejected"
}
}
}, },
) )
} }
@@ -374,7 +389,7 @@ export type PollResponseParams = {
} }
export const makePollResponse = ({event, selectedIds}: PollResponseParams) => export const makePollResponse = ({event, selectedIds}: PollResponseParams) =>
makeEvent(POLL_RESPONSE, { makeEvent(PollResponse, {
content: "", content: "",
tags: [["e", event.id], ...selectedIds.map(selectedId => ["response", selectedId])], tags: [["e", event.id], ...selectedIds.map(selectedId => ["response", selectedId])],
}) })
@@ -709,18 +724,34 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
// Update Profile // Update Profile
export const initProfile = (profile: Profile) => { export const initProfile = (profile: Profile) => {
const event = makeEvent(PROFILE, createProfile(profile)) const template = createProfile(profile)
return publishThunk({event, relays: DEFAULT_RELAYS}) // Start out protected by default
template.tags.push(PROTECTED)
const event = makeEvent(PROFILE, template)
// Don't publish anywhere yet, wait until they join a space
return publishThunk({event, relays: []})
} }
export const updateProfile = ({profile}: {profile: Profile}) => { export const updateProfile = ({
profile,
shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || []),
}: {
profile: Profile
shouldBroadcast?: boolean
}) => {
const router = Router.get() const router = Router.get()
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile) const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
const scenarios = [router.FromRelays(get(userSpaceUrls)), router.FromUser(), router.Index()] const scenarios = [router.FromRelays(get(userSpaceUrls))]
// Remove protected tag, we used to add it if (shouldBroadcast) {
scenarios.push(router.FromUser(), router.Index())
template.tags = template.tags.filter(nthNe(0, "-")) template.tags = template.tags.filter(nthNe(0, "-"))
} else {
template.tags = uniqTags([...template.tags, PROTECTED])
}
const event = makeEvent(template.kind, template) const event = makeEvent(template.kind, template)
const relays = router.merge(scenarios).getUrls() const relays = router.merge(scenarios).getUrls()
@@ -735,10 +766,9 @@ export const addSpaceMembers = async (
pubkeys: string[], pubkeys: string[],
): Promise<string | undefined> => { ): Promise<string | undefined> => {
const spaceMembers = get(deriveSpaceMembers(url)) const spaceMembers = get(deriveSpaceMembers(url))
const results = await Promise.all( const results = await Promise.all(
pubkeys pubkeys
.filter(pubkey => !spaceMembers || !spaceMembers.includes(pubkey)) .filter(pubkey => !spaceMembers.includes(pubkey))
.map(pubkey => .map(pubkey =>
manageRelay(url, { manageRelay(url, {
method: ManagementMethod.AllowPubkey, method: ManagementMethod.AllowPubkey,
+50 -87
View File
@@ -1,6 +1,5 @@
import {writable} from "svelte/store" import {get, writable} from "svelte/store"
import { import {
batch,
call, call,
uniq, uniq,
int, int,
@@ -26,8 +25,7 @@ import {
sortEventsDesc, sortEventsDesc,
} from "@welshman/util" } from "@welshman/util"
import type {TrustedEvent, Filter, List} from "@welshman/util" import type {TrustedEvent, Filter, List} from "@welshman/util"
import {load, request, mergeRepositoryUpdates} from "@welshman/net" import {load, request} from "@welshman/net"
import type {RepositoryUpdate} from "@welshman/net"
import {repository, loadRelay, tracker} from "@welshman/app" import {repository, loadRelay, tracker} from "@welshman/app"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
@@ -58,75 +56,57 @@ export const makeFeed = ({
let backwardWindow = [at - interval, at] let backwardWindow = [at - interval, at]
let forwardWindow = [at, at + interval] let forwardWindow = [at, at + interval]
const insertIntoBuffer = (event: TrustedEvent) => { const insertEvent = (event: TrustedEvent) => {
for (let i = 0; i < buffer.length; i++) { let handled = false
if (buffer[i].created_at < event.created_at) {
buffer.splice(i, 0, event)
return
}
}
buffer.push(event)
}
// Batch-insert events into the visible store with a single update
const insertEvents = (newEvents: TrustedEvent[]) => {
const visible: TrustedEvent[] = []
for (const event of newEvents) {
if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) { if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) {
visible.push(event) const $events = get(events)
} else {
insertIntoBuffer(event)
}
}
if (visible.length > 0) {
events.update($events => {
for (const event of visible) {
let inserted = false
for (let i = 0; i < $events.length; i++) { for (let i = 0; i < $events.length; i++) {
if ($events[i].created_at > event.created_at) { if ($events[i].created_at > event.created_at) {
$events = insertAt(i, event, $events) events.set(insertAt(i, event, $events))
inserted = true handled = true
break break
} }
} }
if (!inserted) {
$events = [...$events, event] if (!handled) {
events.set([...$events, event])
}
} else {
for (let i = 0; i < buffer.length; i++) {
if (buffer[i].created_at > event.created_at) {
buffer.splice(i, 0, event)
handled = true
break
} }
} }
return $events
}) if (!handled) {
buffer.push(event)
}
} }
} }
const unsubscribers = [ const unsubscribers = [
on( on(repository, "update", ({added, removed}) => {
repository,
"update",
batch(150, (updates: RepositoryUpdate[]) => {
const {added, removed} = mergeRepositoryUpdates(updates)
if (removed.size > 0) { if (removed.size > 0) {
buffer = buffer.filter(e => !removed.has(e.id)) buffer = buffer.filter(e => !removed.has(e.id))
events.update($events => $events.filter(e => !removed.has(e.id))) events.update($events => $events.filter(e => !removed.has(e.id)))
} }
const matching = added.filter( for (const event of added) {
event => matchFilters(filters, event) && tracker.getRelays(event.id).has(url), if (matchFilters(filters, event) && tracker.getRelays(event.id).has(url)) {
) insertEvent(event)
}
if (matching.length > 0) {
insertEvents(matching)
} }
}), }),
),
on(tracker, "add", (id: string, trackerUrl: string) => { on(tracker, "add", (id: string, trackerUrl: string) => {
if (trackerUrl === url) { if (trackerUrl === url) {
const event = repository.getEvent(id) const event = repository.getEvent(id)
if (event && matchFilters(filters, event)) { if (event && matchFilters(filters, event)) {
insertEvents([event]) insertEvent(event)
} }
} }
}), }),
@@ -152,15 +132,17 @@ export const makeFeed = ({
element, element,
delay: 300, delay: 300,
threshold: 5000, threshold: 5000,
onScroll: async () => { onScroll: () => {
const [since, until] = backwardWindow const [since, until] = backwardWindow
backwardWindow = [since - interval, since] backwardWindow = [since - interval, since]
insertEvents(buffer.splice(0, 30)) for (const event of buffer.splice(0, 30)) {
insertEvent(event)
}
if (until > now() - int(2, YEAR)) { if (until > now() - int(2, YEAR)) {
await loadTimeframe(since, until) loadTimeframe(since, until)
} else if (!buffer.some(e => e.created_at < at)) { } else if (!buffer.some(e => e.created_at < at)) {
backwardScroller.stop() backwardScroller.stop()
onBackwardExhausted?.() onBackwardExhausted?.()
@@ -173,15 +155,17 @@ export const makeFeed = ({
reverse: true, reverse: true,
delay: 300, delay: 300,
threshold: 5000, threshold: 5000,
onScroll: async () => { onScroll: () => {
const [since, until] = forwardWindow const [since, until] = forwardWindow
forwardWindow = [until, until + interval] forwardWindow = [until, until + interval]
insertEvents(buffer.splice(0, 30)) for (const event of buffer.splice(0, 30)) {
insertEvent(event)
}
if (until < now()) { if (until < now()) {
await loadTimeframe(since, until) loadTimeframe(since, until)
} else if (!buffer.some(e => e.created_at > at)) { } else if (!buffer.some(e => e.created_at > at)) {
forwardScroller.stop() forwardScroller.stop()
onForwardExhausted?.() onForwardExhausted?.()
@@ -224,61 +208,40 @@ export const makeCalendarFeed = ({
const events = writable(sortBy(getStart, getEventsForUrl(url, filters))) const events = writable(sortBy(getStart, getEventsForUrl(url, filters)))
// Batch-insert calendar events into the store with a single update const insertEvent = (event: TrustedEvent) => {
const insertEvents = (newEvents: TrustedEvent[]) => {
const valid = newEvents.filter(e => !isNaN(getStart(e)) && !isNaN(getEnd(e)))
if (valid.length === 0) return
events.update($events => {
for (const event of valid) {
const start = getStart(event) const start = getStart(event)
const address = getAddress(event) const address = getAddress(event)
let handled = false if (isNaN(start) || isNaN(getEnd(event))) return
events.update($events => {
for (let i = 0; i < $events.length; i++) { for (let i = 0; i < $events.length; i++) {
if ($events[i].id === event.id) { if ($events[i].id === event.id) return $events
handled = true if (getStart($events[i]) > start) return insertAt(i, event, $events)
break
}
if (getStart($events[i]) > start) {
$events = insertAt(i, event, $events)
handled = true
break
}
} }
if (!handled) { return [...$events.filter(e => getAddress(e) !== address), event]
$events = [...$events.filter(e => getAddress(e) !== address), event]
}
}
return $events
}) })
} }
const unsubscribers = [ const unsubscribers = [
on( on(repository, "update", ({added, removed}) => {
repository,
"update",
batch(150, (updates: RepositoryUpdate[]) => {
const {added, removed} = mergeRepositoryUpdates(updates)
if (removed.size > 0) { if (removed.size > 0) {
events.update($events => $events.filter(e => !removed.has(e.id))) events.update($events => $events.filter(e => !removed.has(e.id)))
} }
const matching = added.filter(event => matchFilters(filters, event)) for (const event of added) {
if (matchFilters(filters, event)) {
if (matching.length > 0) { insertEvent(event)
insertEvents(matching) }
} }
}), }),
),
on(tracker, "add", (id: string, trackerUrl: string) => { on(tracker, "add", (id: string, trackerUrl: string) => {
if (trackerUrl === url) { if (trackerUrl === url) {
const event = repository.getEvent(id) const event = repository.getEvent(id)
if (event && matchFilters(filters, event)) { if (event && matchFilters(filters, event)) {
insertEvents([event]) insertEvent(event)
} }
} }
}), }),
+106 -136
View File
@@ -3,6 +3,7 @@ import {context as pomadeContext} from "@pomade/core"
import {Capacitor} from "@capacitor/core" import {Capacitor} from "@capacitor/core"
import {derived, readable, writable} from "svelte/store" import {derived, readable, writable} from "svelte/store"
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import {Poll} from "nostr-tools/kinds"
import { import {
on, on,
gt, gt,
@@ -92,7 +93,6 @@ import {
THREAD, THREAD,
CLASSIFIED, CLASSIFIED,
WRAP, WRAP,
POLL,
PROFILE, PROFILE,
ZAP_GOAL, ZAP_GOAL,
ZAP_REQUEST, ZAP_REQUEST,
@@ -327,7 +327,7 @@ if (ENABLE_ZAPS) {
REACTION_KINDS.push(ZAP_RESPONSE) REACTION_KINDS.push(ZAP_RESPONSE)
} }
export const CONTENT_KINDS = [ZAP_GOAL, EVENT_TIME, THREAD, CLASSIFIED, POLL] export const CONTENT_KINDS = [ZAP_GOAL, EVENT_TIME, THREAD, CLASSIFIED, Poll]
export const DM_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE] export const DM_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]
@@ -552,7 +552,7 @@ export const chatsById = call(() => {
setTimeout(() => { setTimeout(() => {
addEvents(added) addEvents(added)
removeEvents(removed) removeEvents(removed)
}, 200) }, 50)
}), }),
] ]
@@ -560,9 +560,13 @@ export const chatsById = call(() => {
}) })
}) })
export const deriveChat = makeDeriveItem(chatsById) export const deriveChat = call(() => {
const _deriveChat = makeDeriveItem(chatsById)
export const chatSearch = derived(throttled(1500, chatsById), $chatsByPubkey => { return (pubkeys: string[]) => _deriveChat(makeChatId(pubkeys))
})
export const chatSearch = derived(throttled(800, chatsById), $chatsByPubkey => {
return createSearch( return createSearch(
sortBy(c => -c.last_activity, Array.from($chatsByPubkey.values())), sortBy(c => -c.last_activity, Array.from($chatsByPubkey.values())),
{ {
@@ -589,8 +593,6 @@ export const getRoomType = (room: RoomMeta): RoomType =>
export const makeRoomId = (url: string, h: string) => `${url}'${h}` export const makeRoomId = (url: string, h: string) => `${url}'${h}`
export const isRoomId = (id: string) => id.includes("'")
export const splitRoomId = (id: string) => id.split("'") export const splitRoomId = (id: string) => id.split("'")
export const hasNip29 = (relay?: RelayProfile) => export const hasNip29 = (relay?: RelayProfile) =>
@@ -603,7 +605,7 @@ export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
}) })
export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => { export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => {
const result = new Map<string, Room[]>() const metaByIdByUrl = new Map<string, Map<string, Room>>()
for (const [url, events] of roomMetaEventsByIdByUrl.entries()) { for (const [url, events] of roomMetaEventsByIdByUrl.entries()) {
const [metaEvents, deleteEvents] = partition(spec({kind: ROOM_META}), events.values()) const [metaEvents, deleteEvents] = partition(spec({kind: ROOM_META}), events.values())
@@ -615,8 +617,6 @@ export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByU
} }
} }
const metaById = new Map<string, Room>()
for (const event of metaEvents) { for (const event of metaEvents) {
const meta = tryCatch(() => readRoomMeta(event)) const meta = tryCatch(() => readRoomMeta(event))
@@ -624,14 +624,22 @@ export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByU
continue continue
} }
let metaById = metaByIdByUrl.get(url)
if (!metaById) {
metaById = new Map()
metaByIdByUrl.set(url, metaById)
}
const id = makeRoomId(url, meta.h) const id = makeRoomId(url, meta.h)
metaById.set(id, {...meta, url, id}) metaById.set(id, {...meta, url, id})
} }
if (metaById.size > 0) {
result.set(url, Array.from(metaById.values()))
} }
const result = new Map<string, Room[]>()
for (const [url, metaById] of metaByIdByUrl.entries()) {
result.set(url, Array.from(metaById.values()))
} }
return result return result
@@ -804,49 +812,36 @@ export const deriveOtherRooms = (url: string) =>
// Space/room memberships // Space/room memberships
export const deriveSpaceMembers = (url: string) => const getSpaceMembers = (_url: string, events: TrustedEvent[]) => {
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_MEMBERS]}]), ([event]) => const members = new Set<string>()
uniq(getTagValues("member", event?.tags ?? [])),
)
export const deriveRoomMembers = (url: string, h: string) => { for (const event of sortEventsAsc(events)) {
const filters: Filter[] = [{kinds: [ROOM_MEMBERS], "#d": [h]}] if (event.kind === RELAY_MEMBERS) {
members.clear()
return derived(deriveEventsForUrl(url, filters), ([event]) => for (const pubkey of uniq(getTagValues("member", event.tags))) {
uniq(getPubkeyTagValues(event?.tags ?? [])), members.add(pubkey)
)
} }
export type BannedPubkeyItem = { continue
pubkey: string
reason: string
} }
export const spaceBannedPubkeyItems = new Map<string, BannedPubkeyItem[]>() const pubkeys = getPubkeyTagValues(event.tags)
export const deriveSpaceBannedPubkeyItems = (url: string) => { if (event.kind === RELAY_ADD_MEMBER) {
const store = writable(spaceBannedPubkeyItems.get(url) || []) for (const pubkey of pubkeys) {
members.add(pubkey)
manageRelay(url, {method: ManagementMethod.ListBannedPubkeys, params: []}).then(res => { }
spaceBannedPubkeyItems.set(url, res.result)
store.set(res.result)
})
return store
} }
export const deriveRoomAdmins = (url: string, h: string) => { if (event.kind === RELAY_REMOVE_MEMBER) {
const filters: Filter[] = [{kinds: [ROOM_ADMINS], "#d": [h]}] for (const pubkey of pubkeys) {
members.delete(pubkey)
return derived(deriveEventsForUrl(url, filters), $events => { }
const adminsEvent = first($events) }
if (adminsEvent) {
return getPubkeyTagValues(adminsEvent.tags)
} }
return [] return Array.from(members)
})
} }
const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => { const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
@@ -885,6 +880,53 @@ const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
return Array.from(members) return Array.from(members)
} }
export const deriveSpaceMembers = (url: string) =>
derived(
deriveRelaySignedEvents(url, [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS]}]),
$events => getSpaceMembers(url, $events),
)
export type BannedPubkeyItem = {
pubkey: string
reason: string
}
export const spaceBannedPubkeyItems = new Map<string, BannedPubkeyItem[]>()
export const deriveSpaceBannedPubkeyItems = (url: string) => {
const store = writable(spaceBannedPubkeyItems.get(url) || [])
manageRelay(url, {method: ManagementMethod.ListBannedPubkeys, params: []}).then(res => {
spaceBannedPubkeyItems.set(url, res.result)
store.set(res.result)
})
return store
}
export const deriveRoomMembers = (url: string, h: string) => {
const filters: Filter[] = [
{kinds: [ROOM_MEMBERS], "#d": [h]},
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
]
return derived(deriveEventsForUrl(url, filters), $events => getRoomMembers(url, h, $events))
}
export const deriveRoomAdmins = (url: string, h: string) => {
const filters: Filter[] = [{kinds: [ROOM_ADMINS], "#d": [h]}]
return derived(deriveEventsForUrl(url, filters), $events => {
const adminsEvent = first($events)
if (adminsEvent) {
return getPubkeyTagValues(adminsEvent.tags)
}
return []
})
}
// Action items (admin review queue) // Action items (admin review queue)
// const pendingJoins: TrustedEvent[] = [] // const pendingJoins: TrustedEvent[] = []
@@ -905,33 +947,18 @@ export const deriveSpaceActionItems = (url: string) =>
for (const [h, roomEvents] of groupBy(getRoomId, $events)) { for (const [h, roomEvents] of groupBy(getRoomId, $events)) {
if (!h) continue if (!h) continue
const roomJoins: TrustedEvent[] = [] const roomJoins = roomEvents.filter(spec({kind: ROOM_JOIN}))
const roomLeaves: TrustedEvent[] = [] const roomLeaves = roomEvents.filter(spec({kind: ROOM_LEAVE}))
const roomMembershipEvents: TrustedEvent[] = [] const roomMembershipEvents = roomEvents.filter(event =>
[ROOM_MEMBERS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER].includes(event.kind),
for (const event of roomEvents) { )
switch (event.kind) {
case ROOM_JOIN:
roomJoins.push(event)
break
case ROOM_LEAVE:
roomLeaves.push(event)
break
case ROOM_MEMBERS:
case ROOM_ADD_MEMBER:
case ROOM_REMOVE_MEMBER:
roomMembershipEvents.push(event)
break
}
}
const roomMembers = new Set(getRoomMembers(url, h, roomMembershipEvents)) const roomMembers = new Set(getRoomMembers(url, h, roomMembershipEvents))
pendingJoins.push( pendingJoins.push(
...removeUndefined( ...removeUndefined(
Array.from(groupBy(e => e.pubkey, roomJoins).values()).map(events => Array.from(groupBy(e => e.pubkey, roomJoins).values())
first(sortEventsDesc(events)), .map(sortEventsDesc)
), .map(first),
).filter(({pubkey, created_at}) => { ).filter(({pubkey, created_at}) => {
if (roomMembers.has(pubkey)) return false if (roomMembers.has(pubkey)) return false
if ( if (
@@ -981,49 +1008,19 @@ export const deriveUserIsSpaceAdmin = memoize((url?: string) => {
}) })
export const deriveUserSpaceMembershipStatus = (url: string) => { export const deriveUserSpaceMembershipStatus = (url: string) => {
// Fetch member list and user add/remove events directly in this derivation. const filters: Filter[] = [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]
const memberListFilters: Filter[] = [{kinds: [RELAY_MEMBERS]}]
const userEventFilters: Filter[] = [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]}]
return derived( return derived(
[ [
pubkey, pubkey,
deriveRelaySignedEvents(url, memberListFilters), deriveSpaceMembers(url),
deriveRelaySignedEvents(url, userEventFilters), deriveEventsForUrl(url, filters),
deriveEventsForUrl(url, [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]),
deriveUserIsSpaceAdmin(url), deriveUserIsSpaceAdmin(url),
], ],
([$pubkey, $memberListEvents, $userAddRemoveEvents, $joinLeaveEvents, $isAdmin]) => { ([$pubkey, $members, $events, $isAdmin]) => {
// If admin, always granted. const isMember = $members.includes($pubkey!) || $isAdmin
if ($isAdmin) {
return MembershipStatus.Granted
}
const membersEvent = $memberListEvents.find(spec({kind: RELAY_MEMBERS})) for (const event of $events) {
const memberList = membersEvent ? uniq(getTagValues("member", membersEvent.tags)) : undefined
let isMember = false
if (memberList) {
// Member list exists - check if user is in it.
isMember = memberList.includes($pubkey!)
} else {
// No member list available - replay the user's add/remove history.
for (const event of sortBy(e => e.created_at, $userAddRemoveEvents)) {
if (event.pubkey !== $pubkey) {
continue
}
if (event.kind === RELAY_ADD_MEMBER) {
isMember = true
} else if (event.kind === RELAY_REMOVE_MEMBER) {
isMember = false
}
}
}
for (const event of $joinLeaveEvents) {
// Join events indicate pending or granted status, leave resets to initial.
if (event.pubkey !== $pubkey) { if (event.pubkey !== $pubkey) {
continue continue
} }
@@ -1049,46 +1046,19 @@ export const deriveUserIsRoomAdmin = (url: string, h: string) =>
) )
export const deriveUserRoomMembershipStatus = (url: string, h: string) => { export const deriveUserRoomMembershipStatus = (url: string, h: string) => {
// Fetch the room member list and the current user's add/remove events. const filters: Filter[] = [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]
const userEventFilters: Filter[] = [{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]}]
const joinLeaveFilters: Filter[] = [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]
return derived( return derived(
[ [
pubkey, pubkey,
deriveRoomMembers(url, h), deriveRoomMembers(url, h),
deriveEventsForUrl(url, userEventFilters), deriveEventsForUrl(url, filters),
deriveEventsForUrl(url, joinLeaveFilters),
deriveUserIsRoomAdmin(url, h), deriveUserIsRoomAdmin(url, h),
], ],
([$pubkey, $memberList, $userAddRemoveEvents, $joinLeaveEvents, $isAdmin]) => { ([$pubkey, $members, $events, $isAdmin]) => {
// If admin of this room's space, always granted. const isMember = $members.includes($pubkey!) || $isAdmin
if ($isAdmin) {
return MembershipStatus.Granted
}
let isMember = false for (const event of $events) {
if ($memberList) {
// Member list exists - check if user is in it.
isMember = $memberList.includes($pubkey!)
} else {
// No member list available - replay the user's add/remove history.
for (const event of sortEventsAsc($userAddRemoveEvents)) {
if (event.pubkey !== $pubkey) {
continue
}
if (event.kind === ROOM_ADD_MEMBER) {
isMember = true
} else if (event.kind === ROOM_REMOVE_MEMBER) {
isMember = false
}
}
}
for (const event of $joinLeaveEvents) {
// Join events indicate pending or granted status, leave resets to initial.
if (event.pubkey !== $pubkey) { if (event.pubkey !== $pubkey) {
continue continue
} }
+62 -10
View File
@@ -1,6 +1,7 @@
import {page} from "$app/stores" import {page} from "$app/stores"
import type {Unsubscriber} from "svelte/store" import type {Unsubscriber} from "svelte/store"
import {last, call, assoc, chunk, WEEK, ago} from "@welshman/lib" import {last, call, ifLet, assoc, chunk, WEEK, ago} from "@welshman/lib"
import {PollResponse} from "nostr-tools/kinds"
import {merged} from "@welshman/store" import {merged} from "@welshman/store"
import { import {
getListTags, getListTags,
@@ -19,9 +20,9 @@ import {
RELAY_ADD_MEMBER, RELAY_ADD_MEMBER,
RELAY_REMOVE_MEMBER, RELAY_REMOVE_MEMBER,
MESSAGE, MESSAGE,
POLL_RESPONSE,
isSignedEvent, isSignedEvent,
unionFilters, unionFilters,
getTagValue,
} from "@welshman/util" } from "@welshman/util"
import type {Filter, List, PublishedList, TrustedEvent} from "@welshman/util" import type {Filter, List, PublishedList, TrustedEvent} from "@welshman/util"
import {request, requestOne, Difference, DifferenceEvent} from "@welshman/net" import {request, requestOne, Difference, DifferenceEvent} from "@welshman/net"
@@ -267,21 +268,58 @@ const syncUserData = () => {
// Spaces // Spaces
const syncSpace = (url: string) => { const syncSpace = (url: string, rooms: string[]) => {
const since = ago(WEEK) const since = ago(WEEK)
const seen = new Set<string>()
const controller = new AbortController() const controller = new AbortController()
const relayKinds = [RELAY_MEMBERS]
const pullRoomContent = (room: string) => {
if (!seen.has(room)) {
seen.add(room)
pullAndListen({
url,
signal: controller.signal,
filters: [
{kinds: [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS], "#d": [room]},
{kinds: [MESSAGE], since, "#h": [room]},
makeCommentFilter(CONTENT_KINDS, {since, "#h": [room]}),
{
kinds: [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
"#h": [room],
},
{kinds: [PollResponse], since},
],
})
}
}
for (const room of rooms) {
pullRoomContent(room)
}
const relayKinds = [RELAY_MEMBERS, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]
const roomMetaKinds = [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS] const roomMetaKinds = [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS]
const roomDeleteKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE] const roomMemberKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]
pullAndListen({ pullAndListen({
url, url,
signal: controller.signal, signal: controller.signal,
filters: [ filters: [
{kinds: [...relayKinds, ...roomMetaKinds, ...roomDeleteKinds, ...CONTENT_KINDS, MESSAGE]}, {kinds: [...relayKinds, ...roomMetaKinds, ...roomMemberKinds, MESSAGE]},
makeCommentFilter(CONTENT_KINDS, {since}), makeCommentFilter(CONTENT_KINDS, {since}),
{kinds: [...REACTION_KINDS, POLL_RESPONSE], since}, {kinds: [PollResponse], since},
], ],
onEvent: event => {
if (event.kind === ROOM_META) {
ifLet(getTagValue("d", event.tags), pullRoomContent)
}
},
})
listen({
url,
signal: controller.signal,
filters: [{kinds: REACTION_KINDS}, {kinds: [PollResponse]}],
}) })
return () => controller.abort() return () => controller.abort()
@@ -290,6 +328,7 @@ const syncSpace = (url: string) => {
const syncSpaces = () => { const syncSpaces = () => {
const store = merged([userGroupList, page]) const store = merged([userGroupList, page])
const unsubscribersByUrl = new Map<string, Unsubscriber>() const unsubscribersByUrl = new Map<string, Unsubscriber>()
const roomsByUrl = new Map<string, string>()
const unsubscribe = store.subscribe(([$userGroupList, $page]) => { const unsubscribe = store.subscribe(([$userGroupList, $page]) => {
const urls = new Set(getSpaceUrlsFromGroupList($userGroupList)) const urls = new Set(getSpaceUrlsFromGroupList($userGroupList))
@@ -303,15 +342,28 @@ const syncSpaces = () => {
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) { for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
if (!urls.has(url)) { if (!urls.has(url)) {
unsubscribersByUrl.delete(url) unsubscribersByUrl.delete(url)
roomsByUrl.delete(url)
unsubscribe() unsubscribe()
} }
} }
// Start syncing for new spaces // Start or restart syncing for each space
for (const url of urls) { for (const url of urls) {
if (!unsubscribersByUrl.has(url)) { const rooms = getSpaceRoomsFromGroupList(url, $userGroupList)
unsubscribersByUrl.set(url, syncSpace(url))
if (currentUrl === url && $page.params.h && !rooms.includes($page.params.h)) {
rooms.push($page.params.h)
} }
const roomsKey = rooms.join(",")
if (unsubscribersByUrl.has(url) && roomsByUrl.get(url) === roomsKey) continue
// Tear down existing sync if rooms changed
unsubscribersByUrl.get(url)?.()
roomsByUrl.set(url, roomsKey)
unsubscribersByUrl.set(url, syncSpace(url, rooms))
} }
}) })
-42
View File
@@ -1,42 +0,0 @@
import {mergeAttributes, Node} from "@tiptap/core"
import {RoomReferenceNodeView} from "@app/editor/RoomReferenceNodeView"
export const RoomReferenceExtension = Node.create({
name: "roomref",
atom: true,
inline: true,
group: "inline",
selectable: true,
priority: 1000,
addAttributes() {
return {
url: {default: undefined},
h: {default: undefined},
}
},
parseHTML() {
return [{tag: `span[data-type="${this.name}"]`}]
},
renderHTML({HTMLAttributes}) {
return ["span", mergeAttributes(HTMLAttributes, {"data-type": this.name}), "~"]
},
renderText({node}) {
const url = typeof node.attrs.url === "string" ? node.attrs.url : ""
const h = typeof node.attrs.h === "string" ? node.attrs.h : ""
return `${url}'${h}`
},
addNodeView() {
return RoomReferenceNodeView
},
})
-29
View File
@@ -1,29 +0,0 @@
import type {NodeViewRendererProps} from "@tiptap/core"
import {displayRelayUrl} from "@welshman/util"
import {deriveRoom} from "@app/core/state"
export const RoomReferenceNodeView = ({node}: NodeViewRendererProps) => {
const dom = document.createElement("span")
const url = typeof node.attrs.url === "string" ? node.attrs.url : ""
const h = typeof node.attrs.h === "string" ? node.attrs.h : ""
const room = deriveRoom(url, h)
dom.classList.add("tiptap-object")
const unsubRoom = room.subscribe($room => {
dom.textContent = `~${displayRelayUrl(url)} / ${$room.name || h}`
})
return {
dom,
destroy: () => {
unsubRoom()
},
selectNode() {
dom.classList.add("tiptap-active")
},
deselectNode() {
dom.classList.remove("tiptap-active")
},
}
}
-17
View File
@@ -1,17 +0,0 @@
<script lang="ts">
import {displayRelayUrl} from "@welshman/util"
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
import {splitRoomId} from "@app/core/state"
type Props = {
value: string
}
const {value}: Props = $props()
const [url = "", h = ""] = splitRoomId(value)
</script>
<div class="max-w-full overflow-hidden text-ellipsis flex flex-col">
<RoomNameWithImage {url} {h} />
<span class="text-primary text-sm">{displayRelayUrl(url)}<span> </span></span>
</div>
-81
View File
@@ -1,81 +0,0 @@
import {Clipboard} from "@capacitor/clipboard"
import {Capacitor} from "@capacitor/core"
import {Extension} from "@tiptap/core"
import {Plugin, PluginKey} from "@tiptap/pm/state"
const nativeClipboardAvailable = () =>
Capacitor.isNativePlatform() && Capacitor.isPluginAvailable("Clipboard")
const hasStandardPastePayload = (event: ClipboardEvent) => {
const clipboardData = event.clipboardData
if (!clipboardData) {
return false
}
if (Array.from(clipboardData.items).some(item => item.kind === "file")) {
return true
}
if (clipboardData.types.includes("text/html")) {
return true
}
return clipboardData.getData("text/plain") !== ""
}
const getNativeClipboardImage = async () => {
try {
const {type, value} = await Clipboard.read()
if (!type.startsWith("image/") || value === "") {
return undefined
}
const imageData = value.startsWith("data:") ? value : `data:${type};base64,${value}`
const blob = await fetch(imageData).then(res => res.blob())
if (!blob.type.startsWith("image/")) {
return undefined
}
const extension = type.split("/")[1]?.split("+")[0] || "png"
return new File([blob], `clipboard-image.${extension}`, {type: blob.type || type})
} catch {
return undefined
}
}
export const NativeClipboardPasteExtension = Extension.create({
name: "nativeClipboardPaste",
addProseMirrorPlugins() {
const editor = this.editor
return [
new Plugin({
key: new PluginKey("nativeClipboardPaste"),
props: {
handlePaste: (_view, event) => {
if (!nativeClipboardAvailable() || hasStandardPastePayload(event)) {
return false
}
event.preventDefault()
void getNativeClipboardImage().then(file => {
if (!file) {
return
}
editor.commands.addFile(file, editor.state.selection.from + 1)
})
return true
},
},
}),
]
},
})
+4 -68
View File
@@ -4,7 +4,7 @@ import {get, derived} from "svelte/store"
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
import {dec, inc} from "@welshman/lib" import {dec, inc} from "@welshman/lib"
import {throttled} from "@welshman/store" import {throttled} from "@welshman/store"
import type {PublishedProfile, RoomMeta} from "@welshman/util" import type {PublishedProfile} from "@welshman/util"
import { import {
createSearch, createSearch,
profiles, profiles,
@@ -14,27 +14,12 @@ import {
getWotGraph, getWotGraph,
} from "@welshman/app" } from "@welshman/app"
import type {FileAttributes} from "@welshman/editor" import type {FileAttributes} from "@welshman/editor"
import { import {Editor, MentionSuggestion, WelshmanExtension, editorProps} from "@welshman/editor"
Editor,
MentionSuggestion,
TippySuggestion,
WelshmanExtension,
editorProps,
} from "@welshman/editor"
import {escapeHtml} from "@lib/html" import {escapeHtml} from "@lib/html"
import {makeMentionNodeView} from "@app/editor/MentionNodeView" import {makeMentionNodeView} from "@app/editor/MentionNodeView"
import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte" import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
import {RoomReferenceExtension} from "@app/editor/RoomReferenceExtension"
import RoomSuggestion from "@app/editor/RoomSuggestion.svelte"
import {NativeClipboardPasteExtension} from "@app/editor/clipboard"
import {uploadFile} from "@app/core/commands" import {uploadFile} from "@app/core/commands"
import { import {deriveSpaceMembers} from "@app/core/state"
deriveSpaceMembers,
makeRoomId,
splitRoomId,
userSpaceUrls,
roomsByUrl,
} from "@app/core/state"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
export const makeEditor = async ({ export const makeEditor = async ({
@@ -79,7 +64,7 @@ export const makeEditor = async ({
getValue: (profile: PublishedProfile) => profile.event.pubkey, getValue: (profile: PublishedProfile) => profile.event.pubkey,
sortFn: ({score = 1, item}) => { sortFn: ({score = 1, item}) => {
const wotScore = getWotGraph().get(item.event.pubkey) || 0 const wotScore = getWotGraph().get(item.event.pubkey) || 0
const membershipScale = $spaceMembers?.includes(item.event.pubkey) ? 2 : 1 const membershipScale = $spaceMembers.includes(item.event.pubkey) ? 2 : 1
return dec(score) * inc(wotScore / getMaxWot()) * membershipScale return dec(score) * inc(wotScore / getMaxWot()) * membershipScale
}, },
@@ -97,36 +82,11 @@ export const makeEditor = async ({
}, },
) )
const roomReferenceSearch = derived(
[throttled(800, userSpaceUrls), throttled(800, roomsByUrl)],
([$userSpaceUrls, $roomsByUrl]) => {
const roomIdByMeta = new WeakMap<RoomMeta, string>()
const options: RoomMeta[] = []
for (const roomUrl of $userSpaceUrls) {
for (const room of $roomsByUrl.get(roomUrl) || []) {
roomIdByMeta.set(room, makeRoomId(roomUrl, room.h))
options.push(room)
}
}
return createSearch(options, {
getValue: item => roomIdByMeta.get(item) || item.h,
fuseOptions: {
keys: ["name", "h"],
threshold: 0.3,
shouldSort: false,
},
})
},
)
const ed = new Editor({ const ed = new Editor({
content: typeof content === "string" ? escapeHtml(content) : content, content: typeof content === "string" ? escapeHtml(content) : content,
editorProps, editorProps,
element: document.createElement("div"), element: document.createElement("div"),
extensions: [ extensions: [
RoomReferenceExtension,
WelshmanExtension.configure({ WelshmanExtension.configure({
submit, submit,
extensions: { extensions: {
@@ -168,29 +128,6 @@ export const makeEditor = async ({
mount(ProfileSuggestion, {target, props: {value, url}}) mount(ProfileSuggestion, {target, props: {value, url}})
return target
},
}),
TippySuggestion({
char: "~",
name: "roomref",
editor: (this as any).editor,
search: (term: string) => get(roomReferenceSearch).searchValues(term),
updateSignal: roomReferenceSearch,
select: (id: string, props) => {
const [roomUrl, h] = splitRoomId(id)
if (!roomUrl || !h) {
return
}
return props.command({url: roomUrl, h})
},
createSuggestion: (value: string) => {
const target = document.createElement("div")
mount(RoomSuggestion, {target, props: {value}})
return target return target
}, },
}), }),
@@ -200,7 +137,6 @@ export const makeEditor = async ({
}, },
}, },
}), }),
NativeClipboardPasteExtension,
], ],
onUpdate({editor}) { onUpdate({editor}) {
wordCount?.set(editor.storage.wordCount.words) wordCount?.set(editor.storage.wordCount.words)
-8
View File
@@ -1,4 +1,3 @@
import {App} from "@capacitor/app"
import {Capacitor} from "@capacitor/core" import {Capacitor} from "@capacitor/core"
import {Keyboard} from "@capacitor/keyboard" import {Keyboard} from "@capacitor/keyboard"
import {noop} from "@welshman/lib" import {noop} from "@welshman/lib"
@@ -14,16 +13,9 @@ export const syncKeyboard = () => {
document.body.classList.remove("keyboard-open") document.body.classList.remove("keyboard-open")
}) })
// On Android, system-dismissing the IME during pause doesn't fire keyboardWillHide,
// so on resume we force a hide to re-sync native insets and clear our CSS state.
const resumeListener = App.addListener("appStateChange", ({isActive}) => {
if (isActive) Keyboard.hide()
})
return () => { return () => {
showListener.then(listener => listener.remove()) showListener.then(listener => listener.remove())
hideListener.then(listener => listener.remove()) hideListener.then(listener => listener.remove())
resumeListener.then(listener => listener.remove())
document.body.classList.remove("keyboard-open") document.body.classList.remove("keyboard-open")
} }
} }
+1 -33
View File
@@ -1,4 +1,4 @@
import {get, writable} from "svelte/store" import {writable} from "svelte/store"
import type {Nip46ResponseWithResult} from "@welshman/signer" import type {Nip46ResponseWithResult} from "@welshman/signer"
import {Nip46Broker} from "@welshman/signer" import {Nip46Broker} from "@welshman/signer"
import {makeSecret} from "@welshman/util" import {makeSecret} from "@welshman/util"
@@ -11,22 +11,6 @@ import {
} from "@app/core/state" } from "@app/core/state"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
const APP_SCHEME = "social.flotilla"
const makeSignerCallbackUrl = (path: string) => `${APP_SCHEME}://x-callback-url/${path}`
const makeSignerLaunchUrl = (nostrconnectUrl: string) => {
const params = new URLSearchParams({
method: "connect",
nostrconnect: nostrconnectUrl,
"x-source": APP_SCHEME,
"x-success": makeSignerCallbackUrl("authSuccess"),
"x-error": makeSignerCallbackUrl("authError"),
})
return `nostrsigner://x-callback-url/auth/nip46?${params.toString()}`
}
export class Nip46Controller { export class Nip46Controller {
url = writable("") url = writable("")
bunker = writable("") bunker = writable("")
@@ -70,22 +54,6 @@ export class Nip46Controller {
} }
} }
launchSigner() {
const nostrconnectUrl = get(this.url)
const signerUrl = nostrconnectUrl && makeSignerLaunchUrl(nostrconnectUrl)
if (!signerUrl) {
pushToast({
theme: "error",
message: "Unable to open signer app right now. Please try again.",
})
return
}
window.location.href = signerUrl
}
stop() { stop() {
this.broker.cleanup() this.broker.cleanup()
this.abortController.abort() this.abortController.abort()
+5 -20
View File
@@ -1,4 +1,4 @@
import {derived, get, writable} from "svelte/store" import {derived} from "svelte/store"
import {Badge} from "@capawesome/capacitor-badge" import {Badge} from "@capawesome/capacitor-badge"
import {synced, throttled, withGetter} from "@welshman/store" import {synced, throttled, withGetter} from "@welshman/store"
import {pubkey, tracker, repository, relaysByUrl} from "@welshman/app" import {pubkey, tracker, repository, relaysByUrl} from "@welshman/app"
@@ -8,7 +8,6 @@ import {deriveEventsByIdByUrl} from "@welshman/store"
import {sortEventsDesc, getTagValue, MESSAGE} from "@welshman/util" import {sortEventsDesc, getTagValue, MESSAGE} from "@welshman/util"
import {makeSpacePath, makeRoomPath, makeSpaceChatPath, makeChatPath} from "@app/util/routes" import {makeSpacePath, makeRoomPath, makeSpaceChatPath, makeChatPath} from "@app/util/routes"
import { import {
CONTENT_KINDS,
notificationSettings, notificationSettings,
chatsById, chatsById,
userGroupList, userGroupList,
@@ -36,9 +35,6 @@ export const deriveChecked = (key: string) => derived(checked, prop<number>(key)
export const setChecked = (key: string) => checked.update(assoc(key, now())) export const setChecked = (key: string) => checked.update(assoc(key, now()))
/** Room path while video call UI hides chat; checked + badge stay active until chat is shown. */
export const deferredRoomPath = writable<string | undefined>(undefined)
export const syncChecked = () => { export const syncChecked = () => {
let prev = "" let prev = ""
@@ -60,11 +56,8 @@ export const syncChecked = () => {
// Set checked when we visit a given page - but delay it a tad // Set checked when we visit a given page - but delay it a tad
setTimeout(() => { setTimeout(() => {
const defer = get(deferredRoomPath)
checked.update($checked => { checked.update($checked => {
for (const path of getPaths($page.url.pathname)) { for (const path of getPaths($page.url.pathname)) {
if (defer && path === defer) continue
$checked[path] = now() $checked[path] = now()
} }
@@ -91,7 +84,7 @@ export const allNotifications = derived(
deriveEventsByIdByUrl({ deriveEventsByIdByUrl({
tracker, tracker,
repository, repository,
filters: [{kinds: [MESSAGE, ...CONTENT_KINDS]}, makeCommentFilter(CONTENT_KINDS)], filters: [{kinds: [MESSAGE]}, makeCommentFilter([MESSAGE])],
}), }),
], ],
identity, identity,
@@ -157,17 +150,9 @@ export const allNotifications = derived(
}, },
) )
export const notifications = derived( export const notifications = derived([page, allNotifications], ([$page, $allNotifications]) => {
[page, allNotifications, deferredRoomPath], return new Set([...$allNotifications].filter(p => !$page.url.pathname.startsWith(p)))
([$page, $allNotifications, $deferredRoomPath]) => })
new Set(
[...$allNotifications].filter(p => {
if (!$page.url.pathname.startsWith(p)) return true
if ($deferredRoomPath && p === $deferredRoomPath) return true
return false
}),
),
)
// Badges // Badges
-11
View File
@@ -1,11 +0,0 @@
export const POMADE_INVALID_LOGIN_MESSAGE = "Invalid login information"
export const POMADE_NETWORK_ERROR_MESSAGE = "Network error, please try again"
type PomadeMessage = {
res?: unknown
}
export const getPomadeLoginFailureMessage = (messages: PomadeMessage[]) =>
messages.some(message => message.res !== undefined)
? POMADE_INVALID_LOGIN_MESSAGE
: POMADE_NETWORK_ERROR_MESSAGE
+2 -6
View File
@@ -23,7 +23,6 @@ import {
} from "@welshman/util" } from "@welshman/util"
import { import {
DM_KINDS, DM_KINDS,
CONTENT_KINDS,
notificationSettings, notificationSettings,
pushState, pushState,
shouldNotify, shouldNotify,
@@ -45,10 +44,7 @@ export type PushPermissionResult = {
} }
export const onNotification = call(() => { export const onNotification = call(() => {
const allFilters = [ const allFilters = [{kinds: [MESSAGE, ...DM_KINDS]}, makeCommentFilter([MESSAGE])]
{kinds: [MESSAGE, ...CONTENT_KINDS, ...DM_KINDS]},
makeCommentFilter(CONTENT_KINDS),
]
const filters = allFilters.map(assoc("since", now())) const filters = allFilters.map(assoc("since", now()))
const subscribers: Subscriber<TrustedEvent>[] = [] const subscribers: Subscriber<TrustedEvent>[] = []
@@ -161,7 +157,7 @@ export const syncRelaySubscriptions = (
userSettingsValues, userSettingsValues,
]).subscribe( ]).subscribe(
throttle(3000, ([$userSpaceUrls, {spaces, mentions}, {alerts}]) => { throttle(3000, ([$userSpaceUrls, {spaces, mentions}, {alerts}]) => {
const baseFilters = [{kinds: [MESSAGE, ...CONTENT_KINDS]}, makeCommentFilter(CONTENT_KINDS)] const baseFilters = [{kinds: [MESSAGE]}, makeCommentFilter([MESSAGE])]
for (const url of $userSpaceUrls) { for (const url of $userSpaceUrls) {
const {notify = true, exceptions = []} = alerts.find(spec({url})) || {} const {notify = true, exceptions = []} = alerts.find(spec({url})) || {}
+6 -6
View File
@@ -6,6 +6,7 @@ import {page} from "$app/stores"
import {nthEq} from "@welshman/lib" import {nthEq} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {getAddress} from "@welshman/util" import {getAddress} from "@welshman/util"
import {Poll} from "nostr-tools/kinds"
import {tracker, userMessagingRelayList} from "@welshman/app" import {tracker, userMessagingRelayList} from "@welshman/app"
import {identity} from "@welshman/lib" import {identity} from "@welshman/lib"
import { import {
@@ -15,7 +16,6 @@ import {
CLASSIFIED, CLASSIFIED,
ZAP_GOAL, ZAP_GOAL,
EVENT_TIME, EVENT_TIME,
POLL,
getPubkeyTagValues, getPubkeyTagValues,
getRelaysFromList, getRelaysFromList,
} from "@welshman/util" } from "@welshman/util"
@@ -63,11 +63,11 @@ export const goToSpace = async (url: string) => {
const prevPath = lastPageBySpaceUrl.get(encodeRelay(url)) const prevPath = lastPageBySpaceUrl.get(encodeRelay(url))
if (prevPath && prevPath !== makeSpacePath(url)) { if (prevPath && prevPath !== makeSpacePath(url)) {
goto(prevPath, {replaceState: true}) goto(prevPath)
} else if (window.matchMedia(`(min-width: ${theme.screens.md})`).matches) { } else if (window.matchMedia(`(min-width: ${theme.screens.md})`).matches) {
goto(makeSpacePath(url, "recent"), {replaceState: true}) goto(makeSpacePath(url, "recent"))
} else { } else {
goto(makeSpacePath(url), {replaceState: true}) goto(makeSpacePath(url))
} }
} }
@@ -149,7 +149,7 @@ export const getEventPath = (event: TrustedEvent, urls: string[]) => {
return makeCalendarPath(url, getAddress(event)) return makeCalendarPath(url, getAddress(event))
} }
if (event.kind === POLL) { if (event.kind === Poll) {
return makePollPath(url, event.id) return makePollPath(url, event.id)
} }
@@ -199,7 +199,7 @@ export const getRoomItemPath = (url: string, event: TrustedEvent) => {
return makeGoalPath(url, event.id) return makeGoalPath(url, event.id)
case EVENT_TIME: case EVENT_TIME:
return makeCalendarPath(url, getAddress(event)) return makeCalendarPath(url, getAddress(event))
case POLL: case Poll:
return makePollPath(url, event.id) return makePollPath(url, event.id)
} }
} }
+4 -17
View File
@@ -48,18 +48,6 @@ import {
import type {Unsubscriber} from "svelte/store" import type {Unsubscriber} from "svelte/store"
import {db} from "@app/core/storage" import {db} from "@app/core/storage"
// Shared interval for all non-critical store flushes, so they batch on the same cadence
const FLUSH_INTERVAL = 3000
// Wraps a write callback to run during idle time (non-critical persistence)
const idleWrite = <T>(f: (xs: T[]) => void): ((xs: T[]) => void) => {
if (typeof requestIdleCallback !== "undefined") {
return (xs: T[]) => requestIdleCallback(() => f(xs))
}
return f
}
const kinds = { const kinds = {
meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS], meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS],
alert: [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID], alert: [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID],
@@ -211,15 +199,14 @@ const loadCriticalRelays = async () => {
relaysByUrl.set(indexBy(r => r.url, await table.getAll())) relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
} }
const syncRelays = () => const syncRelays = () => onRelay(batch(1000, db.table<RelayProfile>("relays").bulkPut))
onRelay(batch(FLUSH_INTERVAL, idleWrite(db.table<RelayProfile>("relays").bulkPut)))
const initRelayStats = async () => { const initRelayStats = async () => {
const table = db.table<RelayStats>("relayStats") const table = db.table<RelayStats>("relayStats")
relayStatsByUrl.set(indexBy(r => r.url, await table.getAll())) relayStatsByUrl.set(indexBy(r => r.url, await table.getAll()))
return onRelayStats(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))) return onRelayStats(batch(1000, table.bulkPut))
} }
const initHandles = async () => { const initHandles = async () => {
@@ -227,7 +214,7 @@ const initHandles = async () => {
handlesByNip05.set(indexBy(r => r.nip05, await table.getAll())) handlesByNip05.set(indexBy(r => r.nip05, await table.getAll()))
return onHandle(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))) return onHandle(batch(1000, table.bulkPut))
} }
const initZappers = async () => { const initZappers = async () => {
@@ -235,7 +222,7 @@ const initZappers = async () => {
zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll())) zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll()))
return onZapper(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))) return onZapper(batch(3000, table.bulkPut))
} }
const initPlaintext = async () => { const initPlaintext = async () => {
-8
View File
@@ -1,8 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="6" r="1.5" fill="#000000"/>
<circle cx="15" cy="6" r="1.5" fill="#000000"/>
<circle cx="9" cy="12" r="1.5" fill="#000000"/>
<circle cx="15" cy="12" r="1.5" fill="#000000"/>
<circle cx="9" cy="18" r="1.5" fill="#000000"/>
<circle cx="15" cy="18" r="1.5" fill="#000000"/>
</svg>

Before

Width:  |  Height:  |  Size: 400 B

-5
View File
@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 11.5C2 8.21252 2 6.56878 2.90796 5.46243C3.07418 5.25989 3.25989 5.07418 3.46243 4.90796C4.56878 4 6.21252 4 9.5 4C12.7875 4 14.4312 4 15.5376 4.90796C15.7401 5.07418 15.9258 5.25989 16.092 5.46243C17 6.56878 17 8.21252 17 11.5V12.5C17 15.7875 17 17.4312 16.092 18.5376C15.9258 18.7401 15.7401 18.9258 15.5376 19.092C14.4312 20 12.7875 20 9.5 20C6.21252 20 4.56878 20 3.46243 19.092C3.25989 18.9258 3.07418 18.7401 2.90796 18.5376C2 17.4312 2 15.7875 2 12.5V11.5Z" stroke="#000000" stroke-width="1.5"/>
<path d="M17 9.50019L17.6584 9.17101C19.6042 8.19807 20.5772 7.7116 21.2886 8.15127C22 8.59094 22 9.67872 22 11.8543V12.1461C22 14.3217 22 15.4094 21.2886 15.8491C20.5772 16.2888 19.6042 15.8023 17.6584 14.8294L17 14.5002V9.50019Z" stroke="#000000" stroke-width="1.5"/>
<path d="M22 2L2 22" stroke="#000000" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 970 B

+1 -1
View File
@@ -17,7 +17,7 @@
"aria-pressed"?: boolean "aria-pressed"?: boolean
} = $props() } = $props()
const className = $derived(`text-left cursor-pointer ${restProps.class}`) const className = $derived(`text-left ${restProps.class}`)
const onClick = (e: Event) => { const onClick = (e: Event) => {
e.preventDefault() e.preventDefault()
+6 -7
View File
@@ -68,23 +68,22 @@
}) })
</script> </script>
<div class="relative focus-within:z-modal grid grid-cols-2 gap-2" bind:this={element}> <div class="relative grid grid-cols-2 gap-2" bind:this={element}>
<div class="relative group z-popover"> <div class="relative">
<DateInput format="yyyy-MM-dd" placeholder="" bind:value={date} /> <DateInput format="yyyy-MM-dd" placeholder="" bind:value={date} />
<div <div class="absolute right-2 top-0 flex h-12 cursor-pointer items-center gap-2">
class="absolute right-2 top-0 flex h-12 cursor-pointer items-center gap-2 opacity-100 group-focus-within:opacity-0 group-focus-within:pointer-events-none transition-opacity pointer-events-none">
{#if date} {#if date}
<Button onclick={clear} class="h-5 pointer-events-auto"> <Button onclick={clear} class="h-5">
<Icon icon={CloseCircle} /> <Icon icon={CloseCircle} />
</Button> </Button>
{:else} {:else}
<Button onclick={focusDate} class="h-5 pointer-events-auto"> <Button onclick={focusDate} class="h-5">
<Icon icon={CalendarMinimalistic} /> <Icon icon={CalendarMinimalistic} />
</Button> </Button>
{/if} {/if}
</div> </div>
</div> </div>
<label class="input input-bordered flex items-center relative"> <label class="input input-bordered flex items-center">
<input <input
list="time-options" list="time-options"
class="grow" class="grow"
+2 -2
View File
@@ -37,9 +37,9 @@
) )
const buttonClass = $derived( const buttonClass = $derived(
cx("absolute right-3 z-tooltip btn btn-circle btn-neutral btn-sm", { cx("absolute right-3 btn btn-circle btn-neutral btn-sm", {
"top-3": fullscreen, "top-3": fullscreen,
"-top-4 mr-sai": !fullscreen, "-top-4": !fullscreen,
}), }),
) )
</script> </script>
-1
View File
@@ -31,7 +31,6 @@
<svelte:document onmousemove={onMouseMove} /> <svelte:document onmousemove={onMouseMove} />
<Tippy <Tippy
class="flex"
bind:popover bind:popover
component={EmojiPicker} component={EmojiPicker}
props={{onClick}} props={{onClick}}
+1 -2
View File
@@ -10,7 +10,6 @@
<script lang="ts"> <script lang="ts">
import "emoji-picker-element" import "emoji-picker-element"
import emojiDataUrl from "emoji-picker-element-data/en/emojibase/data.json?url"
import type {Emoji} from "emoji-picker-element/shared" import type {Emoji} from "emoji-picker-element/shared"
import {onMount} from "svelte" import {onMount} from "svelte"
@@ -27,4 +26,4 @@
}) })
</script> </script>
<emoji-picker bind:this={element} data-source={emojiDataUrl} class="m-auto"></emoji-picker> <emoji-picker bind:this={element} class="m-auto"></emoji-picker>
+1 -1
View File
@@ -14,7 +14,7 @@
} = $props() } = $props()
</script> </script>
<div class={cx("fixed bottom-20 mb-sai right-4 z-nav hide-on-keyboard md:hidden", className)}> <div class={cx("fixed bottom-20 right-4 z-nav hide-on-keyboard md:hidden", className)}>
<Button <Button
class="btn btn-primary border-none shadow-xl hover:opacity-90 transition-all size-[50px] rounded-xl p-0" class="btn btn-primary border-none shadow-xl hover:opacity-90 transition-all size-[50px] rounded-xl p-0"
{onclick}> {onclick}>
+8 -14
View File
@@ -9,22 +9,16 @@
const {...props}: Props = $props() const {...props}: Props = $props()
</script> </script>
<div class="flex flex-col gap-2 {props.class}"> <div class="grid grid-cols-1 gap-2 lg:gap-6 lg:grid-cols-3 {props.class}">
<div class="flex items-center justify-between w-full gap-2"> <label class="flex items-center gap-2 font-bold">
{#if props.label} {@render props.label?.()}
<label class="flex items-center gap-2 min-w-[30%] max-w-[80%] md:max-w-none">
{@render props.label()}
</label> </label>
{/if} <div class="col-span-2 flex items-center gap-2">
<div class="flex items-center gap-2 justify-end grow"> {@render props.input?.()}
{#if props.input}
{@render props.input()}
{/if}
</div>
</div> </div>
<p class="flex-end text-sm opacity-50 lg:col-span-3">
{#if props.info} {#if props.info}
<p class="text-sm opacity-50"> {@render props.info?.()}
{@render props.info()}
</p>
{/if} {/if}
</p>
</div> </div>

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