forked from coracle/flotilla
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec507b05d6 | |||
| 339bb1afac | |||
| c441012e02 | |||
| 0d61278c56 | |||
| ffd06ab561 | |||
| eb8dd330b6 | |||
| 6267e52bdf | |||
| ab21008f34 | |||
| 0998639d59 | |||
| eccde07d06 | |||
| 770cdc5f13 | |||
| 6bafb62414 | |||
| 6ce0fbbbe6 | |||
| 8fe42e6f22 | |||
| 47a6209730 | |||
| 24d3f867f8 | |||
| 9db60374e4 | |||
| 8ef4b21dab | |||
| 8f56812dd1 | |||
| 3833cb093d | |||
| 94db65b85e | |||
| 6f731e48d2 | |||
| 99fe0e543c | |||
| c6b0799b2a | |||
| 861f2286db | |||
| 9af3e3b2e9 | |||
| 341c1b45b2 | |||
| 89f5d8cdf5 | |||
| ca3270437d | |||
| bbbc6f7363 | |||
| 8a0abacf6f | |||
| 976ccdabd4 | |||
| 99b26680b6 | |||
| c5be477855 | |||
| 32c1501e9c | |||
| 463837e7d4 | |||
| d74f142cdd | |||
| 53954aae89 | |||
| 24aa62a503 | |||
| 2618bb9c63 | |||
| 32a31045ef | |||
| 56edad77a8 | |||
| fdb604e350 | |||
| 3c66dfd83c | |||
| 81633b0a1e | |||
| 4a967de184 | |||
| 59961cbdb5 | |||
| 95d9d8bf23 | |||
| 2fd9741a2b | |||
| fe9c325580 | |||
| 61e93d4071 | |||
| 1e4a4e43dc | |||
| e1a7b051bd | |||
| 7a7af58f5c | |||
| 016ae86d50 | |||
| 2bff060a5e | |||
| 68231504d0 | |||
| 0658a8ee44 | |||
| 43fb3d35e6 | |||
| 4cc1cc95ca | |||
| 964ef441ec | |||
| 796f37d320 | |||
| b46fd94578 | |||
| bdc8e75640 | |||
| ef08821796 | |||
| 9f386f6968 | |||
| ec0b6a99e2 | |||
| f6d9e52c6e | |||
| 90f86b833d | |||
| 29bb33c26c | |||
| c740bd21d4 | |||
| 1d92709c76 | |||
| a42e1df1a7 | |||
| e33beee17d | |||
| b10ea04cb3 | |||
| e8c94177ca | |||
| f1f2083c88 | |||
| f42889c3c2 | |||
| a75e1f96eb | |||
| 85c5293082 | |||
| 37efa6a62c |
@@ -4,7 +4,6 @@ ios
|
|||||||
build
|
build
|
||||||
|
|
||||||
# Git
|
# Git
|
||||||
.git
|
|
||||||
.gitignore
|
.gitignore
|
||||||
|
|
||||||
# Env files (keep .env for build; exclude local overrides)
|
# Env files (keep .env for build; exclude local overrides)
|
||||||
|
|||||||
@@ -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=
|
VITE_THUMBNAIL_URL=https://vthumbs.coracle.social
|
||||||
VITE_GLITCHTIP_API_KEY=
|
VITE_GLITCHTIP_API_KEY=
|
||||||
GLITCHTIP_AUTH_TOKEN=
|
GLITCHTIP_AUTH_TOKEN=
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
src/assets
|
src/assets
|
||||||
|
.claude
|
||||||
target
|
target
|
||||||
build
|
build
|
||||||
.idea
|
.idea
|
||||||
@@ -13,4 +14,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,12 +1,17 @@
|
|||||||
name: Docker
|
name: Container Image Build and Publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: gitea.coracle.social
|
||||||
IMAGE_NAME: coracle-social/flotilla
|
IMAGE_NAME: coracle/flotilla
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push-image:
|
build-and-push-image:
|
||||||
@@ -23,8 +28,8 @@ jobs:
|
|||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
username: hodlbod
|
||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
password: ${{ secrets.PACKAGE_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
@@ -32,6 +37,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
|
type=sha
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@@ -45,6 +51,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
target: production
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -27,6 +27,7 @@ android/app/src/main/assets/public/
|
|||||||
node_modules/
|
node_modules/
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
build/
|
build/
|
||||||
|
build-server/
|
||||||
.svelte-kit/
|
.svelte-kit/
|
||||||
.next/
|
.next/
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ GoogleService-Info.plist
|
|||||||
.roo
|
.roo
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.claude/
|
||||||
|
|
||||||
# OS generated
|
# OS generated
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1,5 +1,62 @@
|
|||||||
# 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
|
||||||
|
|||||||
+24
-25
@@ -1,32 +1,31 @@
|
|||||||
# Stage 1: Build
|
# Build and run the Flotilla web server.
|
||||||
# Uses .env from build context for config (logo, branding, etc.)
|
#
|
||||||
# Optional: docker build --build-arg VITE_BUILD_HASH=$(git rev-parse --short HEAD) -t flotilla .
|
# docker build -t flotilla .
|
||||||
|
# docker run -p 3000:3000 flotilla
|
||||||
|
#
|
||||||
|
# Pass --build-arg VITE_BUILD_HASH=$(git rev-parse --short HEAD) to stamp the build.
|
||||||
|
# A .env in the build context is picked up by build.sh for branding config.
|
||||||
|
|
||||||
FROM node:20-bookworm AS builder
|
# https://pnpm.io/docker#example-3-build-on-cicd
|
||||||
|
FROM node:24-slim AS builder
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends curl
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN npm install -g pnpm@latest
|
RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml ./
|
|
||||||
RUN pnpm i
|
|
||||||
|
|
||||||
# Copy everything (including .env when present) - build.sh will source it
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
ARG VITE_BUILD_HASH
|
|
||||||
ENV VITE_BUILD_HASH=${VITE_BUILD_HASH}
|
|
||||||
|
|
||||||
ENV NODE_OPTIONS=--max_old_space_size=16384
|
ENV NODE_OPTIONS=--max_old_space_size=16384
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
RUN pnpm i --frozen-lockfile
|
||||||
|
COPY . .
|
||||||
|
ARG VITE_BUILD_HASH
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
RUN pnpm run build:server
|
||||||
|
|
||||||
FROM node:20-alpine
|
FROM node:24-slim AS production
|
||||||
|
ENV NODE_ENV=production
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/build /app/build
|
||||||
# Copy only the built output - no source, no .env, no dev deps
|
COPY --from=builder /app/build-server/server.js /app/server.js
|
||||||
COPY --from=builder /app/build ./build
|
EXPOSE 3000
|
||||||
|
USER node
|
||||||
CMD ["npx", "serve", "-s", "build"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -31,18 +31,18 @@ To run your own Flotilla, it's as simple as:
|
|||||||
```sh
|
```sh
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run build
|
pnpm run build
|
||||||
npx serve -s build
|
pnpm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you prefer to use a container:
|
Or, if you prefer to use a container:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
podman run -d -p 3000:3000 ghcr.io/coracle-social/flotilla:latest
|
docker run -d -p 3000:3000 gitea.coracle.social/coracle/flotilla:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, you can copy the build files into a directory of your choice and serve it yourself:
|
Alternatively, you can copy the build files into a directory of your choice and serve it yourself:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
mkdir ./mount
|
mkdir ./mount
|
||||||
podman run -v ./mount:/app/mount ghcr.io/coracle-social/flotilla:latest bash -c 'cp -r build/* mount'
|
docker run -v ./mount:/app/mount gitea.coracle.social/coracle/flotilla:latest bash -c 'cp -r build/* mount'
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ android {
|
|||||||
applicationId "social.flotilla"
|
applicationId "social.flotilla"
|
||||||
minSdk rootProject.ext.minSdkVersion
|
minSdk rootProject.ext.minSdkVersion
|
||||||
targetSdk rootProject.ext.targetSdkVersion
|
targetSdk rootProject.ext.targetSdkVersion
|
||||||
versionCode 44
|
versionCode 47
|
||||||
versionName "1.7.2"
|
versionName "1.8.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ 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,6 +7,7 @@ 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
|
||||||
@@ -76,6 +77,7 @@ 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(
|
||||||
|
|||||||
+9
-10
@@ -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 = 20L
|
private const val SOCKET_TIMEOUT_SECONDS = 30L
|
||||||
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,6 +72,8 @@ 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()
|
||||||
}
|
}
|
||||||
@@ -88,7 +90,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>()
|
||||||
var latestPair: Pair<String, JSONObject>? = null
|
val newEvents = mutableListOf<Pair<String, JSONObject>>()
|
||||||
|
|
||||||
for (sub in subscriptions) {
|
for (sub in subscriptions) {
|
||||||
val cursorKey = CURSOR_PREFIX + sub.relay + ":" + sub.key
|
val cursorKey = CURSOR_PREFIX + sub.relay + ":" + sub.key
|
||||||
@@ -102,23 +104,19 @@ 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)) {
|
||||||
val createdAt = event.optLong("created_at", 0L)
|
newEvents.add(Pair(sub.relay, event))
|
||||||
if (latestPair == null || createdAt > latestPair!!.second.optLong("created_at", 0L)) {
|
|
||||||
latestPair = Pair(sub.relay, event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (latestPair != null) {
|
for ((relay, event) in newEvents) {
|
||||||
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.success()
|
return Result.retry()
|
||||||
} finally {
|
} finally {
|
||||||
pool.closeAll()
|
pool.closeAll()
|
||||||
client.dispatcher.executorService.shutdown()
|
client.dispatcher.executorService.shutdown()
|
||||||
@@ -214,7 +212,8 @@ class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Wo
|
|||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
NotificationManagerCompat.from(context).notify(1, notification)
|
val notificationId = id.hashCode().let { if (it == 0) 1 else it }
|
||||||
|
NotificationManagerCompat.from(context).notify(notificationId, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun matchesFilter(filter: JSONObject, event: JSONObject): Boolean {
|
private fun matchesFilter(filter: JSONObject, event: JSONObject): Boolean {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ 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')
|
||||||
|
|
||||||
@@ -23,6 +26,9 @@ 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')
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 48;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -131,8 +131,9 @@
|
|||||||
504EC2FC1FED79650016851F /* Project object */ = {
|
504EC2FC1FED79650016851F /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 920;
|
LastSwiftUpdateCheck = 920;
|
||||||
LastUpgradeCheck = 920;
|
LastUpgradeCheck = 2630;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
504EC3031FED79650016851F = {
|
504EC3031FED79650016851F = {
|
||||||
CreatedOnToolsVersion = 9.2;
|
CreatedOnToolsVersion = 9.2;
|
||||||
@@ -257,6 +258,7 @@
|
|||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
CLANG_WARN_COMMA = YES;
|
CLANG_WARN_COMMA = YES;
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
@@ -264,8 +266,10 @@
|
|||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
@@ -275,8 +279,10 @@
|
|||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
@@ -295,6 +301,7 @@
|
|||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
};
|
};
|
||||||
@@ -314,6 +321,7 @@
|
|||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
CLANG_WARN_COMMA = YES;
|
CLANG_WARN_COMMA = YES;
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
@@ -321,8 +329,10 @@
|
|||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
@@ -332,8 +342,10 @@
|
|||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
@@ -345,7 +357,9 @@
|
|||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -358,14 +372,16 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 35;
|
CURRENT_PROJECT_VERSION = 38;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
MARKETING_VERSION = 1.7.2;
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.8.0;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -385,14 +401,16 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 35;
|
CURRENT_PROJECT_VERSION = 38;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
MARKETING_VERSION = 1.7.2;
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.8.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ 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
|
||||||
|
|||||||
+23
-13
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "1.7.2",
|
"version": "1.8.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "./build.sh",
|
"build": "./build.sh",
|
||||||
|
"build:server": "vite build --config vite.config.server.ts",
|
||||||
|
"start": "node server.js",
|
||||||
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
||||||
"tauri:dev": "tauri dev",
|
"tauri:dev": "tauri dev",
|
||||||
"tauri:build": "tauri build",
|
"tauri:build": "tauri build",
|
||||||
@@ -48,41 +50,48 @@
|
|||||||
"@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.2",
|
"@pomade/core": "^0.2.3",
|
||||||
"@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.12",
|
"@welshman/app": "^0.8.15",
|
||||||
"@welshman/content": "^0.8.12",
|
"@welshman/content": "^0.8.15",
|
||||||
"@welshman/editor": "^0.8.12",
|
"@welshman/editor": "^0.8.15",
|
||||||
"@welshman/feeds": "^0.8.12",
|
"@welshman/feeds": "^0.8.15",
|
||||||
"@welshman/lib": "^0.8.12",
|
"@welshman/lib": "^0.8.15",
|
||||||
"@welshman/net": "^0.8.12",
|
"@welshman/net": "^0.8.15",
|
||||||
"@welshman/router": "^0.8.12",
|
"@welshman/router": "^0.8.15",
|
||||||
"@welshman/signer": "^0.8.12",
|
"@welshman/signer": "^0.8.15",
|
||||||
"@welshman/store": "^0.8.12",
|
"@welshman/store": "^0.8.15",
|
||||||
"@welshman/util": "^0.8.12",
|
"@welshman/util": "^0.8.15",
|
||||||
|
"cheerio": "^1.2.0",
|
||||||
"compressorjs-next": "^1.1.2",
|
"compressorjs-next": "^1.1.2",
|
||||||
"daisyui": "^5.5.19",
|
"daisyui": "^5.5.19",
|
||||||
"date-picker-svelte": "^2.17.0",
|
"date-picker-svelte": "^2.17.0",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"emoji-picker-element": "^1.28.1",
|
"emoji-picker-element": "^1.28.1",
|
||||||
|
"emoji-picker-element-data": "^1.8.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
|
"hono": "^4.12.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",
|
||||||
@@ -105,5 +114,6 @@
|
|||||||
"overrides": {
|
"overrides": {
|
||||||
"sharp": "0.35.0-rc.0"
|
"sharp": "0.35.0-rc.0"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+290
-114
@@ -26,6 +26,9 @@ 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
|
||||||
@@ -44,6 +47,9 @@ 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)
|
||||||
@@ -56,12 +62,15 @@ 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.2
|
specifier: ^0.2.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))
|
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))
|
||||||
'@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))
|
||||||
@@ -71,6 +80,9 @@ 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
|
||||||
@@ -84,35 +96,38 @@ 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.12
|
specifier: ^0.8.15
|
||||||
version: 0.8.12(3074ef6691f94dc03952d8dbc98013a7)
|
version: 0.8.15(ff026297546a8274624eb18a0ea86191)
|
||||||
'@welshman/content':
|
'@welshman/content':
|
||||||
specifier: ^0.8.12
|
specifier: ^0.8.15
|
||||||
version: 0.8.12(nostr-tools@2.20.0(typescript@5.9.3))
|
version: 0.8.15(nostr-tools@2.20.0(typescript@5.9.3))
|
||||||
'@welshman/editor':
|
'@welshman/editor':
|
||||||
specifier: ^0.8.12
|
specifier: ^0.8.15
|
||||||
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))
|
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))
|
||||||
'@welshman/feeds':
|
'@welshman/feeds':
|
||||||
specifier: ^0.8.12
|
specifier: ^0.8.15
|
||||||
version: 0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)
|
version: 0.8.15(6e55dcd4e7516745e7b0228620d35545)
|
||||||
'@welshman/lib':
|
'@welshman/lib':
|
||||||
specifier: ^0.8.12
|
specifier: ^0.8.15
|
||||||
version: 0.8.12
|
version: 0.8.15
|
||||||
'@welshman/net':
|
'@welshman/net':
|
||||||
specifier: ^0.8.12
|
specifier: ^0.8.15
|
||||||
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)
|
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)
|
||||||
'@welshman/router':
|
'@welshman/router':
|
||||||
specifier: ^0.8.12
|
specifier: ^0.8.15
|
||||||
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)))
|
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)))
|
||||||
'@welshman/signer':
|
'@welshman/signer':
|
||||||
specifier: ^0.8.12
|
specifier: ^0.8.15
|
||||||
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))
|
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))
|
||||||
'@welshman/store':
|
'@welshman/store':
|
||||||
specifier: ^0.8.12
|
specifier: ^0.8.15
|
||||||
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)
|
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)
|
||||||
'@welshman/util':
|
'@welshman/util':
|
||||||
specifier: ^0.8.12
|
specifier: ^0.8.15
|
||||||
version: 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
|
version: 0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(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
|
||||||
@@ -128,9 +143,15 @@ 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
|
||||||
@@ -791,6 +812,11 @@ 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==}
|
||||||
|
|
||||||
@@ -827,6 +853,11 @@ 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==}
|
||||||
|
|
||||||
@@ -1077,6 +1108,12 @@ 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'}
|
||||||
@@ -1427,9 +1464,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.2':
|
'@pomade/core@0.2.3':
|
||||||
resolution: {integrity: sha512-FoilLsO0gVjiKMW3LV63pmXU7x3gh8YVGVulyR6QJr4h47XrsBg8vPkZtKWr4+sH3sW31e2tNIPUb3ptiuhrMA==}
|
resolution: {integrity: sha512-36+abWfMH1Mif9FjBO7xICCkGZE4IqQpy7Csxlauyt0bhYQ9GsB07LqK5Qm3GgEHNwF9rFXdSZawkM+E6BeIfg==}
|
||||||
version: 0.2.2
|
version: 0.2.3
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@frostr/bifrost': ^1.0.7
|
'@frostr/bifrost': ^1.0.7
|
||||||
@@ -2131,83 +2168,83 @@ packages:
|
|||||||
'@vite-pwa/assets-generator':
|
'@vite-pwa/assets-generator':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@welshman/app@0.8.12':
|
'@welshman/app@0.8.15':
|
||||||
resolution: {integrity: sha512-kRp+AVzn4i3FvZmdlyMknFUAb/5SnUz9A/cFKkDqWHsd+N3PbNcL2ZOlV9v5NI77GtsDF2ez6PEQfsZxWvkS/g==}
|
resolution: {integrity: sha512-GDo6w+UI/ldnh47c5IEDYWw8nbiyhnH4abJNy/q/jLBUwJ9SuiJ7GVVvhZ+t4XEo5NEMq+y4OLZs08+abf85MQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@pomade/core': ^0.2.1
|
'@pomade/core': ^0.2.1
|
||||||
'@welshman/feeds': 0.8.12
|
'@welshman/feeds': 0.8.15
|
||||||
'@welshman/lib': 0.8.12
|
'@welshman/lib': 0.8.15
|
||||||
'@welshman/net': 0.8.12
|
'@welshman/net': 0.8.15
|
||||||
'@welshman/router': 0.8.12
|
'@welshman/router': 0.8.15
|
||||||
'@welshman/signer': 0.8.12
|
'@welshman/signer': 0.8.15
|
||||||
'@welshman/store': 0.8.12
|
'@welshman/store': 0.8.15
|
||||||
'@welshman/util': 0.8.12
|
'@welshman/util': 0.8.15
|
||||||
svelte: ^4.0.0 || ^5.0.0
|
svelte: ^4.0.0 || ^5.0.0
|
||||||
|
|
||||||
'@welshman/content@0.8.12':
|
'@welshman/content@0.8.15':
|
||||||
resolution: {integrity: sha512-hviVTXdyGf04Xq7mGo/82fq6lnbyuUYOGPkf8pJqPkfGh0f3i9nKof6gkzPvjZeEYSczneI2GpLIxkaZ3w1/tw==}
|
resolution: {integrity: sha512-5qe+6Es1r62HkVdeHJPsWkOpLjhdxBTtw3d4+Or1JXl8BgpUE2JV7e+5HQQqnPRVHt3nt14YPt0oirar5p1Fvg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
nostr-tools: ^2.19.4
|
nostr-tools: ^2.19.4
|
||||||
|
|
||||||
'@welshman/editor@0.8.12':
|
'@welshman/editor@0.8.15':
|
||||||
resolution: {integrity: sha512-CEXszH6pfM1kZHU9WnB6Z97YlxEOtgao6R3hhnBL/kRXy2tUTLpmFWyMONg2vu8Uzxtwz665eZhucLsju60U6w==}
|
resolution: {integrity: sha512-lqTLQGf54yPioBn2KQsF7F5ExWM6Co31wgGaUAhCSeUGiTzUQgMEut4/N8VB1rFZ0wqU6zyPG5jgeuhFhRJWSw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@welshman/lib': 0.8.12
|
'@welshman/lib': 0.8.15
|
||||||
'@welshman/util': 0.8.12
|
'@welshman/util': 0.8.15
|
||||||
nostr-editor: ^1.1.1
|
nostr-editor: ^1.1.1
|
||||||
nostr-tools: ^2.19.4
|
nostr-tools: ^2.19.4
|
||||||
|
|
||||||
'@welshman/feeds@0.8.12':
|
'@welshman/feeds@0.8.15':
|
||||||
resolution: {integrity: sha512-Dp/063qdrbe096z6IpIneazsqscfzSwsg09ZNHB6c3OsVb6iLEXLe7mEpATGcRsZ8memuFNLVLIP51eVkBqPcQ==}
|
resolution: {integrity: sha512-xIQDKdV6uLxOz5qJUbc/2HC6qnikgH1GPoHQwBpwKH7Lga6a7IGLOR6kvghUaPpulKcuF4MxG9gmvEHqgsQkJw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@welshman/lib': 0.8.12
|
'@welshman/lib': 0.8.15
|
||||||
'@welshman/net': 0.8.12
|
'@welshman/net': 0.8.15
|
||||||
'@welshman/router': 0.8.12
|
'@welshman/router': 0.8.15
|
||||||
'@welshman/signer': 0.8.12
|
'@welshman/signer': 0.8.15
|
||||||
'@welshman/util': 0.8.12
|
'@welshman/util': 0.8.15
|
||||||
|
|
||||||
'@welshman/lib@0.8.12':
|
'@welshman/lib@0.8.15':
|
||||||
resolution: {integrity: sha512-7Y1GjAcABquWF47A1Jni5JdP+k0GH2yRmEbVhIU+0R0TubCwPAKS38J2LTvtuE9CJMX6hPS9IKEZS6qTOAaVuw==}
|
resolution: {integrity: sha512-d7o6WUSVYXOstpWTqOBDfkSyr3GOBm/UMbgFx3RXCxzib0cWm7z0w1oLWvy1N7fjHc/Jp65G2KRpT6//B9yAww==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
'@welshman/net@0.8.12':
|
'@welshman/net@0.8.15':
|
||||||
resolution: {integrity: sha512-Ba71jwb8BBwUfPPtWHKYLB0HqeDYK64oqwxfo5bYldtPfGYrlD+a7lHFSZvNyOhvyXygCaIMnaye1QxWAHP8ng==}
|
resolution: {integrity: sha512-AeJ/Vy7T6ruf1mjzzEUdH+aX5JriQKBzRn1zWZ4l8VEgxwc4w2bVte9a6aPnNJWc7JZT8ws8z+wOi4ECb6NPNA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@welshman/lib': 0.8.12
|
'@welshman/lib': 0.8.15
|
||||||
'@welshman/util': 0.8.12
|
'@welshman/util': 0.8.15
|
||||||
|
|
||||||
'@welshman/router@0.8.12':
|
'@welshman/router@0.8.15':
|
||||||
resolution: {integrity: sha512-Rr7ryBNTvTvjoLsDRMKPuoNJbBv2MgyqN2338p4vVhPMK6MOGM3Nx1og0LpHDGiLlCFuPM8RpParpIWfNnWbKw==}
|
resolution: {integrity: sha512-3lxcCYMaPX0gFaoM1GjBRvXr4UrnPA3o/mBII2Zm3gJeFuXN3XG+REwIN6QNhvTB7syTCTwx+dRdHgvqHl9N6g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@welshman/lib': 0.8.12
|
'@welshman/lib': 0.8.15
|
||||||
'@welshman/net': 0.8.12
|
'@welshman/net': 0.8.15
|
||||||
'@welshman/util': 0.8.12
|
'@welshman/util': 0.8.15
|
||||||
|
|
||||||
'@welshman/signer@0.8.12':
|
'@welshman/signer@0.8.15':
|
||||||
resolution: {integrity: sha512-eO4mw2QOR2d2oCS4zgptkCgjC1s8X+1vNnXfWDbtlEwhk7PD4ySSCkpNVMeElq+uluLOugmKvgDc4gfnIH3p2A==}
|
resolution: {integrity: sha512-Y96XZtsCHz8h7NK28sSi3CX+8lGG6WhLyVNyhlEhfypAxxx8Zpfr4GlSPApvp4tvm1//YfDtXHIIZTPXbmnqvA==}
|
||||||
version: 0.8.12
|
version: 0.8.15
|
||||||
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.12
|
'@welshman/lib': 0.8.15
|
||||||
'@welshman/net': 0.8.12
|
'@welshman/net': 0.8.15
|
||||||
'@welshman/util': 0.8.12
|
'@welshman/util': 0.8.15
|
||||||
nostr-signer-capacitor-plugin: '*'
|
nostr-signer-capacitor-plugin: '*'
|
||||||
nostr-tools: ^2.19.4
|
nostr-tools: ^2.19.4
|
||||||
|
|
||||||
'@welshman/store@0.8.12':
|
'@welshman/store@0.8.15':
|
||||||
resolution: {integrity: sha512-3IUzPRMMVF6Pcw3DaKkfJ4S6bYzVtk6Ze3FnaHs1xJIxkQj9GyBO9VreiovULPW0djsnrP5+1UEN4mKZsG3hXg==}
|
resolution: {integrity: sha512-3rQVhAsQ1z5tcUzkJPkzVp3iBkMrUKVoBi07AYefqlhRoddhwB2pDBVhdZYoP2kl9wVPZlPV58vlD6BTo6TEwA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@welshman/lib': 0.8.12
|
'@welshman/lib': 0.8.15
|
||||||
'@welshman/net': 0.8.12
|
'@welshman/net': 0.8.15
|
||||||
'@welshman/util': 0.8.12
|
'@welshman/util': 0.8.15
|
||||||
svelte: ^4.0.0 || ^5.0.0
|
svelte: ^4.0.0 || ^5.0.0
|
||||||
|
|
||||||
'@welshman/util@0.8.12':
|
'@welshman/util@0.8.15':
|
||||||
resolution: {integrity: sha512-lgftFt2moXZdN5fuL0RoAnAARV0n0d2+Q56gt7KrBSevjoCbtJgBVX5idvxL5PCEfh81veovJtty6eHxrhQv5A==}
|
resolution: {integrity: sha512-zeNWMyOtIpOqj9/hBAT8qWvnp5w/IyrcT7CmDKLkWt6NU6ZoZ3pF5duTwtOYZqcftYJaHXgohOt0RsHVPR3M7w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@noble/curves': ^1.9.7
|
'@noble/curves': ^1.9.7
|
||||||
'@welshman/lib': 0.8.12
|
'@welshman/lib': 0.8.15
|
||||||
nostr-tools: ^2.19.4
|
nostr-tools: ^2.19.4
|
||||||
|
|
||||||
'@xml-tools/parser@1.0.11':
|
'@xml-tools/parser@1.0.11':
|
||||||
@@ -2446,6 +2483,13 @@ 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==}
|
||||||
|
|
||||||
@@ -2805,12 +2849,18 @@ 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'}
|
||||||
@@ -2822,6 +2872,14 @@ 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'}
|
||||||
@@ -3215,6 +3273,10 @@ 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==}
|
||||||
|
|
||||||
@@ -3222,6 +3284,9 @@ 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'}
|
||||||
@@ -3230,6 +3295,10 @@ 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==}
|
||||||
|
|
||||||
@@ -3996,6 +4065,15 @@ 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'}
|
||||||
@@ -4437,6 +4515,9 @@ 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==}
|
||||||
|
|
||||||
@@ -4872,6 +4953,10 @@ 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'}
|
||||||
@@ -4990,6 +5075,15 @@ 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==}
|
||||||
|
|
||||||
@@ -5969,6 +6063,10 @@ 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
|
||||||
@@ -6006,6 +6104,10 @@ 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)':
|
||||||
@@ -6201,6 +6303,10 @@ 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':
|
||||||
@@ -6610,15 +6716,15 @@ snapshots:
|
|||||||
|
|
||||||
'@polka/url@1.0.0-next.29': {}
|
'@polka/url@1.0.0-next.29': {}
|
||||||
|
|
||||||
'@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))':
|
'@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))':
|
||||||
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.12
|
'@welshman/lib': 0.8.15
|
||||||
'@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/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.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/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.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(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))
|
||||||
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)
|
||||||
@@ -7276,26 +7382,26 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@vite-pwa/assets-generator': 0.2.6
|
'@vite-pwa/assets-generator': 0.2.6
|
||||||
|
|
||||||
'@welshman/app@0.8.12(3074ef6691f94dc03952d8dbc98013a7)':
|
'@welshman/app@0.8.15(ff026297546a8274624eb18a0ea86191)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@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))
|
'@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))
|
||||||
'@welshman/feeds': 0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)
|
'@welshman/feeds': 0.8.15(6e55dcd4e7516745e7b0228620d35545)
|
||||||
'@welshman/lib': 0.8.12
|
'@welshman/lib': 0.8.15
|
||||||
'@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/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/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/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/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/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/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/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/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(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))
|
||||||
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.12(nostr-tools@2.20.0(typescript@5.9.3))':
|
'@welshman/content@0.8.15(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.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/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))':
|
||||||
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))
|
||||||
@@ -7310,64 +7416,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.12
|
'@welshman/lib': 0.8.15
|
||||||
'@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/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-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.12(d5b74f0c83250e052e0b96f7ff5804e8)':
|
'@welshman/feeds@0.8.15(6e55dcd4e7516745e7b0228620d35545)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.8.12
|
'@welshman/lib': 0.8.15
|
||||||
'@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/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/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/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/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/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.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(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))
|
||||||
trava: 1.2.1
|
trava: 1.2.1
|
||||||
|
|
||||||
'@welshman/lib@0.8.12':
|
'@welshman/lib@0.8.15':
|
||||||
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.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/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)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.8.12
|
'@welshman/lib': 0.8.15
|
||||||
'@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/util': 0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(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.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/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)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.8.12
|
'@welshman/lib': 0.8.15
|
||||||
'@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/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.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(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/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/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))':
|
||||||
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.12
|
'@welshman/lib': 0.8.15
|
||||||
'@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/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.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(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-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.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/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)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@welshman/lib': 0.8.12
|
'@welshman/lib': 0.8.15
|
||||||
'@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/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.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(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))
|
||||||
svelte: 5.48.0
|
svelte: 5.48.0
|
||||||
|
|
||||||
'@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/util@0.8.15(@noble/curves@1.9.7)(@welshman/lib@0.8.15)(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.12
|
'@welshman/lib': 0.8.15
|
||||||
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
|
||||||
@@ -7615,6 +7721,29 @@ 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
|
||||||
@@ -8001,10 +8130,17 @@ 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
|
||||||
@@ -8014,6 +8150,10 @@ 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: {}
|
||||||
@@ -8523,16 +8663,29 @@ 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: {}
|
||||||
@@ -9239,6 +9392,19 @@ 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: {}
|
||||||
@@ -9677,6 +9843,8 @@ 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: {}
|
||||||
@@ -10202,6 +10370,8 @@ 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:
|
||||||
@@ -10282,6 +10452,12 @@ 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
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
import path from "node:path"
|
||||||
|
import {promises as fs} from "node:fs"
|
||||||
|
import {fileURLToPath} from "node:url"
|
||||||
|
|
||||||
|
import "dotenv/config"
|
||||||
|
import {serve} from "@hono/node-server"
|
||||||
|
import {serveStatic} from "@hono/node-server/serve-static"
|
||||||
|
import {loadRelay} from "@welshman/app"
|
||||||
|
import {displayRelayUrl, normalizeRelayUrl} from "@welshman/util"
|
||||||
|
import {load} from "cheerio"
|
||||||
|
import {Hono} from "hono"
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
|
const BUILD_DIR = path.join(__dirname, "build")
|
||||||
|
const INDEX_PATH = path.join(BUILD_DIR, "index.html")
|
||||||
|
|
||||||
|
const PORT = parseInt(process.env.PORT || "", 10) || 3000
|
||||||
|
const HOST = process.env.HOST || "0.0.0.0"
|
||||||
|
|
||||||
|
let TEMPLATE_HTML = ""
|
||||||
|
try {
|
||||||
|
TEMPLATE_HTML = await fs.readFile(INDEX_PATH, "utf8")
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Unable to read ${INDEX_PATH}. Run "pnpm run build" first.`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLATFORM_NAME = process.env.VITE_PLATFORM_NAME
|
||||||
|
const PLATFORM_DESCRIPTION = process.env.VITE_PLATFORM_DESCRIPTION
|
||||||
|
|
||||||
|
// Match client-side decode logic
|
||||||
|
const decodeRelay = url => {
|
||||||
|
try {
|
||||||
|
return normalizeRelayUrl(decodeURIComponent(url))
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestUrlFromContext = context => {
|
||||||
|
const requestUrl = new URL(context.req.url)
|
||||||
|
const forwardedProto = context.req.header("x-forwarded-proto")?.split(",")[0]?.trim()
|
||||||
|
const forwardedHost = context.req.header("x-forwarded-host")?.split(",")[0]?.trim()
|
||||||
|
|
||||||
|
if (forwardedProto === "http" || forwardedProto === "https") {
|
||||||
|
requestUrl.protocol = `${forwardedProto}:`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forwardedHost) {
|
||||||
|
requestUrl.host = forwardedHost
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchRelayMeta = async relayUrl => {
|
||||||
|
if (!relayUrl) return undefined
|
||||||
|
try {
|
||||||
|
return await loadRelay(normalizeRelayUrl(relayUrl))
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to fetch relay metadata for ${relayUrl}:`, err)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildDefaultImage = requestUrl => {
|
||||||
|
return new URL("/maskable-icon-512x512.png", requestUrl.origin).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMetadataForInvite = async (url, match) => {
|
||||||
|
const relayParam = url.searchParams.get("r")
|
||||||
|
if (!relayParam) return undefined
|
||||||
|
|
||||||
|
const relayMetadata = await fetchRelayMeta(relayParam)
|
||||||
|
if (!relayMetadata) return undefined
|
||||||
|
|
||||||
|
const relayDisplay = displayRelayUrl(relayParam)
|
||||||
|
const spaceName = relayMetadata.name
|
||||||
|
const relayDescription = relayMetadata.description
|
||||||
|
|
||||||
|
const title = spaceName
|
||||||
|
? `Invite to ${spaceName} on ${PLATFORM_NAME}`
|
||||||
|
: `Invite to a Space on ${PLATFORM_NAME}`
|
||||||
|
|
||||||
|
const parts = []
|
||||||
|
if (spaceName) {
|
||||||
|
parts.push(`You are invited to join ${spaceName} on ${PLATFORM_NAME}.`)
|
||||||
|
} else {
|
||||||
|
parts.push(`You are invited to join a space on ${PLATFORM_NAME}.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relayDisplay) parts.push(`Relay: ${relayDisplay}.`)
|
||||||
|
if (relayDescription) parts.push(relayDescription)
|
||||||
|
else parts.push(PLATFORM_DESCRIPTION)
|
||||||
|
|
||||||
|
const description = parts.join(" ")
|
||||||
|
const image =
|
||||||
|
relayMetadata.icon ||
|
||||||
|
relayMetadata.picture ||
|
||||||
|
relayMetadata.image ||
|
||||||
|
buildDefaultImage(url)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
image,
|
||||||
|
url: url.toString(),
|
||||||
|
site: url.origin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMetadataForSpace = async (url, match) => {
|
||||||
|
const relayParam = decodeRelay(match[1])
|
||||||
|
if (!relayParam) return undefined
|
||||||
|
|
||||||
|
const relayMetadata = await fetchRelayMeta(relayParam)
|
||||||
|
if (!relayMetadata) return undefined
|
||||||
|
|
||||||
|
const spaceName = relayMetadata.name || displayRelayUrl(relayParam)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${spaceName} on ${PLATFORM_NAME}`,
|
||||||
|
description: relayMetadata.description || PLATFORM_DESCRIPTION,
|
||||||
|
image:
|
||||||
|
relayMetadata.icon ||
|
||||||
|
relayMetadata.picture ||
|
||||||
|
relayMetadata.image ||
|
||||||
|
buildDefaultImage(url),
|
||||||
|
url: url.toString(),
|
||||||
|
site: url.origin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMetadataForSpaceSection = async (url, match) => {
|
||||||
|
const spaceMeta = await getMetadataForSpace(url, match)
|
||||||
|
if (!spaceMeta) return undefined
|
||||||
|
|
||||||
|
const section = match[2]
|
||||||
|
const sectionName = section.charAt(0).toUpperCase() + section.slice(1)
|
||||||
|
spaceMeta.title = `${sectionName} on ${spaceMeta.title}`
|
||||||
|
return spaceMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMetadataForSpaceItem = async (url, match) => {
|
||||||
|
const spaceMeta = await getMetadataForSpace(url, match)
|
||||||
|
if (!spaceMeta) return undefined
|
||||||
|
|
||||||
|
const section = match[2]
|
||||||
|
let itemType = "Item"
|
||||||
|
if (section === "calendar") itemType = "Event"
|
||||||
|
if (section === "threads") itemType = "Thread"
|
||||||
|
if (section === "polls") itemType = "Poll"
|
||||||
|
if (section === "goals") itemType = "Goal"
|
||||||
|
if (section === "classifieds") itemType = "Listing"
|
||||||
|
|
||||||
|
spaceMeta.title = `${itemType} on ${spaceMeta.title}`
|
||||||
|
return spaceMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMetadataForRoom = async (url, match) => {
|
||||||
|
const spaceMeta = await getMetadataForSpace(url, match)
|
||||||
|
if (!spaceMeta) return undefined
|
||||||
|
|
||||||
|
// Room metadata requires fetching from Nostr, which can be added later.
|
||||||
|
spaceMeta.title = `Room on ${spaceMeta.title}`
|
||||||
|
return spaceMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
[/^\/join\/?$/, getMetadataForInvite],
|
||||||
|
[/^\/spaces\/([^/]+)\/(calendar|chat|threads|polls|goals|classifieds|recent)\/?$/, getMetadataForSpaceSection],
|
||||||
|
[/^\/spaces\/([^/]+)\/(calendar|threads|polls|goals|classifieds)\/([^/]+)\/?$/, getMetadataForSpaceItem],
|
||||||
|
[/^\/spaces\/([^/]+)\/([^/]+)\/?$/, getMetadataForRoom],
|
||||||
|
[/^\/spaces\/([^/]+)\/?$/, getMetadataForSpace],
|
||||||
|
]
|
||||||
|
|
||||||
|
const getMetadataForRoute = async url => {
|
||||||
|
for (const [regex, getMetadata] of routes) {
|
||||||
|
const match = url.pathname.match(regex)
|
||||||
|
if (match) {
|
||||||
|
try {
|
||||||
|
return await getMetadata(url, match)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error generating metadata for route ${url.pathname}:`, err)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const injectMeta = metadata => {
|
||||||
|
const $ = load(TEMPLATE_HTML)
|
||||||
|
|
||||||
|
if (metadata.title) {
|
||||||
|
$("title").text(metadata.title)
|
||||||
|
$('meta[property="og:title"]').attr("content", metadata.title)
|
||||||
|
$('meta[name="twitter:title"]').attr("content", metadata.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.description) {
|
||||||
|
$('meta[name="description"]').attr("content", metadata.description)
|
||||||
|
$('meta[property="og:description"]').attr("content", metadata.description)
|
||||||
|
$('meta[name="twitter:description"]').attr("content", metadata.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.image) {
|
||||||
|
$('meta[property="og:image"]').attr("content", metadata.image)
|
||||||
|
$('meta[name="twitter:image"]').attr("content", metadata.image)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.url) {
|
||||||
|
$('meta[property="og:url"]').attr("content", metadata.url)
|
||||||
|
$('meta[name="twitter:site"]').attr("content", metadata.site)
|
||||||
|
$('meta[name="twitter:url"]').attr("content", metadata.url)
|
||||||
|
$('link[rel="canonical"]').attr("href", metadata.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $.html()
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
// Only allow GET and HEAD requests
|
||||||
|
app.use("*", async (context, next) => {
|
||||||
|
const method = context.req.method
|
||||||
|
if (method !== "GET" && method !== "HEAD") {
|
||||||
|
return context.text("Method Not Allowed", 405, {Allow: "GET, HEAD"})
|
||||||
|
}
|
||||||
|
await next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Serve static assets with appropriate caching
|
||||||
|
app.use(
|
||||||
|
"*",
|
||||||
|
serveStatic({
|
||||||
|
root: BUILD_DIR,
|
||||||
|
onFound: (filePath, context) => {
|
||||||
|
const isImmutable = filePath.split(path.sep).join("/").includes("/_app/immutable/")
|
||||||
|
const cacheControl =
|
||||||
|
path.basename(filePath) === "index.html"
|
||||||
|
? "no-cache"
|
||||||
|
: isImmutable
|
||||||
|
? "public, max-age=31536000, immutable"
|
||||||
|
: "public, max-age=3600"
|
||||||
|
|
||||||
|
context.header("Cache-Control", cacheControl)
|
||||||
|
|
||||||
|
// Immutable assets are content-hashed by Vite, so the filename is itself a
|
||||||
|
// stable content identifier. Exposing it as an ETag lets clients that
|
||||||
|
// revalidate explicitly (e.g. emoji-picker-element checks its data source
|
||||||
|
// on every load) skip re-downloading large files when nothing changed.
|
||||||
|
if (isImmutable) {
|
||||||
|
context.header("ETag", `"${path.basename(filePath)}"`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// SPA fallback for routes that don't match static files
|
||||||
|
app.get("*", async context => {
|
||||||
|
const requestUrl = requestUrlFromContext(context)
|
||||||
|
|
||||||
|
// If the path has an extension, it's likely a missing static asset, not an SPA route
|
||||||
|
if (path.extname(requestUrl.pathname)) {
|
||||||
|
return context.text("Not found", 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = await getMetadataForRoute(requestUrl)
|
||||||
|
const html = metadata ? injectMeta(metadata) : TEMPLATE_HTML
|
||||||
|
|
||||||
|
return context.html(html, 200, {
|
||||||
|
"Cache-Control": metadata ? "no-store" : "no-cache",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
serve(
|
||||||
|
{
|
||||||
|
fetch: app.fetch,
|
||||||
|
hostname: HOST,
|
||||||
|
port: PORT,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
console.log(`Flotilla server running on http://${HOST}:${PORT}`)
|
||||||
|
},
|
||||||
|
)
|
||||||
+26
-13
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
@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);
|
||||||
}
|
}
|
||||||
@@ -22,16 +32,6 @@
|
|||||||
@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,6 +235,7 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: Lato;
|
font-family: Lato;
|
||||||
|
text-size-adjust: 100%;
|
||||||
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
|
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
|
||||||
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
||||||
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
|
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
|
||||||
@@ -327,12 +328,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 h-auto min-h-32 p-[.65rem] pb-6;
|
@apply input rounded-box block h-auto min-h-32 w-full 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 h-auto p-[.65rem];
|
@apply input block h-auto p-[.65rem];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* link-content, based on tiptap */
|
/* link-content, based on tiptap */
|
||||||
@@ -416,16 +417,28 @@ 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 relative z-compose mb-14 shrink-0 md:mb-0;
|
@apply z-compose relative mb-14 shrink-0 md:mb-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat__compose .chat__compose-inner {
|
.chat__compose .chat__compose-inner {
|
||||||
|
|||||||
+7
-4
@@ -2,15 +2,18 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
<title>{NAME}</title>
|
||||||
|
<link rel="canonical" href="{URL}" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||||
<meta name="theme-color" content="{ACCENT}" />
|
<meta name="theme-color" content="{ACCENT}" />
|
||||||
<meta name="description" content="{DESCRIPTION}" />
|
<meta name="description" content="{DESCRIPTION}" />
|
||||||
<meta name="og:url" content="{URL}" />
|
<meta property="og:url" content="{URL}" />
|
||||||
<meta name="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta name="og:title" content="{NAME}" />
|
<meta property="og:title" content="{NAME}" />
|
||||||
<meta name="og:description" content="{DESCRIPTION}" />
|
<meta property="og:description" content="{DESCRIPTION}" />
|
||||||
|
<meta property="og:image" content="" />
|
||||||
<meta name="twitter:card" content="summary" />
|
<meta name="twitter:card" content="summary" />
|
||||||
<meta name="twitter:site" content="{URL}" />
|
<meta name="twitter:site" content="{URL}" />
|
||||||
<meta name="twitter:title" content="{NAME}" />
|
<meta name="twitter:title" content="{NAME}" />
|
||||||
|
|||||||
+18
-3
@@ -6,11 +6,13 @@ export type VoiceSession = {
|
|||||||
url: string
|
url: string
|
||||||
h: string
|
h: string
|
||||||
room: LiveKitRoom
|
room: LiveKitRoom
|
||||||
muted: boolean
|
|
||||||
cameraOn: boolean
|
cameraOn: boolean
|
||||||
screenShareOn: boolean
|
screenShareOn: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Mic mute state is separate so toggling it does not re-render video tiles. */
|
||||||
|
export const voiceMicMuted = writable(true)
|
||||||
|
|
||||||
export type Pubkey = string
|
export type Pubkey = string
|
||||||
|
|
||||||
export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
|
export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
|
||||||
@@ -27,8 +29,6 @@ export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
|
|||||||
|
|
||||||
export const currentVoiceRoom = writable<Room | undefined>(undefined)
|
export const currentVoiceRoom = writable<Room | undefined>(undefined)
|
||||||
|
|
||||||
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
|
|
||||||
|
|
||||||
export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined =>
|
export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined =>
|
||||||
/^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined
|
/^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined
|
||||||
|
|
||||||
@@ -41,6 +41,21 @@ export const participantKey = (p: VoiceParticipant) => p.pubkey ?? p.identity
|
|||||||
|
|
||||||
export const speakingParticipants = writable<VoiceParticipant[]>([])
|
export const speakingParticipants = writable<VoiceParticipant[]>([])
|
||||||
|
|
||||||
|
export const participantMediaState = writable(
|
||||||
|
new Map<string, {muted: boolean; cameraOn: boolean}>(),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const mediaStateByIdentity = derived(
|
||||||
|
[participantMediaState, currentVoiceSession, voiceMicMuted],
|
||||||
|
([$media, $session, $micMuted]) =>
|
||||||
|
(identity: string) => {
|
||||||
|
if ($session?.room.localParticipant.identity === identity) {
|
||||||
|
return {muted: $micMuted, cameraOn: $session.cameraOn}
|
||||||
|
}
|
||||||
|
return $media.get(identity) ?? {muted: true, cameraOn: false}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export const isParticipantSpeaking = derived(
|
export const isParticipantSpeaking = derived(
|
||||||
speakingParticipants,
|
speakingParticipants,
|
||||||
$participants => (p: VoiceParticipant) =>
|
$participants => (p: VoiceParticipant) =>
|
||||||
|
|||||||
+40
-29
@@ -6,14 +6,16 @@ import {
|
|||||||
DisconnectReason,
|
DisconnectReason,
|
||||||
LocalParticipant,
|
LocalParticipant,
|
||||||
LocalTrackPublication,
|
LocalTrackPublication,
|
||||||
|
Participant,
|
||||||
Room as LiveKitRoom,
|
Room as LiveKitRoom,
|
||||||
RoomEvent,
|
RoomEvent,
|
||||||
Track,
|
Track,
|
||||||
|
TrackPublication,
|
||||||
supportsAudioOutputSelection,
|
supportsAudioOutputSelection,
|
||||||
type AudioCaptureOptions,
|
type AudioCaptureOptions,
|
||||||
} from "livekit-client"
|
} from "livekit-client"
|
||||||
import {derived, get} from "svelte/store"
|
import {derived, get} from "svelte/store"
|
||||||
import {map, removeUndefined, uniqBy} from "@welshman/lib"
|
import {map, not, nthEq, reject, removeUndefined, uniqBy} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
||||||
import {signer} from "@welshman/app"
|
import {signer} from "@welshman/app"
|
||||||
@@ -22,10 +24,10 @@ import {AbortError, whenAborted, whenTimeout} from "$lib/util"
|
|||||||
import {
|
import {
|
||||||
currentVoiceRoom,
|
currentVoiceRoom,
|
||||||
currentVoiceSession,
|
currentVoiceSession,
|
||||||
|
voiceMicMuted,
|
||||||
participantFromLiveKitIdentity,
|
participantFromLiveKitIdentity,
|
||||||
participantKey,
|
participantKey,
|
||||||
participantPubkeyMap,
|
participantMediaState,
|
||||||
pubkeyFromLiveKitIdentity,
|
|
||||||
speakingParticipants,
|
speakingParticipants,
|
||||||
VoiceState,
|
VoiceState,
|
||||||
type VoiceParticipant,
|
type VoiceParticipant,
|
||||||
@@ -75,20 +77,23 @@ export const switchVoiceActiveDevice = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addParticipant = (identity: string) => {
|
const deleteParticipant = (identity: string) => {
|
||||||
participantPubkeyMap.update(m => {
|
participantMediaState.update(m => new Map(reject(nthEq(0, identity), [...m])))
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncParticipantMedia = (participant: Participant) => {
|
||||||
|
const state = {muted: !participant.isMicrophoneEnabled, cameraOn: participant.isCameraEnabled}
|
||||||
|
participantMediaState.update(m => {
|
||||||
|
const prev = m.get(participant.identity)
|
||||||
|
if (prev?.muted === state.muted && prev?.cameraOn === state.cameraOn) return m
|
||||||
const next = new Map(m)
|
const next = new Map(m)
|
||||||
next.set(identity, pubkeyFromLiveKitIdentity(identity) ?? "")
|
next.set(participant.identity, state)
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteParticipant = (identity: string) => {
|
const onParticipantMediaChanged = (_publication: TrackPublication, participant: Participant) => {
|
||||||
participantPubkeyMap.update(m => {
|
syncParticipantMedia(participant)
|
||||||
const next = new Map(m)
|
|
||||||
next.delete(identity)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchLivekitToken = async (
|
const fetchLivekitToken = async (
|
||||||
@@ -124,15 +129,15 @@ export const deriveVoiceParticipants = (url: string, h: string) =>
|
|||||||
// We use the livekit identity list while in a call, and fall back to the list in kind 39004.
|
// We use the livekit identity list while in a call, and fall back to the list in kind 39004.
|
||||||
derived(
|
derived(
|
||||||
[
|
[
|
||||||
participantPubkeyMap,
|
participantMediaState,
|
||||||
currentVoiceRoom,
|
currentVoiceRoom,
|
||||||
deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
|
deriveLatestEventForUrl(url, [{kinds: [LIVEKIT_PARTICIPANTS], "#d": [h]}]),
|
||||||
],
|
],
|
||||||
([$participantPubkeyMap, $currentVoiceRoom, $publishedParticipantList]) => {
|
([$participantMediaState, $currentVoiceRoom, $publishedParticipantList]) => {
|
||||||
const inCall = $participantPubkeyMap.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
|
const inCall = $participantMediaState.size > 0 && $currentVoiceRoom?.id === makeRoomId(url, h)
|
||||||
|
|
||||||
if (inCall) {
|
if (inCall) {
|
||||||
const participants = [...$participantPubkeyMap.keys()].map(participantFromLiveKitIdentity)
|
const participants = [...$participantMediaState.keys()].map(participantFromLiveKitIdentity)
|
||||||
return uniqBy((p: VoiceParticipant) => participantKey(p), participants)
|
return uniqBy((p: VoiceParticipant) => participantKey(p), participants)
|
||||||
} else {
|
} else {
|
||||||
const latestEvent = $publishedParticipantList as TrustedEvent | undefined
|
const latestEvent = $publishedParticipantList as TrustedEvent | undefined
|
||||||
@@ -173,6 +178,7 @@ const setUpMicrophone = async (
|
|||||||
|
|
||||||
const onRoomDisconnected = (reason?: DisconnectReason) => {
|
const onRoomDisconnected = (reason?: DisconnectReason) => {
|
||||||
videoPrimaryTileKey.set(undefined)
|
videoPrimaryTileKey.set(undefined)
|
||||||
|
voiceMicMuted.set(true)
|
||||||
currentVoiceSession.set(undefined)
|
currentVoiceSession.set(undefined)
|
||||||
resetVideoCallLayout()
|
resetVideoCallLayout()
|
||||||
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
|
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
|
||||||
@@ -184,7 +190,7 @@ const onRoomDisconnected = (reason?: DisconnectReason) => {
|
|||||||
pushToast({theme: "error", message})
|
pushToast({theme: "error", message})
|
||||||
}
|
}
|
||||||
speakingParticipants.set([])
|
speakingParticipants.set([])
|
||||||
participantPubkeyMap.set(new Map())
|
participantMediaState.set(new Map())
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTrackSubscribed = (track: Track) => {
|
const onTrackSubscribed = (track: Track) => {
|
||||||
@@ -214,8 +220,8 @@ const playJoinSound = () => {
|
|||||||
audio.play().catch(() => {})
|
audio.play().catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onParticipantConnected = (participant: {identity: string}) => {
|
const onParticipantConnected = (participant: Participant) => {
|
||||||
addParticipant(participant.identity)
|
syncParticipantMedia(participant)
|
||||||
playJoinSound()
|
playJoinSound()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,11 +279,16 @@ 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(5_000, {
|
whenTimeout(15_000, {
|
||||||
message: "Connection timed out. Please check your network and try again.",
|
message: "Connection timed out. Please check your network and try again.",
|
||||||
}),
|
}),
|
||||||
whenAborted(signal),
|
whenAborted(signal),
|
||||||
@@ -287,19 +298,19 @@ export const joinVoiceRoom = async (
|
|||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
participantPubkeyMap.set(new Map())
|
participantMediaState.set(new Map())
|
||||||
addParticipant(liveKitRoom.localParticipant.identity)
|
syncParticipantMedia(liveKitRoom.localParticipant)
|
||||||
for (const p of liveKitRoom.remoteParticipants.values()) {
|
for (const p of liveKitRoom.remoteParticipants.values()) {
|
||||||
addParticipant(p.identity)
|
syncParticipantMedia(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
|
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
|
||||||
|
|
||||||
|
voiceMicMuted.set(muted)
|
||||||
currentVoiceSession.set({
|
currentVoiceSession.set({
|
||||||
url,
|
url,
|
||||||
h,
|
h,
|
||||||
room: liveKitRoom,
|
room: liveKitRoom,
|
||||||
muted,
|
|
||||||
cameraOn: false,
|
cameraOn: false,
|
||||||
screenShareOn: false,
|
screenShareOn: false,
|
||||||
})
|
})
|
||||||
@@ -339,11 +350,12 @@ export const leaveVoiceRoom = async () => {
|
|||||||
|
|
||||||
voiceState.set(VoiceState.Disconnected)
|
voiceState.set(VoiceState.Disconnected)
|
||||||
videoPrimaryTileKey.set(undefined)
|
videoPrimaryTileKey.set(undefined)
|
||||||
|
voiceMicMuted.set(true)
|
||||||
currentVoiceSession.set(undefined)
|
currentVoiceSession.set(undefined)
|
||||||
resetVideoCallLayout()
|
resetVideoCallLayout()
|
||||||
session.room.disconnect()
|
session.room.disconnect()
|
||||||
speakingParticipants.set([])
|
speakingParticipants.set([])
|
||||||
participantPubkeyMap.set(new Map())
|
participantMediaState.set(new Map())
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rejoinVoiceRoom = async (): Promise<void> => {
|
export const rejoinVoiceRoom = async (): Promise<void> => {
|
||||||
@@ -356,18 +368,17 @@ export const toggleMute = async () => {
|
|||||||
const session = get(currentVoiceSession)
|
const session = get(currentVoiceSession)
|
||||||
if (!session) return
|
if (!session) return
|
||||||
|
|
||||||
const muted = !session.muted
|
voiceMicMuted.update(not)
|
||||||
if (muted) {
|
if (get(voiceMicMuted)) {
|
||||||
// Disable and re-enable microphone to trigger permission prompt
|
// Disable and re-enable microphone to trigger permission prompt
|
||||||
session.room.localParticipant.setMicrophoneEnabled(false)
|
session.room.localParticipant.setMicrophoneEnabled(false)
|
||||||
currentVoiceSession.set({...session, muted})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await session.room.localParticipant.setMicrophoneEnabled(true)
|
await session.room.localParticipant.setMicrophoneEnabled(true)
|
||||||
currentVoiceSession.set({...session, muted})
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
voiceMicMuted.set(true)
|
||||||
pushToast({theme: "error", message: "Could not access microphone"})
|
pushToast({theme: "error", message: "Could not access microphone"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
h?: string
|
h?: string
|
||||||
|
shareToChat?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, h}: Props = $props()
|
const {url, h, shareToChat = false}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CalendarEventForm {url} {h}>
|
<CalendarEventForm {url} {h} {shareToChat}>
|
||||||
{#snippet header()}
|
{#snippet header()}
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<ModalTitle>Create an Event</ModalTitle>
|
<ModalTitle>Create an Event</ModalTitle>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {randomId, HOUR} from "@welshman/lib"
|
import {randomId, HOUR} from "@welshman/lib"
|
||||||
import {makeEvent, EVENT_TIME} from "@welshman/util"
|
import {makeEvent, EVENT_TIME} from "@welshman/util"
|
||||||
import {publishThunk} from "@welshman/app"
|
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import {daysBetween} from "@lib/util"
|
import {daysBetween} from "@lib/util"
|
||||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {DraftKey} from "@app/util/drafts"
|
import {DraftKey} from "@app/util/drafts"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {canEnforceNip70} from "@app/core/commands"
|
import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
|
||||||
|
|
||||||
type Values = {
|
type Values = {
|
||||||
d: string
|
d: string
|
||||||
@@ -36,11 +36,12 @@
|
|||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
h?: string
|
h?: string
|
||||||
|
shareToChat?: boolean
|
||||||
header: Snippet
|
header: Snippet
|
||||||
initialValues?: Values
|
initialValues?: Values
|
||||||
}
|
}
|
||||||
|
|
||||||
let {url, h, header, initialValues}: Props = $props()
|
let {url, h, shareToChat = false, header, initialValues}: Props = $props()
|
||||||
|
|
||||||
const draftKey = new DraftKey<Values>(`calendar:${url}:${h ?? ""}`)
|
const draftKey = new DraftKey<Values>(`calendar:${url}:${h ?? ""}`)
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
const selectFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
const selectFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading) return
|
if ($uploading || loading) return
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -92,7 +93,12 @@
|
|||||||
...ed.storage.nostr.getEditorTags(),
|
...ed.storage.nostr.getEditorTags(),
|
||||||
]
|
]
|
||||||
|
|
||||||
if (await shouldProtect) {
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const protect = await shouldProtect
|
||||||
|
|
||||||
|
if (protect) {
|
||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,13 +107,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const event = makeEvent(EVENT_TIME, {content, tags})
|
const event = makeEvent(EVENT_TIME, {content, tags})
|
||||||
|
const calendarThunk = publishThunk({event, relays: [url]})
|
||||||
|
const error = await waitForThunkError(calendarThunk)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return pushToast({theme: "error", message: error})
|
||||||
|
}
|
||||||
|
|
||||||
pushToast({message: "Your event has been saved!"})
|
|
||||||
publishThunk({event, relays: [url]})
|
|
||||||
draftKey.clear()
|
draftKey.clear()
|
||||||
history.back()
|
history.back()
|
||||||
|
|
||||||
|
if (shareToChat) {
|
||||||
|
publishRoomQuote({url, h, parent: calendarThunk.event, protect})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pushToast({message: "Your event has been saved!"})
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
|
||||||
const d = $state(initialValues?.d ?? randomId())
|
const d = $state(initialValues?.d ?? randomId())
|
||||||
let title = $state(initialValues?.title ?? "")
|
let title = $state(initialValues?.title ?? "")
|
||||||
let location = $state(initialValues?.location ?? "")
|
let location = $state(initialValues?.location ?? "")
|
||||||
@@ -158,7 +179,11 @@
|
|||||||
<div class="input-editor grow overflow-hidden">
|
<div class="input-editor grow overflow-hidden">
|
||||||
<EditorContent {editor} />
|
<EditorContent {editor} />
|
||||||
</div>
|
</div>
|
||||||
<Button data-tip="Add an image" class="center btn tooltip" onclick={selectFiles}>
|
<Button
|
||||||
|
data-tip="Add an image"
|
||||||
|
class="center btn tooltip"
|
||||||
|
onclick={selectFiles}
|
||||||
|
disabled={loading}>
|
||||||
{#if $uploading}
|
{#if $uploading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -197,12 +222,12 @@
|
|||||||
</Field>
|
</Field>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary" disabled={$uploading}>
|
<Button type="submit" class="btn btn-primary" disabled={$uploading || loading}>
|
||||||
<Spinner loading={$uploading}>Save Event</Spinner>
|
<Spinner loading={$uploading || loading}>Save Event</Spinner>
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -19,15 +19,17 @@
|
|||||||
const end = $derived(parseInt(meta.end))
|
const end = $derived(parseInt(meta.end))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex grow flex-wrap justify-between gap-2">
|
<div class="flex flex-col justify-between gap-1">
|
||||||
<p class="text-xl">{meta.title || meta.name}</p>
|
<p class="text-lg">{meta.title || meta.name}</p>
|
||||||
{#if !isNaN(start) && !isNaN(end)}
|
{#if !isNaN(start) && !isNaN(end)}
|
||||||
{@const startDateDisplay = formatTimestampAsDate(start)}
|
{@const startDateDisplay = formatTimestampAsDate(start)}
|
||||||
{@const endDateDisplay = formatTimestampAsDate(end)}
|
{@const endDateDisplay = formatTimestampAsDate(end)}
|
||||||
{@const isSingleDay = startDateDisplay === endDateDisplay}
|
{@const isSingleDay = startDateDisplay === endDateDisplay}
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex flex-wrap gap-2 text-xs">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<Icon icon={ClockCircle} size={4} />
|
<Icon icon={ClockCircle} size={4} />
|
||||||
<span class="hidden sm:block">{formatTimestampAsDate(start)}</span>
|
{formatTimestampAsDate(start)}
|
||||||
|
</div>
|
||||||
{formatTimestampAsTime(start)} — {isSingleDay
|
{formatTimestampAsTime(start)} — {isSingleDay
|
||||||
? formatTimestampAsTime(end)
|
? formatTimestampAsTime(end)
|
||||||
: formatTimestamp(end)}
|
: formatTimestamp(end)}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte"
|
import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte"
|
||||||
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
||||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
import {userSettingsValues, deriveChat} from "@app/core/state"
|
import {userSettingsValues, deriveChat, makeChatId} from "@app/core/state"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {DraftKey} from "@app/util/drafts"
|
import {DraftKey} from "@app/util/drafts"
|
||||||
import {makeDelete, prependParent} from "@app/core/commands"
|
import {makeDelete, prependParent} from "@app/core/commands"
|
||||||
@@ -66,8 +66,9 @@
|
|||||||
|
|
||||||
const {pubkeys, info}: Props = $props()
|
const {pubkeys, info}: Props = $props()
|
||||||
|
|
||||||
const chat = deriveChat(pubkeys)
|
const chatId = makeChatId(pubkeys)
|
||||||
const draftKey = new DraftKey<{content?: string | object}>(`dm:${$chat?.id}`)
|
const chat = deriveChat(chatId)
|
||||||
|
const draftKey = new DraftKey<{content?: string | object}>(`dm:${chatId}`)
|
||||||
const others = remove($pubkey!, pubkeys)
|
const others = remove($pubkey!, pubkeys)
|
||||||
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
|
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
|
||||||
|
|
||||||
@@ -279,7 +280,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</PageBar>
|
</PageBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col-reverse gap-2 pt-4">
|
<PageContent class="flex flex-col-reverse gap-2 py-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">
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
h?: string
|
h?: string
|
||||||
|
shareToChat?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, h}: Props = $props()
|
const {url, h, shareToChat = false}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ClassifiedForm {url} {h}>
|
<ClassifiedForm {url} {h} {shareToChat}>
|
||||||
{#snippet header()}
|
{#snippet header()}
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<ModalTitle>Create a Classified Listing</ModalTitle>
|
<ModalTitle>Create a Classified Listing</ModalTitle>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {removeUndefined, randomId, uniq} from "@welshman/lib"
|
import {removeUndefined, randomId, uniq} from "@welshman/lib"
|
||||||
import {makeEvent, CLASSIFIED} from "@welshman/util"
|
import {makeEvent, CLASSIFIED} from "@welshman/util"
|
||||||
import {publishThunk} from "@welshman/app"
|
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import {normalizeTopic} from "@lib/util"
|
import {normalizeTopic} from "@lib/util"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
import {PROTECTED} from "@app/core/state"
|
import {PROTECTED} from "@app/core/state"
|
||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {DraftKey} from "@app/util/drafts"
|
import {DraftKey} from "@app/util/drafts"
|
||||||
import {canEnforceNip70, uploadFile} from "@app/core/commands"
|
import {canEnforceNip70, publishRoomQuote, uploadFile} from "@app/core/commands"
|
||||||
|
|
||||||
type Values = {
|
type Values = {
|
||||||
d: string
|
d: string
|
||||||
@@ -37,11 +37,12 @@
|
|||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
h?: string
|
h?: string
|
||||||
|
shareToChat?: boolean
|
||||||
header: Snippet
|
header: Snippet
|
||||||
initialValues?: Values
|
initialValues?: Values
|
||||||
}
|
}
|
||||||
|
|
||||||
let {url, h, header, initialValues}: Props = $props()
|
let {url, h, shareToChat = false, header, initialValues}: Props = $props()
|
||||||
|
|
||||||
const draftKey = new DraftKey<Values>(`classified:${url}:${h ?? ""}`)
|
const draftKey = new DraftKey<Values>(`classified:${url}:${h ?? ""}`)
|
||||||
|
|
||||||
@@ -87,7 +88,9 @@
|
|||||||
tags.push(["t", topic])
|
tags.push(["t", topic])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await shouldProtect) {
|
const protect = await shouldProtect
|
||||||
|
|
||||||
|
if (protect) {
|
||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,13 +117,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
publishThunk({
|
const classifiedThunk = publishThunk({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
event: makeEvent(CLASSIFIED, {content, tags}),
|
event: makeEvent(CLASSIFIED, {content, tags}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const error = await waitForThunkError(classifiedThunk)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return pushToast({theme: "error", message: error})
|
||||||
|
}
|
||||||
|
|
||||||
draftKey.clear()
|
draftKey.clear()
|
||||||
history.back()
|
history.back()
|
||||||
|
|
||||||
|
if (shareToChat) {
|
||||||
|
publishRoomQuote({url, h, parent: classifiedThunk.event, protect})
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,15 +22,15 @@
|
|||||||
|
|
||||||
const {url, h, onClick}: Props = $props()
|
const {url, h, onClick}: Props = $props()
|
||||||
|
|
||||||
const createGoal = () => pushModal(GoalCreate, {url, h})
|
const createGoal = () => pushModal(GoalCreate, {url, h, shareToChat: true})
|
||||||
|
|
||||||
const createCalendarEvent = () => pushModal(CalendarEventCreate, {url, h})
|
const createCalendarEvent = () => pushModal(CalendarEventCreate, {url, h, shareToChat: true})
|
||||||
|
|
||||||
const createThread = () => pushModal(ThreadCreate, {url, h})
|
const createThread = () => pushModal(ThreadCreate, {url, h, shareToChat: true})
|
||||||
|
|
||||||
const createClassified = () => pushModal(ClassifiedCreate, {url, h})
|
const createClassified = () => pushModal(ClassifiedCreate, {url, h, shareToChat: true})
|
||||||
|
|
||||||
const createPoll = () => pushModal(PollCreate, {url, h})
|
const createPoll = () => pushModal(PollCreate, {url, h, shareToChat: true})
|
||||||
|
|
||||||
let ul: Element
|
let ul: Element
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
truncate,
|
truncate,
|
||||||
renderAsHtml,
|
renderAsHtml,
|
||||||
isText,
|
isText,
|
||||||
|
isEmail,
|
||||||
isEmoji,
|
isEmoji,
|
||||||
isTopic,
|
isTopic,
|
||||||
isCode,
|
isCode,
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ContentToken from "@app/components/ContentToken.svelte"
|
import ContentToken from "@app/components/ContentToken.svelte"
|
||||||
import ContentEmoji from "@app/components/ContentEmoji.svelte"
|
import ContentEmoji from "@app/components/ContentEmoji.svelte"
|
||||||
|
import ContentEmail from "@app/components/ContentEmail.svelte"
|
||||||
import ContentCode from "@app/components/ContentCode.svelte"
|
import ContentCode from "@app/components/ContentCode.svelte"
|
||||||
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
|
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
|
||||||
import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
|
import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
|
||||||
@@ -159,6 +161,8 @@
|
|||||||
<ContentTopic value={parsed.value} />
|
<ContentTopic value={parsed.value} />
|
||||||
{:else if isEmoji(parsed)}
|
{:else if isEmoji(parsed)}
|
||||||
<ContentEmoji value={parsed.value} />
|
<ContentEmoji value={parsed.value} />
|
||||||
|
{:else if isEmail(parsed)}
|
||||||
|
<ContentEmail value={parsed.value} />
|
||||||
{:else if isCode(parsed)}
|
{:else if isCode(parsed)}
|
||||||
<ContentCode
|
<ContentCode
|
||||||
value={parsed.value}
|
value={parsed.value}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
|
||||||
|
export let value: string
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Link external href="mailto:{value}">
|
||||||
|
<Icon icon={LinkRound} size={3} />
|
||||||
|
{value}
|
||||||
|
</Link>
|
||||||
@@ -5,30 +5,32 @@
|
|||||||
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,
|
||||||
PLATFORM_URL,
|
|
||||||
IMAGE_CONTENT_TYPES,
|
IMAGE_CONTENT_TYPES,
|
||||||
|
PLATFORM_URL,
|
||||||
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 fileType = getTagValue("file-type", event.tags) || ""
|
const isRoomOrRelay = isRoomId(url) || isRelayUrl(url)
|
||||||
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)}`
|
||||||
@@ -54,6 +56,11 @@
|
|||||||
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)}
|
||||||
@@ -97,3 +104,4 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {call, displayUrl} from "@welshman/lib"
|
import {displayUrl} from "@welshman/lib"
|
||||||
import {isRelayUrl, getTagValue} from "@welshman/util"
|
import {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 {PLATFORM_URL, IMAGE_CONTENT_TYPES} from "@app/core/state"
|
import {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>
|
||||||
@@ -34,8 +27,5 @@
|
|||||||
{displayUrl(url)}
|
{displayUrl(url)}
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<Link {external} {href} class="link-content whitespace-nowrap">
|
<ContentLinkUrl {url} class="link-content whitespace-nowrap" />
|
||||||
<Icon icon={LinkRound} size={3} class="inline-block" />
|
|
||||||
{displayUrl(url)}
|
|
||||||
</Link>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<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>
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
renderAsHtml,
|
renderAsHtml,
|
||||||
isText,
|
isText,
|
||||||
isEmoji,
|
isEmoji,
|
||||||
|
isEmail,
|
||||||
isTopic,
|
isTopic,
|
||||||
isCode,
|
isCode,
|
||||||
isCashu,
|
isCashu,
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ContentToken from "@app/components/ContentToken.svelte"
|
import ContentToken from "@app/components/ContentToken.svelte"
|
||||||
import ContentEmoji from "@app/components/ContentEmoji.svelte"
|
import ContentEmoji from "@app/components/ContentEmoji.svelte"
|
||||||
|
import ContentEmail from "@app/components/ContentEmail.svelte"
|
||||||
import ContentCode from "@app/components/ContentCode.svelte"
|
import ContentCode from "@app/components/ContentCode.svelte"
|
||||||
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
|
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
|
||||||
import ContentNewline from "@app/components/ContentNewline.svelte"
|
import ContentNewline from "@app/components/ContentNewline.svelte"
|
||||||
@@ -109,6 +111,8 @@
|
|||||||
<ContentTopic value={parsed.value} />
|
<ContentTopic value={parsed.value} />
|
||||||
{:else if isEmoji(parsed)}
|
{:else if isEmoji(parsed)}
|
||||||
<ContentEmoji value={parsed.value} />
|
<ContentEmoji value={parsed.value} />
|
||||||
|
{:else if isEmail(parsed)}
|
||||||
|
<ContentEmail value={parsed.value} />
|
||||||
{:else if isCode(parsed)}
|
{:else if isCode(parsed)}
|
||||||
<ContentCode
|
<ContentCode
|
||||||
value={parsed.value}
|
value={parsed.value}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
<NoteContentMinimal trimParent {url} event={$quote} />
|
<NoteContentMinimal trimParent {url} event={$quote} />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<NoteCard event={$quote} {url} class="bg-alt rounded-box p-4">
|
<NoteCard noShadow event={$quote} {url} class="bg-alt rounded-box p-4">
|
||||||
<NoteContentMinimal {url} event={$quote} />
|
<NoteContentMinimal {url} event={$quote} />
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
let popover: Instance | undefined = $state()
|
let popover: Instance | undefined = $state()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button class="join rounded-full">
|
<div class="join items-center rounded-full">
|
||||||
{#if ENABLE_ZAPS && !hideZap}
|
{#if ENABLE_ZAPS && !hideZap}
|
||||||
<ZapButton {url} {event} class="btn join-item btn-neutral btn-xs">
|
<ZapButton {url} {event} class="btn join-item btn-neutral btn-xs">
|
||||||
<Icon icon={Bolt} size={4} />
|
<Icon icon={Bolt} size={4} />
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
<Icon icon={SmileCircle} size={4} />
|
<Icon icon={SmileCircle} size={4} />
|
||||||
</EmojiButton>
|
</EmojiButton>
|
||||||
<Tippy
|
<Tippy
|
||||||
|
class="flex"
|
||||||
bind:popover
|
bind:popover
|
||||||
component={EventMenu}
|
component={EventMenu}
|
||||||
props={{url, noun, event, customActions, onClick: hidePopover}}
|
props={{url, noun, event, customActions, onClick: hidePopover}}
|
||||||
@@ -60,4 +61,4 @@
|
|||||||
<Icon icon={MenuDots} size={4} />
|
<Icon icon={MenuDots} size={4} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tippy>
|
</Tippy>
|
||||||
</Button>
|
</div>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
spacer!.style.minHeight = `${form!.offsetHeight}px`
|
spacer!.style.minHeight = `${form!.offsetHeight + 60}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 ml-2 pl-2 fixed z-feature">
|
class="left-content bottom-sai right-sai fixed z-feature mb-14 md:mb-0 w-full md:w-auto pr-2">
|
||||||
<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">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {makeEvent, ZAP_GOAL} from "@welshman/util"
|
import {makeEvent, ZAP_GOAL} from "@welshman/util"
|
||||||
import {publishThunk} from "@welshman/app"
|
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
||||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
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 ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
import {PROTECTED} from "@app/core/state"
|
import {PROTECTED} from "@app/core/state"
|
||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {DraftKey} from "@app/util/drafts"
|
import {DraftKey} from "@app/util/drafts"
|
||||||
import {canEnforceNip70} from "@app/core/commands"
|
import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
|
||||||
|
|
||||||
type Values = {
|
type Values = {
|
||||||
title: string
|
title: string
|
||||||
@@ -33,9 +34,10 @@
|
|||||||
url: string
|
url: string
|
||||||
h?: string
|
h?: string
|
||||||
initialValues?: Values
|
initialValues?: Values
|
||||||
|
shareToChat?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let {url, h, initialValues}: Props = $props()
|
let {url, h, initialValues, shareToChat = false}: Props = $props()
|
||||||
|
|
||||||
const draftKey = new DraftKey<Values>(`goal:${url}:${h ?? ""}`)
|
const draftKey = new DraftKey<Values>(`goal:${url}:${h ?? ""}`)
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@
|
|||||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading) return
|
if ($uploading || loading) return
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -78,7 +80,12 @@
|
|||||||
["relays", url],
|
["relays", url],
|
||||||
]
|
]
|
||||||
|
|
||||||
if (await shouldProtect) {
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const protect = await shouldProtect
|
||||||
|
|
||||||
|
if (protect) {
|
||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,14 +93,29 @@
|
|||||||
tags.push(["h", h])
|
tags.push(["h", h])
|
||||||
}
|
}
|
||||||
|
|
||||||
publishThunk({
|
const goalThunk = publishThunk({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
event: makeEvent(ZAP_GOAL, {content: title, tags}),
|
event: makeEvent(ZAP_GOAL, {content: title, tags}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const error = await waitForThunkError(goalThunk)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return pushToast({theme: "error", message: error})
|
||||||
|
}
|
||||||
|
|
||||||
draftKey.clear()
|
draftKey.clear()
|
||||||
history.back()
|
history.back()
|
||||||
|
|
||||||
|
if (shareToChat) {
|
||||||
|
publishRoomQuote({url, h, parent: goalThunk.event, protect})
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
|
||||||
let title = $state(initialValues?.title ?? "")
|
let title = $state(initialValues?.title ?? "")
|
||||||
let amount = $state(initialValues?.amount ?? 1000)
|
let amount = $state(initialValues?.amount ?? 1000)
|
||||||
@@ -154,7 +176,8 @@
|
|||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||||
onclick={selectFiles}>
|
onclick={selectFiles}
|
||||||
|
disabled={loading}>
|
||||||
{#if $uploading}
|
{#if $uploading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -169,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 items-center gap-2">
|
<label class="input input-bordered flex w-auto items-center gap-2">
|
||||||
<Icon icon={Bolt} />
|
<Icon icon={Bolt} />
|
||||||
<input bind:value={amount} type="number" class="w-28" />
|
<input bind:value={amount} type="number" class="w-28 grow" />
|
||||||
<p class="opacity-50">sats</p>
|
<p class="shrink-0 opacity-50">sats</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
<input
|
<input
|
||||||
class="range range-primary -mt-2"
|
class="range range-primary -mt-2 w-full"
|
||||||
type="range"
|
type="range"
|
||||||
min="1000"
|
min="1000"
|
||||||
max="100000"
|
max="100000"
|
||||||
@@ -188,10 +211,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary">Create Goal</Button>
|
<Button type="submit" class="btn btn-primary" disabled={$uploading || loading}>
|
||||||
|
<Spinner {loading}>Create Goal</Spinner>
|
||||||
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ProgressBar from "@app/components/ProgressBar.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {PLATFORM_NAME} from "@app/core/state"
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
|
|
||||||
@@ -22,9 +23,11 @@
|
|||||||
secret: string
|
secret: string
|
||||||
next: () => unknown
|
next: () => unknown
|
||||||
submitText?: string
|
submitText?: string
|
||||||
|
step?: number
|
||||||
|
totalSteps?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const {secret, next, submitText = "Continue"}: Props = $props()
|
const {secret, next, submitText = "Continue", step, totalSteps}: Props = $props()
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
@@ -150,6 +153,9 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
{#if step && totalSteps}
|
||||||
|
<ProgressBar current={step} total={totalSteps} />
|
||||||
|
{/if}
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<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"
|
||||||
@@ -103,10 +104,16 @@
|
|||||||
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(() => {
|
||||||
@@ -138,6 +145,9 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import LogInOTP from "@app/components/LogInOTP.svelte"
|
import LogInOTP from "@app/components/LogInOTP.svelte"
|
||||||
import LogInSelect from "@app/components/LogInSelect.svelte"
|
import LogInSelect from "@app/components/LogInSelect.svelte"
|
||||||
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
|
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
|
||||||
|
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
|
||||||
import {pushModal, clearModals} from "@app/util/modal"
|
import {pushModal, clearModals} from "@app/util/modal"
|
||||||
import {setChecked} from "@app/util/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
|
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Sorry, we were unable to log you in.",
|
message: getPomadeLoginFailureMessage(messages),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +65,17 @@
|
|||||||
|
|
||||||
pushToast({
|
pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Sorry, we were unable to log you in.",
|
message: getPomadeLoginFailureMessage(res.messages),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login error:", error)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: POMADE_NETWORK_ERROR_MESSAGE,
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
@@ -90,7 +98,7 @@
|
|||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
<Icon icon={Letter} />
|
<Icon icon={Letter} />
|
||||||
<input bind:value={email} />
|
<input type="email" bind:value={email} />
|
||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import LogInOTPConfirm from "@app/components/LogInOTPConfirm.svelte"
|
import LogInOTPConfirm from "@app/components/LogInOTPConfirm.svelte"
|
||||||
|
import {POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
@@ -35,11 +36,20 @@
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
pushModal(LogInOTPConfirm, {email, peersByPrefix})
|
pushModal(LogInOTPConfirm, {email, peersByPrefix})
|
||||||
} else {
|
} else {
|
||||||
|
console.error("Pomade challenge request failed during OTP login")
|
||||||
|
|
||||||
pushToast({
|
pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Sorry, we were unable to request a login code.",
|
message: POMADE_NETWORK_ERROR_MESSAGE,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: POMADE_NETWORK_ERROR_MESSAGE,
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
@@ -61,7 +71,7 @@
|
|||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
<Icon icon={Letter} />
|
<Icon icon={Letter} />
|
||||||
<input bind:value={email} />
|
<input type="email" bind:value={email} />
|
||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
|
|||||||
@@ -15,10 +15,11 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import StringMultiInput from "@lib/components/StringMultiInput.svelte"
|
import StringMultiInput from "@lib/components/StringMultiInput.svelte"
|
||||||
import LogInSelect from "@app/components/LogInSelect.svelte"
|
import LogInSelect from "@app/components/LogInSelect.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
|
||||||
import {setChecked} from "@app/util/notifications"
|
|
||||||
import {pushModal, clearModals} from "@app/util/modal"
|
import {pushModal, clearModals} from "@app/util/modal"
|
||||||
|
import {setChecked} from "@app/util/notifications"
|
||||||
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
|
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
|
||||||
|
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string
|
email: string
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
|
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Sorry, we were unable to log you in.",
|
message: getPomadeLoginFailureMessage(messages),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +65,17 @@
|
|||||||
|
|
||||||
pushToast({
|
pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Sorry, we were unable to log you in.",
|
message: getPomadeLoginFailureMessage(res.messages),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login error:", error)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: POMADE_NETWORK_ERROR_MESSAGE,
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
|
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
|
||||||
|
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
|
||||||
import {setChecked} from "@app/util/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
import {clearModals} from "@app/util/modal"
|
import {clearModals} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
@@ -46,9 +47,16 @@
|
|||||||
|
|
||||||
pushToast({
|
pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Sorry, we were unable to log you in.",
|
message: getPomadeLoginFailureMessage(res.messages),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login error:", error)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: POMADE_NETWORK_ERROR_MESSAGE,
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
children,
|
children,
|
||||||
minimal = false,
|
minimal = false,
|
||||||
hideProfile = false,
|
hideProfile = false,
|
||||||
|
noShadow = false,
|
||||||
url,
|
url,
|
||||||
...restProps
|
...restProps
|
||||||
}: {
|
}: {
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
children: Snippet
|
children: Snippet
|
||||||
minimal?: boolean
|
minimal?: boolean
|
||||||
hideProfile?: boolean
|
hideProfile?: boolean
|
||||||
|
noShadow?: boolean
|
||||||
url?: string
|
url?: string
|
||||||
class?: string
|
class?: string
|
||||||
} = $props()
|
} = $props()
|
||||||
@@ -34,7 +36,7 @@
|
|||||||
let muted = $state($isEventMuted(event))
|
let muted = $state($isEventMuted(event))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 shadow-md {restProps.class}">
|
<div class="flex flex-col gap-2 {restProps.class}" class:shadow-md={!noShadow}>
|
||||||
{#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">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {ComponentProps} from "svelte"
|
import type {ComponentProps} from "svelte"
|
||||||
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED} from "@welshman/util"
|
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} 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"
|
||||||
@@ -21,7 +20,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} />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {ComponentProps} from "svelte"
|
import type {ComponentProps} from "svelte"
|
||||||
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED} from "@welshman/util"
|
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} 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"
|
||||||
@@ -21,7 +20,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 {PollResponse} from "nostr-tools/kinds"
|
import {POLL_RESPONSE} from "@welshman/util"
|
||||||
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: [PollResponse], "#e": [props.event.id]}])
|
const responses = deriveEvents([{kinds: [POLL_RESPONSE], "#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,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 {PollResponse} from "nostr-tools/kinds"
|
import {POLL_RESPONSE} from "@welshman/util"
|
||||||
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: [PollResponse], "#e": [props.event.id]}],
|
filters: [{kinds: [POLL_RESPONSE], "#e": [props.event.id]}],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<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} from "@welshman/util"
|
import {makeEvent, POLL} from "@welshman/util"
|
||||||
import {publishThunk} 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"
|
||||||
@@ -13,6 +12,7 @@
|
|||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
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 ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {PROTECTED} from "@app/core/state"
|
import {PROTECTED} from "@app/core/state"
|
||||||
import {canEnforceNip70} from "@app/core/commands"
|
import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
|
||||||
import {DraftKey} from "@app/util/drafts"
|
import {DraftKey} from "@app/util/drafts"
|
||||||
import type {PollType} from "@app/util/polls"
|
import type {PollType} from "@app/util/polls"
|
||||||
|
|
||||||
@@ -40,9 +40,10 @@
|
|||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
h?: string
|
h?: string
|
||||||
|
shareToChat?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, h}: Props = $props()
|
const {url, h, shareToChat = false}: Props = $props()
|
||||||
const draftKey = new DraftKey<Values>(`poll:${url}:${h ?? ""}`)
|
const draftKey = new DraftKey<Values>(`poll:${url}:${h ?? ""}`)
|
||||||
const initialValues = draftKey.get()
|
const initialValues = draftKey.get()
|
||||||
|
|
||||||
@@ -102,6 +103,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
|
if (loading) return
|
||||||
|
|
||||||
if (!title.trim()) {
|
if (!title.trim()) {
|
||||||
return pushToast({theme: "error", message: "Please provide a title for your poll."})
|
return pushToast({theme: "error", message: "Please provide a title for your poll."})
|
||||||
}
|
}
|
||||||
@@ -130,18 +133,38 @@
|
|||||||
tags.push(["h", h])
|
tags.push(["h", h])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await shouldProtect) {
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const protect = await shouldProtect
|
||||||
|
|
||||||
|
if (protect) {
|
||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return pushToast({theme: "error", message: error})
|
||||||
|
}
|
||||||
|
|
||||||
draftKey.clear()
|
draftKey.clear()
|
||||||
history.back()
|
history.back()
|
||||||
|
|
||||||
|
if (shareToChat) {
|
||||||
|
publishRoomQuote({url, h, parent: pollThunk.event, protect})
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
|
||||||
let draggedOptionId = $state<string | undefined>()
|
let draggedOptionId = $state<string | undefined>()
|
||||||
let title = $state(initialValues?.title ?? "")
|
let title = $state(initialValues?.title ?? "")
|
||||||
@@ -246,10 +269,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary">Create Poll</Button>
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Create Poll</Spinner>
|
||||||
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -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: [PollResponse], "#e": [event.id]}])
|
const responses = deriveEvents([{kinds: [POLL_RESPONSE], "#e": [event.id]}])
|
||||||
|
|
||||||
const pollType = getPollType(event)
|
const pollType = getPollType(event)
|
||||||
const options = getPollOptions(event)
|
const options = getPollOptions(event)
|
||||||
|
|||||||
@@ -13,13 +13,16 @@
|
|||||||
|
|
||||||
const onClick = () => goToSpace(url)
|
const onClick = () => goToSpace(url)
|
||||||
|
|
||||||
|
const path = makeSpacePath(url)
|
||||||
|
|
||||||
const display = $derived(deriveRelayDisplay(url))
|
const display = $derived(deriveRelayDisplay(url))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
|
href={path}
|
||||||
onclick={onClick}
|
onclick={onClick}
|
||||||
title={$display}
|
title={$display}
|
||||||
class="tooltip-right"
|
class="tooltip-right"
|
||||||
notification={$notifications.has(makeSpacePath(url))}>
|
notification={$notifications.has(path)}>
|
||||||
<RelayIcon {url} size={10} class="rounded-full" />
|
<RelayIcon {url} size={10} class="rounded-full" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
import type {Filter} from "@welshman/util"
|
import type {Filter} from "@welshman/util"
|
||||||
import {deriveEventsDesc, deriveEventsById} from "@welshman/store"
|
import {deriveEventsDesc, deriveEventsById} from "@welshman/store"
|
||||||
import {formatTimestampRelative} from "@welshman/lib"
|
import {formatTimestampRelative} from "@welshman/lib"
|
||||||
import {NOTE, ROOMS, COMMENT} from "@welshman/util"
|
import {NOTE, ROOMS, COMMENT, MESSAGE} from "@welshman/util"
|
||||||
import {repository, loadRelayList} from "@welshman/app"
|
import {repository, loadRelayList} from "@welshman/app"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
|
import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
|
||||||
import {deriveGroupList, getSpaceUrlsFromGroupList, MESSAGE_KINDS} from "@app/core/state"
|
import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/core/state"
|
||||||
import {goToEvent} from "@app/util/routes"
|
import {goToEvent} from "@app/util/routes"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
load({
|
load({
|
||||||
filters: [
|
filters: [
|
||||||
{authors: [pubkey], kinds: [ROOMS]},
|
{authors: [pubkey], kinds: [ROOMS]},
|
||||||
{authors: [pubkey], limit: 1, kinds: [NOTE, COMMENT, ...MESSAGE_KINDS]},
|
{authors: [pubkey], limit: 1, kinds: [NOTE, COMMENT, MESSAGE]},
|
||||||
],
|
],
|
||||||
relays: Router.get().FromPubkeys([pubkey]).getUrls(),
|
relays: Router.get().FromPubkeys([pubkey]).getUrls(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Profile} from "@welshman/util"
|
import type {Profile} from "@welshman/util"
|
||||||
import {getTag, makeProfile} from "@welshman/util"
|
import {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,26 +10,18 @@
|
|||||||
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 shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || [])
|
const initialValues = {profile}
|
||||||
const initialValues = {profile, shouldBroadcast}
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onsubmit = async ({
|
const onsubmit = async ({profile}: {profile: Profile}) => {
|
||||||
profile,
|
|
||||||
shouldBroadcast,
|
|
||||||
}: {
|
|
||||||
profile: Profile
|
|
||||||
shouldBroadcast: boolean
|
|
||||||
}) => {
|
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const error = await waitForThunkError(updateProfile({profile, shouldBroadcast}))
|
const error = await waitForThunkError(updateProfile({profile}))
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({
|
pushToast({
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
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"
|
||||||
@@ -17,7 +16,6 @@
|
|||||||
|
|
||||||
type Values = {
|
type Values = {
|
||||||
profile: Profile
|
profile: Profile
|
||||||
shouldBroadcast: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -25,9 +23,10 @@
|
|||||||
onsubmit: (values: Values) => void
|
onsubmit: (values: Values) => void
|
||||||
isSignup?: boolean
|
isSignup?: boolean
|
||||||
footer: Snippet
|
footer: Snippet
|
||||||
|
progressBar?: Snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
const {initialValues, isSignup, onsubmit, footer}: Props = $props()
|
const {initialValues, isSignup, onsubmit, footer, progressBar}: Props = $props()
|
||||||
|
|
||||||
const values = $state(initialValues)
|
const values = $state(initialValues)
|
||||||
|
|
||||||
@@ -77,7 +76,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<textarea
|
<textarea
|
||||||
class="textarea textarea-bordered leading-4"
|
class="textarea textarea-bordered leading-4 w-full"
|
||||||
rows="5"
|
rows="5"
|
||||||
bind:value={values.profile.about}></textarea>
|
bind:value={values.profile.about}></textarea>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -104,26 +103,10 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !isSignup}
|
|
||||||
<FieldInline>
|
|
||||||
{#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}
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
{#if progressBar}
|
||||||
|
{@render progressBar()}
|
||||||
|
{/if}
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{@render footer()}
|
{@render footer()}
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
const {current, total}: {current: number; total: number} = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex w-full">
|
||||||
|
{#each Array(total) as _, i}
|
||||||
|
<div class="h-1 flex-1 transition-colors {i < current ? 'bg-primary' : 'bg-base-300'}"></div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
url?: string
|
url?: string
|
||||||
reactionClass?: string
|
reactionClass?: string
|
||||||
noTooltip?: boolean
|
noTooltip?: boolean
|
||||||
|
innerEvent?: TrustedEvent
|
||||||
children?: Snippet
|
children?: Snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,23 +44,36 @@
|
|||||||
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": [event.id]}]}),
|
deriveEventsById({repository, filters: [{kinds: [REACTION], "#e": eventIds}]}),
|
||||||
)
|
)
|
||||||
|
|
||||||
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": [event.id]}],
|
filters: [{kinds: [ZAP_RESPONSE], "#e": eventIds}],
|
||||||
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
|
eventToItem: (response: TrustedEvent) => {
|
||||||
|
const zap = getValidZap(response, event)
|
||||||
|
|
||||||
|
if (zap) {
|
||||||
|
return zap
|
||||||
|
}
|
||||||
|
|
||||||
|
if (innerEvent) {
|
||||||
|
return getValidZap(response, innerEvent)
|
||||||
|
}
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,12 @@
|
|||||||
|
|
||||||
<div class="col-4 text-left">
|
<div class="col-4 text-left">
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<div class="relative flex gap-4">
|
<div class="relative flex gap-2 sm:gap-4">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="avatar relative">
|
<div class="avatar relative">
|
||||||
<div
|
<div
|
||||||
class="center flex! h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300">
|
class="center flex! h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300">
|
||||||
<RelayIcon {url} />
|
<RelayIcon {url} size={10} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $rooms.includes(url)}
|
{#if $rooms.includes(url)}
|
||||||
@@ -36,13 +36,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h2 class="ellipsize whitespace-nowrap text-xl">
|
<RelayName {url} class="ellipsize whitespace-nowrap text-lg sm:text-xl" />
|
||||||
<RelayName {url} />
|
<p class="text-xs sm:text-sm opacity-75">{url}</p>
|
||||||
</h2>
|
|
||||||
<p class="text-sm opacity-75">{url}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RelayDescription {url} />
|
<RelayDescription {url} class="text-sm sm:text-md" />
|
||||||
</div>
|
</div>
|
||||||
{#if !hideFavorites && $favorited.size > 0}
|
{#if !hideFavorites && $favorited.size > 0}
|
||||||
<div class="row-2 card2 card2-sm bg-alt">
|
<div class="row-2 card2 card2-sm bg-alt">
|
||||||
|
|||||||
@@ -68,8 +68,6 @@
|
|||||||
const content = ed.getText({blockSeparator: "\n"}).trim()
|
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
const tags = ed.storage.nostr.getEditorTags()
|
const tags = ed.storage.nostr.getEditorTags()
|
||||||
|
|
||||||
if (!content) return
|
|
||||||
|
|
||||||
onSubmit({content, tags})
|
onSubmit({content, tags})
|
||||||
|
|
||||||
draftKey?.clear()
|
draftKey?.clear()
|
||||||
|
|||||||
@@ -243,7 +243,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $members.length > 0}
|
{#if $members !== undefined && $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,6 +251,10 @@
|
|||||||
</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>
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {hash, now, displayList, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
|
import {readable} from "svelte/store"
|
||||||
|
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} from "@welshman/util"
|
import {MESSAGE, COMMENT, getTag} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
thunks,
|
thunks,
|
||||||
pubkey,
|
pubkey,
|
||||||
@@ -27,7 +35,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} from "@app/core/state"
|
import {colors, ENABLE_ZAPS, deriveEventsForUrl, deriveEvent} 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"
|
||||||
@@ -38,7 +46,6 @@
|
|||||||
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
|
||||||
}
|
}
|
||||||
@@ -49,7 +56,6 @@
|
|||||||
replyTo = undefined,
|
replyTo = undefined,
|
||||||
showPubkey = false,
|
showPubkey = false,
|
||||||
addSpaceBelow = false,
|
addSpaceBelow = false,
|
||||||
inert = false,
|
|
||||||
canEdit,
|
canEdit,
|
||||||
onEdit,
|
onEdit,
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
@@ -60,7 +66,15 @@
|
|||||||
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
|
||||||
@@ -78,7 +92,7 @@
|
|||||||
|
|
||||||
<TapTarget
|
<TapTarget
|
||||||
data-event={event.id}
|
data-event={event.id}
|
||||||
onTap={inert ? null : onTap}
|
{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},
|
||||||
@@ -111,7 +125,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
|
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
|
||||||
<RoomItemContent {url} {event} />
|
<RoomItemContent {url} event={$innerEvent ?? event} />
|
||||||
{#if thunk}
|
{#if thunk}
|
||||||
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
|
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -124,9 +138,10 @@
|
|||||||
{event}
|
{event}
|
||||||
{deleteReaction}
|
{deleteReaction}
|
||||||
{createReaction}
|
{createReaction}
|
||||||
reactionClass="tooltip-right" />
|
reactionClass="tooltip-right"
|
||||||
{#if path && $comments.length > 0}
|
innerEvent={$innerEvent} />
|
||||||
{@const pubkeys = $comments.map(e => e.pubkey)}
|
{#if path && $innerComments.length > 0}
|
||||||
|
{@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`}
|
||||||
@@ -138,7 +153,7 @@
|
|||||||
"btn-primary": isOwn,
|
"btn-primary": isOwn,
|
||||||
})}>
|
})}>
|
||||||
<Icon icon={ReplyAlt} />
|
<Icon icon={ReplyAlt} />
|
||||||
<span>{$comments.length} comment{$comments.length === 1 ? "" : "s"}</span>
|
<span>{$innerComments.length} comment{$innerComments.length === 1 ? "" : "s"}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -8,16 +8,17 @@
|
|||||||
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} />
|
<NoteContent {...props} {minLength} {maxLength} />
|
||||||
</Link>
|
</Link>
|
||||||
{:else}
|
{:else}
|
||||||
<NoteContent {...props} />
|
<NoteContent {...props} {minLength} {maxLength} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,6 +73,15 @@
|
|||||||
</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">
|
||||||
@@ -101,6 +110,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -56,6 +56,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
|
|
||||||
|
|||||||
@@ -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-3">
|
<div class="flex items-center gap-2">
|
||||||
<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} />
|
||||||
|
|||||||
@@ -62,9 +62,10 @@
|
|||||||
|
|
||||||
const flows = {
|
const flows = {
|
||||||
email: {
|
email: {
|
||||||
start: () => pushModal(SignUpEmail, {next: flows.email.profile}),
|
start: () => pushModal(SignUpEmail, {next: flows.email.profile, step: 1, totalSteps: 3}),
|
||||||
profile: () => pushModal(SignUpProfile, {next: flows.email.complete}),
|
profile: () => pushModal(SignUpProfile, {next: flows.email.complete, step: 2, totalSteps: 3}),
|
||||||
complete: () => pushModal(SignUpComplete, {next: flows.email.finalize}),
|
complete: () =>
|
||||||
|
pushModal(SignUpComplete, {next: flows.email.finalize, step: 3, totalSteps: 3}),
|
||||||
finalize: () => {
|
finalize: () => {
|
||||||
const email = getKey<string>("signup.email")!
|
const email = getKey<string>("signup.email")!
|
||||||
const clientOptions = getKey<ClientOptions>("signup.clientOptions")!
|
const clientOptions = getKey<ClientOptions>("signup.clientOptions")!
|
||||||
@@ -74,9 +75,10 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
nostr: {
|
nostr: {
|
||||||
start: () => pushModal(SignUpProfile, {next: flows.nostr.key}),
|
start: () => pushModal(SignUpProfile, {next: flows.nostr.key, step: 1, totalSteps: 3}),
|
||||||
key: () => pushModal(SignUpKey, {next: flows.nostr.complete}),
|
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}),
|
||||||
complete: () => pushModal(SignUpComplete, {next: flows.nostr.finalize}),
|
complete: () =>
|
||||||
|
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
|
||||||
finalize: () => {
|
finalize: () => {
|
||||||
const secret = getKey<string>("signup.secret")!
|
const secret = getKey<string>("signup.secret")!
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,15 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ProgressBar from "@app/components/ProgressBar.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
next: () => void
|
next: () => void
|
||||||
|
step?: number
|
||||||
|
totalSteps?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const {next}: Props = $props()
|
const {next, step, totalSteps}: Props = $props()
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
</script>
|
</script>
|
||||||
@@ -33,6 +36,9 @@
|
|||||||
on groups you've already joined. Click below to get started!
|
on groups you've already joined. Click below to get started!
|
||||||
</p>
|
</p>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
{#if step && totalSteps}
|
||||||
|
<ProgressBar current={step} total={totalSteps} />
|
||||||
|
{/if}
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
|
|||||||
@@ -18,14 +18,17 @@
|
|||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
|
import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
|
||||||
|
import ProgressBar from "@app/components/ProgressBar.svelte"
|
||||||
import {pushToast, popToast} from "@app/util/toast"
|
import {pushToast, popToast} from "@app/util/toast"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
next: () => void
|
next: () => void
|
||||||
|
step?: number
|
||||||
|
totalSteps?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const {next}: Props = $props()
|
const {next, step, totalSteps}: Props = $props()
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
@@ -81,7 +84,7 @@
|
|||||||
setKey("signup.clientOptions", clientOptions)
|
setKey("signup.clientOptions", clientOptions)
|
||||||
|
|
||||||
popToast(toastId)
|
popToast(toastId)
|
||||||
pushModal(SignUpEmailConfirm, {next})
|
pushModal(SignUpEmailConfirm, {next, step, totalSteps})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|
||||||
@@ -120,7 +123,7 @@
|
|||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
<Icon icon={Letter} />
|
<Icon icon={Letter} />
|
||||||
<input bind:value={email} />
|
<input type="email" bind:value={email} />
|
||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
@@ -134,8 +137,14 @@
|
|||||||
<input type="password" bind:value={password} />
|
<input type="password" bind:value={password} />
|
||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
Must be at least 12 characters long.
|
||||||
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
{#if step && totalSteps}
|
||||||
|
<ProgressBar current={step} total={totalSteps} />
|
||||||
|
{/if}
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
|
|||||||
@@ -15,12 +15,15 @@
|
|||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ProgressBar from "@app/components/ProgressBar.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
next: () => void
|
next: () => void
|
||||||
|
step?: number
|
||||||
|
totalSteps?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const {next}: Props = $props()
|
const {next, step, totalSteps}: Props = $props()
|
||||||
|
|
||||||
const email = getKey<string>("signup.email")
|
const email = getKey<string>("signup.email")
|
||||||
|
|
||||||
@@ -61,6 +64,9 @@
|
|||||||
above.
|
above.
|
||||||
</p>
|
</p>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
{#if step && totalSteps}
|
||||||
|
<ProgressBar current={step} total={totalSteps} />
|
||||||
|
{/if}
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
next: () => void
|
next: () => void
|
||||||
|
step?: number
|
||||||
|
totalSteps?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const {next}: Props = $props()
|
const {next, step, totalSteps}: Props = $props()
|
||||||
|
|
||||||
const secret = getKey<string>("signup.secret")!
|
const secret = getKey<string>("signup.secret")!
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<KeyDownload {secret} {next} />
|
<KeyDownload {secret} {next} {step} {totalSteps} />
|
||||||
|
|||||||
@@ -5,19 +5,20 @@
|
|||||||
import {getKey, setKey} from "@lib/implicit"
|
import {getKey, setKey} from "@lib/implicit"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
|
||||||
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||||
|
import ProgressBar from "@app/components/ProgressBar.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
next: () => void
|
next: () => void
|
||||||
|
step?: number
|
||||||
|
totalSteps?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const {next}: Props = $props()
|
const {next, step, totalSteps}: Props = $props()
|
||||||
|
|
||||||
const profile = getKey<Profile>("signup.profile")!
|
const profile = getKey<Profile>("signup.profile")!
|
||||||
|
|
||||||
const initialValues = {profile, shouldBroadcast: false}
|
const initialValues = {profile}
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
@@ -27,8 +28,6 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
|
||||||
<ModalBody>
|
|
||||||
<ProfileEditForm isSignup {initialValues} {onsubmit}>
|
<ProfileEditForm isSignup {initialValues} {onsubmit}>
|
||||||
{#snippet footer()}
|
{#snippet footer()}
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
@@ -40,6 +39,9 @@
|
|||||||
<Icon icon={AltArrowRight} />
|
<Icon icon={AltArrowRight} />
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
{#snippet progressBar()}
|
||||||
|
{#if step && totalSteps}
|
||||||
|
<ProgressBar current={step} total={totalSteps} />
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
</ProfileEditForm>
|
</ProfileEditForm>
|
||||||
</ModalBody>
|
|
||||||
</Modal>
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
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"
|
||||||
@@ -23,36 +25,72 @@
|
|||||||
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(3000),
|
signal: AbortSignal.timeout(10000),
|
||||||
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>
|
||||||
|
|
||||||
@@ -70,20 +108,36 @@
|
|||||||
<p class="center">
|
<p class="center">
|
||||||
<Spinner {loading}>Requesting an invite link...</Spinner>
|
<Spinner {loading}>Requesting an invite link...</Spinner>
|
||||||
</p>
|
</p>
|
||||||
{:else if $authError}
|
{:else if isGenericError}
|
||||||
|
<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()}
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
<div class="flex w-full gap-2">
|
||||||
<Icon icon={LinkRound} />
|
{#if canShare}
|
||||||
<input bind:value={invite} class="grow" type="text" />
|
<Button
|
||||||
<Button onclick={copyInvite}>
|
class="input input-bordered flex shrink-0 w-12 items-center justify-center p-0"
|
||||||
|
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>
|
||||||
|
|||||||
@@ -112,6 +112,15 @@
|
|||||||
{/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">
|
||||||
@@ -120,7 +129,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if canBan || canUnallow}
|
{#if canBan || canUnallow}
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
|
<Button
|
||||||
|
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}
|
||||||
@@ -152,6 +163,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED} from "@welshman/util"
|
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util"
|
||||||
import {Poll} from "nostr-tools/kinds"
|
import {deriveRelay, createSearch, pubkey} from "@welshman/app"
|
||||||
import {deriveRelay, deriveRelayDisplay, createSearch, pubkey} from "@welshman/app"
|
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||||
@@ -66,7 +65,6 @@
|
|||||||
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")
|
||||||
@@ -145,9 +143,7 @@
|
|||||||
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
|
<strong class="flex items-center gap-1 relative">
|
||||||
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"
|
||||||
@@ -181,7 +177,11 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -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">
|
<div class="badge badge-neutral text-wrap h-auto">
|
||||||
<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">
|
<div class="badge badge-neutral text-wrap h-auto">
|
||||||
<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">
|
<div class="badge badge-neutral text-wrap h-auto">
|
||||||
<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">
|
<div class="badge badge-neutral text-wrap h-auto">
|
||||||
<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">
|
<p class="badge badge-neutral text-wrap h-auto">
|
||||||
<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">
|
<p class="badge badge-warning text-wrap h-auto">
|
||||||
<span class="ellipsize">Min PoW: {limitation?.min_pow_difficulty}</span>
|
<span class="ellipsize">Min PoW: {limitation?.min_pow_difficulty}</span>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
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 {formatTimestampAsDate, groupBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
|
import {repository, tracker} from "@welshman/app"
|
||||||
|
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 {sortEventsDesc} from "@welshman/util"
|
import {MESSAGE, 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"
|
||||||
@@ -53,8 +54,11 @@
|
|||||||
|
|
||||||
const getFilter = (searchTerm: string): Filter =>
|
const getFilter = (searchTerm: string): Filter =>
|
||||||
h
|
h
|
||||||
? {kinds: CONTENT_KINDS, "#h": [h], search: searchTerm}
|
? {kinds: [MESSAGE, ...CONTENT_KINDS], "#h": [h], search: searchTerm}
|
||||||
: {kinds: CONTENT_KINDS, search: searchTerm}
|
: {kinds: [MESSAGE, ...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()
|
||||||
@@ -68,18 +72,23 @@
|
|||||||
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: [getFilter(searchTerm.trim())],
|
filters: [filter],
|
||||||
})
|
})
|
||||||
|
|
||||||
results = sortEventsDesc(events)
|
results = sortEventsDesc(uniqBy((e: TrustedEvent) => e.id, [...events, ...localResults]))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof DOMException && error.name === "AbortError")) {
|
if (!(error instanceof DOMException && error.name === "AbortError")) {
|
||||||
results = []
|
results = sortEventsDesc(localResults)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {makeEvent, THREAD} from "@welshman/util"
|
import {makeEvent, THREAD} from "@welshman/util"
|
||||||
import {publishThunk} from "@welshman/app"
|
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.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 Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
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 ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
import {PROTECTED} from "@app/core/state"
|
import {PROTECTED} from "@app/core/state"
|
||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {DraftKey} from "@app/util/drafts"
|
import {DraftKey} from "@app/util/drafts"
|
||||||
import {canEnforceNip70} from "@app/core/commands"
|
import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
|
||||||
|
|
||||||
type Values = {
|
type Values = {
|
||||||
content?: string | object
|
content?: string | object
|
||||||
@@ -29,9 +30,10 @@
|
|||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
h?: string
|
h?: string
|
||||||
|
shareToChat?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, h}: Props = $props()
|
const {url, h, shareToChat = false}: Props = $props()
|
||||||
const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`)
|
const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`)
|
||||||
const initialValues = draftKey.get()
|
const initialValues = draftKey.get()
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
@@ -43,7 +45,7 @@
|
|||||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading) return
|
if ($uploading || loading) return
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -64,7 +66,12 @@
|
|||||||
|
|
||||||
const tags = [...ed.storage.nostr.getEditorTags(), ["title", title]]
|
const tags = [...ed.storage.nostr.getEditorTags(), ["title", title]]
|
||||||
|
|
||||||
if (await shouldProtect) {
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const protect = await shouldProtect
|
||||||
|
|
||||||
|
if (protect) {
|
||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,14 +79,29 @@
|
|||||||
tags.push(["h", h])
|
tags.push(["h", h])
|
||||||
}
|
}
|
||||||
|
|
||||||
publishThunk({
|
const threadThunk = publishThunk({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
event: makeEvent(THREAD, {content, tags}),
|
event: makeEvent(THREAD, {content, tags}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const error = await waitForThunkError(threadThunk)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return pushToast({theme: "error", message: error})
|
||||||
|
}
|
||||||
|
|
||||||
draftKey.clear()
|
draftKey.clear()
|
||||||
history.back()
|
history.back()
|
||||||
|
|
||||||
|
if (shareToChat) {
|
||||||
|
publishRoomQuote({url, h, parent: threadThunk.event, protect})
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
|
||||||
let title = $state(initialValues?.title ?? "")
|
let title = $state(initialValues?.title ?? "")
|
||||||
let content = $state(initialValues?.content ?? "")
|
let content = $state(initialValues?.content ?? "")
|
||||||
@@ -138,7 +160,8 @@
|
|||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||||
onclick={selectFiles}>
|
onclick={selectFiles}
|
||||||
|
disabled={loading}>
|
||||||
{#if $uploading}
|
{#if $uploading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -148,10 +171,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary">Create Thread</Button>
|
<Button type="submit" class="btn btn-primary" disabled={$uploading || loading}>
|
||||||
|
<Spinner {loading}>Create Thread</Spinner>
|
||||||
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
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,
|
||||||
@@ -18,7 +19,12 @@
|
|||||||
ViewportSize,
|
ViewportSize,
|
||||||
videoPrimaryTileKey,
|
videoPrimaryTileKey,
|
||||||
} from "@app/call/video"
|
} from "@app/call/video"
|
||||||
import {currentVoiceSession, currentVoiceRoom, pubkeyFromLiveKitIdentity} from "@app/call/stores"
|
import {
|
||||||
|
currentVoiceSession,
|
||||||
|
currentVoiceRoom,
|
||||||
|
mediaStateByIdentity,
|
||||||
|
pubkeyFromLiveKitIdentity,
|
||||||
|
} from "@app/call/stores"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
layout: VideoCallLayout
|
layout: VideoCallLayout
|
||||||
@@ -121,6 +127,25 @@
|
|||||||
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
|
||||||
@@ -144,6 +169,9 @@
|
|||||||
|
|
||||||
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
|
||||||
@@ -184,6 +212,7 @@
|
|||||||
</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",
|
||||||
@@ -203,6 +232,15 @@
|
|||||||
<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)" : ""}
|
||||||
@@ -213,8 +251,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 btn-ghost",
|
"absolute right-1 top-1 z-20 btn btn-xs btn-square",
|
||||||
pinned ? "btn-active bg-primary/25 text-primary" : "bg-base-100/70",
|
pinned ? "btn-primary" : "btn-ghost bg-base-100",
|
||||||
)}
|
)}
|
||||||
onclick={spotlightHandlerFor(tileKey(tile))}>
|
onclick={spotlightHandlerFor(tileKey(tile))}>
|
||||||
<Icon icon={Pin} size={3} />
|
<Icon icon={Pin} size={3} />
|
||||||
@@ -238,8 +276,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if useMultiGrid}
|
{:else if useMultiGrid}
|
||||||
<div
|
<div class={cx("grid min-h-0 flex-1 content-start gap-2 overflow-y-auto", multiGridClass)}>
|
||||||
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}
|
||||||
@@ -254,8 +291,10 @@
|
|||||||
{: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 camera or screen share yet.</p>
|
<p>No one is sharing video yet.</p>
|
||||||
<p class="text-xs">Use the camera or screen share control to share video.</p>
|
<p class="text-xs">
|
||||||
|
Participants appear here when they turn on their camera or share their screen.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<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}
|
||||||
@@ -9,11 +9,13 @@
|
|||||||
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,
|
||||||
@@ -83,9 +85,17 @@
|
|||||||
)}>
|
)}>
|
||||||
<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 text-xs opacity-70">
|
<span class="ellipsize min-w-0 flex-1 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}
|
||||||
|
|||||||
@@ -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 Videocamera from "@assets/icons/videocamera.svg?dataurl"
|
import VideocameraOff from "@assets/icons/videocamera-off.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,18 +183,16 @@
|
|||||||
</Button>
|
</Button>
|
||||||
{:else if $voiceState === VoiceState.Connected && $currentVoiceSession}
|
{:else if $voiceState === VoiceState.Connected && $currentVoiceSession}
|
||||||
<Button
|
<Button
|
||||||
data-tip={$currentVoiceSession.muted ? "Unmute" : "Mute"}
|
data-tip={$voiceMicMuted ? "Unmute" : "Mute"}
|
||||||
class={cx(
|
class={cx(
|
||||||
mediaToggleClass,
|
mediaToggleClass,
|
||||||
"overflow-visible",
|
"overflow-visible",
|
||||||
!$currentVoiceSession.muted && $isLocalSpeaking && "text-primary",
|
$voiceMicMuted && "text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
|
||||||
$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 $currentVoiceSession.muted}
|
{#if $voiceMicMuted}
|
||||||
<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">
|
||||||
@@ -207,9 +205,17 @@
|
|||||||
</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(mediaToggleClass, $currentVoiceSession.cameraOn && "text-primary")}
|
class={cx(
|
||||||
|
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={$currentVoiceSession.cameraOn ? VideocameraRecord : Videocamera} size={4} />
|
<Icon
|
||||||
|
icon={$currentVoiceSession.cameraOn ? VideocameraRecord : VideocameraOff}
|
||||||
|
size={4} />
|
||||||
</Button>
|
</Button>
|
||||||
{#if !Capacitor.isNativePlatform()}
|
{#if !Capacitor.isNativePlatform()}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
+42
-43
@@ -18,10 +18,10 @@ 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,
|
||||||
|
MESSAGE,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
MESSAGING_RELAYS,
|
MESSAGING_RELAYS,
|
||||||
RELAYS,
|
RELAYS,
|
||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
ROOMS,
|
ROOMS,
|
||||||
COMMENT,
|
COMMENT,
|
||||||
APP_DATA,
|
APP_DATA,
|
||||||
|
POLL_RESPONSE,
|
||||||
isSignedEvent,
|
isSignedEvent,
|
||||||
makeEvent,
|
makeEvent,
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
@@ -52,7 +53,6 @@ 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,7 +73,6 @@ import {
|
|||||||
waitForThunkError,
|
waitForThunkError,
|
||||||
getPubkeyRelays,
|
getPubkeyRelays,
|
||||||
userBlossomServerList,
|
userBlossomServerList,
|
||||||
getThunkError,
|
|
||||||
addRoomMember,
|
addRoomMember,
|
||||||
manageRelay,
|
manageRelay,
|
||||||
getRelay,
|
getRelay,
|
||||||
@@ -84,6 +83,7 @@ import {
|
|||||||
SETTINGS,
|
SETTINGS,
|
||||||
PROTECTED,
|
PROTECTED,
|
||||||
INDEXER_RELAYS,
|
INDEXER_RELAYS,
|
||||||
|
DEFAULT_RELAYS,
|
||||||
DEFAULT_BLOSSOM_SERVERS,
|
DEFAULT_BLOSSOM_SERVERS,
|
||||||
userSpaceUrls,
|
userSpaceUrls,
|
||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
@@ -122,6 +122,34 @@ export const prependParent = (
|
|||||||
return {content, tags}
|
return {content, tags}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const publishRoomQuote = ({
|
||||||
|
url,
|
||||||
|
h,
|
||||||
|
parent,
|
||||||
|
protect,
|
||||||
|
delay,
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
h?: string
|
||||||
|
parent: TrustedEvent
|
||||||
|
protect: boolean
|
||||||
|
delay?: number
|
||||||
|
}) => {
|
||||||
|
const tags: string[][] = []
|
||||||
|
|
||||||
|
if (h) {
|
||||||
|
tags.push(["h", h])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protect) {
|
||||||
|
tags.push(PROTECTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = makeEvent(MESSAGE, prependParent(parent, {content: "", tags}, url))
|
||||||
|
|
||||||
|
return publishThunk({relays: [url], event, delay})
|
||||||
|
}
|
||||||
|
|
||||||
// Synchronization
|
// Synchronization
|
||||||
|
|
||||||
export const broadcastUserData = async (relays: string[]) => {
|
export const broadcastUserData = async (relays: string[]) => {
|
||||||
@@ -235,16 +263,12 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
|
|||||||
return stripPrefix(error)
|
return stripPrefix(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deriveRelayAuthError = (url: string, claim = "") => {
|
export const deriveRelayAuthError = (url: string) => {
|
||||||
// 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(
|
||||||
[thunk, relaysMostlyRestricted, deriveSocket(url)],
|
[relaysMostlyRestricted, deriveSocket(url)],
|
||||||
([$thunk, $relaysMostlyRestricted, $socket]) => {
|
([$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)
|
||||||
}
|
}
|
||||||
@@ -252,16 +276,6 @@ export const deriveRelayAuthError = (url: string, claim = "") => {
|
|||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -360,7 +374,7 @@ export type PollResponseParams = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const makePollResponse = ({event, selectedIds}: PollResponseParams) =>
|
export const makePollResponse = ({event, selectedIds}: PollResponseParams) =>
|
||||||
makeEvent(PollResponse, {
|
makeEvent(POLL_RESPONSE, {
|
||||||
content: "",
|
content: "",
|
||||||
tags: [["e", event.id], ...selectedIds.map(selectedId => ["response", selectedId])],
|
tags: [["e", event.id], ...selectedIds.map(selectedId => ["response", selectedId])],
|
||||||
})
|
})
|
||||||
@@ -695,34 +709,18 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
|
|||||||
// Update Profile
|
// Update Profile
|
||||||
|
|
||||||
export const initProfile = (profile: Profile) => {
|
export const initProfile = (profile: Profile) => {
|
||||||
const template = createProfile(profile)
|
const event = makeEvent(PROFILE, createProfile(profile))
|
||||||
|
|
||||||
// Start out protected by default
|
return publishThunk({event, relays: DEFAULT_RELAYS})
|
||||||
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 = ({
|
export const updateProfile = ({profile}: {profile: Profile}) => {
|
||||||
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))]
|
const scenarios = [router.FromRelays(get(userSpaceUrls)), router.FromUser(), router.Index()]
|
||||||
|
|
||||||
if (shouldBroadcast) {
|
// Remove protected tag, we used to add it
|
||||||
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()
|
||||||
@@ -737,9 +735,10 @@ 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.includes(pubkey))
|
.filter(pubkey => !spaceMembers || !spaceMembers.includes(pubkey))
|
||||||
.map(pubkey =>
|
.map(pubkey =>
|
||||||
manageRelay(url, {
|
manageRelay(url, {
|
||||||
method: ManagementMethod.AllowPubkey,
|
method: ManagementMethod.AllowPubkey,
|
||||||
|
|||||||
+91
-54
@@ -1,5 +1,6 @@
|
|||||||
import {get, writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {
|
import {
|
||||||
|
batch,
|
||||||
call,
|
call,
|
||||||
uniq,
|
uniq,
|
||||||
int,
|
int,
|
||||||
@@ -25,7 +26,8 @@ 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} from "@welshman/net"
|
import {load, request, mergeRepositoryUpdates} 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"
|
||||||
@@ -56,57 +58,75 @@ export const makeFeed = ({
|
|||||||
let backwardWindow = [at - interval, at]
|
let backwardWindow = [at - interval, at]
|
||||||
let forwardWindow = [at, at + interval]
|
let forwardWindow = [at, at + interval]
|
||||||
|
|
||||||
const insertEvent = (event: TrustedEvent) => {
|
const insertIntoBuffer = (event: TrustedEvent) => {
|
||||||
let handled = false
|
|
||||||
|
|
||||||
if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) {
|
|
||||||
const $events = get(events)
|
|
||||||
|
|
||||||
for (let i = 0; i < $events.length; i++) {
|
|
||||||
if ($events[i].created_at > event.created_at) {
|
|
||||||
events.set(insertAt(i, event, $events))
|
|
||||||
handled = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!handled) {
|
|
||||||
events.set([...$events, event])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < buffer.length; i++) {
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
if (buffer[i].created_at > event.created_at) {
|
if (buffer[i].created_at < event.created_at) {
|
||||||
buffer.splice(i, 0, event)
|
buffer.splice(i, 0, event)
|
||||||
handled = true
|
return
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!handled) {
|
|
||||||
buffer.push(event)
|
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)) {
|
||||||
|
visible.push(event)
|
||||||
|
} 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++) {
|
||||||
|
if ($events[i].created_at > event.created_at) {
|
||||||
|
$events = insertAt(i, event, $events)
|
||||||
|
inserted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!inserted) {
|
||||||
|
$events = [...$events, event]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $events
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribers = [
|
const unsubscribers = [
|
||||||
on(repository, "update", ({added, removed}) => {
|
on(
|
||||||
|
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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of added) {
|
const matching = added.filter(
|
||||||
if (matchFilters(filters, event) && tracker.getRelays(event.id).has(url)) {
|
event => 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)) {
|
||||||
insertEvent(event)
|
insertEvents([event])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -132,17 +152,15 @@ export const makeFeed = ({
|
|||||||
element,
|
element,
|
||||||
delay: 300,
|
delay: 300,
|
||||||
threshold: 5000,
|
threshold: 5000,
|
||||||
onScroll: () => {
|
onScroll: async () => {
|
||||||
const [since, until] = backwardWindow
|
const [since, until] = backwardWindow
|
||||||
|
|
||||||
backwardWindow = [since - interval, since]
|
backwardWindow = [since - interval, since]
|
||||||
|
|
||||||
for (const event of buffer.splice(0, 30)) {
|
insertEvents(buffer.splice(0, 30))
|
||||||
insertEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (until > now() - int(2, YEAR)) {
|
if (until > now() - int(2, YEAR)) {
|
||||||
loadTimeframe(since, until)
|
await 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?.()
|
||||||
@@ -155,17 +173,15 @@ export const makeFeed = ({
|
|||||||
reverse: true,
|
reverse: true,
|
||||||
delay: 300,
|
delay: 300,
|
||||||
threshold: 5000,
|
threshold: 5000,
|
||||||
onScroll: () => {
|
onScroll: async () => {
|
||||||
const [since, until] = forwardWindow
|
const [since, until] = forwardWindow
|
||||||
|
|
||||||
forwardWindow = [until, until + interval]
|
forwardWindow = [until, until + interval]
|
||||||
|
|
||||||
for (const event of buffer.splice(0, 30)) {
|
insertEvents(buffer.splice(0, 30))
|
||||||
insertEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (until < now()) {
|
if (until < now()) {
|
||||||
loadTimeframe(since, until)
|
await 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?.()
|
||||||
@@ -208,40 +224,61 @@ export const makeCalendarFeed = ({
|
|||||||
|
|
||||||
const events = writable(sortBy(getStart, getEventsForUrl(url, filters)))
|
const events = writable(sortBy(getStart, getEventsForUrl(url, filters)))
|
||||||
|
|
||||||
const insertEvent = (event: TrustedEvent) => {
|
// Batch-insert calendar events into the store with a single update
|
||||||
|
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)
|
||||||
|
|
||||||
if (isNaN(start) || isNaN(getEnd(event))) return
|
let handled = false
|
||||||
|
|
||||||
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) return $events
|
if ($events[i].id === event.id) {
|
||||||
if (getStart($events[i]) > start) return insertAt(i, event, $events)
|
handled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (getStart($events[i]) > start) {
|
||||||
|
$events = insertAt(i, event, $events)
|
||||||
|
handled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...$events.filter(e => getAddress(e) !== address), event]
|
if (!handled) {
|
||||||
|
$events = [...$events.filter(e => getAddress(e) !== address), event]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $events
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribers = [
|
const unsubscribers = [
|
||||||
on(repository, "update", ({added, removed}) => {
|
on(
|
||||||
|
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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of added) {
|
const matching = added.filter(event => matchFilters(filters, event))
|
||||||
if (matchFilters(filters, event)) {
|
|
||||||
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)) {
|
||||||
insertEvent(event)
|
insertEvents([event])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
+135
-107
@@ -3,7 +3,6 @@ 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,
|
||||||
@@ -93,6 +92,7 @@ import {
|
|||||||
THREAD,
|
THREAD,
|
||||||
CLASSIFIED,
|
CLASSIFIED,
|
||||||
WRAP,
|
WRAP,
|
||||||
|
POLL,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
ZAP_GOAL,
|
ZAP_GOAL,
|
||||||
ZAP_REQUEST,
|
ZAP_REQUEST,
|
||||||
@@ -327,9 +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 MESSAGE_KINDS = [...CONTENT_KINDS, MESSAGE]
|
|
||||||
|
|
||||||
export const DM_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]
|
export const DM_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]
|
||||||
|
|
||||||
@@ -554,7 +552,7 @@ export const chatsById = call(() => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
addEvents(added)
|
addEvents(added)
|
||||||
removeEvents(removed)
|
removeEvents(removed)
|
||||||
}, 50)
|
}, 200)
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -562,13 +560,9 @@ export const chatsById = call(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deriveChat = call(() => {
|
export const deriveChat = makeDeriveItem(chatsById)
|
||||||
const _deriveChat = makeDeriveItem(chatsById)
|
|
||||||
|
|
||||||
return (pubkeys: string[]) => _deriveChat(makeChatId(pubkeys))
|
export const chatSearch = derived(throttled(1500, chatsById), $chatsByPubkey => {
|
||||||
})
|
|
||||||
|
|
||||||
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())),
|
||||||
{
|
{
|
||||||
@@ -595,6 +589,8 @@ 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) =>
|
||||||
@@ -607,7 +603,7 @@ export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => {
|
export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => {
|
||||||
const metaByIdByUrl = new Map<string, Map<string, Room>>()
|
const result = new 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())
|
||||||
@@ -619,6 +615,8 @@ 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))
|
||||||
|
|
||||||
@@ -626,23 +624,15 @@ 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})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const result = new Map<string, Room[]>()
|
if (metaById.size > 0) {
|
||||||
|
|
||||||
for (const [url, metaById] of metaByIdByUrl.entries()) {
|
|
||||||
result.set(url, Array.from(metaById.values()))
|
result.set(url, Array.from(metaById.values()))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
@@ -814,36 +804,49 @@ export const deriveOtherRooms = (url: string) =>
|
|||||||
|
|
||||||
// Space/room memberships
|
// Space/room memberships
|
||||||
|
|
||||||
const getSpaceMembers = (_url: string, events: TrustedEvent[]) => {
|
export const deriveSpaceMembers = (url: string) =>
|
||||||
const members = new Set<string>()
|
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_MEMBERS]}]), ([event]) =>
|
||||||
|
uniq(getTagValues("member", event?.tags ?? [])),
|
||||||
|
)
|
||||||
|
|
||||||
for (const event of sortEventsAsc(events)) {
|
export const deriveRoomMembers = (url: string, h: string) => {
|
||||||
if (event.kind === RELAY_MEMBERS) {
|
const filters: Filter[] = [{kinds: [ROOM_MEMBERS], "#d": [h]}]
|
||||||
members.clear()
|
|
||||||
|
|
||||||
for (const pubkey of uniq(getTagValues("member", event.tags))) {
|
return derived(deriveEventsForUrl(url, filters), ([event]) =>
|
||||||
members.add(pubkey)
|
uniq(getPubkeyTagValues(event?.tags ?? [])),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
export type BannedPubkeyItem = {
|
||||||
|
pubkey: string
|
||||||
|
reason: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const pubkeys = getPubkeyTagValues(event.tags)
|
export const spaceBannedPubkeyItems = new Map<string, BannedPubkeyItem[]>()
|
||||||
|
|
||||||
if (event.kind === RELAY_ADD_MEMBER) {
|
export const deriveSpaceBannedPubkeyItems = (url: string) => {
|
||||||
for (const pubkey of pubkeys) {
|
const store = writable(spaceBannedPubkeyItems.get(url) || [])
|
||||||
members.add(pubkey)
|
|
||||||
}
|
manageRelay(url, {method: ManagementMethod.ListBannedPubkeys, params: []}).then(res => {
|
||||||
|
spaceBannedPubkeyItems.set(url, res.result)
|
||||||
|
store.set(res.result)
|
||||||
|
})
|
||||||
|
|
||||||
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.kind === RELAY_REMOVE_MEMBER) {
|
export const deriveRoomAdmins = (url: string, h: string) => {
|
||||||
for (const pubkey of pubkeys) {
|
const filters: Filter[] = [{kinds: [ROOM_ADMINS], "#d": [h]}]
|
||||||
members.delete(pubkey)
|
|
||||||
}
|
return derived(deriveEventsForUrl(url, filters), $events => {
|
||||||
}
|
const adminsEvent = first($events)
|
||||||
|
|
||||||
|
if (adminsEvent) {
|
||||||
|
return getPubkeyTagValues(adminsEvent.tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(members)
|
return []
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
|
const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
|
||||||
@@ -882,53 +885,6 @@ 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[] = []
|
||||||
|
|
||||||
@@ -949,18 +905,33 @@ 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 = roomEvents.filter(spec({kind: ROOM_JOIN}))
|
const roomJoins: TrustedEvent[] = []
|
||||||
const roomLeaves = roomEvents.filter(spec({kind: ROOM_LEAVE}))
|
const roomLeaves: TrustedEvent[] = []
|
||||||
const roomMembershipEvents = roomEvents.filter(event =>
|
const roomMembershipEvents: TrustedEvent[] = []
|
||||||
[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())
|
Array.from(groupBy(e => e.pubkey, roomJoins).values()).map(events =>
|
||||||
.map(sortEventsDesc)
|
first(sortEventsDesc(events)),
|
||||||
.map(first),
|
),
|
||||||
).filter(({pubkey, created_at}) => {
|
).filter(({pubkey, created_at}) => {
|
||||||
if (roomMembers.has(pubkey)) return false
|
if (roomMembers.has(pubkey)) return false
|
||||||
if (
|
if (
|
||||||
@@ -1010,19 +981,49 @@ export const deriveUserIsSpaceAdmin = memoize((url?: string) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const deriveUserSpaceMembershipStatus = (url: string) => {
|
export const deriveUserSpaceMembershipStatus = (url: string) => {
|
||||||
const filters: Filter[] = [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]
|
// Fetch member list and user add/remove events directly in this derivation.
|
||||||
|
const memberListFilters: Filter[] = [{kinds: [RELAY_MEMBERS]}]
|
||||||
|
const userEventFilters: Filter[] = [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]}]
|
||||||
|
|
||||||
return derived(
|
return derived(
|
||||||
[
|
[
|
||||||
pubkey,
|
pubkey,
|
||||||
deriveSpaceMembers(url),
|
deriveRelaySignedEvents(url, memberListFilters),
|
||||||
deriveEventsForUrl(url, filters),
|
deriveRelaySignedEvents(url, userEventFilters),
|
||||||
|
deriveEventsForUrl(url, [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]),
|
||||||
deriveUserIsSpaceAdmin(url),
|
deriveUserIsSpaceAdmin(url),
|
||||||
],
|
],
|
||||||
([$pubkey, $members, $events, $isAdmin]) => {
|
([$pubkey, $memberListEvents, $userAddRemoveEvents, $joinLeaveEvents, $isAdmin]) => {
|
||||||
const isMember = $members.includes($pubkey!) || $isAdmin
|
// If admin, always granted.
|
||||||
|
if ($isAdmin) {
|
||||||
|
return MembershipStatus.Granted
|
||||||
|
}
|
||||||
|
|
||||||
for (const event of $events) {
|
const membersEvent = $memberListEvents.find(spec({kind: RELAY_MEMBERS}))
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -1048,19 +1049,46 @@ export const deriveUserIsRoomAdmin = (url: string, h: string) =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const deriveUserRoomMembershipStatus = (url: string, h: string) => {
|
export const deriveUserRoomMembershipStatus = (url: string, h: string) => {
|
||||||
const filters: Filter[] = [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]
|
// Fetch the room member list and the current user's add/remove events.
|
||||||
|
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, filters),
|
deriveEventsForUrl(url, userEventFilters),
|
||||||
|
deriveEventsForUrl(url, joinLeaveFilters),
|
||||||
deriveUserIsRoomAdmin(url, h),
|
deriveUserIsRoomAdmin(url, h),
|
||||||
],
|
],
|
||||||
([$pubkey, $members, $events, $isAdmin]) => {
|
([$pubkey, $memberList, $userAddRemoveEvents, $joinLeaveEvents, $isAdmin]) => {
|
||||||
const isMember = $members.includes($pubkey!) || $isAdmin
|
// If admin of this room's space, always granted.
|
||||||
|
if ($isAdmin) {
|
||||||
|
return MembershipStatus.Granted
|
||||||
|
}
|
||||||
|
|
||||||
for (const event of $events) {
|
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 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
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-63
@@ -1,7 +1,6 @@
|
|||||||
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, ifLet, assoc, chunk, WEEK, ago} from "@welshman/lib"
|
import {last, call, 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 +18,10 @@ import {
|
|||||||
RELAY_MEMBERS,
|
RELAY_MEMBERS,
|
||||||
RELAY_ADD_MEMBER,
|
RELAY_ADD_MEMBER,
|
||||||
RELAY_REMOVE_MEMBER,
|
RELAY_REMOVE_MEMBER,
|
||||||
|
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"
|
||||||
@@ -43,7 +43,6 @@ import {
|
|||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
REACTION_KINDS,
|
REACTION_KINDS,
|
||||||
MESSAGE_KINDS,
|
|
||||||
CONTENT_KINDS,
|
CONTENT_KINDS,
|
||||||
INDEXER_RELAYS,
|
INDEXER_RELAYS,
|
||||||
loadSettings,
|
loadSettings,
|
||||||
@@ -268,58 +267,21 @@ const syncUserData = () => {
|
|||||||
|
|
||||||
// Spaces
|
// Spaces
|
||||||
|
|
||||||
const syncSpace = (url: string, rooms: string[]) => {
|
const syncSpace = (url: 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_KINDS, 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 roomMemberKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]
|
const roomDeleteKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE]
|
||||||
|
|
||||||
pullAndListen({
|
pullAndListen({
|
||||||
url,
|
url,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
filters: [
|
filters: [
|
||||||
{kinds: [...relayKinds, ...roomMetaKinds, ...roomMemberKinds, ...MESSAGE_KINDS]},
|
{kinds: [...relayKinds, ...roomMetaKinds, ...roomDeleteKinds, ...CONTENT_KINDS, MESSAGE]},
|
||||||
makeCommentFilter(CONTENT_KINDS, {since}),
|
makeCommentFilter(CONTENT_KINDS, {since}),
|
||||||
{kinds: [PollResponse], since},
|
{kinds: [...REACTION_KINDS, POLL_RESPONSE], 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()
|
||||||
@@ -328,7 +290,6 @@ const syncSpace = (url: string, rooms: 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))
|
||||||
@@ -342,28 +303,15 @@ 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 or restart syncing for each space
|
// Start syncing for new spaces
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
const rooms = getSpaceRoomsFromGroupList(url, $userGroupList)
|
if (!unsubscribersByUrl.has(url)) {
|
||||||
|
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))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
+68
-4
@@ -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} from "@welshman/util"
|
import type {PublishedProfile, RoomMeta} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
createSearch,
|
createSearch,
|
||||||
profiles,
|
profiles,
|
||||||
@@ -14,12 +14,27 @@ import {
|
|||||||
getWotGraph,
|
getWotGraph,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import type {FileAttributes} from "@welshman/editor"
|
import type {FileAttributes} from "@welshman/editor"
|
||||||
import {Editor, MentionSuggestion, WelshmanExtension, editorProps} from "@welshman/editor"
|
import {
|
||||||
|
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 {deriveSpaceMembers} from "@app/core/state"
|
import {
|
||||||
|
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 ({
|
||||||
@@ -64,7 +79,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
|
||||||
},
|
},
|
||||||
@@ -82,11 +97,36 @@ 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: {
|
||||||
@@ -128,6 +168,29 @@ 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
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -137,6 +200,7 @@ export const makeEditor = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
NativeClipboardPasteExtension,
|
||||||
],
|
],
|
||||||
onUpdate({editor}) {
|
onUpdate({editor}) {
|
||||||
wordCount?.set(editor.storage.wordCount.words)
|
wordCount?.set(editor.storage.wordCount.words)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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"
|
||||||
@@ -13,9 +14,16 @@ 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-1
@@ -1,4 +1,4 @@
|
|||||||
import {writable} from "svelte/store"
|
import {get, 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,6 +11,22 @@ 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("")
|
||||||
@@ -54,6 +70,22 @@ 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()
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import {derived} from "svelte/store"
|
import {derived, get, writable} 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"
|
||||||
import {assoc, prop, first, identity, groupBy, now} from "@welshman/lib"
|
import {assoc, prop, first, identity, groupBy, now} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {deriveEventsByIdByUrl} from "@welshman/store"
|
import {deriveEventsByIdByUrl} from "@welshman/store"
|
||||||
import {sortEventsDesc, getTagValue} 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 {
|
||||||
MESSAGE_KINDS,
|
CONTENT_KINDS,
|
||||||
notificationSettings,
|
notificationSettings,
|
||||||
chatsById,
|
chatsById,
|
||||||
userGroupList,
|
userGroupList,
|
||||||
@@ -36,6 +36,9 @@ 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 = ""
|
||||||
|
|
||||||
@@ -57,8 +60,11 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +91,7 @@ export const allNotifications = derived(
|
|||||||
deriveEventsByIdByUrl({
|
deriveEventsByIdByUrl({
|
||||||
tracker,
|
tracker,
|
||||||
repository,
|
repository,
|
||||||
filters: [{kinds: MESSAGE_KINDS}, makeCommentFilter(MESSAGE_KINDS)],
|
filters: [{kinds: [MESSAGE, ...CONTENT_KINDS]}, makeCommentFilter(CONTENT_KINDS)],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
identity,
|
identity,
|
||||||
@@ -151,9 +157,17 @@ export const allNotifications = derived(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export const notifications = derived([page, allNotifications], ([$page, $allNotifications]) => {
|
export const notifications = derived(
|
||||||
return new Set([...$allNotifications].filter(p => !$page.url.pathname.startsWith(p)))
|
[page, allNotifications, deferredRoomPath],
|
||||||
})
|
([$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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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
|
||||||
@@ -17,13 +17,13 @@ import {
|
|||||||
getRelaysFromList,
|
getRelaysFromList,
|
||||||
getTagValue,
|
getTagValue,
|
||||||
matchFilters,
|
matchFilters,
|
||||||
|
MESSAGE,
|
||||||
type Filter,
|
type Filter,
|
||||||
type TrustedEvent,
|
type TrustedEvent,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
DM_KINDS,
|
DM_KINDS,
|
||||||
CONTENT_KINDS,
|
CONTENT_KINDS,
|
||||||
MESSAGE_KINDS,
|
|
||||||
notificationSettings,
|
notificationSettings,
|
||||||
pushState,
|
pushState,
|
||||||
shouldNotify,
|
shouldNotify,
|
||||||
@@ -45,7 +45,10 @@ export type PushPermissionResult = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const onNotification = call(() => {
|
export const onNotification = call(() => {
|
||||||
const allFilters = [{kinds: [...MESSAGE_KINDS, ...DM_KINDS]}, makeCommentFilter(MESSAGE_KINDS)]
|
const allFilters = [
|
||||||
|
{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>[] = []
|
||||||
|
|
||||||
@@ -158,7 +161,7 @@ export const syncRelaySubscriptions = (
|
|||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
]).subscribe(
|
]).subscribe(
|
||||||
throttle(3000, ([$userSpaceUrls, {spaces, mentions}, {alerts}]) => {
|
throttle(3000, ([$userSpaceUrls, {spaces, mentions}, {alerts}]) => {
|
||||||
const baseFilters = [{kinds: MESSAGE_KINDS}, makeCommentFilter(CONTENT_KINDS)]
|
const baseFilters = [{kinds: [MESSAGE, ...CONTENT_KINDS]}, makeCommentFilter(CONTENT_KINDS)]
|
||||||
|
|
||||||
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,7 +6,6 @@ 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 {
|
||||||
@@ -16,6 +15,7 @@ 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)
|
goto(prevPath, {replaceState: true})
|
||||||
} else if (window.matchMedia(`(min-width: ${theme.screens.md})`).matches) {
|
} else if (window.matchMedia(`(min-width: ${theme.screens.md})`).matches) {
|
||||||
goto(makeSpacePath(url, "recent"))
|
goto(makeSpacePath(url, "recent"), {replaceState: true})
|
||||||
} else {
|
} else {
|
||||||
goto(makeSpacePath(url))
|
goto(makeSpacePath(url), {replaceState: true})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-4
@@ -48,6 +48,18 @@ 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],
|
||||||
@@ -199,14 +211,15 @@ const loadCriticalRelays = async () => {
|
|||||||
relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
|
relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncRelays = () => onRelay(batch(1000, db.table<RelayProfile>("relays").bulkPut))
|
const syncRelays = () =>
|
||||||
|
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(1000, table.bulkPut))
|
return onRelayStats(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const initHandles = async () => {
|
const initHandles = async () => {
|
||||||
@@ -214,7 +227,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(1000, table.bulkPut))
|
return onHandle(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const initZappers = async () => {
|
const initZappers = async () => {
|
||||||
@@ -222,7 +235,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(3000, table.bulkPut))
|
return onZapper(batch(FLUSH_INTERVAL, idleWrite(table.bulkPut)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const initPlaintext = async () => {
|
const initPlaintext = async () => {
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 400 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 970 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user