Compare commits
78 Commits
81ab03311a
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 1d5f91fb6c | |||
| ef18655776 | |||
| b786e858d9 | |||
| f4ebc4e99e |
@@ -4,7 +4,6 @@ ios
|
||||
build
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# 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_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
|
||||
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
||||
VITE_THUMBNAIL_URL=
|
||||
VITE_THUMBNAIL_URL=https://vthumbs.coracle.social
|
||||
VITE_GLITCHTIP_API_KEY=
|
||||
GLITCHTIP_AUTH_TOKEN=
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
src/assets
|
||||
.claude
|
||||
target
|
||||
build
|
||||
.idea
|
||||
@@ -13,4 +14,4 @@ ios/App/Pods/
|
||||
android/capacitor-cordova-android-plugins
|
||||
android/app/src/androidTest
|
||||
android/app/src/test
|
||||
|
||||
node_modules
|
||||
|
||||
@@ -5,8 +5,8 @@ on:
|
||||
branches: [master]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: coracle-social/flotilla
|
||||
REGISTRY: gitea.coracle.social
|
||||
IMAGE_NAME: coracle/flotilla
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
@@ -23,8 +23,8 @@ jobs:
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
username: hodlbod
|
||||
password: ${{ secrets.PACKAGE_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
|
||||
@@ -70,6 +70,7 @@ GoogleService-Info.plist
|
||||
.roo
|
||||
.idea/
|
||||
.vscode/
|
||||
.claude/
|
||||
|
||||
# OS generated
|
||||
.DS_Store
|
||||
|
||||
@@ -1,5 +1,62 @@
|
||||
# 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
|
||||
|
||||
* Fix race condition in nip 46
|
||||
|
||||
+15
-20
@@ -1,32 +1,27 @@
|
||||
# Stage 1: Build
|
||||
# Uses .env from build context for config (logo, branding, etc.)
|
||||
# Optional: docker build --build-arg VITE_BUILD_HASH=$(git rev-parse --short HEAD) -t flotilla .
|
||||
# Build and run the Flotilla web server.
|
||||
#
|
||||
# docker build -t flotilla .
|
||||
# docker run -p 3000:3000 flotilla
|
||||
#
|
||||
# Pass --build-arg VITE_BUILD_HASH=$(git rev-parse --short HEAD) to stamp the build.
|
||||
# A .env in the build context is picked up by build.sh for branding config.
|
||||
|
||||
FROM node:20-bookworm AS builder
|
||||
FROM node:22-bookworm
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl
|
||||
|
||||
RUN npm install -g pnpm@latest
|
||||
RUN npm install -g pnpm@10.33.0
|
||||
|
||||
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}
|
||||
RUN pnpm i --frozen-lockfile
|
||||
|
||||
ENV NODE_OPTIONS=--max_old_space_size=16384
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pnpm run build
|
||||
|
||||
FROM node:20-alpine
|
||||
EXPOSE 3000
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy only the built output - no source, no .env, no dev deps
|
||||
COPY --from=builder /app/build ./build
|
||||
|
||||
CMD ["npx", "serve", "-s", "build"]
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
@@ -31,18 +31,18 @@ To run your own Flotilla, it's as simple as:
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm run build
|
||||
npx serve -s build
|
||||
pnpm run start
|
||||
```
|
||||
|
||||
Or, if you prefer to use a container:
|
||||
|
||||
```sh
|
||||
podman run -d -p 3000:3000 ghcr.io/coracle-social/flotilla:latest
|
||||
docker run -d -p 3000:3000 gitea.coracle.social/coracle/flotilla:latest
|
||||
```
|
||||
|
||||
Alternatively, you can copy the build files into a directory of your choice and serve it yourself:
|
||||
|
||||
```sh
|
||||
mkdir ./mount
|
||||
podman run -v ./mount:/app/mount ghcr.io/coracle-social/flotilla:latest bash -c 'cp -r build/* mount'
|
||||
docker run -v ./mount:/app/mount gitea.coracle.social/coracle/flotilla:latest bash -c 'cp -r build/* mount'
|
||||
```
|
||||
|
||||
@@ -8,8 +8,8 @@ android {
|
||||
applicationId "social.flotilla"
|
||||
minSdk rootProject.ext.minSdkVersion
|
||||
targetSdk rootProject.ext.targetSdkVersion
|
||||
versionCode 44
|
||||
versionName "1.7.2"
|
||||
versionCode 47
|
||||
versionName "1.8.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -12,10 +12,12 @@ dependencies {
|
||||
implementation project(':aparajita-capacitor-secure-storage')
|
||||
implementation project(':capacitor-community-safe-area')
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-clipboard')
|
||||
implementation project(':capacitor-filesystem')
|
||||
implementation project(':capacitor-keyboard')
|
||||
implementation project(':capacitor-preferences')
|
||||
implementation project(':capacitor-push-notifications')
|
||||
implementation project(':capacitor-share')
|
||||
implementation project(':capawesome-capacitor-android-dark-mode-support')
|
||||
implementation project(':capawesome-capacitor-badge')
|
||||
implementation project(':nostr-signer-capacitor-plugin')
|
||||
|
||||
@@ -44,4 +44,7 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
|
||||
</manifest>
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.OutOfQuotaPolicy
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import com.getcapacitor.JSObject
|
||||
@@ -76,6 +77,7 @@ class AndroidPushFallbackPlugin : Plugin() {
|
||||
|
||||
val immediate = OneTimeWorkRequest.Builder(AndroidPushFallbackWorker::class.java)
|
||||
.setConstraints(constraints)
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
|
||||
+9
-10
@@ -43,7 +43,7 @@ class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Wo
|
||||
private const val TAG = "PushFallback"
|
||||
private const val CHANNEL_ID = "flotilla_fallback"
|
||||
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 KIND_RELAY_AUTH = 22242
|
||||
private const val KIND_NIP46_RPC = 24133
|
||||
@@ -72,6 +72,8 @@ class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Wo
|
||||
}
|
||||
|
||||
override fun doWork(): Result {
|
||||
Log.i(TAG, "doWork() started")
|
||||
|
||||
if (isAppInForeground()) {
|
||||
return Result.success()
|
||||
}
|
||||
@@ -88,7 +90,7 @@ class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Wo
|
||||
|
||||
val activeSince = state.optLong("activeSince", 0L)
|
||||
val seen = mutableSetOf<String>()
|
||||
var latestPair: Pair<String, JSONObject>? = null
|
||||
val newEvents = mutableListOf<Pair<String, JSONObject>>()
|
||||
|
||||
for (sub in subscriptions) {
|
||||
val cursorKey = CURSOR_PREFIX + sub.relay + ":" + sub.key
|
||||
@@ -102,23 +104,19 @@ class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Wo
|
||||
for (event in result.events) {
|
||||
val id = event.optString("id", "")
|
||||
if (id.isNotEmpty() && seen.add(id)) {
|
||||
val createdAt = event.optLong("created_at", 0L)
|
||||
if (latestPair == null || createdAt > latestPair!!.second.optLong("created_at", 0L)) {
|
||||
latestPair = Pair(sub.relay, event)
|
||||
}
|
||||
newEvents.add(Pair(sub.relay, event))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (latestPair != null) {
|
||||
val (relay, event) = latestPair!!
|
||||
for ((relay, event) in newEvents) {
|
||||
postNotification(relay, event)
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Worker failed", e)
|
||||
return Result.success()
|
||||
return Result.retry()
|
||||
} finally {
|
||||
pool.closeAll()
|
||||
client.dispatcher.executorService.shutdown()
|
||||
@@ -214,7 +212,8 @@ class AndroidPushFallbackWorker(context: Context, params: WorkerParameters) : Wo
|
||||
.setContentIntent(pendingIntent)
|
||||
.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 {
|
||||
|
||||
@@ -11,6 +11,9 @@ project(':capacitor-community-safe-area').projectDir = new File('../node_modules
|
||||
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')
|
||||
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
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;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 48;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -131,8 +131,9 @@
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 920;
|
||||
LastUpgradeCheck = 920;
|
||||
LastUpgradeCheck = 2630;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
@@ -257,6 +258,7 @@
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -264,8 +266,10 @@
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
@@ -275,8 +279,10 @@
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
@@ -295,6 +301,7 @@
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
@@ -314,6 +321,7 @@
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -321,8 +329,10 @@
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
@@ -332,8 +342,10 @@
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
@@ -345,7 +357,9 @@
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
@@ -358,14 +372,16 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.7.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.8.0;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -385,14 +401,16 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 35;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.7.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.8.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Flotilla uses the camera when you enable it in a voice room.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Flotilla uses the microphone for voice chat in rooms.</string>
|
||||
<string>Flotilla uses the microphone when you enable it in a voice room.</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
|
||||
@@ -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 '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 '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 '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 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications'
|
||||
pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/share'
|
||||
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge'
|
||||
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin'
|
||||
end
|
||||
|
||||
+22
-13
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "flotilla",
|
||||
"version": "1.7.2",
|
||||
"version": "1.8.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "./build.sh",
|
||||
"start": "node server.js",
|
||||
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build",
|
||||
@@ -48,41 +49,48 @@
|
||||
"@capacitor/android": "^8.0.1",
|
||||
"@capacitor/app": "^8.0.0",
|
||||
"@capacitor/cli": "^8.0.1",
|
||||
"@capacitor/clipboard": "^8.0.1",
|
||||
"@capacitor/core": "^8.0.1",
|
||||
"@capacitor/filesystem": "^8.1.0",
|
||||
"@capacitor/ios": "^8.0.1",
|
||||
"@capacitor/keyboard": "^8.0.0",
|
||||
"@capacitor/preferences": "^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-badge": "^8.0.0",
|
||||
"@getalby/lightning-tools": "^6.1.0",
|
||||
"@getalby/sdk": "^5.1.2",
|
||||
"@hono/node-server": "^2.0.0",
|
||||
"@noble/curves": "^1.9.7",
|
||||
"@pomade/core": "^0.2.2",
|
||||
"@pomade/core": "^0.2.3",
|
||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@tiptap/core": "^2.27.2",
|
||||
"@tiptap/pm": "^2.27.2",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"@vite-pwa/sveltekit": "^0.6.8",
|
||||
"@welshman/app": "^0.8.12",
|
||||
"@welshman/content": "^0.8.12",
|
||||
"@welshman/editor": "^0.8.12",
|
||||
"@welshman/feeds": "^0.8.12",
|
||||
"@welshman/lib": "^0.8.12",
|
||||
"@welshman/net": "^0.8.12",
|
||||
"@welshman/router": "^0.8.12",
|
||||
"@welshman/signer": "^0.8.12",
|
||||
"@welshman/store": "^0.8.12",
|
||||
"@welshman/util": "^0.8.12",
|
||||
"@welshman/app": "^0.8.15",
|
||||
"@welshman/content": "^0.8.15",
|
||||
"@welshman/editor": "^0.8.15",
|
||||
"@welshman/feeds": "^0.8.15",
|
||||
"@welshman/lib": "^0.8.15",
|
||||
"@welshman/net": "^0.8.15",
|
||||
"@welshman/router": "^0.8.15",
|
||||
"@welshman/signer": "^0.8.15",
|
||||
"@welshman/store": "^0.8.15",
|
||||
"@welshman/util": "^0.8.15",
|
||||
"cheerio": "^1.2.0",
|
||||
"compressorjs-next": "^1.1.2",
|
||||
"daisyui": "^5.5.19",
|
||||
"date-picker-svelte": "^2.17.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"emoji-picker-element": "^1.28.1",
|
||||
"emoji-picker-element-data": "^1.8.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"hono": "^4.12.15",
|
||||
"husky": "^9.1.7",
|
||||
"idb": "^8.0.3",
|
||||
"livekit-client": "^2.17.2",
|
||||
@@ -105,5 +113,6 @@
|
||||
"overrides": {
|
||||
"sharp": "0.35.0-rc.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
|
||||
}
|
||||
|
||||
Generated
+290
-114
@@ -26,6 +26,9 @@ importers:
|
||||
'@capacitor/cli':
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1
|
||||
'@capacitor/clipboard':
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1(@capacitor/core@8.0.1)
|
||||
'@capacitor/core':
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1
|
||||
@@ -44,6 +47,9 @@ importers:
|
||||
'@capacitor/push-notifications':
|
||||
specifier: ^8.0.0
|
||||
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':
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(@capacitor/core@8.0.1)
|
||||
@@ -56,12 +62,15 @@ importers:
|
||||
'@getalby/sdk':
|
||||
specifier: ^5.1.2
|
||||
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':
|
||||
specifier: ^1.9.7
|
||||
version: 1.9.7
|
||||
'@pomade/core':
|
||||
specifier: ^0.2.2
|
||||
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))
|
||||
specifier: ^0.2.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':
|
||||
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))
|
||||
@@ -71,6 +80,9 @@ importers:
|
||||
'@tiptap/core':
|
||||
specifier: ^2.27.2
|
||||
version: 2.27.2(@tiptap/pm@2.27.2)
|
||||
'@tiptap/pm':
|
||||
specifier: ^2.27.2
|
||||
version: 2.27.2
|
||||
'@types/qrcode':
|
||||
specifier: ^1.5.6
|
||||
version: 1.5.6
|
||||
@@ -84,35 +96,38 @@ importers:
|
||||
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))
|
||||
'@welshman/app':
|
||||
specifier: ^0.8.12
|
||||
version: 0.8.12(3074ef6691f94dc03952d8dbc98013a7)
|
||||
specifier: ^0.8.15
|
||||
version: 0.8.15(ff026297546a8274624eb18a0ea86191)
|
||||
'@welshman/content':
|
||||
specifier: ^0.8.12
|
||||
version: 0.8.12(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
specifier: ^0.8.15
|
||||
version: 0.8.15(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/editor':
|
||||
specifier: ^0.8.12
|
||||
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))
|
||||
specifier: ^0.8.15
|
||||
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':
|
||||
specifier: ^0.8.12
|
||||
version: 0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)
|
||||
specifier: ^0.8.15
|
||||
version: 0.8.15(6e55dcd4e7516745e7b0228620d35545)
|
||||
'@welshman/lib':
|
||||
specifier: ^0.8.12
|
||||
version: 0.8.12
|
||||
specifier: ^0.8.15
|
||||
version: 0.8.15
|
||||
'@welshman/net':
|
||||
specifier: ^0.8.12
|
||||
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)
|
||||
specifier: ^0.8.15
|
||||
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':
|
||||
specifier: ^0.8.12
|
||||
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)))
|
||||
specifier: ^0.8.15
|
||||
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':
|
||||
specifier: ^0.8.12
|
||||
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))
|
||||
specifier: ^0.8.15
|
||||
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':
|
||||
specifier: ^0.8.12
|
||||
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)
|
||||
specifier: ^0.8.15
|
||||
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':
|
||||
specifier: ^0.8.12
|
||||
version: 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
specifier: ^0.8.15
|
||||
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:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2
|
||||
@@ -128,9 +143,15 @@ importers:
|
||||
emoji-picker-element:
|
||||
specifier: ^1.28.1
|
||||
version: 1.28.1
|
||||
emoji-picker-element-data:
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
fuse.js:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
hono:
|
||||
specifier: ^4.12.15
|
||||
version: 4.12.15
|
||||
husky:
|
||||
specifier: ^9.1.7
|
||||
version: 9.1.7
|
||||
@@ -791,6 +812,11 @@ packages:
|
||||
engines: {node: '>=22.0.0'}
|
||||
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':
|
||||
resolution: {integrity: sha512-5UqSWxGMp/B8KhYu7rAijqNtYslhcLh+TrbfU48PfdMDsPfaU/VY48sMNzC22xL8BmoFoql/3SKyP+pavTOvOA==}
|
||||
|
||||
@@ -827,6 +853,11 @@ packages:
|
||||
peerDependencies:
|
||||
'@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':
|
||||
resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==}
|
||||
|
||||
@@ -1077,6 +1108,12 @@ packages:
|
||||
resolution: {integrity: sha512-yUF9LhuvdIFOwjV1aG0ryzfwDiGBFk/CRLkRvrrM9dsE38SUjKsf1FDga5jxsKMu80nWcPZR9TiGGASWedoYPA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@hono/node-server@2.0.0':
|
||||
resolution: {integrity: sha512-n3GfHwwCvHCkGmOwKfxUPOlbfzuO64Sbc5XC4NGPIXxkuOnJrdgExdRKmHfF924r914WRJPT397GdqLvdYTeyQ==}
|
||||
engines: {node: '>=20'}
|
||||
peerDependencies:
|
||||
hono: ^4
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@@ -1427,9 +1464,9 @@ packages:
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
'@pomade/core@0.2.2':
|
||||
resolution: {integrity: sha512-FoilLsO0gVjiKMW3LV63pmXU7x3gh8YVGVulyR6QJr4h47XrsBg8vPkZtKWr4+sH3sW31e2tNIPUb3ptiuhrMA==}
|
||||
version: 0.2.2
|
||||
'@pomade/core@0.2.3':
|
||||
resolution: {integrity: sha512-36+abWfMH1Mif9FjBO7xICCkGZE4IqQpy7Csxlauyt0bhYQ9GsB07LqK5Qm3GgEHNwF9rFXdSZawkM+E6BeIfg==}
|
||||
version: 0.2.3
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
'@frostr/bifrost': ^1.0.7
|
||||
@@ -2131,83 +2168,83 @@ packages:
|
||||
'@vite-pwa/assets-generator':
|
||||
optional: true
|
||||
|
||||
'@welshman/app@0.8.12':
|
||||
resolution: {integrity: sha512-kRp+AVzn4i3FvZmdlyMknFUAb/5SnUz9A/cFKkDqWHsd+N3PbNcL2ZOlV9v5NI77GtsDF2ez6PEQfsZxWvkS/g==}
|
||||
'@welshman/app@0.8.15':
|
||||
resolution: {integrity: sha512-GDo6w+UI/ldnh47c5IEDYWw8nbiyhnH4abJNy/q/jLBUwJ9SuiJ7GVVvhZ+t4XEo5NEMq+y4OLZs08+abf85MQ==}
|
||||
peerDependencies:
|
||||
'@pomade/core': ^0.2.1
|
||||
'@welshman/feeds': 0.8.12
|
||||
'@welshman/lib': 0.8.12
|
||||
'@welshman/net': 0.8.12
|
||||
'@welshman/router': 0.8.12
|
||||
'@welshman/signer': 0.8.12
|
||||
'@welshman/store': 0.8.12
|
||||
'@welshman/util': 0.8.12
|
||||
'@welshman/feeds': 0.8.15
|
||||
'@welshman/lib': 0.8.15
|
||||
'@welshman/net': 0.8.15
|
||||
'@welshman/router': 0.8.15
|
||||
'@welshman/signer': 0.8.15
|
||||
'@welshman/store': 0.8.15
|
||||
'@welshman/util': 0.8.15
|
||||
svelte: ^4.0.0 || ^5.0.0
|
||||
|
||||
'@welshman/content@0.8.12':
|
||||
resolution: {integrity: sha512-hviVTXdyGf04Xq7mGo/82fq6lnbyuUYOGPkf8pJqPkfGh0f3i9nKof6gkzPvjZeEYSczneI2GpLIxkaZ3w1/tw==}
|
||||
'@welshman/content@0.8.15':
|
||||
resolution: {integrity: sha512-5qe+6Es1r62HkVdeHJPsWkOpLjhdxBTtw3d4+Or1JXl8BgpUE2JV7e+5HQQqnPRVHt3nt14YPt0oirar5p1Fvg==}
|
||||
peerDependencies:
|
||||
nostr-tools: ^2.19.4
|
||||
|
||||
'@welshman/editor@0.8.12':
|
||||
resolution: {integrity: sha512-CEXszH6pfM1kZHU9WnB6Z97YlxEOtgao6R3hhnBL/kRXy2tUTLpmFWyMONg2vu8Uzxtwz665eZhucLsju60U6w==}
|
||||
'@welshman/editor@0.8.15':
|
||||
resolution: {integrity: sha512-lqTLQGf54yPioBn2KQsF7F5ExWM6Co31wgGaUAhCSeUGiTzUQgMEut4/N8VB1rFZ0wqU6zyPG5jgeuhFhRJWSw==}
|
||||
peerDependencies:
|
||||
'@welshman/lib': 0.8.12
|
||||
'@welshman/util': 0.8.12
|
||||
'@welshman/lib': 0.8.15
|
||||
'@welshman/util': 0.8.15
|
||||
nostr-editor: ^1.1.1
|
||||
nostr-tools: ^2.19.4
|
||||
|
||||
'@welshman/feeds@0.8.12':
|
||||
resolution: {integrity: sha512-Dp/063qdrbe096z6IpIneazsqscfzSwsg09ZNHB6c3OsVb6iLEXLe7mEpATGcRsZ8memuFNLVLIP51eVkBqPcQ==}
|
||||
'@welshman/feeds@0.8.15':
|
||||
resolution: {integrity: sha512-xIQDKdV6uLxOz5qJUbc/2HC6qnikgH1GPoHQwBpwKH7Lga6a7IGLOR6kvghUaPpulKcuF4MxG9gmvEHqgsQkJw==}
|
||||
peerDependencies:
|
||||
'@welshman/lib': 0.8.12
|
||||
'@welshman/net': 0.8.12
|
||||
'@welshman/router': 0.8.12
|
||||
'@welshman/signer': 0.8.12
|
||||
'@welshman/util': 0.8.12
|
||||
'@welshman/lib': 0.8.15
|
||||
'@welshman/net': 0.8.15
|
||||
'@welshman/router': 0.8.15
|
||||
'@welshman/signer': 0.8.15
|
||||
'@welshman/util': 0.8.15
|
||||
|
||||
'@welshman/lib@0.8.12':
|
||||
resolution: {integrity: sha512-7Y1GjAcABquWF47A1Jni5JdP+k0GH2yRmEbVhIU+0R0TubCwPAKS38J2LTvtuE9CJMX6hPS9IKEZS6qTOAaVuw==}
|
||||
'@welshman/lib@0.8.15':
|
||||
resolution: {integrity: sha512-d7o6WUSVYXOstpWTqOBDfkSyr3GOBm/UMbgFx3RXCxzib0cWm7z0w1oLWvy1N7fjHc/Jp65G2KRpT6//B9yAww==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
'@welshman/net@0.8.12':
|
||||
resolution: {integrity: sha512-Ba71jwb8BBwUfPPtWHKYLB0HqeDYK64oqwxfo5bYldtPfGYrlD+a7lHFSZvNyOhvyXygCaIMnaye1QxWAHP8ng==}
|
||||
'@welshman/net@0.8.15':
|
||||
resolution: {integrity: sha512-AeJ/Vy7T6ruf1mjzzEUdH+aX5JriQKBzRn1zWZ4l8VEgxwc4w2bVte9a6aPnNJWc7JZT8ws8z+wOi4ECb6NPNA==}
|
||||
peerDependencies:
|
||||
'@welshman/lib': 0.8.12
|
||||
'@welshman/util': 0.8.12
|
||||
'@welshman/lib': 0.8.15
|
||||
'@welshman/util': 0.8.15
|
||||
|
||||
'@welshman/router@0.8.12':
|
||||
resolution: {integrity: sha512-Rr7ryBNTvTvjoLsDRMKPuoNJbBv2MgyqN2338p4vVhPMK6MOGM3Nx1og0LpHDGiLlCFuPM8RpParpIWfNnWbKw==}
|
||||
'@welshman/router@0.8.15':
|
||||
resolution: {integrity: sha512-3lxcCYMaPX0gFaoM1GjBRvXr4UrnPA3o/mBII2Zm3gJeFuXN3XG+REwIN6QNhvTB7syTCTwx+dRdHgvqHl9N6g==}
|
||||
peerDependencies:
|
||||
'@welshman/lib': 0.8.12
|
||||
'@welshman/net': 0.8.12
|
||||
'@welshman/util': 0.8.12
|
||||
'@welshman/lib': 0.8.15
|
||||
'@welshman/net': 0.8.15
|
||||
'@welshman/util': 0.8.15
|
||||
|
||||
'@welshman/signer@0.8.12':
|
||||
resolution: {integrity: sha512-eO4mw2QOR2d2oCS4zgptkCgjC1s8X+1vNnXfWDbtlEwhk7PD4ySSCkpNVMeElq+uluLOugmKvgDc4gfnIH3p2A==}
|
||||
version: 0.8.12
|
||||
'@welshman/signer@0.8.15':
|
||||
resolution: {integrity: sha512-Y96XZtsCHz8h7NK28sSi3CX+8lGG6WhLyVNyhlEhfypAxxx8Zpfr4GlSPApvp4tvm1//YfDtXHIIZTPXbmnqvA==}
|
||||
version: 0.8.15
|
||||
peerDependencies:
|
||||
'@noble/curves': ^1.9.7
|
||||
'@noble/hashes': ^2.0.1
|
||||
'@welshman/lib': 0.8.12
|
||||
'@welshman/net': 0.8.12
|
||||
'@welshman/util': 0.8.12
|
||||
'@welshman/lib': 0.8.15
|
||||
'@welshman/net': 0.8.15
|
||||
'@welshman/util': 0.8.15
|
||||
nostr-signer-capacitor-plugin: '*'
|
||||
nostr-tools: ^2.19.4
|
||||
|
||||
'@welshman/store@0.8.12':
|
||||
resolution: {integrity: sha512-3IUzPRMMVF6Pcw3DaKkfJ4S6bYzVtk6Ze3FnaHs1xJIxkQj9GyBO9VreiovULPW0djsnrP5+1UEN4mKZsG3hXg==}
|
||||
'@welshman/store@0.8.15':
|
||||
resolution: {integrity: sha512-3rQVhAsQ1z5tcUzkJPkzVp3iBkMrUKVoBi07AYefqlhRoddhwB2pDBVhdZYoP2kl9wVPZlPV58vlD6BTo6TEwA==}
|
||||
peerDependencies:
|
||||
'@welshman/lib': 0.8.12
|
||||
'@welshman/net': 0.8.12
|
||||
'@welshman/util': 0.8.12
|
||||
'@welshman/lib': 0.8.15
|
||||
'@welshman/net': 0.8.15
|
||||
'@welshman/util': 0.8.15
|
||||
svelte: ^4.0.0 || ^5.0.0
|
||||
|
||||
'@welshman/util@0.8.12':
|
||||
resolution: {integrity: sha512-lgftFt2moXZdN5fuL0RoAnAARV0n0d2+Q56gt7KrBSevjoCbtJgBVX5idvxL5PCEfh81veovJtty6eHxrhQv5A==}
|
||||
'@welshman/util@0.8.15':
|
||||
resolution: {integrity: sha512-zeNWMyOtIpOqj9/hBAT8qWvnp5w/IyrcT7CmDKLkWt6NU6ZoZ3pF5duTwtOYZqcftYJaHXgohOt0RsHVPR3M7w==}
|
||||
peerDependencies:
|
||||
'@noble/curves': ^1.9.7
|
||||
'@welshman/lib': 0.8.12
|
||||
'@welshman/lib': 0.8.15
|
||||
nostr-tools: ^2.19.4
|
||||
|
||||
'@xml-tools/parser@1.0.11':
|
||||
@@ -2446,6 +2483,13 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==}
|
||||
|
||||
@@ -2805,12 +2849,18 @@ packages:
|
||||
resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-8c64IPish2PWoV9oYCo2pvuPHwIv+uK9bO0dfpPyMupDAvaWL9ZvYhWNTAR+2sx7BhfRjciImqP6CIUgNX+DMg==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -2822,6 +2872,14 @@ packages:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3215,6 +3273,10 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
hono@4.12.15:
|
||||
resolution: {integrity: sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
hosted-git-info@2.8.9:
|
||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||
|
||||
@@ -3222,6 +3284,9 @@ packages:
|
||||
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
htmlparser2@10.1.0:
|
||||
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
|
||||
|
||||
husky@9.1.7:
|
||||
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3230,6 +3295,10 @@ packages:
|
||||
ico-endec@0.1.6:
|
||||
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:
|
||||
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
|
||||
|
||||
@@ -3996,6 +4065,15 @@ packages:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -4437,6 +4515,9 @@ packages:
|
||||
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
sax@1.1.4:
|
||||
resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==}
|
||||
|
||||
@@ -4872,6 +4953,10 @@ packages:
|
||||
undici-types@7.16.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -4990,6 +5075,15 @@ packages:
|
||||
resolution: {integrity: sha512-5ZZY1+lGq8LEKuDlg9M2RPJHlH3R7OVwyHqMcUsLKCgd9Wvf+QrFTCItkXXYPmrJn8H6gRLXbSgxLLdexiqHxw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
@@ -5969,6 +6063,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@capacitor/clipboard@8.0.1(@capacitor/core@8.0.1)':
|
||||
dependencies:
|
||||
'@capacitor/core': 8.0.1
|
||||
|
||||
'@capacitor/core@8.0.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -6006,6 +6104,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@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': {}
|
||||
|
||||
'@capawesome/capacitor-android-dark-mode-support@8.0.0(@capacitor/core@8.0.1)':
|
||||
@@ -6201,6 +6303,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@hono/node-server@2.0.0(hono@4.12.15)':
|
||||
dependencies:
|
||||
hono: 4.12.15
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
@@ -6610,15 +6716,15 @@ snapshots:
|
||||
|
||||
'@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:
|
||||
'@frostr/bifrost': 1.0.7(typescript@5.9.3)
|
||||
'@noble/hashes': 2.0.1
|
||||
'@peculiar/x509': 1.14.3
|
||||
'@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))
|
||||
'@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))
|
||||
cbor-x: 1.6.0
|
||||
hash-wasm: 4.12.0
|
||||
nostr-tools: 2.20.0(typescript@5.9.3)
|
||||
@@ -7276,26 +7382,26 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@vite-pwa/assets-generator': 0.2.6
|
||||
|
||||
'@welshman/app@0.8.12(3074ef6691f94dc03952d8dbc98013a7)':
|
||||
'@welshman/app@0.8.15(ff026297546a8274624eb18a0ea86191)':
|
||||
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))
|
||||
'@welshman/feeds': 0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)
|
||||
'@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/router': 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))
|
||||
'@welshman/signer': 0.8.12(@noble/curves@1.9.7)(@noble/hashes@2.0.1)(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/be4bb90a1a15c8eec0934f2f66ce9e82ecc72d51(@capacitor/core@8.0.1))(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@welshman/store': 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(svelte@5.48.0)
|
||||
'@welshman/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(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.15(6e55dcd4e7516745e7b0228620d35545)
|
||||
'@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/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.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.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.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
|
||||
svelte: 5.48.0
|
||||
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:
|
||||
'@braintree/sanitize-url': 7.1.1
|
||||
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:
|
||||
'@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/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/util': 0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3))
|
||||
'@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)
|
||||
tippy.js: 6.3.7
|
||||
|
||||
'@welshman/feeds@0.8.12(d5b74f0c83250e052e0b96f7ff5804e8)':
|
||||
'@welshman/feeds@0.8.15(6e55dcd4e7516745e7b0228620d35545)':
|
||||
dependencies:
|
||||
'@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/router': 0.8.12(@welshman/lib@0.8.12)(@welshman/net@0.8.12(@welshman/lib@0.8.12)(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))(ws@8.18.3))(@welshman/util@0.8.12(@noble/curves@1.9.7)(@welshman/lib@0.8.12)(nostr-tools@2.20.0(typescript@5.9.3)))
|
||||
'@welshman/signer': 0.8.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))
|
||||
'@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/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.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))
|
||||
trava: 1.2.1
|
||||
|
||||
'@welshman/lib@0.8.12':
|
||||
'@welshman/lib@0.8.15':
|
||||
dependencies:
|
||||
'@scure/base': 1.2.6
|
||||
'@types/events': 3.0.3
|
||||
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:
|
||||
'@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))
|
||||
'@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))
|
||||
events: 3.3.0
|
||||
isomorphic-ws: 5.0.0(ws@8.18.3)
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@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/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))':
|
||||
dependencies:
|
||||
'@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))
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@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/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))':
|
||||
dependencies:
|
||||
'@noble/curves': 1.9.7
|
||||
'@types/ws': 8.18.1
|
||||
'@welshman/lib': 0.8.12
|
||||
'@welshman/lib': 0.8.15
|
||||
js-base64: 3.7.8
|
||||
nostr-tools: 2.20.0(typescript@5.9.3)
|
||||
nostr-wasm: 0.1.0
|
||||
@@ -7615,6 +7721,29 @@ snapshots:
|
||||
ansi-styles: 4.3.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:
|
||||
dependencies:
|
||||
regexp-to-ast: 0.5.0
|
||||
@@ -8001,10 +8130,17 @@ snapshots:
|
||||
dependencies:
|
||||
sax: 1.1.4
|
||||
|
||||
emoji-picker-element-data@1.8.0: {}
|
||||
|
||||
emoji-picker-element@1.28.1: {}
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -8014,6 +8150,10 @@ snapshots:
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
entities@6.0.1: {}
|
||||
|
||||
entities@7.0.1: {}
|
||||
|
||||
env-paths@2.2.1: {}
|
||||
|
||||
env-paths@3.0.0: {}
|
||||
@@ -8523,16 +8663,29 @@ snapshots:
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
hono@4.12.15: {}
|
||||
|
||||
hosted-git-info@2.8.9: {}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
dependencies:
|
||||
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: {}
|
||||
|
||||
ico-endec@0.1.6: {}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
idb@7.1.1: {}
|
||||
|
||||
idb@8.0.3: {}
|
||||
@@ -9239,6 +9392,19 @@ snapshots:
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
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@4.0.0: {}
|
||||
@@ -9677,6 +9843,8 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-regex: 1.2.1
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
sax@1.1.4: {}
|
||||
|
||||
sax@1.4.4: {}
|
||||
@@ -10202,6 +10370,8 @@ snapshots:
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
undici@7.25.0: {}
|
||||
|
||||
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
||||
|
||||
unicode-match-property-ecmascript@2.0.0:
|
||||
@@ -10282,6 +10452,12 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
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
-3
@@ -2,6 +2,16 @@
|
||||
|
||||
@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 {
|
||||
padding-top: var(--sait);
|
||||
}
|
||||
@@ -225,6 +235,7 @@
|
||||
|
||||
:root {
|
||||
font-family: Lato;
|
||||
text-size-adjust: 100%;
|
||||
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
|
||||
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
||||
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
|
||||
@@ -317,12 +328,12 @@
|
||||
|
||||
.note-editor .tiptap {
|
||||
--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 {
|
||||
--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 */
|
||||
@@ -406,16 +417,28 @@ progress[value]::-webkit-progress-value {
|
||||
@apply md:left-[calc(18.5rem+var(--sail))];
|
||||
}
|
||||
|
||||
.left-content-full {
|
||||
@apply md:left-[calc(3.5rem+var(--sail))];
|
||||
}
|
||||
|
||||
/* Keyboard open state adjustments */
|
||||
|
||||
body.keyboard-open {
|
||||
--saib: 0px;
|
||||
}
|
||||
|
||||
body.keyboard-open .hide-on-keyboard {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.keyboard-open .chat__compose {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* chat view */
|
||||
|
||||
.chat__compose {
|
||||
@apply z-compose relative mb-14 grow md:mb-0;
|
||||
@apply z-compose relative mb-14 shrink-0 md:mb-0;
|
||||
}
|
||||
|
||||
.chat__compose .chat__compose-inner {
|
||||
|
||||
+7
-4
@@ -2,15 +2,18 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{NAME}</title>
|
||||
<link rel="canonical" href="{URL}" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||
<meta name="theme-color" content="{ACCENT}" />
|
||||
<meta name="description" content="{DESCRIPTION}" />
|
||||
<meta name="og:url" content="{URL}" />
|
||||
<meta name="og:type" content="website" />
|
||||
<meta name="og:title" content="{NAME}" />
|
||||
<meta name="og:description" content="{DESCRIPTION}" />
|
||||
<meta property="og:url" content="{URL}" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="{NAME}" />
|
||||
<meta property="og:description" content="{DESCRIPTION}" />
|
||||
<meta property="og:image" content="" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="{URL}" />
|
||||
<meta name="twitter:title" content="{NAME}" />
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import {Room as LiveKitRoom} from "livekit-client"
|
||||
import {derived, writable} from "svelte/store"
|
||||
import {type Room} from "@app/core/state"
|
||||
|
||||
export type VoiceSession = {
|
||||
url: string
|
||||
h: string
|
||||
room: LiveKitRoom
|
||||
muted: boolean
|
||||
cameraOn: boolean
|
||||
screenShareOn: boolean
|
||||
}
|
||||
|
||||
export type Pubkey = string
|
||||
|
||||
export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
|
||||
|
||||
export enum VoiceState {
|
||||
Joining = "joining",
|
||||
Connected = "connected",
|
||||
Disconnected = "disconnected",
|
||||
}
|
||||
|
||||
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
|
||||
|
||||
export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
|
||||
|
||||
export const currentVoiceRoom = writable<Room | undefined>(undefined)
|
||||
|
||||
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
|
||||
|
||||
export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined =>
|
||||
/^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined
|
||||
|
||||
export const participantFromLiveKitIdentity = (identity: string): VoiceParticipant => {
|
||||
const pk = pubkeyFromLiveKitIdentity(identity)
|
||||
return pk ? {pubkey: pk, identity} : {identity}
|
||||
}
|
||||
|
||||
export const participantKey = (p: VoiceParticipant) => p.pubkey ?? p.identity
|
||||
|
||||
export const speakingParticipants = writable<VoiceParticipant[]>([])
|
||||
|
||||
export const isParticipantSpeaking = derived(
|
||||
speakingParticipants,
|
||||
$participants => (p: VoiceParticipant) =>
|
||||
$participants.some(sp => participantKey(sp) === participantKey(p)),
|
||||
)
|
||||
|
||||
export const isLocalSpeaking = derived(
|
||||
[currentVoiceSession, speakingParticipants],
|
||||
([$session, $speaking]) => {
|
||||
if (!$session?.room) return false
|
||||
const local = participantFromLiveKitIdentity($session.room.localParticipant.identity)
|
||||
return $speaking.some(sp => participantKey(sp) === participantKey(local))
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,99 @@
|
||||
import {Track} from "livekit-client"
|
||||
import {MediaQuery} from "svelte/reactivity"
|
||||
import {derived, get, writable} from "svelte/store"
|
||||
import {currentVoiceSession, VoiceState, type VoiceSession, voiceState} from "@app/call/stores"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
export enum VideoCallLayout {
|
||||
Chat = "chat",
|
||||
Video = "video",
|
||||
Split = "split",
|
||||
}
|
||||
|
||||
export const isDesktopLayout = new MediaQuery("min-width: 768px", false)
|
||||
|
||||
export enum ViewportSize {
|
||||
Desktop = "desktop",
|
||||
Mobile = "mobile",
|
||||
}
|
||||
|
||||
export const videoCallViewportSync = {
|
||||
previousLayout: undefined as ViewportSize | undefined,
|
||||
}
|
||||
|
||||
export const videoCallLayout = writable<VideoCallLayout>(VideoCallLayout.Split)
|
||||
|
||||
export const resetVideoCallLayout = () => {
|
||||
videoCallViewportSync.previousLayout = undefined
|
||||
videoCallLayout.set(VideoCallLayout.Chat)
|
||||
}
|
||||
|
||||
export const videoPrimaryTileKey = writable<string | undefined>(undefined)
|
||||
|
||||
export const toggleVideoPrimaryTile = (key: string) => {
|
||||
videoPrimaryTileKey.update(k => (k === key ? undefined : key))
|
||||
}
|
||||
|
||||
const VISUAL_SOURCES = [Track.Source.Camera, Track.Source.ScreenShare] as const
|
||||
|
||||
const countLiveVisualFeeds = (session: VoiceSession): number => {
|
||||
const room = session.room
|
||||
let n = 0
|
||||
const lp = room.localParticipant
|
||||
if (session.cameraOn) {
|
||||
const pub = lp.getTrackPublication(Track.Source.Camera)
|
||||
if (pub?.track) n += 1
|
||||
}
|
||||
if (session.screenShareOn) {
|
||||
const pub = lp.getTrackPublication(Track.Source.ScreenShare)
|
||||
if (pub?.track) n += 1
|
||||
}
|
||||
for (const rp of room.remoteParticipants.values()) {
|
||||
for (const source of VISUAL_SOURCES) {
|
||||
const pub = rp.getTrackPublication(source)
|
||||
if (pub?.isSubscribed && pub.track) n += 1
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
export const triggerVideoFeedCount = () => {
|
||||
currentVoiceSession.update(s => (s ? {...s} : s))
|
||||
}
|
||||
|
||||
export const videoTileCount = derived([currentVoiceSession, voiceState], ([$session, $state]) => {
|
||||
if ($state !== VoiceState.Connected || !$session) return 0
|
||||
return countLiveVisualFeeds($session)
|
||||
})
|
||||
|
||||
export const toggleCamera = async () => {
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session) return
|
||||
|
||||
const cameraOn = !session.cameraOn
|
||||
try {
|
||||
await session.room.localParticipant.setCameraEnabled(cameraOn)
|
||||
currentVoiceSession.set({...session, cameraOn})
|
||||
} catch {
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: cameraOn ? "Could not access camera" : "Could not turn off camera",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleScreenShare = async () => {
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session) return
|
||||
|
||||
const screenShareOn = !session.screenShareOn
|
||||
try {
|
||||
await session.room.localParticipant.setScreenShareEnabled(screenShareOn)
|
||||
currentVoiceSession.set({...session, screenShareOn})
|
||||
} catch {
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: screenShareOn ? "Could not start screen sharing" : "Could not stop screen sharing",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,35 @@
|
||||
*/
|
||||
import {
|
||||
DisconnectReason,
|
||||
LocalParticipant,
|
||||
LocalTrackPublication,
|
||||
Room as LiveKitRoom,
|
||||
RoomEvent,
|
||||
Track,
|
||||
supportsAudioOutputSelection,
|
||||
type AudioCaptureOptions,
|
||||
type LocalParticipant,
|
||||
} from "livekit-client"
|
||||
import {derived, get, writable} from "svelte/store"
|
||||
import {derived, get} from "svelte/store"
|
||||
import {map, removeUndefined, uniqBy} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
||||
import {signer} from "@welshman/app"
|
||||
import {getLivekitEndpoint} from "$lib/livekit"
|
||||
import {AbortError, whenAborted, whenTimeout} from "$lib/util"
|
||||
import {deriveLatestEventForUrl, deriveRoom, makeRoomId, type Room} from "@app/core/state"
|
||||
import {
|
||||
currentVoiceRoom,
|
||||
currentVoiceSession,
|
||||
participantFromLiveKitIdentity,
|
||||
participantKey,
|
||||
participantPubkeyMap,
|
||||
pubkeyFromLiveKitIdentity,
|
||||
speakingParticipants,
|
||||
VoiceState,
|
||||
type VoiceParticipant,
|
||||
voiceState,
|
||||
} from "@app/call/stores"
|
||||
import {resetVideoCallLayout, triggerVideoFeedCount, videoPrimaryTileKey} from "@app/call/video"
|
||||
import {deriveLatestEventForUrl, deriveRoom, makeRoomId} from "@app/core/state"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
export const LIVEKIT_PARTICIPANTS = 39004
|
||||
@@ -27,30 +41,12 @@ export {checkRelayHasLivekit} from "$lib/livekit"
|
||||
|
||||
export {supportsAudioOutputSelection}
|
||||
|
||||
export type VoiceSession = {
|
||||
url: string
|
||||
h: string
|
||||
room: LiveKitRoom
|
||||
muted: boolean
|
||||
}
|
||||
|
||||
export type Pubkey = string
|
||||
|
||||
export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
|
||||
|
||||
export enum VoiceState {
|
||||
Joining = "joining",
|
||||
Connected = "connected",
|
||||
Disconnected = "disconnected",
|
||||
}
|
||||
|
||||
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
|
||||
|
||||
const LIVEKIT_DEFAULT_DEVICE_ID = "default"
|
||||
|
||||
export enum DeviceKind {
|
||||
AudioInput = "audioinput",
|
||||
AudioOutput = "audiooutput",
|
||||
VideoInput = "videoinput",
|
||||
}
|
||||
|
||||
export const switchVoiceActiveDevice = async (
|
||||
@@ -71,17 +67,14 @@ export const switchVoiceActiveDevice = async (
|
||||
case DeviceKind.AudioOutput:
|
||||
label = "speaker"
|
||||
break
|
||||
case DeviceKind.VideoInput:
|
||||
label = "camera"
|
||||
break
|
||||
}
|
||||
pushToast({theme: "error", message: `Error changing ${label}`})
|
||||
}
|
||||
}
|
||||
|
||||
export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
|
||||
|
||||
export const currentVoiceRoom = writable<Room | undefined>(undefined)
|
||||
|
||||
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
|
||||
|
||||
const addParticipant = (identity: string) => {
|
||||
participantPubkeyMap.update(m => {
|
||||
const next = new Map(m)
|
||||
@@ -98,24 +91,6 @@ const deleteParticipant = (identity: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined =>
|
||||
/^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined
|
||||
|
||||
export const participantFromLiveKitIdentity = (identity: string): VoiceParticipant => {
|
||||
const pk = pubkeyFromLiveKitIdentity(identity)
|
||||
return pk ? {pubkey: pk, identity} : {identity}
|
||||
}
|
||||
|
||||
export const participantKey = (p: VoiceParticipant) => p.pubkey ?? p.identity
|
||||
|
||||
export const speakingParticipants = writable<VoiceParticipant[]>([])
|
||||
|
||||
export const isParticipantSpeaking = derived(
|
||||
speakingParticipants,
|
||||
$participants => (p: VoiceParticipant) =>
|
||||
$participants.some(sp => participantKey(sp) === participantKey(p)),
|
||||
)
|
||||
|
||||
const fetchLivekitToken = async (
|
||||
url: string,
|
||||
groupId: string,
|
||||
@@ -197,7 +172,9 @@ const setUpMicrophone = async (
|
||||
}
|
||||
|
||||
const onRoomDisconnected = (reason?: DisconnectReason) => {
|
||||
videoPrimaryTileKey.set(undefined)
|
||||
currentVoiceSession.set(undefined)
|
||||
resetVideoCallLayout()
|
||||
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
|
||||
voiceState.set(VoiceState.Disconnected)
|
||||
const message =
|
||||
@@ -216,11 +193,16 @@ const onTrackSubscribed = (track: Track) => {
|
||||
element.style.display = "none"
|
||||
document.body.appendChild(element)
|
||||
element.play().catch(() => {})
|
||||
} else if (track.kind === Track.Kind.Video) {
|
||||
triggerVideoFeedCount()
|
||||
}
|
||||
}
|
||||
|
||||
const onTrackUnsubscribed = (track: Track) => {
|
||||
track.detach().forEach(el => el.remove())
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
triggerVideoFeedCount()
|
||||
}
|
||||
}
|
||||
|
||||
const onActiveSpeakersChanged = (participants: {identity: string}[]) => {
|
||||
@@ -241,6 +223,17 @@ const onParticipantDisconnected = (participant: {identity: string}) => {
|
||||
deleteParticipant(participant.identity)
|
||||
}
|
||||
|
||||
const onLocalTrackUnpublished = (
|
||||
publication: LocalTrackPublication,
|
||||
participant: LocalParticipant,
|
||||
) => {
|
||||
if (publication.source !== Track.Source.ScreenShare) return
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session || participant.identity !== session.room.localParticipant.identity) return
|
||||
if (!session.screenShareOn) return
|
||||
currentVoiceSession.set({...session, screenShareOn: false})
|
||||
}
|
||||
|
||||
let joinAbortController: AbortController | undefined
|
||||
|
||||
export const cancelJoinVoiceRoom = () => {
|
||||
@@ -278,12 +271,13 @@ export const joinVoiceRoom = async (
|
||||
liveKitRoom.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected)
|
||||
liveKitRoom.on(RoomEvent.TrackSubscribed, onTrackSubscribed)
|
||||
liveKitRoom.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed)
|
||||
liveKitRoom.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished)
|
||||
liveKitRoom.on(RoomEvent.ActiveSpeakersChanged, onActiveSpeakersChanged)
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
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.",
|
||||
}),
|
||||
whenAborted(signal),
|
||||
@@ -301,7 +295,14 @@ export const joinVoiceRoom = async (
|
||||
|
||||
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
|
||||
|
||||
currentVoiceSession.set({url, h, room: liveKitRoom, muted})
|
||||
currentVoiceSession.set({
|
||||
url,
|
||||
h,
|
||||
room: liveKitRoom,
|
||||
muted,
|
||||
cameraOn: false,
|
||||
screenShareOn: false,
|
||||
})
|
||||
voiceState.set(VoiceState.Connected)
|
||||
playJoinSound()
|
||||
} catch (e) {
|
||||
@@ -320,8 +321,26 @@ export const leaveVoiceRoom = async () => {
|
||||
const audio = new Audio("/leave-voice-room.mp3")
|
||||
audio.play().catch(() => {})
|
||||
|
||||
if (session.cameraOn) {
|
||||
try {
|
||||
await session.room.localParticipant.setCameraEnabled(false)
|
||||
} catch {
|
||||
pushToast({theme: "error", message: "Error turning off camera."})
|
||||
}
|
||||
}
|
||||
|
||||
if (session.screenShareOn) {
|
||||
try {
|
||||
await session.room.localParticipant.setScreenShareEnabled(false)
|
||||
} catch {
|
||||
pushToast({theme: "error", message: "Error turning off screen sharing."})
|
||||
}
|
||||
}
|
||||
|
||||
voiceState.set(VoiceState.Disconnected)
|
||||
videoPrimaryTileKey.set(undefined)
|
||||
currentVoiceSession.set(undefined)
|
||||
resetVideoCallLayout()
|
||||
session.room.disconnect()
|
||||
speakingParticipants.set([])
|
||||
participantPubkeyMap.set(new Map())
|
||||
@@ -7,12 +7,13 @@
|
||||
type Props = {
|
||||
url: string
|
||||
h?: string
|
||||
shareToChat?: boolean
|
||||
}
|
||||
|
||||
const {url, h}: Props = $props()
|
||||
const {url, h, shareToChat = false}: Props = $props()
|
||||
</script>
|
||||
|
||||
<CalendarEventForm {url} {h}>
|
||||
<CalendarEventForm {url} {h} {shareToChat}>
|
||||
{#snippet header()}
|
||||
<ModalHeader>
|
||||
<ModalTitle>Create an Event</ModalTitle>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import {writable} from "svelte/store"
|
||||
import {randomId, HOUR} from "@welshman/lib"
|
||||
import {makeEvent, EVENT_TIME} from "@welshman/util"
|
||||
import {publishThunk} from "@welshman/app"
|
||||
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import {daysBetween} from "@lib/util"
|
||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||
@@ -22,7 +22,7 @@
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {DraftKey} from "@app/util/drafts"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {canEnforceNip70} from "@app/core/commands"
|
||||
import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
|
||||
|
||||
type Values = {
|
||||
d: string
|
||||
@@ -36,11 +36,12 @@
|
||||
type Props = {
|
||||
url: string
|
||||
h?: string
|
||||
shareToChat?: boolean
|
||||
header: Snippet
|
||||
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 ?? ""}`)
|
||||
|
||||
@@ -57,7 +58,7 @@
|
||||
const selectFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||
|
||||
const submit = async () => {
|
||||
if ($uploading) return
|
||||
if ($uploading || loading) return
|
||||
|
||||
if (!title) {
|
||||
return pushToast({
|
||||
@@ -92,22 +93,42 @@
|
||||
...ed.storage.nostr.getEditorTags(),
|
||||
]
|
||||
|
||||
if (await shouldProtect) {
|
||||
tags.push(PROTECTED)
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const protect = await shouldProtect
|
||||
|
||||
if (protect) {
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
|
||||
draftKey.clear()
|
||||
history.back()
|
||||
|
||||
if (shareToChat) {
|
||||
publishRoomQuote({url, h, parent: calendarThunk.event, protect})
|
||||
}
|
||||
|
||||
pushToast({message: "Your event has been saved!"})
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
const event = makeEvent(EVENT_TIME, {content, tags})
|
||||
|
||||
pushToast({message: "Your event has been saved!"})
|
||||
publishThunk({event, relays: [url]})
|
||||
draftKey.clear()
|
||||
history.back()
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
|
||||
const d = $state(initialValues?.d ?? randomId())
|
||||
let title = $state(initialValues?.title ?? "")
|
||||
let location = $state(initialValues?.location ?? "")
|
||||
@@ -158,7 +179,11 @@
|
||||
<div class="input-editor grow overflow-hidden">
|
||||
<EditorContent {editor} />
|
||||
</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}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
@@ -197,12 +222,12 @@
|
||||
</Field>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={$uploading}>
|
||||
<Spinner loading={$uploading}>Save Event</Spinner>
|
||||
<Button type="submit" class="btn btn-primary" disabled={$uploading || loading}>
|
||||
<Spinner loading={$uploading || loading}>Save Event</Spinner>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
||||
@@ -19,15 +19,17 @@
|
||||
const end = $derived(parseInt(meta.end))
|
||||
</script>
|
||||
|
||||
<div class="flex grow flex-wrap justify-between gap-2">
|
||||
<p class="text-xl">{meta.title || meta.name}</p>
|
||||
<div class="flex flex-col justify-between gap-1">
|
||||
<p class="text-lg">{meta.title || meta.name}</p>
|
||||
{#if !isNaN(start) && !isNaN(end)}
|
||||
{@const startDateDisplay = formatTimestampAsDate(start)}
|
||||
{@const endDateDisplay = formatTimestampAsDate(end)}
|
||||
{@const isSingleDay = startDateDisplay === endDateDisplay}
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Icon icon={ClockCircle} size={4} />
|
||||
<span class="hidden sm:block">{formatTimestampAsDate(start)}</span>
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={ClockCircle} size={4} />
|
||||
{formatTimestampAsDate(start)}
|
||||
</div>
|
||||
{formatTimestampAsTime(start)} — {isSingleDay
|
||||
? formatTimestampAsTime(end)
|
||||
: formatTimestamp(end)}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte"
|
||||
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import {userSettingsValues, deriveChat} from "@app/core/state"
|
||||
import {userSettingsValues, deriveChat, makeChatId} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {DraftKey} from "@app/util/drafts"
|
||||
import {makeDelete, prependParent} from "@app/core/commands"
|
||||
@@ -66,8 +66,9 @@
|
||||
|
||||
const {pubkeys, info}: Props = $props()
|
||||
|
||||
const chat = deriveChat(pubkeys)
|
||||
const draftKey = new DraftKey<{content?: string | object}>(`dm:${$chat?.id}`)
|
||||
const chatId = makeChatId(pubkeys)
|
||||
const chat = deriveChat(chatId)
|
||||
const draftKey = new DraftKey<{content?: string | object}>(`dm:${chatId}`)
|
||||
const others = remove($pubkey!, pubkeys)
|
||||
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
|
||||
|
||||
@@ -279,7 +280,7 @@
|
||||
</div>
|
||||
</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}
|
||||
<div class="py-12">
|
||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
type Props = {
|
||||
url: string
|
||||
h?: string
|
||||
shareToChat?: boolean
|
||||
}
|
||||
|
||||
const {url, h}: Props = $props()
|
||||
const {url, h, shareToChat = false}: Props = $props()
|
||||
</script>
|
||||
|
||||
<ClassifiedForm {url} {h}>
|
||||
<ClassifiedForm {url} {h} {shareToChat}>
|
||||
{#snippet header()}
|
||||
<ModalHeader>
|
||||
<ModalTitle>Create a Classified Listing</ModalTitle>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type {Snippet} from "svelte"
|
||||
import {removeUndefined, randomId, uniq} from "@welshman/lib"
|
||||
import {makeEvent, CLASSIFIED} from "@welshman/util"
|
||||
import {publishThunk} from "@welshman/app"
|
||||
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import {normalizeTopic} from "@lib/util"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
@@ -21,7 +21,7 @@
|
||||
import {PROTECTED} from "@app/core/state"
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {DraftKey} from "@app/util/drafts"
|
||||
import {canEnforceNip70, uploadFile} from "@app/core/commands"
|
||||
import {canEnforceNip70, publishRoomQuote, uploadFile} from "@app/core/commands"
|
||||
|
||||
type Values = {
|
||||
d: string
|
||||
@@ -37,11 +37,12 @@
|
||||
type Props = {
|
||||
url: string
|
||||
h?: string
|
||||
shareToChat?: boolean
|
||||
header: Snippet
|
||||
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 ?? ""}`)
|
||||
|
||||
@@ -87,7 +88,9 @@
|
||||
tags.push(["t", topic])
|
||||
}
|
||||
|
||||
if (await shouldProtect) {
|
||||
const protect = await shouldProtect
|
||||
|
||||
if (protect) {
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
@@ -114,13 +117,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
publishThunk({
|
||||
const classifiedThunk = publishThunk({
|
||||
relays: [url],
|
||||
event: makeEvent(CLASSIFIED, {content, tags}),
|
||||
})
|
||||
|
||||
const error = await waitForThunkError(classifiedThunk)
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
draftKey.clear()
|
||||
history.back()
|
||||
|
||||
if (shareToChat) {
|
||||
publishRoomQuote({url, h, parent: classifiedThunk.event, protect})
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
||||
@@ -22,15 +22,15 @@
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
truncate,
|
||||
renderAsHtml,
|
||||
isText,
|
||||
isEmail,
|
||||
isEmoji,
|
||||
isTopic,
|
||||
isCode,
|
||||
@@ -26,6 +27,7 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ContentToken from "@app/components/ContentToken.svelte"
|
||||
import ContentEmoji from "@app/components/ContentEmoji.svelte"
|
||||
import ContentEmail from "@app/components/ContentEmail.svelte"
|
||||
import ContentCode from "@app/components/ContentCode.svelte"
|
||||
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
|
||||
import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
|
||||
@@ -159,6 +161,8 @@
|
||||
<ContentTopic value={parsed.value} />
|
||||
{:else if isEmoji(parsed)}
|
||||
<ContentEmoji value={parsed.value} />
|
||||
{:else if isEmail(parsed)}
|
||||
<ContentEmail value={parsed.value} />
|
||||
{:else if isCode(parsed)}
|
||||
<ContentCode
|
||||
value={parsed.value}
|
||||
|
||||
@@ -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 Link from "@lib/components/Link.svelte"
|
||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||
import ContentLinkUrl from "@app/components/ContentLinkUrl.svelte"
|
||||
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {
|
||||
dufflepud,
|
||||
PLATFORM_URL,
|
||||
IMAGE_CONTENT_TYPES,
|
||||
PLATFORM_URL,
|
||||
VIDEO_CONTENT_TYPES,
|
||||
THUMBNAIL_URL,
|
||||
isRoomId,
|
||||
} from "@app/core/state"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
|
||||
const {value, event} = $props()
|
||||
|
||||
let hideImage = $state(false)
|
||||
|
||||
const url = value.url.toString()
|
||||
const fileType = getTagValue("file-type", event.tags) || ""
|
||||
const isRoomOrRelay = isRoomId(url) || isRelayUrl(url)
|
||||
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 fileType = getTagValue("file-type", event.tags) || ""
|
||||
|
||||
const getVideoPoster = (videoUrl: string): string | undefined => {
|
||||
if (Capacitor.getPlatform() === "android" && THUMBNAIL_URL) {
|
||||
return `${THUMBNAIL_URL}/thumbnail?url=${encodeURIComponent(videoUrl)}`
|
||||
@@ -54,46 +56,52 @@
|
||||
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
|
||||
</script>
|
||||
|
||||
<Link {external} {href} class="my-2 block">
|
||||
<div class="overflow-hidden rounded-box">
|
||||
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}
|
||||
<video
|
||||
controls
|
||||
src={url}
|
||||
poster={getVideoPoster(url)}
|
||||
preload="metadata"
|
||||
class="max-h-96 rounded-box object-contain object-center">
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
{:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
|
||||
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
|
||||
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
|
||||
</button>
|
||||
{:else}
|
||||
{#await loadPreview()}
|
||||
<div class="center my-12 w-full">
|
||||
<span class="loading loading-spinner"></span>
|
||||
</div>
|
||||
{:then preview}
|
||||
<div class="bg-alt flex max-w-xl flex-col leading-normal">
|
||||
{#if preview.image && !hideImage}
|
||||
<img
|
||||
alt=""
|
||||
onerror={onError}
|
||||
src={preview.image}
|
||||
class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2 p-4">
|
||||
<strong class="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>{preview.title || displayUrl(url)}</strong>
|
||||
<p>{ellipsize(preview.description, 140)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:catch}
|
||||
<p class="bg-alt p-12 text-center leading-normal">
|
||||
Unable to load a preview for {url}
|
||||
</p>
|
||||
{/await}
|
||||
{/if}
|
||||
{#if isRoomOrRelay}
|
||||
<div>
|
||||
<ContentLinkUrl {url} class="link-content whitespace-nowrap" />
|
||||
</div>
|
||||
</Link>
|
||||
{:else}
|
||||
<Link {external} {href} class="my-2 block">
|
||||
<div class="overflow-hidden rounded-box">
|
||||
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}
|
||||
<video
|
||||
controls
|
||||
src={url}
|
||||
poster={getVideoPoster(url)}
|
||||
preload="metadata"
|
||||
class="max-h-96 rounded-box object-contain object-center">
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
{:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
|
||||
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
|
||||
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
|
||||
</button>
|
||||
{:else}
|
||||
{#await loadPreview()}
|
||||
<div class="center my-12 w-full">
|
||||
<span class="loading loading-spinner"></span>
|
||||
</div>
|
||||
{:then preview}
|
||||
<div class="bg-alt flex max-w-xl flex-col leading-normal">
|
||||
{#if preview.image && !hideImage}
|
||||
<img
|
||||
alt=""
|
||||
onerror={onError}
|
||||
src={preview.image}
|
||||
class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2 p-4">
|
||||
<strong class="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>{preview.title || displayUrl(url)}</strong>
|
||||
<p>{ellipsize(preview.description, 140)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:catch}
|
||||
<p class="bg-alt p-12 text-center leading-normal">
|
||||
Unable to load a preview for {url}
|
||||
</p>
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
</Link>
|
||||
{/if}
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
<script lang="ts">
|
||||
import {call, displayUrl} from "@welshman/lib"
|
||||
import {isRelayUrl, getTagValue} from "@welshman/util"
|
||||
import {displayUrl} from "@welshman/lib"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import {preventDefault, stopPropagation} from "@lib/html"
|
||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||
import ContentLinkUrl from "@app/components/ContentLinkUrl.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {PLATFORM_URL, IMAGE_CONTENT_TYPES} from "@app/core/state"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {IMAGE_CONTENT_TYPES} from "@app/core/state"
|
||||
|
||||
const {value, event} = $props()
|
||||
|
||||
const url = value.url.toString()
|
||||
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})
|
||||
</script>
|
||||
@@ -34,8 +27,5 @@
|
||||
{displayUrl(url)}
|
||||
</a>
|
||||
{:else}
|
||||
<Link {external} {href} class="link-content whitespace-nowrap">
|
||||
<Icon icon={LinkRound} size={3} class="inline-block" />
|
||||
{displayUrl(url)}
|
||||
</Link>
|
||||
<ContentLinkUrl {url} class="link-content whitespace-nowrap" />
|
||||
{/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,
|
||||
isText,
|
||||
isEmoji,
|
||||
isEmail,
|
||||
isTopic,
|
||||
isCode,
|
||||
isCashu,
|
||||
@@ -24,6 +25,7 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ContentToken from "@app/components/ContentToken.svelte"
|
||||
import ContentEmoji from "@app/components/ContentEmoji.svelte"
|
||||
import ContentEmail from "@app/components/ContentEmail.svelte"
|
||||
import ContentCode from "@app/components/ContentCode.svelte"
|
||||
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
|
||||
import ContentNewline from "@app/components/ContentNewline.svelte"
|
||||
@@ -109,6 +111,8 @@
|
||||
<ContentTopic value={parsed.value} />
|
||||
{:else if isEmoji(parsed)}
|
||||
<ContentEmoji value={parsed.value} />
|
||||
{:else if isEmail(parsed)}
|
||||
<ContentEmail value={parsed.value} />
|
||||
{:else if isCode(parsed)}
|
||||
<ContentCode
|
||||
value={parsed.value}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<NoteContentMinimal trimParent {url} event={$quote} />
|
||||
</div>
|
||||
{: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} />
|
||||
</NoteCard>
|
||||
{/if}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
let popover: Instance | undefined = $state()
|
||||
</script>
|
||||
|
||||
<Button class="join rounded-full">
|
||||
<div class="join items-center rounded-full">
|
||||
{#if ENABLE_ZAPS && !hideZap}
|
||||
<ZapButton {url} {event} class="btn join-item btn-neutral btn-xs">
|
||||
<Icon icon={Bolt} size={4} />
|
||||
@@ -52,6 +52,7 @@
|
||||
<Icon icon={SmileCircle} size={4} />
|
||||
</EmojiButton>
|
||||
<Tippy
|
||||
class="flex"
|
||||
bind:popover
|
||||
component={EventMenu}
|
||||
props={{url, noun, event, customActions, onClick: hidePopover}}
|
||||
@@ -60,4 +61,4 @@
|
||||
<Icon icon={MenuDots} size={4} />
|
||||
</Button>
|
||||
</Tippy>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
})
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
spacer!.style.minHeight = `${form!.offsetHeight}px`
|
||||
spacer!.style.minHeight = `${form!.offsetHeight + 60}px`
|
||||
})
|
||||
|
||||
observer.observe(form!)
|
||||
@@ -84,7 +84,7 @@
|
||||
in:fly
|
||||
bind:this={form}
|
||||
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="relative">
|
||||
<div class="note-editor grow overflow-hidden">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {writable} from "svelte/store"
|
||||
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 Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||
@@ -10,6 +10,7 @@
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
@@ -21,7 +22,7 @@
|
||||
import {PROTECTED} from "@app/core/state"
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {DraftKey} from "@app/util/drafts"
|
||||
import {canEnforceNip70} from "@app/core/commands"
|
||||
import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
|
||||
|
||||
type Values = {
|
||||
title: string
|
||||
@@ -33,9 +34,10 @@
|
||||
url: string
|
||||
h?: string
|
||||
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 ?? ""}`)
|
||||
|
||||
@@ -52,7 +54,7 @@
|
||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
||||
|
||||
const submit = async () => {
|
||||
if ($uploading) return
|
||||
if ($uploading || loading) return
|
||||
|
||||
if (!title) {
|
||||
return pushToast({
|
||||
@@ -78,23 +80,43 @@
|
||||
["relays", url],
|
||||
]
|
||||
|
||||
if (await shouldProtect) {
|
||||
tags.push(PROTECTED)
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const protect = await shouldProtect
|
||||
|
||||
if (protect) {
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
const goalThunk = publishThunk({
|
||||
relays: [url],
|
||||
event: makeEvent(ZAP_GOAL, {content: title, tags}),
|
||||
})
|
||||
|
||||
const error = await waitForThunkError(goalThunk)
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
draftKey.clear()
|
||||
history.back()
|
||||
|
||||
if (shareToChat) {
|
||||
publishRoomQuote({url, h, parent: goalThunk.event, protect})
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
publishThunk({
|
||||
relays: [url],
|
||||
event: makeEvent(ZAP_GOAL, {content: title, tags}),
|
||||
})
|
||||
|
||||
draftKey.clear()
|
||||
history.back()
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
|
||||
let title = $state(initialValues?.title ?? "")
|
||||
let amount = $state(initialValues?.amount ?? 1000)
|
||||
let content = $state(initialValues?.content ?? "")
|
||||
@@ -154,7 +176,8 @@
|
||||
<Button
|
||||
data-tip="Add an image"
|
||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||
onclick={selectFiles}>
|
||||
onclick={selectFiles}
|
||||
disabled={loading}>
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
@@ -169,16 +192,16 @@
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<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} />
|
||||
<input bind:value={amount} type="number" class="w-28" />
|
||||
<p class="opacity-50">sats</p>
|
||||
<input bind:value={amount} type="number" class="w-28 grow" />
|
||||
<p class="shrink-0 opacity-50">sats</p>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<input
|
||||
class="range range-primary -mt-2"
|
||||
class="range range-primary -mt-2 w-full"
|
||||
type="range"
|
||||
min="1000"
|
||||
max="100000"
|
||||
@@ -188,10 +211,12 @@
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</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>
|
||||
</Modal>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ProgressBar from "@app/components/ProgressBar.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {PLATFORM_NAME} from "@app/core/state"
|
||||
|
||||
@@ -22,9 +23,11 @@
|
||||
secret: string
|
||||
next: () => unknown
|
||||
submitText?: string
|
||||
step?: number
|
||||
totalSteps?: number
|
||||
}
|
||||
|
||||
const {secret, next, submitText = "Continue"}: Props = $props()
|
||||
const {secret, next, submitText = "Continue", step, totalSteps}: Props = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
@@ -150,6 +153,9 @@
|
||||
</Button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
{#if step && totalSteps}
|
||||
<ProgressBar current={step} total={totalSteps} />
|
||||
{/if}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import {onMount, onDestroy} from "svelte"
|
||||
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
||||
import {Nip46Broker} from "@welshman/signer"
|
||||
@@ -103,10 +104,16 @@
|
||||
mode = "connect"
|
||||
}
|
||||
|
||||
const openSigner = () => {
|
||||
controller.launchSigner()
|
||||
}
|
||||
|
||||
const selectBunker = () => {
|
||||
mode = "bunker"
|
||||
}
|
||||
|
||||
const isIos = Capacitor.getPlatform() === "ios"
|
||||
|
||||
let mode: string = $state("bunker")
|
||||
|
||||
$effect(() => {
|
||||
@@ -138,6 +145,9 @@
|
||||
<BunkerUrl {controller} />
|
||||
<Button class="btn {$bunker ? 'btn-neutral' : 'btn-primary'}" onclick={selectConnect}
|
||||
>Log in with a QR code instead</Button>
|
||||
{#if isIos}
|
||||
<Button class="btn btn-neutral" onclick={openSigner}>Open in Signer</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import LogInOTP from "@app/components/LogInOTP.svelte"
|
||||
import LogInSelect from "@app/components/LogInSelect.svelte"
|
||||
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
|
||||
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
|
||||
import {pushModal, clearModals} from "@app/util/modal"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
@@ -44,7 +45,7 @@
|
||||
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we were unable to log you in.",
|
||||
message: getPomadeLoginFailureMessage(messages),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,10 +65,17 @@
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we were unable to log you in.",
|
||||
message: getPomadeLoginFailureMessage(res.messages),
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Login error:", error)
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: POMADE_NETWORK_ERROR_MESSAGE,
|
||||
})
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
@@ -90,7 +98,7 @@
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Letter} />
|
||||
<input bind:value={email} />
|
||||
<input type="email" bind:value={email} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import LogInOTPConfirm from "@app/components/LogInOTPConfirm.svelte"
|
||||
import {POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
@@ -35,11 +36,20 @@
|
||||
if (ok) {
|
||||
pushModal(LogInOTPConfirm, {email, peersByPrefix})
|
||||
} else {
|
||||
console.error("Pomade challenge request failed during OTP login")
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we were unable to request a login code.",
|
||||
message: POMADE_NETWORK_ERROR_MESSAGE,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: POMADE_NETWORK_ERROR_MESSAGE,
|
||||
})
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
@@ -61,7 +71,7 @@
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Letter} />
|
||||
<input bind:value={email} />
|
||||
<input type="email" bind:value={email} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import StringMultiInput from "@lib/components/StringMultiInput.svelte"
|
||||
import LogInSelect from "@app/components/LogInSelect.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {pushModal, clearModals} from "@app/util/modal"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
|
||||
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
type Props = {
|
||||
email: string
|
||||
@@ -44,7 +45,7 @@
|
||||
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we were unable to log you in.",
|
||||
message: getPomadeLoginFailureMessage(messages),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,10 +65,17 @@
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we were unable to log you in.",
|
||||
message: getPomadeLoginFailureMessage(res.messages),
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Login error:", error)
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: POMADE_NETWORK_ERROR_MESSAGE,
|
||||
})
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import Profile from "@app/components/Profile.svelte"
|
||||
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
|
||||
import {getPomadeLoginFailureMessage, POMADE_NETWORK_ERROR_MESSAGE} from "@app/util/pomadeErrors"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {clearModals} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
@@ -46,9 +47,16 @@
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we were unable to log you in.",
|
||||
message: getPomadeLoginFailureMessage(res.messages),
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Login error:", error)
|
||||
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: POMADE_NETWORK_ERROR_MESSAGE,
|
||||
})
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
children,
|
||||
minimal = false,
|
||||
hideProfile = false,
|
||||
noShadow = false,
|
||||
url,
|
||||
...restProps
|
||||
}: {
|
||||
@@ -23,6 +24,7 @@
|
||||
children: Snippet
|
||||
minimal?: boolean
|
||||
hideProfile?: boolean
|
||||
noShadow?: boolean
|
||||
url?: string
|
||||
class?: string
|
||||
} = $props()
|
||||
@@ -34,7 +36,7 @@
|
||||
let muted = $state($isEventMuted(event))
|
||||
</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}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="row-2 relative">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type {ComponentProps} from "svelte"
|
||||
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED} from "@welshman/util"
|
||||
import {Poll} from "nostr-tools/kinds"
|
||||
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util"
|
||||
import NoteContentEventTime from "@app/components/NoteContentEventTime.svelte"
|
||||
import NoteContentThread from "@app/components/NoteContentThread.svelte"
|
||||
import NoteContentClassified from "@app/components/NoteContentClassified.svelte"
|
||||
@@ -21,7 +20,7 @@
|
||||
<NoteContentClassified {...props} />
|
||||
{:else if props.event.kind === ZAP_GOAL}
|
||||
<NoteContentGoal {...props} />
|
||||
{:else if props.event.kind === Poll}
|
||||
{:else if props.event.kind === POLL}
|
||||
<NoteContentPoll {...props} />
|
||||
{:else}
|
||||
<Content {...props} />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type {ComponentProps} from "svelte"
|
||||
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED} from "@welshman/util"
|
||||
import {Poll} from "nostr-tools/kinds"
|
||||
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util"
|
||||
import NoteContentMinimalEventTime from "@app/components/NoteContentMinimalEventTime.svelte"
|
||||
import NoteContentMinimalThread from "@app/components/NoteContentMinimalThread.svelte"
|
||||
import NoteContentMinimalClassified from "@app/components/NoteContentMinimalClassified.svelte"
|
||||
@@ -21,7 +20,7 @@
|
||||
<NoteContentMinimalClassified {...props} />
|
||||
{:else if props.event.kind === ZAP_GOAL}
|
||||
<NoteContentMinimalGoal {...props} />
|
||||
{:else if props.event.kind === Poll}
|
||||
{:else if props.event.kind === POLL}
|
||||
<NoteContentMinimalPoll {...props} />
|
||||
{:else}
|
||||
<ContentMinimal {...props} />
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type {ComponentProps} from "svelte"
|
||||
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 {deriveEvents} from "@app/core/state"
|
||||
import {getPollResults} from "@app/util/polls"
|
||||
|
||||
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))
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type {ComponentProps} from "svelte"
|
||||
import {onMount} from "svelte"
|
||||
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 Content from "@app/components/Content.svelte"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
request({
|
||||
relays: [props.url],
|
||||
filters: [{kinds: [PollResponse], "#e": [props.event.id]}],
|
||||
filters: [{kinds: [POLL_RESPONSE], "#e": [props.event.id]}],
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib"
|
||||
import {makeEvent} from "@welshman/util"
|
||||
import {publishThunk} from "@welshman/app"
|
||||
import {Poll} from "nostr-tools/kinds"
|
||||
import {makeEvent, POLL} from "@welshman/util"
|
||||
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import HamburgerMenu from "@assets/icons/hamburger-menu.svg?dataurl"
|
||||
@@ -13,6 +12,7 @@
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
@@ -21,7 +21,7 @@
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
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 type {PollType} from "@app/util/polls"
|
||||
|
||||
@@ -40,9 +40,10 @@
|
||||
type Props = {
|
||||
url: 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 initialValues = draftKey.get()
|
||||
|
||||
@@ -102,6 +103,8 @@
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (loading) return
|
||||
|
||||
if (!title.trim()) {
|
||||
return pushToast({theme: "error", message: "Please provide a title for your poll."})
|
||||
}
|
||||
@@ -130,19 +133,39 @@
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
if (await shouldProtect) {
|
||||
tags.push(PROTECTED)
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const protect = await shouldProtect
|
||||
|
||||
if (protect) {
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
const pollThunk = publishThunk({
|
||||
relays: [url],
|
||||
event: makeEvent(POLL, {content: title.trim(), tags}),
|
||||
})
|
||||
|
||||
const error = await waitForThunkError(pollThunk)
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
draftKey.clear()
|
||||
history.back()
|
||||
|
||||
if (shareToChat) {
|
||||
publishRoomQuote({url, h, parent: pollThunk.event, protect})
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
||||
publishThunk({
|
||||
relays: [url],
|
||||
event: makeEvent(Poll, {content: title.trim(), tags}),
|
||||
})
|
||||
|
||||
draftKey.clear()
|
||||
history.back()
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
|
||||
let draggedOptionId = $state<string | undefined>()
|
||||
let title = $state(initialValues?.title ?? "")
|
||||
let pollType = $state<PollType>(initialValues?.pollType ?? "singlechoice")
|
||||
@@ -246,10 +269,12 @@
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</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>
|
||||
</Modal>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {onDestroy} from "svelte"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {POLL_RESPONSE} from "@welshman/util"
|
||||
import {pubkey, publishThunk, abortThunk} from "@welshman/app"
|
||||
import {PollResponse} from "nostr-tools/kinds"
|
||||
import {formatTimestampRelative} from "@welshman/lib"
|
||||
import {deriveEvents} from "@app/core/state"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
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 options = getPollOptions(event)
|
||||
|
||||
@@ -13,13 +13,16 @@
|
||||
|
||||
const onClick = () => goToSpace(url)
|
||||
|
||||
const path = makeSpacePath(url)
|
||||
|
||||
const display = $derived(deriveRelayDisplay(url))
|
||||
</script>
|
||||
|
||||
<PrimaryNavItem
|
||||
href={path}
|
||||
onclick={onClick}
|
||||
title={$display}
|
||||
class="tooltip-right"
|
||||
notification={$notifications.has(makeSpacePath(url))}>
|
||||
notification={$notifications.has(path)}>
|
||||
<RelayIcon {url} size={10} class="rounded-full" />
|
||||
</PrimaryNavItem>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {deriveEventsDesc, deriveEventsById} from "@welshman/store"
|
||||
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 Button from "@lib/components/Button.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 {pushModal} from "@app/util/modal"
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
load({
|
||||
filters: [
|
||||
{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(),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import {errorMessage} from "@lib/util"
|
||||
@@ -10,26 +10,18 @@
|
||||
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||
import {clearModals} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {PROTECTED} from "@app/core/state"
|
||||
import {updateProfile} from "@app/core/commands"
|
||||
|
||||
const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
|
||||
const shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || [])
|
||||
const initialValues = {profile, shouldBroadcast}
|
||||
const initialValues = {profile}
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const onsubmit = async ({
|
||||
profile,
|
||||
shouldBroadcast,
|
||||
}: {
|
||||
profile: Profile
|
||||
shouldBroadcast: boolean
|
||||
}) => {
|
||||
const onsubmit = async ({profile}: {profile: Profile}) => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const error = await waitForThunkError(updateProfile({profile, shouldBroadcast}))
|
||||
const error = await waitForThunkError(updateProfile({profile}))
|
||||
|
||||
if (error) {
|
||||
pushToast({
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import MapPoint from "@assets/icons/map-point.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
@@ -17,7 +16,6 @@
|
||||
|
||||
type Values = {
|
||||
profile: Profile
|
||||
shouldBroadcast: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
@@ -25,9 +23,10 @@
|
||||
onsubmit: (values: Values) => void
|
||||
isSignup?: boolean
|
||||
footer: Snippet
|
||||
progressBar?: Snippet
|
||||
}
|
||||
|
||||
const {initialValues, isSignup, onsubmit, footer}: Props = $props()
|
||||
const {initialValues, isSignup, onsubmit, footer, progressBar}: Props = $props()
|
||||
|
||||
const values = $state(initialValues)
|
||||
|
||||
@@ -77,7 +76,7 @@
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<textarea
|
||||
class="textarea textarea-bordered leading-4"
|
||||
class="textarea textarea-bordered leading-4 w-full"
|
||||
rows="5"
|
||||
bind:value={values.profile.about}></textarea>
|
||||
{/snippet}
|
||||
@@ -104,26 +103,10 @@
|
||||
{/snippet}
|
||||
</Field>
|
||||
{/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>
|
||||
{#if progressBar}
|
||||
{@render progressBar()}
|
||||
{/if}
|
||||
<ModalFooter>
|
||||
{@render footer()}
|
||||
</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
|
||||
reactionClass?: string
|
||||
noTooltip?: boolean
|
||||
innerEvent?: TrustedEvent
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
@@ -43,23 +44,36 @@
|
||||
url = "",
|
||||
reactionClass = "",
|
||||
noTooltip = false,
|
||||
innerEvent = undefined,
|
||||
children,
|
||||
}: Props = $props()
|
||||
|
||||
const eventIds = innerEvent ? [event.id, innerEvent.id] : [event.id]
|
||||
|
||||
const reports = deriveArray(
|
||||
deriveEventsById({repository, filters: [{kinds: [REPORT], "#e": [event.id]}]}),
|
||||
)
|
||||
|
||||
const reactions = deriveArray(
|
||||
deriveEventsById({repository, filters: [{kinds: [REACTION], "#e": [event.id]}]}),
|
||||
deriveEventsById({repository, filters: [{kinds: [REACTION], "#e": eventIds}]}),
|
||||
)
|
||||
|
||||
const zaps = deriveArray(
|
||||
deriveItemsByKey<Zap>({
|
||||
repository,
|
||||
getKey: zap => zap.response.id,
|
||||
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
||||
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
|
||||
filters: [{kinds: [ZAP_RESPONSE], "#e": eventIds}],
|
||||
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-2">
|
||||
<div class="relative flex gap-4">
|
||||
<div class="relative flex gap-2 sm:gap-4">
|
||||
<div class="relative">
|
||||
<div class="avatar relative">
|
||||
<div
|
||||
class="center flex! h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300">
|
||||
<RelayIcon {url} />
|
||||
<RelayIcon {url} size={10} />
|
||||
</div>
|
||||
</div>
|
||||
{#if $rooms.includes(url)}
|
||||
@@ -36,13 +36,11 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h2 class="ellipsize whitespace-nowrap text-xl">
|
||||
<RelayName {url} />
|
||||
</h2>
|
||||
<p class="text-sm opacity-75">{url}</p>
|
||||
<RelayName {url} class="ellipsize whitespace-nowrap text-lg sm:text-xl" />
|
||||
<p class="text-xs sm:text-sm opacity-75">{url}</p>
|
||||
</div>
|
||||
</div>
|
||||
<RelayDescription {url} />
|
||||
<RelayDescription {url} class="text-sm sm:text-md" />
|
||||
</div>
|
||||
{#if !hideFavorites && $favorited.size > 0}
|
||||
<div class="row-2 card2 card2-sm bg-alt">
|
||||
|
||||
@@ -68,8 +68,6 @@
|
||||
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||
const tags = ed.storage.nostr.getEditorTags()
|
||||
|
||||
if (!content) return
|
||||
|
||||
onSubmit({content, tags})
|
||||
|
||||
draftKey?.clear()
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
{/if}
|
||||
</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="flex items-center gap-4">
|
||||
<span>Members:</span>
|
||||
@@ -251,6 +251,10 @@
|
||||
</div>
|
||||
<Button class="btn btn-neutral btn-sm" onclick={showMembers}>View All</Button>
|
||||
</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}
|
||||
<div class="card2 card2-sm bg-alt col-4">
|
||||
<strong class="text-lg">Room Settings</strong>
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<script lang="ts">
|
||||
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 {MESSAGE, COMMENT} from "@welshman/util"
|
||||
import {MESSAGE, COMMENT, getTag} from "@welshman/util"
|
||||
import {
|
||||
thunks,
|
||||
pubkey,
|
||||
@@ -27,7 +35,7 @@
|
||||
import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
|
||||
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.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 {getRoomItemPath} from "@app/util/routes"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
@@ -38,7 +46,6 @@
|
||||
replyTo?: (event: TrustedEvent) => void
|
||||
showPubkey?: boolean
|
||||
addSpaceBelow?: boolean
|
||||
inert?: boolean
|
||||
canEdit: (event: TrustedEvent) => boolean
|
||||
onEdit: (event: TrustedEvent) => void
|
||||
}
|
||||
@@ -49,7 +56,6 @@
|
||||
replyTo = undefined,
|
||||
showPubkey = false,
|
||||
addSpaceBelow = false,
|
||||
inert = false,
|
||||
canEdit,
|
||||
onEdit,
|
||||
}: Props = $props()
|
||||
@@ -60,7 +66,15 @@
|
||||
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
||||
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
|
||||
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 edit = canEdit(event) ? () => onEdit(event) : undefined
|
||||
@@ -78,7 +92,7 @@
|
||||
|
||||
<TapTarget
|
||||
data-event={event.id}
|
||||
onTap={inert ? null : onTap}
|
||||
{onTap}
|
||||
class={cx(
|
||||
"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},
|
||||
@@ -111,7 +125,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
|
||||
<RoomItemContent {url} {event} />
|
||||
<RoomItemContent {url} event={$innerEvent ?? event} />
|
||||
{#if thunk}
|
||||
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
|
||||
{/if}
|
||||
@@ -124,9 +138,10 @@
|
||||
{event}
|
||||
{deleteReaction}
|
||||
{createReaction}
|
||||
reactionClass="tooltip-right" />
|
||||
{#if path && $comments.length > 0}
|
||||
{@const pubkeys = $comments.map(e => e.pubkey)}
|
||||
reactionClass="tooltip-right"
|
||||
innerEvent={$innerEvent} />
|
||||
{#if path && $innerComments.length > 0}
|
||||
{@const pubkeys = $innerComments.map(e => e.pubkey)}
|
||||
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
|
||||
{@const tooltip = `${info} commented`}
|
||||
@@ -138,7 +153,7 @@
|
||||
"btn-primary": isOwn,
|
||||
})}>
|
||||
<Icon icon={ReplyAlt} />
|
||||
<span>{$comments.length} comment{$comments.length === 1 ? "" : "s"}</span>
|
||||
<span>{$innerComments.length} comment{$innerComments.length === 1 ? "" : "s"}</span>
|
||||
</Link>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -8,16 +8,17 @@
|
||||
import {getRoomItemPath} from "@app/util/routes"
|
||||
|
||||
const props: ComponentProps<typeof NoteContent> = $props()
|
||||
|
||||
const path = getRoomItemPath(props.url!, props.event)
|
||||
const minLength = 5000
|
||||
const maxLength = 5500
|
||||
</script>
|
||||
|
||||
<div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}>
|
||||
{#if path && !isMobile}
|
||||
<Link href={path}>
|
||||
<NoteContent {...props} />
|
||||
<NoteContent {...props} {minLength} {maxLength} />
|
||||
</Link>
|
||||
{:else}
|
||||
<NoteContent {...props} />
|
||||
<NoteContent {...props} {minLength} {maxLength} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -73,34 +73,44 @@
|
||||
</ModalSubtitle>
|
||||
</ModalHeader>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each $members as pubkey (pubkey)}
|
||||
<div class="card2 bg-alt relative">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
<div class="relative">
|
||||
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
|
||||
<Icon icon={MenuDots} />
|
||||
</Button>
|
||||
{#if menuPubkey === pubkey}
|
||||
<Popover hideOnClick onClose={closeMenu}>
|
||||
<ul
|
||||
transition:fly
|
||||
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||
<li>
|
||||
<Button class="text-error" onclick={() => removeMember(pubkey)}>
|
||||
<Icon icon={MinusCircle} />
|
||||
Remove Member
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</Popover>
|
||||
{/if}
|
||||
{#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)}
|
||||
<div class="card2 bg-alt relative">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
<div class="relative">
|
||||
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
|
||||
<Icon icon={MenuDots} />
|
||||
</Button>
|
||||
{#if menuPubkey === pubkey}
|
||||
<Popover hideOnClick onClose={closeMenu}>
|
||||
<ul
|
||||
transition:fly
|
||||
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||
<li>
|
||||
<Button class="text-error" onclick={() => removeMember(pubkey)}>
|
||||
<Icon icon={MinusCircle} />
|
||||
Remove Member
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -56,6 +56,11 @@
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!$spaceMembers) {
|
||||
addMembers()
|
||||
return
|
||||
}
|
||||
|
||||
const pubkeysSnapshot = $state.snapshot(pubkeys)
|
||||
const nonSpaceMembers = pubkeysSnapshot.filter(pubkey => !$spaceMembers.includes(pubkey))
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</script>
|
||||
|
||||
<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} />
|
||||
<div class="min-w-0 overflow-hidden text-ellipsis">
|
||||
<RoomName {url} {h} />
|
||||
|
||||
@@ -62,9 +62,10 @@
|
||||
|
||||
const flows = {
|
||||
email: {
|
||||
start: () => pushModal(SignUpEmail, {next: flows.email.profile}),
|
||||
profile: () => pushModal(SignUpProfile, {next: flows.email.complete}),
|
||||
complete: () => pushModal(SignUpComplete, {next: flows.email.finalize}),
|
||||
start: () => pushModal(SignUpEmail, {next: flows.email.profile, step: 1, totalSteps: 3}),
|
||||
profile: () => pushModal(SignUpProfile, {next: flows.email.complete, step: 2, totalSteps: 3}),
|
||||
complete: () =>
|
||||
pushModal(SignUpComplete, {next: flows.email.finalize, step: 3, totalSteps: 3}),
|
||||
finalize: () => {
|
||||
const email = getKey<string>("signup.email")!
|
||||
const clientOptions = getKey<ClientOptions>("signup.clientOptions")!
|
||||
@@ -74,9 +75,10 @@
|
||||
},
|
||||
},
|
||||
nostr: {
|
||||
start: () => pushModal(SignUpProfile, {next: flows.nostr.key}),
|
||||
key: () => pushModal(SignUpKey, {next: flows.nostr.complete}),
|
||||
complete: () => pushModal(SignUpComplete, {next: flows.nostr.finalize}),
|
||||
start: () => pushModal(SignUpProfile, {next: flows.nostr.key, step: 1, totalSteps: 3}),
|
||||
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}),
|
||||
complete: () =>
|
||||
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
|
||||
finalize: () => {
|
||||
const secret = getKey<string>("signup.secret")!
|
||||
|
||||
|
||||
@@ -9,12 +9,15 @@
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ProgressBar from "@app/components/ProgressBar.svelte"
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
step?: number
|
||||
totalSteps?: number
|
||||
}
|
||||
|
||||
const {next}: Props = $props()
|
||||
const {next, step, totalSteps}: Props = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
</script>
|
||||
@@ -33,6 +36,9 @@
|
||||
on groups you've already joined. Click below to get started!
|
||||
</p>
|
||||
</ModalBody>
|
||||
{#if step && totalSteps}
|
||||
<ProgressBar current={step} total={totalSteps} />
|
||||
{/if}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
|
||||
@@ -18,14 +18,17 @@
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
|
||||
import ProgressBar from "@app/components/ProgressBar.svelte"
|
||||
import {pushToast, popToast} from "@app/util/toast"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
step?: number
|
||||
totalSteps?: number
|
||||
}
|
||||
|
||||
const {next}: Props = $props()
|
||||
const {next, step, totalSteps}: Props = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
@@ -81,7 +84,7 @@
|
||||
setKey("signup.clientOptions", clientOptions)
|
||||
|
||||
popToast(toastId)
|
||||
pushModal(SignUpEmailConfirm, {next})
|
||||
pushModal(SignUpEmailConfirm, {next, step, totalSteps})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -120,7 +123,7 @@
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={Letter} />
|
||||
<input bind:value={email} />
|
||||
<input type="email" bind:value={email} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
@@ -134,8 +137,14 @@
|
||||
<input type="password" bind:value={password} />
|
||||
</label>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
Must be at least 12 characters long.
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
</ModalBody>
|
||||
{#if step && totalSteps}
|
||||
<ProgressBar current={step} total={totalSteps} />
|
||||
{/if}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
|
||||
@@ -15,12 +15,15 @@
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ProgressBar from "@app/components/ProgressBar.svelte"
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
step?: number
|
||||
totalSteps?: number
|
||||
}
|
||||
|
||||
const {next}: Props = $props()
|
||||
const {next, step, totalSteps}: Props = $props()
|
||||
|
||||
const email = getKey<string>("signup.email")
|
||||
|
||||
@@ -61,6 +64,9 @@
|
||||
above.
|
||||
</p>
|
||||
</ModalBody>
|
||||
{#if step && totalSteps}
|
||||
<ProgressBar current={step} total={totalSteps} />
|
||||
{/if}
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
step?: number
|
||||
totalSteps?: number
|
||||
}
|
||||
|
||||
const {next}: Props = $props()
|
||||
const {next, step, totalSteps}: Props = $props()
|
||||
|
||||
const secret = getKey<string>("signup.secret")!
|
||||
</script>
|
||||
|
||||
<KeyDownload {secret} {next} />
|
||||
<KeyDownload {secret} {next} {step} {totalSteps} />
|
||||
|
||||
@@ -5,19 +5,20 @@
|
||||
import {getKey, setKey} from "@lib/implicit"
|
||||
import Icon from "@lib/components/Icon.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 ProgressBar from "@app/components/ProgressBar.svelte"
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
step?: number
|
||||
totalSteps?: number
|
||||
}
|
||||
|
||||
const {next}: Props = $props()
|
||||
const {next, step, totalSteps}: Props = $props()
|
||||
|
||||
const profile = getKey<Profile>("signup.profile")!
|
||||
|
||||
const initialValues = {profile, shouldBroadcast: false}
|
||||
const initialValues = {profile}
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
@@ -27,19 +28,20 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal>
|
||||
<ModalBody>
|
||||
<ProfileEditForm isSignup {initialValues} {onsubmit}>
|
||||
{#snippet footer()}
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button class="btn btn-primary" type="submit">
|
||||
Create Account
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</ProfileEditForm>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
<ProfileEditForm isSignup {initialValues} {onsubmit}>
|
||||
{#snippet footer()}
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button class="btn btn-primary" type="submit">
|
||||
Create Account
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#snippet progressBar()}
|
||||
{#if step && totalSteps}
|
||||
<ProgressBar current={step} total={totalSteps} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ProfileEditForm>
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import {sleep} from "@welshman/lib"
|
||||
import {request} from "@welshman/net"
|
||||
import {displayRelayUrl, getTagValue, RELAY_INVITE} from "@welshman/util"
|
||||
import {Share} from "@capacitor/share"
|
||||
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 Spinner from "@lib/components/Spinner.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
@@ -23,36 +25,72 @@
|
||||
const {url} = $props()
|
||||
|
||||
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 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 loading = $state(true)
|
||||
|
||||
let invite = $state("")
|
||||
|
||||
$effect(() => {
|
||||
const relay = displayRelayUrl(url)
|
||||
const params = new URLSearchParams({r: relay, c: claim}).toString()
|
||||
|
||||
invite = PLATFORM_URL + "/join?" + params
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
const [[event]] = await Promise.all([
|
||||
request({
|
||||
relays: [url],
|
||||
autoClose: true,
|
||||
signal: AbortSignal.timeout(3000),
|
||||
filters: [{kinds: [RELAY_INVITE]}],
|
||||
}),
|
||||
sleep(2000),
|
||||
])
|
||||
try {
|
||||
const {value} = await Share.canShare()
|
||||
canShare = value
|
||||
} catch {
|
||||
canShare = false
|
||||
}
|
||||
|
||||
claim = getTagValue("claim", event?.tags || []) || ""
|
||||
loading = false
|
||||
try {
|
||||
const [[event]] = await Promise.all([
|
||||
request({
|
||||
relays: [url],
|
||||
autoClose: true,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
filters: [{kinds: [RELAY_INVITE]}],
|
||||
}),
|
||||
sleep(2000),
|
||||
])
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -70,20 +108,36 @@
|
||||
<p class="center">
|
||||
<Spinner {loading}>Requesting an invite link...</Spinner>
|
||||
</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>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<QRCode code={invite} />
|
||||
<div class="w-48">
|
||||
<QRCode code={invite} />
|
||||
</div>
|
||||
<Field>
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon={LinkRound} />
|
||||
<input bind:value={invite} class="grow" type="text" />
|
||||
<Button onclick={copyInvite}>
|
||||
<Icon icon={Copy} />
|
||||
</Button>
|
||||
</label>
|
||||
<div class="flex w-full gap-2">
|
||||
{#if canShare}
|
||||
<Button
|
||||
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} />
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
|
||||
@@ -112,46 +112,58 @@
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each $members as pubkey (pubkey)}
|
||||
<div class="card2 card2-sm bg-alt relative">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
{#if canBan || canUnallow}
|
||||
<div class="relative">
|
||||
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
|
||||
<Icon icon={MenuDots} />
|
||||
</Button>
|
||||
{#if menuPubkey === pubkey}
|
||||
<Popover hideOnClick onClose={closeMenu}>
|
||||
<ul
|
||||
transition:fly
|
||||
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||
{#if canUnallow}
|
||||
<li>
|
||||
<Button onclick={() => unallowMember(pubkey)}>
|
||||
<Icon icon={UserMinus} />
|
||||
Remove User
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canBan}
|
||||
<li>
|
||||
<Button class="text-error" onclick={() => banMember(pubkey)}>
|
||||
<Icon icon={MinusCircle} />
|
||||
Ban User
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $members === undefined}
|
||||
<div class="card2 bg-base-200 p-4">
|
||||
<span class="text-error">Member list not available from this space</span>
|
||||
</div>
|
||||
{/each}
|
||||
{: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)}
|
||||
<div class="card2 card2-sm bg-alt relative">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Profile {pubkey} {url} />
|
||||
</div>
|
||||
{#if canBan || canUnallow}
|
||||
<div class="relative">
|
||||
<Button
|
||||
class="btn btn-circle btn-ghost btn-sm"
|
||||
onclick={() => toggleMenu(pubkey)}>
|
||||
<Icon icon={MenuDots} />
|
||||
</Button>
|
||||
{#if menuPubkey === pubkey}
|
||||
<Popover hideOnClick onClose={closeMenu}>
|
||||
<ul
|
||||
transition:fly
|
||||
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||
{#if canUnallow}
|
||||
<li>
|
||||
<Button onclick={() => unallowMember(pubkey)}>
|
||||
<Icon icon={UserMinus} />
|
||||
Remove User
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#if canBan}
|
||||
<li>
|
||||
<Button class="text-error" onclick={() => banMember(pubkey)}>
|
||||
<Icon icon={MinusCircle} />
|
||||
Ban User
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {derived} from "svelte/store"
|
||||
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED} from "@welshman/util"
|
||||
import {Poll} from "nostr-tools/kinds"
|
||||
import {deriveRelay, deriveRelayDisplay, createSearch, pubkey} from "@welshman/app"
|
||||
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util"
|
||||
import {deriveRelay, createSearch, pubkey} from "@welshman/app"
|
||||
import {fly} from "@lib/transition"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||
@@ -66,7 +65,6 @@
|
||||
const {url} = $props()
|
||||
|
||||
const relay = deriveRelay(url)
|
||||
const display = deriveRelayDisplay(url)
|
||||
const chatPath = makeSpacePath(url, "chat")
|
||||
const goalsPath = makeSpacePath(url, "goals")
|
||||
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"
|
||||
onclick={openMenu}>
|
||||
<div class="flex items-center justify-between">
|
||||
<strong
|
||||
class="flex items-center gap-1 relative tooltip tooltip-right"
|
||||
data-tip={$display}>
|
||||
<strong class="flex items-center gap-1 relative">
|
||||
<RelayName {url} class="ellipsize" />
|
||||
<div
|
||||
class="absolute -right-3 top-0 h-2 w-2 rounded-full bg-primary transition-all opacity-0"
|
||||
@@ -181,7 +177,11 @@
|
||||
<li>
|
||||
<Button onclick={showMembers}>
|
||||
<Icon icon={UserRounded} />
|
||||
View Members ({$members.length})
|
||||
{#if $members === undefined}
|
||||
View Members
|
||||
{:else}
|
||||
View Members ({$members.length})
|
||||
{/if}
|
||||
</Button>
|
||||
</li>
|
||||
{#if $userIsAdmin}
|
||||
@@ -263,7 +263,7 @@
|
||||
<Icon icon={CalendarMinimalistic} /> Calendar
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if $spaceKinds.has(Poll)}
|
||||
{#if $spaceKinds.has(POLL)}
|
||||
<SecondaryNavItem href={pollsPath}>
|
||||
<Icon icon={Revote} /> Polls
|
||||
</SecondaryNavItem>
|
||||
|
||||
@@ -26,27 +26,27 @@
|
||||
{@const {pubkey, software, version, supported_nips, limitation} = $relay}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#if pubkey}
|
||||
<div class="badge badge-neutral">
|
||||
<div class="badge badge-neutral text-wrap h-auto">
|
||||
<span class="ellipsize">Administrator: <ProfileLink unstyled {pubkey} /></span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $relay?.contact}
|
||||
<div class="badge badge-neutral">
|
||||
<div class="badge badge-neutral text-wrap h-auto">
|
||||
<span class="ellipsize">Contact: {$relay.contact}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if software}
|
||||
<div class="badge badge-neutral">
|
||||
<div class="badge badge-neutral text-wrap h-auto">
|
||||
<span class="ellipsize">Software: {software}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if version}
|
||||
<div class="badge badge-neutral">
|
||||
<div class="badge badge-neutral text-wrap h-auto">
|
||||
<span class="ellipsize">Version: {version}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#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>
|
||||
</p>
|
||||
{/if}
|
||||
@@ -61,7 +61,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
{#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>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import {tick} from "svelte"
|
||||
import {debounce} from "throttle-debounce"
|
||||
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 {sortEventsDesc} from "@welshman/util"
|
||||
import {MESSAGE, sortEventsDesc} from "@welshman/util"
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import {fly} from "@lib/transition"
|
||||
@@ -53,8 +54,11 @@
|
||||
|
||||
const getFilter = (searchTerm: string): Filter =>
|
||||
h
|
||||
? {kinds: CONTENT_KINDS, "#h": [h], search: searchTerm}
|
||||
: {kinds: CONTENT_KINDS, search: searchTerm}
|
||||
? {kinds: [MESSAGE, ...CONTENT_KINDS], "#h": [h], 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) => {
|
||||
controller?.abort()
|
||||
@@ -68,18 +72,23 @@
|
||||
controller = new AbortController()
|
||||
loading = true
|
||||
|
||||
const filter = getFilter(searchTerm.trim())
|
||||
const localResults = getLocalResults(filter)
|
||||
|
||||
results = sortEventsDesc(localResults)
|
||||
|
||||
try {
|
||||
const events = await request({
|
||||
relays: getRelayUrls(),
|
||||
autoClose: true,
|
||||
signal: controller.signal,
|
||||
filters: [getFilter(searchTerm.trim())],
|
||||
filters: [filter],
|
||||
})
|
||||
|
||||
results = sortEventsDesc(events)
|
||||
results = sortEventsDesc(uniqBy((e: TrustedEvent) => e.id, [...events, ...localResults]))
|
||||
} catch (error) {
|
||||
if (!(error instanceof DOMException && error.name === "AbortError")) {
|
||||
results = []
|
||||
results = sortEventsDesc(localResults)
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import {writable} from "svelte/store"
|
||||
import {makeEvent, THREAD} from "@welshman/util"
|
||||
import {publishThunk} from "@welshman/app"
|
||||
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
@@ -19,7 +20,7 @@
|
||||
import {PROTECTED} from "@app/core/state"
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {DraftKey} from "@app/util/drafts"
|
||||
import {canEnforceNip70} from "@app/core/commands"
|
||||
import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
|
||||
|
||||
type Values = {
|
||||
content?: string | object
|
||||
@@ -29,9 +30,10 @@
|
||||
type Props = {
|
||||
url: 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 initialValues = draftKey.get()
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
@@ -43,7 +45,7 @@
|
||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
||||
|
||||
const submit = async () => {
|
||||
if ($uploading) return
|
||||
if ($uploading || loading) return
|
||||
|
||||
if (!title) {
|
||||
return pushToast({
|
||||
@@ -64,23 +66,43 @@
|
||||
|
||||
const tags = [...ed.storage.nostr.getEditorTags(), ["title", title]]
|
||||
|
||||
if (await shouldProtect) {
|
||||
tags.push(PROTECTED)
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const protect = await shouldProtect
|
||||
|
||||
if (protect) {
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
const threadThunk = publishThunk({
|
||||
relays: [url],
|
||||
event: makeEvent(THREAD, {content, tags}),
|
||||
})
|
||||
|
||||
const error = await waitForThunkError(threadThunk)
|
||||
|
||||
if (error) {
|
||||
return pushToast({theme: "error", message: error})
|
||||
}
|
||||
|
||||
draftKey.clear()
|
||||
history.back()
|
||||
|
||||
if (shareToChat) {
|
||||
publishRoomQuote({url, h, parent: threadThunk.event, protect})
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
publishThunk({
|
||||
relays: [url],
|
||||
event: makeEvent(THREAD, {content, tags}),
|
||||
})
|
||||
|
||||
draftKey.clear()
|
||||
history.back()
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
|
||||
let title = $state(initialValues?.title ?? "")
|
||||
let content = $state(initialValues?.content ?? "")
|
||||
|
||||
@@ -138,7 +160,8 @@
|
||||
<Button
|
||||
data-tip="Add an image"
|
||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||
onclick={selectFiles}>
|
||||
onclick={selectFiles}
|
||||
disabled={loading}>
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
@@ -148,10 +171,12 @@
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</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>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {Track} from "livekit-client"
|
||||
import {displayProfileByPubkey, loadProfile} from "@welshman/app"
|
||||
import Pin from "@assets/icons/pin.svg?dataurl"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import VideoCallTile from "@app/components/VideoCallTile.svelte"
|
||||
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
||||
import {get} from "svelte/store"
|
||||
import {
|
||||
VideoCallLayout,
|
||||
isDesktopLayout,
|
||||
toggleVideoPrimaryTile,
|
||||
videoCallLayout,
|
||||
videoCallViewportSync,
|
||||
ViewportSize,
|
||||
videoPrimaryTileKey,
|
||||
} from "@app/call/video"
|
||||
import {currentVoiceSession, currentVoiceRoom, pubkeyFromLiveKitIdentity} from "@app/call/stores"
|
||||
|
||||
type Props = {
|
||||
layout: VideoCallLayout
|
||||
mobile?: boolean
|
||||
url: string
|
||||
h: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
type VideoTileData = {
|
||||
identity: string
|
||||
isLocal: boolean
|
||||
trackSid: string
|
||||
track: Track | undefined
|
||||
source: Track.Source.Camera | Track.Source.ScreenShare
|
||||
}
|
||||
|
||||
type TileLayout = "spotlight" | "default" | "strip"
|
||||
|
||||
const {layout, mobile = false, url, h, class: className = ""}: Props = $props()
|
||||
|
||||
$effect(() => {
|
||||
const currentLayout = isDesktopLayout.current ? ViewportSize.Desktop : ViewportSize.Mobile
|
||||
const {previousLayout} = videoCallViewportSync
|
||||
if (previousLayout === undefined) {
|
||||
videoCallViewportSync.previousLayout = currentLayout
|
||||
return
|
||||
}
|
||||
if (previousLayout === currentLayout) return
|
||||
const p = get(videoCallLayout)
|
||||
if (previousLayout === ViewportSize.Desktop && currentLayout === ViewportSize.Mobile) {
|
||||
if (p === VideoCallLayout.Split) videoCallLayout.set(VideoCallLayout.Video)
|
||||
} else if (previousLayout === ViewportSize.Mobile && currentLayout === ViewportSize.Desktop) {
|
||||
if (p === VideoCallLayout.Chat) videoCallLayout.set(VideoCallLayout.Split)
|
||||
}
|
||||
videoCallViewportSync.previousLayout = currentLayout
|
||||
})
|
||||
|
||||
const isViewingCurrentCallRoom = $derived(
|
||||
$currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h,
|
||||
)
|
||||
|
||||
const showVideoContent = $derived(
|
||||
isViewingCurrentCallRoom &&
|
||||
(mobile
|
||||
? layout === VideoCallLayout.Video
|
||||
: layout === VideoCallLayout.Split || layout === VideoCallLayout.Video),
|
||||
)
|
||||
|
||||
const videoTiles = $derived.by(() => {
|
||||
const session = $currentVoiceSession
|
||||
if (!session || $currentVoiceRoom?.url !== url || $currentVoiceRoom?.h !== h) {
|
||||
return []
|
||||
}
|
||||
|
||||
const room = session.room
|
||||
const videoTiles: VideoTileData[] = []
|
||||
const user = room.localParticipant
|
||||
|
||||
if (session.cameraOn) {
|
||||
const localPub = user.getTrackPublication(Track.Source.Camera)
|
||||
videoTiles.push({
|
||||
identity: user.identity,
|
||||
isLocal: true,
|
||||
trackSid: localPub?.trackSid ?? "local-camera",
|
||||
track: localPub?.track,
|
||||
source: Track.Source.Camera,
|
||||
})
|
||||
}
|
||||
|
||||
if (session.screenShareOn) {
|
||||
const localPub = user.getTrackPublication(Track.Source.ScreenShare)
|
||||
videoTiles.push({
|
||||
identity: user.identity,
|
||||
isLocal: true,
|
||||
trackSid: localPub?.trackSid ?? "local-screen",
|
||||
track: localPub?.track,
|
||||
source: Track.Source.ScreenShare,
|
||||
})
|
||||
}
|
||||
|
||||
for (const rp of room.remoteParticipants.values()) {
|
||||
const camPub = rp.getTrackPublication(Track.Source.Camera)
|
||||
if (camPub?.isSubscribed && camPub.track) {
|
||||
videoTiles.push({
|
||||
identity: rp.identity,
|
||||
isLocal: false,
|
||||
trackSid: camPub.trackSid,
|
||||
track: camPub.track,
|
||||
source: Track.Source.Camera,
|
||||
})
|
||||
}
|
||||
const screenPub = rp.getTrackPublication(Track.Source.ScreenShare)
|
||||
if (screenPub?.isSubscribed && screenPub.track) {
|
||||
videoTiles.push({
|
||||
identity: rp.identity,
|
||||
isLocal: false,
|
||||
trackSid: screenPub.trackSid,
|
||||
track: screenPub.track,
|
||||
source: Track.Source.ScreenShare,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return videoTiles
|
||||
})
|
||||
|
||||
/** Identity + source only — LiveKit can change trackSid after publish, which broke spotlight + stale-key effect. */
|
||||
const tileKey = (t: VideoTileData) => `${t.identity}\x1f${t.source}`
|
||||
|
||||
const primaryTile = $derived.by(() => {
|
||||
const k = $videoPrimaryTileKey
|
||||
if (k === undefined) return undefined
|
||||
return videoTiles.find(t => tileKey(t) === k)
|
||||
})
|
||||
|
||||
const secondaryTiles = $derived.by(() => {
|
||||
const p = primaryTile
|
||||
if (p === undefined) return videoTiles
|
||||
const pk = tileKey(p)
|
||||
return videoTiles.filter(t => tileKey(t) !== pk)
|
||||
})
|
||||
|
||||
const useSpotlightLayout = $derived(primaryTile !== undefined)
|
||||
const useMultiGrid = $derived(!useSpotlightLayout && videoTiles.length > 2)
|
||||
|
||||
$effect(() => {
|
||||
const k = $videoPrimaryTileKey
|
||||
if (k === undefined) return
|
||||
if (!videoTiles.some(t => tileKey(t) === k)) {
|
||||
videoPrimaryTileKey.set(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
for (const t of videoTiles) {
|
||||
const pk = pubkeyFromLiveKitIdentity(t.identity)
|
||||
if (pk) loadProfile(pk)
|
||||
}
|
||||
})
|
||||
|
||||
const labelFor = (identity: string, source: VideoTileData["source"]) => {
|
||||
const pk = pubkeyFromLiveKitIdentity(identity)
|
||||
const name = pk ? displayProfileByPubkey(pk) : "Unknown"
|
||||
return source === Track.Source.ScreenShare ? `${name} · screen` : name
|
||||
}
|
||||
|
||||
const showTileGrid = $derived(videoTiles.length > 0)
|
||||
|
||||
const spotlightHandlerFor = (key: string) => () => {
|
||||
toggleVideoPrimaryTile(key)
|
||||
}
|
||||
|
||||
const panelChrome = $derived(
|
||||
cx(
|
||||
mobile &&
|
||||
"flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden bg-base-200 px-2 pt-4 md:hidden pb-[calc(3.5rem+var(--saib))]",
|
||||
!mobile &&
|
||||
"flex min-h-0 w-full min-w-0 flex-1 flex-col gap-2 overflow-hidden bg-base-200 px-2 pb-2 pt-4",
|
||||
className,
|
||||
),
|
||||
)
|
||||
</script>
|
||||
|
||||
{#snippet videoTile(tile: VideoTileData, layout: TileLayout)}
|
||||
<div
|
||||
class={cx(
|
||||
"relative isolate overflow-hidden rounded-box shadow-sm",
|
||||
layout === "spotlight" && "min-h-0 flex-1",
|
||||
layout === "default" && "aspect-video w-full min-h-0",
|
||||
layout === "strip" && "aspect-video w-44 shrink-0",
|
||||
tile.source === Track.Source.ScreenShare ? "bg-black" : "bg-base-100",
|
||||
)}>
|
||||
{#if tile.track}
|
||||
<VideoCallTile
|
||||
track={tile.track}
|
||||
muted={tile.isLocal}
|
||||
fit={tile.source === Track.Source.ScreenShare ? "contain" : "cover"}
|
||||
class="pointer-events-none absolute inset-0" />
|
||||
{:else}
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<ProfileCircle pubkey={pubkeyFromLiveKitIdentity(tile.identity)} {url} size={14} />
|
||||
</div>
|
||||
{/if}
|
||||
<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">
|
||||
{labelFor(tile.identity, tile.source)}{tile.isLocal ? " (you)" : ""}
|
||||
</span>
|
||||
{#if videoTiles.length > 1}
|
||||
{@const pinned = $videoPrimaryTileKey === tileKey(tile)}
|
||||
<Button
|
||||
data-tip={pinned ? "Exit spotlight" : "Spotlight"}
|
||||
aria-pressed={pinned}
|
||||
class={cx(
|
||||
"absolute right-1 top-1 z-20 btn btn-xs btn-square btn-ghost",
|
||||
pinned ? "btn-active bg-primary/25 text-primary" : "bg-base-100/70",
|
||||
)}
|
||||
onclick={spotlightHandlerFor(tileKey(tile))}>
|
||||
<Icon icon={Pin} size={3} />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet videoPanelBody()}
|
||||
{#if showTileGrid}
|
||||
{#if useSpotlightLayout && primaryTile}
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-2 overflow-hidden">
|
||||
{@render videoTile(primaryTile, "spotlight")}
|
||||
{#if secondaryTiles.length > 0}
|
||||
<div
|
||||
class="flex max-h-40 shrink-0 flex-row gap-2 overflow-x-auto overflow-y-hidden py-0.5">
|
||||
{#each secondaryTiles as tile (tileKey(tile))}
|
||||
{@render videoTile(tile, "strip")}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if useMultiGrid}
|
||||
<div
|
||||
class="grid min-h-0 flex-1 grid-cols-1 content-start gap-2 overflow-y-auto sm:grid-cols-2">
|
||||
{#each videoTiles as tile (tileKey(tile))}
|
||||
{@render videoTile(tile, "default")}
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto">
|
||||
{#each videoTiles as tile (tileKey(tile))}
|
||||
{@render videoTile(tile, "default")}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<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">
|
||||
<p>No camera or screen share yet.</p>
|
||||
<p class="text-xs">Use the camera or screen share control to share video.</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if showVideoContent}
|
||||
<div class={panelChrome}>
|
||||
{#if mobile}
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-2">
|
||||
<div class="min-h-0 flex-1 overflow-hidden">
|
||||
{@render videoPanelBody()}
|
||||
</div>
|
||||
<div class="shrink-0 pb-2">
|
||||
<VoiceWidget />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{@render videoPanelBody()}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import type {Track} from "livekit-client"
|
||||
import cx from "classnames"
|
||||
|
||||
type Props = {
|
||||
track: Track
|
||||
muted?: boolean
|
||||
fit?: "cover" | "contain"
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {track, muted = true, fit = "cover", class: className = ""}: Props = $props()
|
||||
|
||||
let videoElement = $state<HTMLVideoElement | undefined>()
|
||||
|
||||
$effect(() => {
|
||||
const element = videoElement
|
||||
const activeTrack = track
|
||||
if (!element) return
|
||||
activeTrack.attach(element)
|
||||
return () => {
|
||||
activeTrack.detach(element)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<video
|
||||
bind:this={videoElement}
|
||||
class={cx("h-full w-full", fit === "contain" ? "object-contain" : "object-cover", className)}
|
||||
playsinline
|
||||
{muted}></video>
|
||||
@@ -7,13 +7,8 @@
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import {
|
||||
currentVoiceSession,
|
||||
DeviceKind,
|
||||
supportsAudioOutputSelection,
|
||||
switchVoiceActiveDevice,
|
||||
type VoiceSession,
|
||||
} from "@app/voice"
|
||||
import {currentVoiceSession, type VoiceSession} from "@app/call/stores"
|
||||
import {DeviceKind, supportsAudioOutputSelection, switchVoiceActiveDevice} from "@app/call/voice"
|
||||
import {popModal} from "@app/util/modal"
|
||||
|
||||
const selectValueForActiveDevice = (session: VoiceSession, kind: DeviceKind): string => {
|
||||
@@ -26,8 +21,10 @@
|
||||
|
||||
let audioInputs = $state<MediaDeviceInfo[]>([])
|
||||
let audioOutputs = $state<MediaDeviceInfo[]>([])
|
||||
let videoInputs = $state<MediaDeviceInfo[]>([])
|
||||
let selectedInput = $state("")
|
||||
let selectedOutput = $state("")
|
||||
let selectedVideo = $state("")
|
||||
|
||||
const loadDevices = async () => {
|
||||
if (!navigator.mediaDevices?.enumerateDevices) return
|
||||
@@ -35,9 +32,11 @@
|
||||
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||
audioInputs = devices.filter(d => d.kind === "audioinput")
|
||||
audioOutputs = devices.filter(d => d.kind === "audiooutput")
|
||||
videoInputs = devices.filter(d => d.kind === "videoinput")
|
||||
} catch {
|
||||
audioInputs = []
|
||||
audioOutputs = []
|
||||
videoInputs = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +54,7 @@
|
||||
}
|
||||
selectedInput = selectValueForActiveDevice(session, DeviceKind.AudioInput)
|
||||
selectedOutput = selectValueForActiveDevice(session, DeviceKind.AudioOutput)
|
||||
selectedVideo = selectValueForActiveDevice(session, DeviceKind.VideoInput)
|
||||
})
|
||||
|
||||
const onInputChange = () => {
|
||||
@@ -65,6 +65,10 @@
|
||||
void switchVoiceActiveDevice(DeviceKind.AudioOutput, selectedOutput)
|
||||
}
|
||||
|
||||
const onVideoChange = () => {
|
||||
void switchVoiceActiveDevice(DeviceKind.VideoInput, selectedVideo)
|
||||
}
|
||||
|
||||
const onDone = () => {
|
||||
popModal()
|
||||
}
|
||||
@@ -76,8 +80,8 @@
|
||||
<Modal>
|
||||
<ModalBody>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Audio settings</ModalTitle>
|
||||
<ModalSubtitle>Choose microphone and speaker for this call.</ModalSubtitle>
|
||||
<ModalTitle>Call settings</ModalTitle>
|
||||
<ModalSubtitle>Microphone, speaker, and camera for this call.</ModalSubtitle>
|
||||
</ModalHeader>
|
||||
<div class="flex flex-col gap-4 pt-2">
|
||||
<FieldInline>
|
||||
@@ -120,6 +124,25 @@
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
{/if}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Camera</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
bind:value={selectedVideo}
|
||||
onchange={onVideoChange}
|
||||
aria-label="Camera">
|
||||
<option value="">Default camera</option>
|
||||
{#each videoInputs as d (d.deviceId)}
|
||||
<option value={d.deviceId}>
|
||||
{d.label || `Camera ${d.deviceId.slice(0, 8)}…`}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -12,14 +12,13 @@
|
||||
import {makeRoomId} from "@app/core/state"
|
||||
import {
|
||||
VoiceState,
|
||||
deriveVoiceParticipants,
|
||||
cancelJoinVoiceRoom,
|
||||
currentVoiceRoom,
|
||||
voiceState,
|
||||
isParticipantSpeaking,
|
||||
participantKey,
|
||||
voiceState,
|
||||
type VoiceParticipant,
|
||||
} from "@app/voice"
|
||||
} from "@app/call/stores"
|
||||
import {cancelJoinVoiceRoom, deriveVoiceParticipants} from "@app/call/voice"
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import {AbortError, TimeoutError} from "$lib/util"
|
||||
import {displayRoom} from "@app/core/state"
|
||||
import {joinVoiceRoom} from "@app/voice"
|
||||
import {joinVoiceRoom} from "@app/call/voice"
|
||||
import {popModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
<script lang="ts">
|
||||
import {readable} from "svelte/store"
|
||||
import {fly} from "svelte/transition"
|
||||
import {fade, fly} from "svelte/transition"
|
||||
import {goto} from "$app/navigation"
|
||||
import {page} from "$app/stores"
|
||||
import cx from "classnames"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import Microphone from "@assets/icons/microphone.svg?dataurl"
|
||||
import MicrophoneOff from "@assets/icons/microphone-off.svg?dataurl"
|
||||
import Videocamera from "@assets/icons/videocamera.svg?dataurl"
|
||||
import VideocameraRecord from "@assets/icons/videocamera-record.svg?dataurl"
|
||||
import Monitor from "@assets/icons/monitor.svg?dataurl"
|
||||
import PhoneRounded from "@assets/icons/phone-rounded.svg?dataurl"
|
||||
import PhoneCallingRounded from "@assets/icons/phone-calling-rounded.svg?dataurl"
|
||||
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import VoiceCallAudioSettingsDialog from "@app/components/VoiceCallAudioSettingsDialog.svelte"
|
||||
@@ -23,16 +28,23 @@
|
||||
type Room,
|
||||
} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import {makeRoomPath} from "@app/util/routes"
|
||||
import {
|
||||
VideoCallLayout,
|
||||
isDesktopLayout,
|
||||
toggleCamera,
|
||||
toggleScreenShare,
|
||||
videoCallLayout,
|
||||
} from "@app/call/video"
|
||||
import {
|
||||
VoiceState,
|
||||
currentVoiceSession,
|
||||
currentVoiceRoom,
|
||||
voiceState,
|
||||
leaveVoiceRoom,
|
||||
toggleMute,
|
||||
cancelJoinVoiceRoom,
|
||||
} from "@app/voice"
|
||||
isLocalSpeaking,
|
||||
} from "@app/call/stores"
|
||||
import {cancelJoinVoiceRoom, leaveVoiceRoom, toggleMute} from "@app/call/voice"
|
||||
|
||||
const {relay, h} = $derived($page.params)
|
||||
const url = $derived(relay ? decodeRelay(relay) : undefined)
|
||||
@@ -41,6 +53,14 @@
|
||||
)
|
||||
const routeDisplayedRoom = $derived($displayedRoomStore)
|
||||
|
||||
const isViewingCurrentVoiceRoom = $derived(
|
||||
$currentVoiceRoom !== undefined &&
|
||||
url !== undefined &&
|
||||
typeof h === "string" &&
|
||||
$currentVoiceRoom.url === url &&
|
||||
$currentVoiceRoom.h === h,
|
||||
)
|
||||
|
||||
const targetRoom = $derived.by((): Room | undefined => {
|
||||
if ($voiceState === VoiceState.Joining || $voiceState === VoiceState.Connected) {
|
||||
return $currentVoiceRoom
|
||||
@@ -66,9 +86,45 @@
|
||||
pushModal(VoiceRoomJoinDialog, {url: targetRoom.url, h: targetRoom.h})
|
||||
}
|
||||
|
||||
const openAudioSettings = () => {
|
||||
const goToRoom = () => {
|
||||
if (!targetRoom) return
|
||||
const path = makeRoomPath(targetRoom.url, targetRoom.h)
|
||||
if ($page.url.pathname !== path) {
|
||||
void goto(path)
|
||||
}
|
||||
}
|
||||
|
||||
const openCallSettings = () => {
|
||||
pushModal(VoiceCallAudioSettingsDialog)
|
||||
}
|
||||
|
||||
const showChatButton = $derived($voiceState === VoiceState.Connected && isViewingCurrentVoiceRoom)
|
||||
|
||||
const isChatPanelActive = $derived(
|
||||
showChatButton &&
|
||||
(isDesktopLayout.current
|
||||
? $videoCallLayout === VideoCallLayout.Split
|
||||
: $videoCallLayout === VideoCallLayout.Chat),
|
||||
)
|
||||
|
||||
const onChatToggle = () => {
|
||||
if (!showChatButton) return
|
||||
if (isDesktopLayout.current) {
|
||||
videoCallLayout.update(p =>
|
||||
p === VideoCallLayout.Split ? VideoCallLayout.Video : VideoCallLayout.Split,
|
||||
)
|
||||
} else {
|
||||
videoCallLayout.update(p =>
|
||||
p === VideoCallLayout.Video ? VideoCallLayout.Chat : VideoCallLayout.Video,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const chatUnread = $derived(
|
||||
targetRoom !== undefined && $notifications.has(makeRoomPath(targetRoom.url, targetRoom.h)),
|
||||
)
|
||||
|
||||
const mediaToggleClass = "center tooltip tooltip-top btn btn-sm btn-square btn-ghost"
|
||||
</script>
|
||||
|
||||
{#if targetRoom}
|
||||
@@ -76,19 +132,47 @@
|
||||
in:fly={{y: 60, duration: 350}}
|
||||
out:fly={{y: 60, duration: 250}}
|
||||
class="flex flex-col gap-2 rounded-box bg-base-100 p-3">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
{#if $voiceState === VoiceState.Joining}
|
||||
<span class="text-sm font-semibold text-warning">Joining...</span>
|
||||
{:else if $voiceState === VoiceState.Connected}
|
||||
<span class="text-sm font-semibold text-success">Voice Connected</span>
|
||||
{:else}
|
||||
<span class="text-sm font-semibold text-neutral-content">Disconnected</span>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 flex-1 rounded-lg px-1 py-0.5 text-left outline-none hover:bg-base-200/60 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-base-100"
|
||||
onclick={goToRoom}
|
||||
aria-label="Open room {roomName}">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
{#if $voiceState === VoiceState.Joining}
|
||||
<span class="text-sm font-semibold text-warning">Joining...</span>
|
||||
{:else if $voiceState === VoiceState.Connected}
|
||||
<span class="text-sm font-semibold text-success">Voice Connected</span>
|
||||
{:else}
|
||||
<span class="text-sm font-semibold text-neutral-content">Disconnected</span>
|
||||
{/if}
|
||||
<span class="ellipsize text-xs opacity-70">
|
||||
{roomName} / {spaceName}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
{#if showChatButton}
|
||||
<Button
|
||||
data-tip="Toggle Chat"
|
||||
class={cx(
|
||||
mediaToggleClass,
|
||||
"relative shrink-0 overflow-visible",
|
||||
isChatPanelActive && "text-primary",
|
||||
)}
|
||||
onclick={onChatToggle}>
|
||||
<span class="relative inline-flex">
|
||||
<Icon icon={ChatRound} size={4} />
|
||||
{#if chatUnread}
|
||||
<span
|
||||
transition:fade={{duration: 150}}
|
||||
class="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-primary ring-2 ring-base-100"
|
||||
aria-hidden="true"></span>
|
||||
{/if}
|
||||
</span>
|
||||
</Button>
|
||||
{/if}
|
||||
<span class="ellipsize text-xs opacity-70">
|
||||
{roomName} / {spaceName}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{#if $voiceState === VoiceState.Joining}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
<Button
|
||||
@@ -100,16 +184,45 @@
|
||||
{:else if $voiceState === VoiceState.Connected && $currentVoiceSession}
|
||||
<Button
|
||||
data-tip={$currentVoiceSession.muted ? "Unmute" : "Mute"}
|
||||
class="center tooltip tooltip-top btn btn-sm btn-square {$currentVoiceSession.muted
|
||||
? 'btn-error'
|
||||
: 'btn-ghost'}"
|
||||
class={cx(
|
||||
mediaToggleClass,
|
||||
"overflow-visible",
|
||||
!$currentVoiceSession.muted && $isLocalSpeaking && "text-primary",
|
||||
$currentVoiceSession.muted &&
|
||||
"text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
|
||||
)}
|
||||
onclick={toggleMute}>
|
||||
<Icon icon={$currentVoiceSession.muted ? MicrophoneOff : Microphone} size={4} />
|
||||
<span class="relative inline-flex items-center justify-center overflow-visible">
|
||||
<Icon icon={Microphone} size={4} />
|
||||
{#if $currentVoiceSession.muted}
|
||||
<span
|
||||
class="pointer-events-none absolute inset-0 flex items-center justify-center overflow-visible"
|
||||
aria-hidden="true">
|
||||
<span
|
||||
class="h-[1.3px] w-[150%] max-w-none shrink-0 -rotate-45 rounded-full bg-current"
|
||||
></span>
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
data-tip="Audio settings"
|
||||
data-tip={$currentVoiceSession.cameraOn ? "Turn off camera" : "Turn on camera"}
|
||||
class={cx(mediaToggleClass, $currentVoiceSession.cameraOn && "text-primary")}
|
||||
onclick={toggleCamera}>
|
||||
<Icon icon={$currentVoiceSession.cameraOn ? VideocameraRecord : Videocamera} size={4} />
|
||||
</Button>
|
||||
{#if !Capacitor.isNativePlatform()}
|
||||
<Button
|
||||
data-tip={$currentVoiceSession.screenShareOn ? "Stop sharing" : "Share screen"}
|
||||
class={cx(mediaToggleClass, $currentVoiceSession.screenShareOn && "text-primary")}
|
||||
onclick={toggleScreenShare}>
|
||||
<Icon icon={Monitor} size={4} />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
data-tip="Call settings"
|
||||
class="center tooltip tooltip-top btn btn-sm btn-square btn-ghost"
|
||||
onclick={openAudioSettings}>
|
||||
onclick={openCallSettings}>
|
||||
<Icon icon={Settings} size={4} />
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
+43
-44
@@ -18,10 +18,10 @@ import {
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import type {UploadTask} from "@welshman/editor"
|
||||
import type {TrustedEvent, EventContent, Profile, PublishedRoomMeta} from "@welshman/util"
|
||||
import {PollResponse} from "nostr-tools/kinds"
|
||||
import {
|
||||
DELETE,
|
||||
REPORT,
|
||||
MESSAGE,
|
||||
PROFILE,
|
||||
MESSAGING_RELAYS,
|
||||
RELAYS,
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
ROOMS,
|
||||
COMMENT,
|
||||
APP_DATA,
|
||||
POLL_RESPONSE,
|
||||
isSignedEvent,
|
||||
makeEvent,
|
||||
normalizeRelayUrl,
|
||||
@@ -52,7 +53,6 @@ import {
|
||||
isPublishedProfile,
|
||||
editProfile,
|
||||
createProfile,
|
||||
uniqTags,
|
||||
ManagementMethod,
|
||||
} from "@welshman/util"
|
||||
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
||||
@@ -73,7 +73,6 @@ import {
|
||||
waitForThunkError,
|
||||
getPubkeyRelays,
|
||||
userBlossomServerList,
|
||||
getThunkError,
|
||||
addRoomMember,
|
||||
manageRelay,
|
||||
getRelay,
|
||||
@@ -84,6 +83,7 @@ import {
|
||||
SETTINGS,
|
||||
PROTECTED,
|
||||
INDEXER_RELAYS,
|
||||
DEFAULT_RELAYS,
|
||||
DEFAULT_BLOSSOM_SERVERS,
|
||||
userSpaceUrls,
|
||||
userSettingsValues,
|
||||
@@ -122,6 +122,34 @@ export const prependParent = (
|
||||
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
|
||||
|
||||
export const broadcastUserData = async (relays: string[]) => {
|
||||
@@ -235,16 +263,12 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||
return stripPrefix(error)
|
||||
}
|
||||
|
||||
export const deriveRelayAuthError = (url: string, claim = "") => {
|
||||
// Kick off the auth process
|
||||
export const deriveRelayAuthError = (url: string) => {
|
||||
Pool.get().get(url).auth.attemptAuth(sign)
|
||||
|
||||
// Attempt to join the relay
|
||||
const thunk = publishJoinRequest({url, claim})
|
||||
|
||||
return derived(
|
||||
[thunk, relaysMostlyRestricted, deriveSocket(url)],
|
||||
([$thunk, $relaysMostlyRestricted, $socket]) => {
|
||||
[relaysMostlyRestricted, deriveSocket(url)],
|
||||
([$relaysMostlyRestricted, $socket]) => {
|
||||
if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) {
|
||||
return stripPrefix($socket.auth.details)
|
||||
}
|
||||
@@ -252,16 +276,6 @@ export const deriveRelayAuthError = (url: string, claim = "") => {
|
||||
if ($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) =>
|
||||
makeEvent(PollResponse, {
|
||||
makeEvent(POLL_RESPONSE, {
|
||||
content: "",
|
||||
tags: [["e", event.id], ...selectedIds.map(selectedId => ["response", selectedId])],
|
||||
})
|
||||
@@ -695,34 +709,18 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
|
||||
// Update Profile
|
||||
|
||||
export const initProfile = (profile: Profile) => {
|
||||
const template = createProfile(profile)
|
||||
const event = makeEvent(PROFILE, createProfile(profile))
|
||||
|
||||
// Start out protected by default
|
||||
template.tags.push(PROTECTED)
|
||||
|
||||
const event = makeEvent(PROFILE, template)
|
||||
|
||||
// Don't publish anywhere yet, wait until they join a space
|
||||
return publishThunk({event, relays: []})
|
||||
return publishThunk({event, relays: DEFAULT_RELAYS})
|
||||
}
|
||||
|
||||
export const updateProfile = ({
|
||||
profile,
|
||||
shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || []),
|
||||
}: {
|
||||
profile: Profile
|
||||
shouldBroadcast?: boolean
|
||||
}) => {
|
||||
export const updateProfile = ({profile}: {profile: Profile}) => {
|
||||
const router = Router.get()
|
||||
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) {
|
||||
scenarios.push(router.FromUser(), router.Index())
|
||||
template.tags = template.tags.filter(nthNe(0, "-"))
|
||||
} else {
|
||||
template.tags = uniqTags([...template.tags, PROTECTED])
|
||||
}
|
||||
// Remove protected tag, we used to add it
|
||||
template.tags = template.tags.filter(nthNe(0, "-"))
|
||||
|
||||
const event = makeEvent(template.kind, template)
|
||||
const relays = router.merge(scenarios).getUrls()
|
||||
@@ -737,9 +735,10 @@ export const addSpaceMembers = async (
|
||||
pubkeys: string[],
|
||||
): Promise<string | undefined> => {
|
||||
const spaceMembers = get(deriveSpaceMembers(url))
|
||||
|
||||
const results = await Promise.all(
|
||||
pubkeys
|
||||
.filter(pubkey => !spaceMembers.includes(pubkey))
|
||||
.filter(pubkey => !spaceMembers || !spaceMembers.includes(pubkey))
|
||||
.map(pubkey =>
|
||||
manageRelay(url, {
|
||||
method: ManagementMethod.AllowPubkey,
|
||||
|
||||
+106
-69
@@ -1,5 +1,6 @@
|
||||
import {get, writable} from "svelte/store"
|
||||
import {writable} from "svelte/store"
|
||||
import {
|
||||
batch,
|
||||
call,
|
||||
uniq,
|
||||
int,
|
||||
@@ -25,7 +26,8 @@ import {
|
||||
sortEventsDesc,
|
||||
} 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 {createScroller} from "@lib/html"
|
||||
import {daysBetween} from "@lib/util"
|
||||
@@ -56,57 +58,75 @@ export const makeFeed = ({
|
||||
let backwardWindow = [at - interval, at]
|
||||
let forwardWindow = [at, at + interval]
|
||||
|
||||
const insertEvent = (event: TrustedEvent) => {
|
||||
let handled = false
|
||||
const insertIntoBuffer = (event: TrustedEvent) => {
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
if (buffer[i].created_at < event.created_at) {
|
||||
buffer.splice(i, 0, event)
|
||||
return
|
||||
}
|
||||
}
|
||||
buffer.push(event)
|
||||
}
|
||||
|
||||
if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) {
|
||||
const $events = get(events)
|
||||
// Batch-insert events into the visible store with a single update
|
||||
const insertEvents = (newEvents: TrustedEvent[]) => {
|
||||
const visible: TrustedEvent[] = []
|
||||
|
||||
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
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
events.set([...$events, event])
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
if (buffer[i].created_at > event.created_at) {
|
||||
buffer.splice(i, 0, event)
|
||||
handled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
buffer.push(event)
|
||||
}
|
||||
return $events
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribers = [
|
||||
on(repository, "update", ({added, removed}) => {
|
||||
if (removed.size > 0) {
|
||||
buffer = buffer.filter(e => !removed.has(e.id))
|
||||
events.update($events => $events.filter(e => !removed.has(e.id)))
|
||||
}
|
||||
on(
|
||||
repository,
|
||||
"update",
|
||||
batch(150, (updates: RepositoryUpdate[]) => {
|
||||
const {added, removed} = mergeRepositoryUpdates(updates)
|
||||
|
||||
for (const event of added) {
|
||||
if (matchFilters(filters, event) && tracker.getRelays(event.id).has(url)) {
|
||||
insertEvent(event)
|
||||
if (removed.size > 0) {
|
||||
buffer = buffer.filter(e => !removed.has(e.id))
|
||||
events.update($events => $events.filter(e => !removed.has(e.id)))
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
const matching = added.filter(
|
||||
event => matchFilters(filters, event) && tracker.getRelays(event.id).has(url),
|
||||
)
|
||||
|
||||
if (matching.length > 0) {
|
||||
insertEvents(matching)
|
||||
}
|
||||
}),
|
||||
),
|
||||
on(tracker, "add", (id: string, trackerUrl: string) => {
|
||||
if (trackerUrl === url) {
|
||||
const event = repository.getEvent(id)
|
||||
|
||||
if (event && matchFilters(filters, event)) {
|
||||
insertEvent(event)
|
||||
insertEvents([event])
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -132,17 +152,15 @@ export const makeFeed = ({
|
||||
element,
|
||||
delay: 300,
|
||||
threshold: 5000,
|
||||
onScroll: () => {
|
||||
onScroll: async () => {
|
||||
const [since, until] = backwardWindow
|
||||
|
||||
backwardWindow = [since - interval, since]
|
||||
|
||||
for (const event of buffer.splice(0, 30)) {
|
||||
insertEvent(event)
|
||||
}
|
||||
insertEvents(buffer.splice(0, 30))
|
||||
|
||||
if (until > now() - int(2, YEAR)) {
|
||||
loadTimeframe(since, until)
|
||||
await loadTimeframe(since, until)
|
||||
} else if (!buffer.some(e => e.created_at < at)) {
|
||||
backwardScroller.stop()
|
||||
onBackwardExhausted?.()
|
||||
@@ -155,17 +173,15 @@ export const makeFeed = ({
|
||||
reverse: true,
|
||||
delay: 300,
|
||||
threshold: 5000,
|
||||
onScroll: () => {
|
||||
onScroll: async () => {
|
||||
const [since, until] = forwardWindow
|
||||
|
||||
forwardWindow = [until, until + interval]
|
||||
|
||||
for (const event of buffer.splice(0, 30)) {
|
||||
insertEvent(event)
|
||||
}
|
||||
insertEvents(buffer.splice(0, 30))
|
||||
|
||||
if (until < now()) {
|
||||
loadTimeframe(since, until)
|
||||
await loadTimeframe(since, until)
|
||||
} else if (!buffer.some(e => e.created_at > at)) {
|
||||
forwardScroller.stop()
|
||||
onForwardExhausted?.()
|
||||
@@ -208,40 +224,61 @@ export const makeCalendarFeed = ({
|
||||
|
||||
const events = writable(sortBy(getStart, getEventsForUrl(url, filters)))
|
||||
|
||||
const insertEvent = (event: TrustedEvent) => {
|
||||
const start = getStart(event)
|
||||
const address = getAddress(event)
|
||||
|
||||
if (isNaN(start) || isNaN(getEnd(event))) return
|
||||
// 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 (let i = 0; i < $events.length; i++) {
|
||||
if ($events[i].id === event.id) return $events
|
||||
if (getStart($events[i]) > start) return insertAt(i, event, $events)
|
||||
}
|
||||
for (const event of valid) {
|
||||
const start = getStart(event)
|
||||
const address = getAddress(event)
|
||||
|
||||
return [...$events.filter(e => getAddress(e) !== address), event]
|
||||
let handled = false
|
||||
for (let i = 0; i < $events.length; i++) {
|
||||
if ($events[i].id === event.id) {
|
||||
handled = true
|
||||
break
|
||||
}
|
||||
if (getStart($events[i]) > start) {
|
||||
$events = insertAt(i, event, $events)
|
||||
handled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
$events = [...$events.filter(e => getAddress(e) !== address), event]
|
||||
}
|
||||
}
|
||||
return $events
|
||||
})
|
||||
}
|
||||
|
||||
const unsubscribers = [
|
||||
on(repository, "update", ({added, removed}) => {
|
||||
if (removed.size > 0) {
|
||||
events.update($events => $events.filter(e => !removed.has(e.id)))
|
||||
}
|
||||
on(
|
||||
repository,
|
||||
"update",
|
||||
batch(150, (updates: RepositoryUpdate[]) => {
|
||||
const {added, removed} = mergeRepositoryUpdates(updates)
|
||||
|
||||
for (const event of added) {
|
||||
if (matchFilters(filters, event)) {
|
||||
insertEvent(event)
|
||||
if (removed.size > 0) {
|
||||
events.update($events => $events.filter(e => !removed.has(e.id)))
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
const matching = added.filter(event => matchFilters(filters, event))
|
||||
|
||||
if (matching.length > 0) {
|
||||
insertEvents(matching)
|
||||
}
|
||||
}),
|
||||
),
|
||||
on(tracker, "add", (id: string, trackerUrl: string) => {
|
||||
if (trackerUrl === url) {
|
||||
const event = repository.getEvent(id)
|
||||
|
||||
if (event && matchFilters(filters, event)) {
|
||||
insertEvent(event)
|
||||
insertEvents([event])
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
+168
-109
@@ -3,12 +3,10 @@ import {context as pomadeContext} from "@pomade/core"
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import {derived, readable, writable} from "svelte/store"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {Poll} from "nostr-tools/kinds"
|
||||
import {
|
||||
on,
|
||||
gt,
|
||||
max,
|
||||
find,
|
||||
spec,
|
||||
call,
|
||||
first,
|
||||
@@ -94,6 +92,7 @@ import {
|
||||
THREAD,
|
||||
CLASSIFIED,
|
||||
WRAP,
|
||||
POLL,
|
||||
PROFILE,
|
||||
ZAP_GOAL,
|
||||
ZAP_REQUEST,
|
||||
@@ -328,9 +327,7 @@ if (ENABLE_ZAPS) {
|
||||
REACTION_KINDS.push(ZAP_RESPONSE)
|
||||
}
|
||||
|
||||
export const CONTENT_KINDS = [ZAP_GOAL, EVENT_TIME, THREAD, CLASSIFIED, Poll]
|
||||
|
||||
export const MESSAGE_KINDS = [...CONTENT_KINDS, MESSAGE]
|
||||
export const CONTENT_KINDS = [ZAP_GOAL, EVENT_TIME, THREAD, CLASSIFIED, POLL]
|
||||
|
||||
export const DM_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]
|
||||
|
||||
@@ -555,7 +552,7 @@ export const chatsById = call(() => {
|
||||
setTimeout(() => {
|
||||
addEvents(added)
|
||||
removeEvents(removed)
|
||||
}, 50)
|
||||
}, 200)
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -563,13 +560,9 @@ export const chatsById = call(() => {
|
||||
})
|
||||
})
|
||||
|
||||
export const deriveChat = call(() => {
|
||||
const _deriveChat = makeDeriveItem(chatsById)
|
||||
export const deriveChat = makeDeriveItem(chatsById)
|
||||
|
||||
return (pubkeys: string[]) => _deriveChat(makeChatId(pubkeys))
|
||||
})
|
||||
|
||||
export const chatSearch = derived(throttled(800, chatsById), $chatsByPubkey => {
|
||||
export const chatSearch = derived(throttled(1500, chatsById), $chatsByPubkey => {
|
||||
return createSearch(
|
||||
sortBy(c => -c.last_activity, Array.from($chatsByPubkey.values())),
|
||||
{
|
||||
@@ -596,6 +589,8 @@ export const getRoomType = (room: RoomMeta): RoomType =>
|
||||
|
||||
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 hasNip29 = (relay?: RelayProfile) =>
|
||||
@@ -608,7 +603,7 @@ export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
|
||||
})
|
||||
|
||||
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()) {
|
||||
const [metaEvents, deleteEvents] = partition(spec({kind: ROOM_META}), events.values())
|
||||
@@ -620,6 +615,8 @@ export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByU
|
||||
}
|
||||
}
|
||||
|
||||
const metaById = new Map<string, Room>()
|
||||
|
||||
for (const event of metaEvents) {
|
||||
const meta = tryCatch(() => readRoomMeta(event))
|
||||
|
||||
@@ -627,22 +624,14 @@ export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByU
|
||||
continue
|
||||
}
|
||||
|
||||
let metaById = metaByIdByUrl.get(url)
|
||||
if (!metaById) {
|
||||
metaById = new Map()
|
||||
metaByIdByUrl.set(url, metaById)
|
||||
}
|
||||
|
||||
const id = makeRoomId(url, meta.h)
|
||||
|
||||
metaById.set(id, {...meta, url, id})
|
||||
}
|
||||
}
|
||||
|
||||
const result = new Map<string, Room[]>()
|
||||
|
||||
for (const [url, metaById] of metaByIdByUrl.entries()) {
|
||||
result.set(url, Array.from(metaById.values()))
|
||||
if (metaById.size > 0) {
|
||||
result.set(url, Array.from(metaById.values()))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -816,37 +805,18 @@ export const deriveOtherRooms = (url: string) =>
|
||||
// Space/room memberships
|
||||
|
||||
export const deriveSpaceMembers = (url: string) =>
|
||||
derived(
|
||||
deriveRelaySignedEvents(url, [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS]}]),
|
||||
$events => {
|
||||
const membersEvent = $events.find(spec({kind: RELAY_MEMBERS}))
|
||||
|
||||
if (membersEvent) {
|
||||
return uniq(getTagValues("member", membersEvent.tags))
|
||||
}
|
||||
|
||||
const members = new Set<string>()
|
||||
|
||||
for (const event of sortBy(e => e.created_at, $events)) {
|
||||
const pubkeys = getPubkeyTagValues(event.tags)
|
||||
|
||||
if (event.kind === RELAY_ADD_MEMBER) {
|
||||
for (const pubkey of pubkeys) {
|
||||
members.add(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind === RELAY_REMOVE_MEMBER) {
|
||||
for (const pubkey of pubkeys) {
|
||||
members.delete(pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(members)
|
||||
},
|
||||
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_MEMBERS]}]), ([event]) =>
|
||||
uniq(getTagValues("member", event?.tags ?? [])),
|
||||
)
|
||||
|
||||
export const deriveRoomMembers = (url: string, h: string) => {
|
||||
const filters: Filter[] = [{kinds: [ROOM_MEMBERS], "#d": [h]}]
|
||||
|
||||
return derived(deriveEventsForUrl(url, filters), ([event]) =>
|
||||
uniq(getPubkeyTagValues(event?.tags ?? [])),
|
||||
)
|
||||
}
|
||||
|
||||
export type BannedPubkeyItem = {
|
||||
pubkey: string
|
||||
reason: string
|
||||
@@ -865,41 +835,6 @@ export const deriveSpaceBannedPubkeyItems = (url: string) => {
|
||||
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 => {
|
||||
const membersEvent = find(spec({kind: ROOM_MEMBERS}), $events)
|
||||
|
||||
if (membersEvent) {
|
||||
return uniq(getPubkeyTagValues(membersEvent.tags))
|
||||
}
|
||||
|
||||
const members = new Set<string>()
|
||||
|
||||
for (const event of sortEventsAsc($events)) {
|
||||
const pubkeys = getPubkeyTagValues(event.tags)
|
||||
|
||||
if (event.kind === ROOM_ADD_MEMBER) {
|
||||
for (const pubkey of pubkeys) {
|
||||
members.add(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind === ROOM_REMOVE_MEMBER) {
|
||||
for (const pubkey of pubkeys) {
|
||||
members.delete(pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(members)
|
||||
})
|
||||
}
|
||||
|
||||
export const deriveRoomAdmins = (url: string, h: string) => {
|
||||
const filters: Filter[] = [{kinds: [ROOM_ADMINS], "#d": [h]}]
|
||||
|
||||
@@ -914,6 +849,42 @@ export const deriveRoomAdmins = (url: string, h: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const getRoomMembers = (_url: string, h: string, events: TrustedEvent[]) => {
|
||||
const members = new Set<string>()
|
||||
|
||||
for (const event of sortEventsAsc(events)) {
|
||||
if (event.kind === ROOM_MEMBERS && getTagValue("d", event.tags) === h) {
|
||||
members.clear()
|
||||
|
||||
for (const pubkey of uniq(getPubkeyTagValues(event.tags))) {
|
||||
members.add(pubkey)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (getTagValue("h", event.tags) !== h) {
|
||||
continue
|
||||
}
|
||||
|
||||
const pubkeys = getPubkeyTagValues(event.tags)
|
||||
|
||||
if (event.kind === ROOM_ADD_MEMBER) {
|
||||
for (const pubkey of pubkeys) {
|
||||
members.add(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind === ROOM_REMOVE_MEMBER) {
|
||||
for (const pubkey of pubkeys) {
|
||||
members.delete(pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(members)
|
||||
}
|
||||
|
||||
// Action items (admin review queue)
|
||||
// const pendingJoins: TrustedEvent[] = []
|
||||
|
||||
@@ -921,7 +892,7 @@ export const deriveSpaceActionItems = (url: string) =>
|
||||
derived(
|
||||
deriveEventsForUrl(url, [
|
||||
{
|
||||
kinds: [REPORT, ROOM_JOIN, ROOM_LEAVE, ROOM_MEMBERS],
|
||||
kinds: [REPORT, ROOM_JOIN, ROOM_LEAVE, ROOM_MEMBERS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
|
||||
},
|
||||
]),
|
||||
$events => {
|
||||
@@ -934,19 +905,50 @@ export const deriveSpaceActionItems = (url: string) =>
|
||||
for (const [h, roomEvents] of groupBy(getRoomId, $events)) {
|
||||
if (!h) continue
|
||||
|
||||
const roomJoins = roomEvents.filter(spec({kind: ROOM_JOIN}))
|
||||
const roomLeaves = roomEvents.filter(spec({kind: ROOM_LEAVE}))
|
||||
const roomMembersEvent = roomEvents.find(spec({kind: ROOM_MEMBERS}))
|
||||
const roomMembers = getTagValues("p", roomMembersEvent?.tags ?? [])
|
||||
const roomJoins: TrustedEvent[] = []
|
||||
const roomLeaves: TrustedEvent[] = []
|
||||
const roomMembershipEvents: TrustedEvent[] = []
|
||||
|
||||
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))
|
||||
|
||||
pendingJoins.push(
|
||||
...removeUndefined(
|
||||
Array.from(groupBy(e => e.pubkey, roomJoins).values())
|
||||
.map(sortEventsDesc)
|
||||
.map(first),
|
||||
Array.from(groupBy(e => e.pubkey, roomJoins).values()).map(events =>
|
||||
first(sortEventsDesc(events)),
|
||||
),
|
||||
).filter(({pubkey, created_at}) => {
|
||||
if (roomMembers.includes(pubkey)) return false
|
||||
if (gt(roomMembersEvent?.created_at, created_at)) return false
|
||||
if (roomMembers.has(pubkey)) return false
|
||||
if (
|
||||
roomMembershipEvents.some(event => {
|
||||
if (event.created_at <= created_at) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (event.kind === ROOM_MEMBERS) {
|
||||
return true
|
||||
}
|
||||
|
||||
return getPubkeyTagValues(event.tags).includes(pubkey)
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (roomLeaves.some(e => e.pubkey === pubkey && e.created_at > created_at)) return false
|
||||
|
||||
return true
|
||||
@@ -979,19 +981,49 @@ export const deriveUserIsSpaceAdmin = memoize((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(
|
||||
[
|
||||
pubkey,
|
||||
deriveSpaceMembers(url),
|
||||
deriveEventsForUrl(url, filters),
|
||||
deriveRelaySignedEvents(url, memberListFilters),
|
||||
deriveRelaySignedEvents(url, userEventFilters),
|
||||
deriveEventsForUrl(url, [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]),
|
||||
deriveUserIsSpaceAdmin(url),
|
||||
],
|
||||
([$pubkey, $members, $events, $isAdmin]) => {
|
||||
const isMember = $members.includes($pubkey!) || $isAdmin
|
||||
([$pubkey, $memberListEvents, $userAddRemoveEvents, $joinLeaveEvents, $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) {
|
||||
continue
|
||||
}
|
||||
@@ -1017,19 +1049,46 @@ export const deriveUserIsRoomAdmin = (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(
|
||||
[
|
||||
pubkey,
|
||||
deriveRoomMembers(url, h),
|
||||
deriveEventsForUrl(url, filters),
|
||||
deriveEventsForUrl(url, userEventFilters),
|
||||
deriveEventsForUrl(url, joinLeaveFilters),
|
||||
deriveUserIsRoomAdmin(url, h),
|
||||
],
|
||||
([$pubkey, $members, $events, $isAdmin]) => {
|
||||
const isMember = $members.includes($pubkey!) || $isAdmin
|
||||
([$pubkey, $memberList, $userAddRemoveEvents, $joinLeaveEvents, $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) {
|
||||
continue
|
||||
}
|
||||
|
||||
+18
-57
@@ -1,7 +1,6 @@
|
||||
import {page} from "$app/stores"
|
||||
import type {Unsubscriber} from "svelte/store"
|
||||
import {last, call, ifLet, assoc, chunk, WEEK, ago} from "@welshman/lib"
|
||||
import {PollResponse} from "nostr-tools/kinds"
|
||||
import {last, call, assoc, chunk, WEEK, ago} from "@welshman/lib"
|
||||
import {merged} from "@welshman/store"
|
||||
import {
|
||||
getListTags,
|
||||
@@ -13,13 +12,16 @@ import {
|
||||
ROOM_MEMBERS,
|
||||
ROOM_ADD_MEMBER,
|
||||
ROOM_REMOVE_MEMBER,
|
||||
ROOM_JOIN,
|
||||
ROOM_LEAVE,
|
||||
ROOM_CREATE_PERMISSION,
|
||||
RELAY_MEMBERS,
|
||||
RELAY_ADD_MEMBER,
|
||||
RELAY_REMOVE_MEMBER,
|
||||
MESSAGE,
|
||||
POLL_RESPONSE,
|
||||
isSignedEvent,
|
||||
unionFilters,
|
||||
getTagValue,
|
||||
} from "@welshman/util"
|
||||
import type {Filter, List, PublishedList, TrustedEvent} from "@welshman/util"
|
||||
import {request, requestOne, Difference, DifferenceEvent} from "@welshman/net"
|
||||
@@ -41,7 +43,6 @@ import {
|
||||
} from "@welshman/app"
|
||||
import {
|
||||
REACTION_KINDS,
|
||||
MESSAGE_KINDS,
|
||||
CONTENT_KINDS,
|
||||
INDEXER_RELAYS,
|
||||
loadSettings,
|
||||
@@ -55,7 +56,7 @@ import {
|
||||
loadFeedsForPubkey,
|
||||
} from "@app/core/state"
|
||||
import {hasBlossomSupport} from "@app/core/commands"
|
||||
import {LIVEKIT_PARTICIPANTS} from "@app/voice"
|
||||
import {LIVEKIT_PARTICIPANTS} from "@app/call/voice"
|
||||
|
||||
// Utils
|
||||
|
||||
@@ -266,53 +267,21 @@ const syncUserData = () => {
|
||||
|
||||
// Spaces
|
||||
|
||||
const syncSpace = (url: string, rooms: string[]) => {
|
||||
const syncSpace = (url: string) => {
|
||||
const since = ago(WEEK)
|
||||
const seen = new Set<string>()
|
||||
const controller = new AbortController()
|
||||
|
||||
const pullRoomContent = (room: string) => {
|
||||
if (!seen.has(room)) {
|
||||
seen.add(room)
|
||||
pullAndListen({
|
||||
url,
|
||||
signal: controller.signal,
|
||||
filters: [
|
||||
{kinds: MESSAGE_KINDS, since, "#h": [room]},
|
||||
makeCommentFilter(CONTENT_KINDS, {since, "#h": [room]}),
|
||||
{kinds: [PollResponse], since},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const room of rooms) {
|
||||
pullRoomContent(room)
|
||||
}
|
||||
|
||||
const relayKinds = [RELAY_MEMBERS, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]
|
||||
const relayKinds = [RELAY_MEMBERS]
|
||||
const roomMetaKinds = [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS]
|
||||
const roomMemberKinds = [ROOM_DELETE, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]
|
||||
const roomDeleteKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE]
|
||||
|
||||
pullAndListen({
|
||||
url,
|
||||
signal: controller.signal,
|
||||
filters: [
|
||||
{kinds: [...relayKinds, ...roomMetaKinds, ...roomMemberKinds, ...MESSAGE_KINDS]},
|
||||
{kinds: [...relayKinds, ...roomMetaKinds, ...roomDeleteKinds, ...CONTENT_KINDS, MESSAGE]},
|
||||
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()
|
||||
@@ -321,36 +290,28 @@ const syncSpace = (url: string, rooms: string[]) => {
|
||||
const syncSpaces = () => {
|
||||
const store = merged([userGroupList, page])
|
||||
const unsubscribersByUrl = new Map<string, Unsubscriber>()
|
||||
const roomsByUrl = new Map<string, string>()
|
||||
|
||||
const unsubscribe = store.subscribe(([$userGroupList, $page]) => {
|
||||
const urls = new Set(getSpaceUrlsFromGroupList($userGroupList))
|
||||
const currentUrl = $page.params.relay ? decodeRelay($page.params.relay) : undefined
|
||||
|
||||
if ($page.params.relay) {
|
||||
urls.add(decodeRelay($page.params.relay))
|
||||
if (currentUrl) {
|
||||
urls.add(currentUrl)
|
||||
}
|
||||
|
||||
// Stop syncing removed spaces
|
||||
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
|
||||
if (!urls.has(url)) {
|
||||
unsubscribersByUrl.delete(url)
|
||||
roomsByUrl.delete(url)
|
||||
unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
// Start or restart syncing for each space
|
||||
// Start syncing for new spaces
|
||||
for (const url of urls) {
|
||||
const rooms = getSpaceRoomsFromGroupList(url, $userGroupList)
|
||||
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))
|
||||
if (!unsubscribersByUrl.has(url)) {
|
||||
unsubscribersByUrl.set(url, syncSpace(url))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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 {dec, inc} from "@welshman/lib"
|
||||
import {throttled} from "@welshman/store"
|
||||
import type {PublishedProfile} from "@welshman/util"
|
||||
import type {PublishedProfile, RoomMeta} from "@welshman/util"
|
||||
import {
|
||||
createSearch,
|
||||
profiles,
|
||||
@@ -14,12 +14,27 @@ import {
|
||||
getWotGraph,
|
||||
} from "@welshman/app"
|
||||
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 {makeMentionNodeView} from "@app/editor/MentionNodeView"
|
||||
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 {deriveSpaceMembers} from "@app/core/state"
|
||||
import {
|
||||
deriveSpaceMembers,
|
||||
makeRoomId,
|
||||
splitRoomId,
|
||||
userSpaceUrls,
|
||||
roomsByUrl,
|
||||
} from "@app/core/state"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
export const makeEditor = async ({
|
||||
@@ -64,7 +79,7 @@ export const makeEditor = async ({
|
||||
getValue: (profile: PublishedProfile) => profile.event.pubkey,
|
||||
sortFn: ({score = 1, item}) => {
|
||||
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
|
||||
},
|
||||
@@ -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({
|
||||
content: typeof content === "string" ? escapeHtml(content) : content,
|
||||
editorProps,
|
||||
element: document.createElement("div"),
|
||||
extensions: [
|
||||
RoomReferenceExtension,
|
||||
WelshmanExtension.configure({
|
||||
submit,
|
||||
extensions: {
|
||||
@@ -128,6 +168,29 @@ export const makeEditor = async ({
|
||||
|
||||
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
|
||||
},
|
||||
}),
|
||||
@@ -137,6 +200,7 @@ export const makeEditor = async ({
|
||||
},
|
||||
},
|
||||
}),
|
||||
NativeClipboardPasteExtension,
|
||||
],
|
||||
onUpdate({editor}) {
|
||||
wordCount?.set(editor.storage.wordCount.words)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {App} from "@capacitor/app"
|
||||
import {Capacitor} from "@capacitor/core"
|
||||
import {Keyboard} from "@capacitor/keyboard"
|
||||
import {noop} from "@welshman/lib"
|
||||
@@ -13,9 +14,16 @@ export const syncKeyboard = () => {
|
||||
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 () => {
|
||||
showListener.then(listener => listener.remove())
|
||||
hideListener.then(listener => listener.remove())
|
||||
resumeListener.then(listener => listener.remove())
|
||||
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 {Nip46Broker} from "@welshman/signer"
|
||||
import {makeSecret} from "@welshman/util"
|
||||
@@ -11,6 +11,22 @@ import {
|
||||
} from "@app/core/state"
|
||||
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 {
|
||||
url = 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() {
|
||||
this.broker.cleanup()
|
||||
this.abortController.abort()
|
||||
|
||||
@@ -5,10 +5,10 @@ import {pubkey, tracker, repository, relaysByUrl} from "@welshman/app"
|
||||
import {assoc, prop, first, identity, groupBy, now} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
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 {
|
||||
MESSAGE_KINDS,
|
||||
CONTENT_KINDS,
|
||||
notificationSettings,
|
||||
chatsById,
|
||||
userGroupList,
|
||||
@@ -85,7 +85,7 @@ export const allNotifications = derived(
|
||||
deriveEventsByIdByUrl({
|
||||
tracker,
|
||||
repository,
|
||||
filters: [{kinds: MESSAGE_KINDS}, makeCommentFilter(MESSAGE_KINDS)],
|
||||
filters: [{kinds: [MESSAGE, ...CONTENT_KINDS]}, makeCommentFilter(CONTENT_KINDS)],
|
||||
}),
|
||||
],
|
||||
identity,
|
||||
|
||||
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user