Compare commits
10 Commits
5a804d094f
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 74d3a25461 | |||
| 1eb75f4ae7 | |||
| 09adf33333 | |||
| fdb4f859ee | |||
| b428ad3d36 | |||
| 18b52a37bb | |||
| 7fba1752a1 | |||
| c44c3793fa | |||
| 7ec5a28d1f | |||
| fd4e7a9f2d |
@@ -1,6 +1,6 @@
|
|||||||
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||||
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
||||||
VITE_DEFAULT_SPACES=https://chat.flotilla.social/
|
VITE_DEFAULT_SPACES=https://support.flotilla.social/
|
||||||
VITE_POMADE_SIGNERS=https://pomade.coracle.social,https://pomade.fiatjaf.com,https://pomade.nostrver.se,https://pomade.scuttle.works
|
VITE_POMADE_SIGNERS=https://pomade.coracle.social,https://pomade.fiatjaf.com,https://pomade.nostrver.se,https://pomade.scuttle.works
|
||||||
VITE_PLATFORM_URL=https://app.flotilla.social
|
VITE_PLATFORM_URL=https://app.flotilla.social
|
||||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ src/
|
|||||||
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
|
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
|
||||||
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
|
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
|
||||||
- Instead of `getTag(tagName, event.tags)?.[1] || ""`, use `getTagValue(tagName, event.tags)`
|
- Instead of `getTag(tagName, event.tags)?.[1] || ""`, use `getTagValue(tagName, event.tags)`
|
||||||
|
- Do not render a profile's `about` directly (e.g. `profile.about`); use the `ProfileInfo` component instead.
|
||||||
|
- Use `type Props` instead of interface when defining props for svelte components.
|
||||||
|
- When a component's value/prop shape mirrors a subset of an existing type, derive it with `Pick`/`Partial` and `export` that type from the component's `<script module>` (e.g. a `Values` type) for callers to import, instead of re-enumerating its sub-properties.
|
||||||
|
|
||||||
**Human-First Simplicity (Jon Staab Style):**
|
**Human-First Simplicity (Jon Staab Style):**
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 1.8.2
|
||||||
|
|
||||||
|
* Fix thread board styling
|
||||||
|
* Fix space joining errors
|
||||||
|
|
||||||
|
# 1.8.1
|
||||||
|
|
||||||
|
* Add space dashboard and directory
|
||||||
|
* Add space roles
|
||||||
|
* Fix UI bugs
|
||||||
|
* Redesign threads as a linear phpBB-style forum view
|
||||||
|
* Unwrap messages that are only quotes
|
||||||
|
* Use direct zapping for the donate page, link to flotilla space for support
|
||||||
|
* Speed up feed loading
|
||||||
|
* Fix bunker login
|
||||||
|
* Add welshman skill
|
||||||
|
* Replace zap slider with common amount pills
|
||||||
|
* Make join rejections due to an empty claim more forgiving
|
||||||
|
* Show voice room participants before joining
|
||||||
|
* Fix direct links to spaces
|
||||||
|
* Show per-relay publish status on outgoing messages
|
||||||
|
* Fix A/V call bugs
|
||||||
|
* Sync checked read state for cross-device badges
|
||||||
|
* Fix deleted rooms persisting in navigation
|
||||||
|
* Turn on notification defaults and prompt on first DM visit
|
||||||
|
|
||||||
# 1.8.0
|
# 1.8.0
|
||||||
|
|
||||||
* Fix relay badge overflow
|
* Fix relay badge overflow
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ RUN corepack enable
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_OPTIONS=--max_old_space_size=16384
|
ENV NODE_OPTIONS=--max_old_space_size=16384
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
RUN pnpm i --frozen-lockfile
|
RUN pnpm i --frozen-lockfile
|
||||||
COPY . .
|
COPY . .
|
||||||
ARG VITE_BUILD_HASH
|
ARG VITE_BUILD_HASH
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ android {
|
|||||||
applicationId "social.flotilla"
|
applicationId "social.flotilla"
|
||||||
minSdk rootProject.ext.minSdkVersion
|
minSdk rootProject.ext.minSdkVersion
|
||||||
targetSdk rootProject.ext.targetSdkVersion
|
targetSdk rootProject.ext.targetSdkVersion
|
||||||
versionCode 47
|
versionCode 49
|
||||||
versionName "1.8.0"
|
versionName "1.8.2"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ android {
|
|||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':aparajita-capacitor-secure-storage')
|
implementation project(':aparajita-capacitor-secure-storage')
|
||||||
implementation project(':capacitor-community-safe-area')
|
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
implementation project(':capacitor-clipboard')
|
implementation project(':capacitor-clipboard')
|
||||||
implementation project(':capacitor-filesystem')
|
implementation project(':capacitor-filesystem')
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
<item name="android:background">@null</item>
|
<item name="android:background">@null</item>
|
||||||
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:ignore="NewApi">true</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,36 @@
|
|||||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
include ':capacitor-android'
|
include ':capacitor-android'
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/android/capacitor')
|
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.3.4_@capacitor+core@8.3.4/node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
include ':aparajita-capacitor-secure-storage'
|
include ':aparajita-capacitor-secure-storage'
|
||||||
project(':aparajita-capacitor-secure-storage').projectDir = new File('../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage/android')
|
project(':aparajita-capacitor-secure-storage').projectDir = new File('../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage/android')
|
||||||
|
|
||||||
include ':capacitor-community-safe-area'
|
|
||||||
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area/android')
|
|
||||||
|
|
||||||
include ':capacitor-app'
|
include ':capacitor-app'
|
||||||
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app/android')
|
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@8.1.0_@capacitor+core@8.3.4/node_modules/@capacitor/app/android')
|
||||||
|
|
||||||
include ':capacitor-clipboard'
|
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')
|
project(':capacitor-clipboard').projectDir = new File('../node_modules/.pnpm/@capacitor+clipboard@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/clipboard/android')
|
||||||
|
|
||||||
include ':capacitor-filesystem'
|
include ':capacitor-filesystem'
|
||||||
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem/android')
|
project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@8.1.2_@capacitor+core@8.3.4/node_modules/@capacitor/filesystem/android')
|
||||||
|
|
||||||
include ':capacitor-keyboard'
|
include ':capacitor-keyboard'
|
||||||
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/keyboard/android')
|
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@8.0.3_@capacitor+core@8.3.4/node_modules/@capacitor/keyboard/android')
|
||||||
|
|
||||||
include ':capacitor-preferences'
|
include ':capacitor-preferences'
|
||||||
project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences/android')
|
project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/preferences/android')
|
||||||
|
|
||||||
include ':capacitor-push-notifications'
|
include ':capacitor-push-notifications'
|
||||||
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications/android')
|
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@8.1.1_@capacitor+core@8.3.4/node_modules/@capacitor/push-notifications/android')
|
||||||
|
|
||||||
include ':capacitor-share'
|
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')
|
project(':capacitor-share').projectDir = new File('../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/share/android')
|
||||||
|
|
||||||
include ':capawesome-capacitor-android-dark-mode-support'
|
include ':capawesome-capacitor-android-dark-mode-support'
|
||||||
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-android-dark-mode-support/android')
|
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@8.0.1_@capacitor+core@8.3.4/node_modules/@capawesome/capacitor-android-dark-mode-support/android')
|
||||||
|
|
||||||
include ':capawesome-capacitor-badge'
|
include ':capawesome-capacitor-badge'
|
||||||
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge/android')
|
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.2_@capacitor+core@8.3.4/node_modules/@capawesome/capacitor-badge/android')
|
||||||
|
|
||||||
include ':nostr-signer-capacitor-plugin'
|
include ':nostr-signer-capacitor-plugin'
|
||||||
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_a3c0fb15d5bfa83f24d0070ca2583fc9/node_modules/nostr-signer-capacitor-plugin/android')
|
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_fd4b5c957724da9f45e5678eca5c7fd4/node_modules/nostr-signer-capacitor-plugin/android')
|
||||||
|
|||||||
+1
-5
@@ -7,22 +7,18 @@ const config: CapacitorConfig = {
|
|||||||
ios: {
|
ios: {
|
||||||
scheme: "Flotilla Chat",
|
scheme: "Flotilla Chat",
|
||||||
},
|
},
|
||||||
android: {
|
|
||||||
adjustMarginsForEdgeToEdge: true,
|
|
||||||
},
|
|
||||||
plugins: {
|
plugins: {
|
||||||
CapacitorHttp: {
|
CapacitorHttp: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
SystemBars: {
|
SystemBars: {
|
||||||
insetsHandling: "enable",
|
insetsHandling: "css",
|
||||||
},
|
},
|
||||||
SplashScreen: {
|
SplashScreen: {
|
||||||
androidSplashResourceName: "splash",
|
androidSplashResourceName: "splash",
|
||||||
},
|
},
|
||||||
Keyboard: {
|
Keyboard: {
|
||||||
style: "DARK",
|
style: "DARK",
|
||||||
resizeOnFullScreen: true,
|
|
||||||
},
|
},
|
||||||
Badge: {
|
Badge: {
|
||||||
persist: true,
|
persist: true,
|
||||||
|
|||||||
@@ -372,7 +372,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 38;
|
CURRENT_PROJECT_VERSION = 40;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
@@ -381,7 +381,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.8.0;
|
MARKETING_VERSION = 1.8.2;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -401,7 +401,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 38;
|
CURRENT_PROJECT_VERSION = 40;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
@@ -410,7 +410,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.8.0;
|
MARKETING_VERSION = 1.8.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
+12
-13
@@ -1,4 +1,4 @@
|
|||||||
require_relative '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios/scripts/pods_helpers'
|
require_relative '../../node_modules/.pnpm/@capacitor+ios@8.3.4_@capacitor+core@8.3.4/node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||||
|
|
||||||
platform :ios, '15.0'
|
platform :ios, '15.0'
|
||||||
use_frameworks!
|
use_frameworks!
|
||||||
@@ -9,19 +9,18 @@ use_frameworks!
|
|||||||
install! 'cocoapods', :disable_input_output_paths => true
|
install! 'cocoapods', :disable_input_output_paths => true
|
||||||
|
|
||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@8.3.4_@capacitor+core@8.3.4/node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@8.3.4_@capacitor+core@8.3.4/node_modules/@capacitor/ios'
|
||||||
pod 'AparajitaCapacitorSecureStorage', :path => '../../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage'
|
pod 'AparajitaCapacitorSecureStorage', :path => '../../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage'
|
||||||
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor-community/safe-area'
|
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@8.1.0_@capacitor+core@8.3.4/node_modules/@capacitor/app'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/app'
|
pod 'CapacitorClipboard', :path => '../../node_modules/.pnpm/@capacitor+clipboard@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/clipboard'
|
||||||
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.2_@capacitor+core@8.3.4/node_modules/@capacitor/filesystem'
|
||||||
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@8.1.0_@capacitor+core@8.0.1/node_modules/@capacitor/filesystem'
|
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@8.0.3_@capacitor+core@8.3.4/node_modules/@capacitor/keyboard'
|
||||||
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/keyboard'
|
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/preferences'
|
||||||
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences'
|
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.1.1_@capacitor+core@8.3.4/node_modules/@capacitor/push-notifications'
|
||||||
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications'
|
pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/share'
|
||||||
pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.0.1/node_modules/@capacitor/share'
|
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.2_@capacitor+core@8.3.4/node_modules/@capawesome/capacitor-badge'
|
||||||
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge'
|
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_fd4b5c957724da9f45e5678eca5c7fd4/node_modules/nostr-signer-capacitor-plugin'
|
||||||
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_a3c0fb15d5bfa83f24d0070ca2583fc9/node_modules/nostr-signer-capacitor-plugin'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'Flotilla Chat' do
|
target 'Flotilla Chat' do
|
||||||
|
|||||||
+2
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "1.8.0",
|
"version": "1.8.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -45,7 +45,6 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aparajita/capacitor-secure-storage": "^8.0.0",
|
"@aparajita/capacitor-secure-storage": "^8.0.0",
|
||||||
"@capacitor-community/safe-area": "^8.0.1",
|
|
||||||
"@capacitor/android": "^8.0.1",
|
"@capacitor/android": "^8.0.1",
|
||||||
"@capacitor/app": "^8.0.0",
|
"@capacitor/app": "^8.0.0",
|
||||||
"@capacitor/cli": "^8.0.1",
|
"@capacitor/cli": "^8.0.1",
|
||||||
@@ -63,7 +62,7 @@
|
|||||||
"@getalby/sdk": "^5.1.2",
|
"@getalby/sdk": "^5.1.2",
|
||||||
"@hono/node-server": "^2.0.0",
|
"@hono/node-server": "^2.0.0",
|
||||||
"@noble/curves": "^1.9.7",
|
"@noble/curves": "^1.9.7",
|
||||||
"@pomade/core": "^0.3.0",
|
"@pomade/core": "^0.3.1",
|
||||||
"@poppanator/sveltekit-svg": "^7.0.0",
|
"@poppanator/sveltekit-svg": "^7.0.0",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@tiptap/core": "^2.27.2",
|
"@tiptap/core": "^2.27.2",
|
||||||
|
|||||||
Generated
+14
-26
@@ -14,9 +14,6 @@ importers:
|
|||||||
'@aparajita/capacitor-secure-storage':
|
'@aparajita/capacitor-secure-storage':
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.0.0
|
version: 8.0.0
|
||||||
'@capacitor-community/safe-area':
|
|
||||||
specifier: ^8.0.1
|
|
||||||
version: 8.0.1(@capacitor/core@8.3.4)
|
|
||||||
'@capacitor/android':
|
'@capacitor/android':
|
||||||
specifier: ^8.0.1
|
specifier: ^8.0.1
|
||||||
version: 8.3.4(@capacitor/core@8.3.4)
|
version: 8.3.4(@capacitor/core@8.3.4)
|
||||||
@@ -69,8 +66,8 @@ importers:
|
|||||||
specifier: ^1.9.7
|
specifier: ^1.9.7
|
||||||
version: 1.9.7
|
version: 1.9.7
|
||||||
'@pomade/core':
|
'@pomade/core':
|
||||||
specifier: ^0.3.0
|
specifier: ^0.3.1
|
||||||
version: 0.3.0(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/signer@0.8.16(@noble/curves@1.9.7)(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/436adec9ed1e71569748cd56aa697f361e7a8d47(@capacitor/core@8.3.4))(nostr-tools@2.23.5(typescript@5.9.3)))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-tools@2.23.5(typescript@5.9.3))
|
version: 0.3.1(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/signer@0.8.16(@noble/curves@1.9.7)(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/436adec9ed1e71569748cd56aa697f361e7a8d47(@capacitor/core@8.3.4))(nostr-tools@2.23.5(typescript@5.9.3)))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-tools@2.23.5(typescript@5.9.3))
|
||||||
'@poppanator/sveltekit-svg':
|
'@poppanator/sveltekit-svg':
|
||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.0.0(rollup@4.60.4)(svelte@5.55.9(@typescript-eslint/types@8.60.0))(vite@6.4.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))
|
version: 7.0.0(rollup@4.60.4)(svelte@5.55.9(@typescript-eslint/types@8.60.0))(vite@6.4.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))
|
||||||
@@ -97,7 +94,7 @@ importers:
|
|||||||
version: 1.1.0(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.9(@typescript-eslint/types@8.60.0))(vite@6.4.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)))(svelte@5.55.9(@typescript-eslint/types@8.60.0))(typescript@5.9.3)(vite@6.4.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)))(@vite-pwa/assets-generator@1.0.2)(vite@6.4.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))(workbox-build@7.4.1)(workbox-window@7.4.1)
|
version: 1.1.0(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.9(@typescript-eslint/types@8.60.0))(vite@6.4.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)))(svelte@5.55.9(@typescript-eslint/types@8.60.0))(typescript@5.9.3)(vite@6.4.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)))(@vite-pwa/assets-generator@1.0.2)(vite@6.4.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))(workbox-build@7.4.1)(workbox-window@7.4.1)
|
||||||
'@welshman/app':
|
'@welshman/app':
|
||||||
specifier: ^0.8.16
|
specifier: ^0.8.16
|
||||||
version: 0.8.16(7683b6be0f65191b839378ceee4e4014)
|
version: 0.8.16(9e2dd3230191940679c41b23e5e365c3)
|
||||||
'@welshman/content':
|
'@welshman/content':
|
||||||
specifier: ^0.8.16
|
specifier: ^0.8.16
|
||||||
version: 0.8.16(nostr-tools@2.23.5(typescript@5.9.3))
|
version: 0.8.16(nostr-tools@2.23.5(typescript@5.9.3))
|
||||||
@@ -779,11 +776,6 @@ packages:
|
|||||||
'@canvas/image-data@1.1.0':
|
'@canvas/image-data@1.1.0':
|
||||||
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
|
resolution: {integrity: sha512-QdObRRjRbcXGmM1tmJ+MrHcaz1MftF2+W7YI+MsphnsCrmtyfS0d5qJbk0MeSbUeyM/jCb0hmnkXPsy026L7dA==}
|
||||||
|
|
||||||
'@capacitor-community/safe-area@8.0.1':
|
|
||||||
resolution: {integrity: sha512-zVVQ7k94DbOff1mHP8qrZGTJZWyhGZnKHD2eXdcobIoOBFW9CeVphLNZYDCop/DBsHXfv6r8k0/Ac8xMIlwT+A==}
|
|
||||||
peerDependencies:
|
|
||||||
'@capacitor/core': '>=8.0.0'
|
|
||||||
|
|
||||||
'@capacitor/android@8.3.4':
|
'@capacitor/android@8.3.4':
|
||||||
resolution: {integrity: sha512-7gJjrG3X32Am1QMLqgMztWTYMLMVNE+VZwhekNxhvYizH4mOV05vH+rC9B+f17bCkYZfyu/qXQX6hoY7kLeVZw==}
|
resolution: {integrity: sha512-7gJjrG3X32Am1QMLqgMztWTYMLMVNE+VZwhekNxhvYizH4mOV05vH+rC9B+f17bCkYZfyu/qXQX6hoY7kLeVZw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1476,9 +1468,9 @@ packages:
|
|||||||
'@polka/url@1.0.0-next.29':
|
'@polka/url@1.0.0-next.29':
|
||||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||||
|
|
||||||
'@pomade/core@0.3.0':
|
'@pomade/core@0.3.1':
|
||||||
resolution: {integrity: sha512-zWx0wJftbW92GSIEdLnOk8oUzaGAd0DbOqCBOoNeyCtgn9i4aNy0QsVmYDwxI8xKtVujsxppqk+fMJvinIiEqA==}
|
resolution: {integrity: sha512-lNsM60bu2o9JlqPc47JoAz19QACXS5dNYgvoeApLW8LpxuWy7RcMyHDZ3llklVPYd99PXslFtBdVRpbP26oKyQ==}
|
||||||
version: 0.3.0
|
version: 0.3.1
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@frostr/bifrost': ^1.0.7
|
'@frostr/bifrost': ^1.0.7
|
||||||
@@ -5847,10 +5839,6 @@ snapshots:
|
|||||||
|
|
||||||
'@canvas/image-data@1.1.0': {}
|
'@canvas/image-data@1.1.0': {}
|
||||||
|
|
||||||
'@capacitor-community/safe-area@8.0.1(@capacitor/core@8.3.4)':
|
|
||||||
dependencies:
|
|
||||||
'@capacitor/core': 8.3.4
|
|
||||||
|
|
||||||
'@capacitor/android@8.3.4(@capacitor/core@8.3.4)':
|
'@capacitor/android@8.3.4(@capacitor/core@8.3.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@capacitor/core': 8.3.4
|
'@capacitor/core': 8.3.4
|
||||||
@@ -6013,7 +6001,7 @@ snapshots:
|
|||||||
|
|
||||||
'@emnapi/runtime@1.10.0':
|
'@emnapi/runtime@1.10.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.2
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.25.12':
|
'@esbuild/aix-ppc64@0.25.12':
|
||||||
@@ -6318,7 +6306,7 @@ snapshots:
|
|||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
tree-kill: 1.2.2
|
tree-kill: 1.2.2
|
||||||
tslib: 2.6.2
|
tslib: 2.8.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -6336,7 +6324,7 @@ snapshots:
|
|||||||
'@ionic/utils-stream@3.1.6':
|
'@ionic/utils-stream@3.1.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
tslib: 2.6.2
|
tslib: 2.8.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -6356,7 +6344,7 @@ snapshots:
|
|||||||
'@ionic/utils-terminal': 2.3.4
|
'@ionic/utils-terminal': 2.3.4
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
tslib: 2.6.2
|
tslib: 2.8.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -6381,7 +6369,7 @@ snapshots:
|
|||||||
slice-ansi: 4.0.0
|
slice-ansi: 4.0.0
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
tslib: 2.6.2
|
tslib: 2.8.1
|
||||||
untildify: 4.0.0
|
untildify: 4.0.0
|
||||||
wrap-ansi: 7.0.0
|
wrap-ansi: 7.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -6578,7 +6566,7 @@ snapshots:
|
|||||||
|
|
||||||
'@polka/url@1.0.0-next.29': {}
|
'@polka/url@1.0.0-next.29': {}
|
||||||
|
|
||||||
'@pomade/core@0.3.0(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/signer@0.8.16(@noble/curves@1.9.7)(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/436adec9ed1e71569748cd56aa697f361e7a8d47(@capacitor/core@8.3.4))(nostr-tools@2.23.5(typescript@5.9.3)))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-tools@2.23.5(typescript@5.9.3))':
|
'@pomade/core@0.3.1(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/signer@0.8.16(@noble/curves@1.9.7)(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/436adec9ed1e71569748cd56aa697f361e7a8d47(@capacitor/core@8.3.4))(nostr-tools@2.23.5(typescript@5.9.3)))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-tools@2.23.5(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@frostr/bifrost': 1.0.7(typescript@5.9.3)
|
'@frostr/bifrost': 1.0.7(typescript@5.9.3)
|
||||||
'@noble/hashes': 2.2.0
|
'@noble/hashes': 2.2.0
|
||||||
@@ -7197,9 +7185,9 @@ snapshots:
|
|||||||
- workbox-build
|
- workbox-build
|
||||||
- workbox-window
|
- workbox-window
|
||||||
|
|
||||||
'@welshman/app@0.8.16(7683b6be0f65191b839378ceee4e4014)':
|
'@welshman/app@0.8.16(9e2dd3230191940679c41b23e5e365c3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@pomade/core': 0.3.0(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/signer@0.8.16(@noble/curves@1.9.7)(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/436adec9ed1e71569748cd56aa697f361e7a8d47(@capacitor/core@8.3.4))(nostr-tools@2.23.5(typescript@5.9.3)))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-tools@2.23.5(typescript@5.9.3))
|
'@pomade/core': 0.3.1(@frostr/bifrost@1.0.7(typescript@5.9.3))(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/signer@0.8.16(@noble/curves@1.9.7)(@noble/hashes@2.2.0)(@welshman/lib@0.8.16)(@welshman/net@0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-signer-capacitor-plugin@https://codeload.github.com/coracle-social/nostr-signer-capacitor-plugin/tar.gz/436adec9ed1e71569748cd56aa697f361e7a8d47(@capacitor/core@8.3.4))(nostr-tools@2.23.5(typescript@5.9.3)))(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(nostr-tools@2.23.5(typescript@5.9.3))
|
||||||
'@welshman/feeds': 0.8.16(942b3be06d36b211cee078a14ee828c5)
|
'@welshman/feeds': 0.8.16(942b3be06d36b211cee078a14ee828c5)
|
||||||
'@welshman/lib': 0.8.16
|
'@welshman/lib': 0.8.16
|
||||||
'@welshman/net': 0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0)
|
'@welshman/net': 0.8.16(@welshman/lib@0.8.16)(@welshman/util@0.8.16(@noble/curves@1.9.7)(@welshman/lib@0.8.16)(nostr-tools@2.23.5(typescript@5.9.3)))(ws@8.21.0)
|
||||||
|
|||||||
+8
-3
@@ -85,7 +85,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@utility card2 {
|
@utility card2 {
|
||||||
@apply rounded-box text-base-content p-4 sm:p-6;
|
@apply rounded-box text-base-content border-base-content/20 bg-base-100 border border-solid p-4 shadow-xl/5 sm:p-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility column {
|
@utility column {
|
||||||
@@ -276,6 +276,11 @@
|
|||||||
@apply text-base-content p-2 sm:p-4;
|
@apply text-base-content p-2 sm:p-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card2 .card2,
|
||||||
|
.dialog .card2 {
|
||||||
|
@apply shadow-none;
|
||||||
|
}
|
||||||
|
|
||||||
[data-tip]::before {
|
[data-tip]::before {
|
||||||
@apply overflow-hidden text-ellipsis;
|
@apply overflow-hidden text-ellipsis;
|
||||||
}
|
}
|
||||||
@@ -414,11 +419,11 @@ progress[value]::-webkit-progress-value {
|
|||||||
/* content width for fixed elements */
|
/* content width for fixed elements */
|
||||||
|
|
||||||
.left-content {
|
.left-content {
|
||||||
@apply md:left-[calc(18.5rem+var(--sail))];
|
@apply left-sai md:left-[calc(18.5rem+var(--sail))];
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-content-full {
|
.left-content-full {
|
||||||
@apply md:left-[calc(3.5rem+var(--sail))];
|
@apply left-sai md:left-[calc(3.5rem+var(--sail))];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keyboard open state adjustments */
|
/* Keyboard open state adjustments */
|
||||||
|
|||||||
+1
-3
@@ -4,9 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>{NAME}</title>
|
<title>{NAME}</title>
|
||||||
<link rel="canonical" href="{URL}" />
|
<link rel="canonical" href="{URL}" />
|
||||||
<meta
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
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="theme-color" content="{ACCENT}" />
|
||||||
<meta name="description" content="{DESCRIPTION}" />
|
<meta name="description" content="{DESCRIPTION}" />
|
||||||
<meta property="og:url" content="{URL}" />
|
<meta property="og:url" content="{URL}" />
|
||||||
|
|||||||
@@ -18,9 +18,7 @@
|
|||||||
const h = getTagValue("h", event.tags)
|
const h = getTagValue("h", event.tags)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link
|
<Link class="cv col-3 card2 w-full cursor-pointer" href={makeCalendarPath(url, getAddress(event))}>
|
||||||
class="cv col-3 card2 bg-alt w-full cursor-pointer shadow-md"
|
|
||||||
href={makeCalendarPath(url, getAddress(event))}>
|
|
||||||
<CalendarEventHeader {event} />
|
<CalendarEventHeader {event} />
|
||||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
class="cv col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
|
class="cv col-2 card2 w-full cursor-pointer"
|
||||||
href={makeClassifiedPath(url, getAddress(event))}>
|
href={makeClassifiedPath(url, getAddress(event))}>
|
||||||
{#if title}
|
{#if title}
|
||||||
<div class="flex w-full items-center justify-between gap-2">
|
<div class="flex w-full items-center justify-between gap-2">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<code
|
<code
|
||||||
class="w-full overflow-auto whitespace-pre rounded bg-neutral px-1 text-neutral-content"
|
class="w-full overflow-auto whitespace-pre rounded bg-neutral px-1 text-neutral-content text-sm"
|
||||||
class:block={isBlock}>
|
class:block={isBlock}>
|
||||||
{value.trim()}
|
{value.trim()}
|
||||||
</code>
|
</code>
|
||||||
|
|||||||
@@ -77,7 +77,8 @@
|
|||||||
<span class="loading loading-spinner"></span>
|
<span class="loading loading-spinner"></span>
|
||||||
</div>
|
</div>
|
||||||
{:then preview}
|
{:then preview}
|
||||||
<div class="bg-alt flex max-w-xl flex-col leading-normal">
|
<div
|
||||||
|
class="border border-solid border-base-content/20 flex max-w-xl flex-col leading-normal rounded-box">
|
||||||
{#if preview.image && !hideImage}
|
{#if preview.image && !hideImage}
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
@@ -92,7 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:catch}
|
{:catch}
|
||||||
<p class="bg-alt p-12 text-center leading-normal">
|
<p class="border border-solid border-base-content/20 p-12 text-center leading-normal">
|
||||||
Unable to load a preview for {url}
|
Unable to load a preview for {url}
|
||||||
</p>
|
</p>
|
||||||
{/await}
|
{/await}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import cx from "classnames"
|
||||||
import {fromNostrURI} from "@welshman/util"
|
import {fromNostrURI} from "@welshman/util"
|
||||||
import {nthEq} from "@welshman/lib"
|
import {nthEq} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
@@ -37,10 +38,11 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
event: any
|
event: any
|
||||||
trimParent?: boolean
|
trimParent?: boolean
|
||||||
|
singleLine?: boolean
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {event, trimParent = false, url}: Props = $props()
|
const {event, trimParent = false, singleLine = false, url}: Props = $props()
|
||||||
|
|
||||||
const fullContent = parse(event)
|
const fullContent = parse(event)
|
||||||
|
|
||||||
@@ -104,10 +106,18 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-hidden text-ellipsis wrap-break-word">
|
<div
|
||||||
|
class={cx(
|
||||||
|
"overflow-hidden text-ellipsis",
|
||||||
|
singleLine ? "whitespace-nowrap" : "wrap-break-word",
|
||||||
|
)}>
|
||||||
{#each shortContent as parsed, i}
|
{#each shortContent as parsed, i}
|
||||||
{#if isNewline(parsed)}
|
{#if isNewline(parsed)}
|
||||||
<ContentNewline value={parsed.value} />
|
{#if singleLine}
|
||||||
|
{" "}
|
||||||
|
{:else}
|
||||||
|
<ContentNewline value={parsed.value} />
|
||||||
|
{/if}
|
||||||
{:else if isTopic(parsed)}
|
{:else if isTopic(parsed)}
|
||||||
<ContentTopic value={parsed.value} />
|
<ContentTopic value={parsed.value} />
|
||||||
{:else if isEmoji(parsed)}
|
{:else if isEmoji(parsed)}
|
||||||
|
|||||||
@@ -53,7 +53,11 @@
|
|||||||
<NoteContentMinimal trimParent {url} event={$quote} />
|
<NoteContentMinimal trimParent {url} event={$quote} />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<NoteCard noShadow event={$quote} {url} class="bg-alt rounded-box p-4">
|
<NoteCard
|
||||||
|
noShadow
|
||||||
|
event={$quote}
|
||||||
|
{url}
|
||||||
|
class="border border-solid border-base-content/20 rounded-box p-4">
|
||||||
<NoteContentMinimal {url} event={$quote} />
|
<NoteContentMinimal {url} event={$quote} />
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import InputList from "@lib/components/InputList.svelte"
|
||||||
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
|
import {setFeaturedContent} from "@app/featured"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
initial: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, initial}: Props = $props()
|
||||||
|
|
||||||
|
let content = $state([...initial])
|
||||||
|
let loading = $state(false)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = await setFeaturedContent(url, content)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Featured content updated!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Featured Content</ModalTitle>
|
||||||
|
<ModalSubtitle>on <RelayName {url} class="text-primary" /></ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<Field>
|
||||||
|
{#snippet info()}
|
||||||
|
<p>
|
||||||
|
Each entry is shown on the space's About page. Links will be fetched and displayed
|
||||||
|
automatically.
|
||||||
|
</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<InputList bind:value={content} placeholder="URL or nevent...">
|
||||||
|
{#snippet addLabel()}
|
||||||
|
Add content
|
||||||
|
{/snippet}
|
||||||
|
</InputList>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" onclick={submit} disabled={loading}>
|
||||||
|
<Spinner {loading}>Save changes</Spinner>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -20,9 +20,7 @@
|
|||||||
const h = getTagValue("h", event.tags)
|
const h = getTagValue("h", event.tags)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link
|
<Link class="cv col-2 card2 w-full cursor-pointer" href={makeGoalPath(url, event.id)}>
|
||||||
class="cv col-2 card2 bg-alt w-full cursor-pointer shadow-md"
|
|
||||||
href={makeGoalPath(url, event.id)}>
|
|
||||||
<p class="text-2xl">{event.content}</p>
|
<p class="text-2xl">{event.content}</p>
|
||||||
<Content
|
<Content
|
||||||
event={{content: summary, tags: event.tags}}
|
event={{content: summary, tags: event.tags}}
|
||||||
|
|||||||
@@ -4,16 +4,11 @@
|
|||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {Router} from "@welshman/router"
|
import {Router} from "@welshman/router"
|
||||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Popover from "@lib/components/Popover.svelte"
|
|
||||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||||
import NoteContent from "@app/components/NoteContent.svelte"
|
import NoteContent from "@app/components/NoteContent.svelte"
|
||||||
import NoteCard from "@app/components/NoteCard.svelte"
|
import NoteCard from "@app/components/NoteCard.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import ProfileNoteMenu from "@app/components/ProfileNoteMenu.svelte"
|
|
||||||
import {publishDelete} from "@app/deletes"
|
import {publishDelete} from "@app/deletes"
|
||||||
import {publishReaction} from "@app/reactions"
|
import {publishReaction} from "@app/reactions"
|
||||||
import {canEnforceNip70} from "@app/relays"
|
import {canEnforceNip70} from "@app/relays"
|
||||||
@@ -22,10 +17,9 @@
|
|||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
children?: Snippet
|
children?: Snippet
|
||||||
url?: string
|
url?: string
|
||||||
editable?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, event, children, editable = false}: Props = $props()
|
const {url, event, children}: Props = $props()
|
||||||
|
|
||||||
const relays = url ? [url] : Router.get().Event(event).getUrls()
|
const relays = url ? [url] : Router.get().Event(event).getUrls()
|
||||||
|
|
||||||
@@ -44,35 +38,9 @@
|
|||||||
content: emoji.unicode,
|
content: emoji.unicode,
|
||||||
protect: await shouldProtect,
|
protect: await shouldProtect,
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleMenu = () => {
|
|
||||||
showMenu = !showMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
|
|
||||||
let showMenu = $state(false)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NoteCard {event} {url} class="cv card2 bg-alt">
|
<NoteCard {event} {url} class="cv card2">
|
||||||
{#if editable}
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<div class="relative">
|
|
||||||
<Button class="btn btn-circle btn-ghost btn-xs" onclick={toggleMenu}>
|
|
||||||
<Icon icon={MenuDots} size={4} />
|
|
||||||
</Button>
|
|
||||||
{#if showMenu}
|
|
||||||
<Popover hideOnClick onClose={closeMenu}>
|
|
||||||
<div transition:fly class="absolute right-0 z-popover">
|
|
||||||
<ProfileNoteMenu {event} onClose={closeMenu} />
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<NoteContent {event} expandMode="inline" />
|
<NoteContent {event} expandMode="inline" />
|
||||||
<div class="flex w-full justify-between gap-2">
|
<div class="flex w-full justify-between gap-2">
|
||||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-right">
|
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-right">
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
||||||
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
||||||
import {makeProfilePath} from "@app/routes"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
@@ -13,20 +14,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {pubkey, url}: Props = $props()
|
const {pubkey, url}: Props = $props()
|
||||||
|
|
||||||
|
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<Profile {pubkey} {url} />
|
<Profile {pubkey} {url} />
|
||||||
<Link href={makeProfilePath(pubkey)} class="btn btn-primary hidden sm:flex">
|
<Button onclick={openProfile} class="btn btn-primary hidden sm:flex">
|
||||||
<Icon icon={UserCircle} />
|
<Icon icon={UserCircle} />
|
||||||
View Profile
|
View Profile
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ProfileInfo {pubkey} {url} />
|
<ProfileInfo {pubkey} {url} />
|
||||||
<ProfileBadges {pubkey} {url} />
|
<ProfileBadges {pubkey} {url} />
|
||||||
<Link href={makeProfilePath(pubkey)} class="btn btn-primary sm:hidden">
|
<Button onclick={openProfile} class="btn btn-primary sm:hidden">
|
||||||
<Icon icon={UserCircle} />
|
<Icon icon={UserCircle} />
|
||||||
View Profile
|
View Profile
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="ml-sai mt-sai mb-sai relative z-popover isolate hidden w-14 shrink-0 bg-base-200 pt-2 md:block">
|
class="ml-sai mt-sai mb-sai relative z-popover isolate hidden w-14 shrink-0 bg-base-200 pt-2 md:block border-r border-solid border-base-content/15 dark:border-base-content/10">
|
||||||
<div class="flex h-full flex-col" class:justify-between={PLATFORM_RELAYS.length === 0}>
|
<div class="flex h-full flex-col" class:justify-between={PLATFORM_RELAYS.length === 0}>
|
||||||
<PrimaryNavSpaces />
|
<PrimaryNavSpaces />
|
||||||
{#if PLATFORM_RELAYS.length > 0}
|
{#if PLATFORM_RELAYS.length > 0}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
const copyPubkey = () => clip(nip19.npubEncode(pubkey))
|
const copyPubkey = () => clip(nip19.npubEncode(pubkey))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex max-w-full items-start gap-3">
|
<div class="flex max-w-full items-start gap-2">
|
||||||
{#if inert}
|
{#if inert}
|
||||||
<span class="py-1">
|
<span class="py-1">
|
||||||
<ProfileCircle {pubkey} size={avatarSize} />
|
<ProfileCircle {pubkey} size={avatarSize} />
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<div class="flex min-w-0 flex-col">
|
<div class="flex min-w-0 flex-col">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if inert}
|
{#if inert}
|
||||||
<span class="text-bold overflow-hidden text-ellipsis">
|
<span class="text-bold overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
{$profileDisplay}
|
{$profileDisplay}
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -29,8 +29,10 @@
|
|||||||
const openSpaces = () => pushModal(ProfileSpaces, {pubkey, url})
|
const openSpaces = () => pushModal(ProfileSpaces, {pubkey, url})
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
// Make sure we have their relay selections before we load their posts
|
||||||
await loadRelayList(pubkey)
|
await loadRelayList(pubkey)
|
||||||
|
|
||||||
|
// Load groups and at least one note, regardless of time frame
|
||||||
load({
|
load({
|
||||||
filters: [
|
filters: [
|
||||||
{authors: [pubkey], kinds: [ROOMS]},
|
{authors: [pubkey], kinds: [ROOMS]},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {loadProfile} from "@welshman/app"
|
import {getProfile, loadProfile} from "@welshman/app"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
|
|
||||||
@@ -22,13 +22,17 @@
|
|||||||
: {box: "h-8 w-8", overlap: "-mr-3", overflow: "text-xs"},
|
: {box: "h-8 w-8", overlap: "-mr-3", overflow: "text-xs"},
|
||||||
)
|
)
|
||||||
|
|
||||||
$effect(() => {
|
for (const pubkey of pubkeys) {
|
||||||
for (const pk of pubkeys) {
|
loadProfile(pubkey)
|
||||||
loadProfile(pk)
|
}
|
||||||
}
|
|
||||||
|
const visiblePubkeys = $derived.by(() => {
|
||||||
|
const filtered = pubkeys.filter(pubkey => getProfile(pubkey)?.picture)
|
||||||
|
|
||||||
|
return filtered.length > 0 ? filtered : pubkeys.slice(0, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
const displayPubkeys = $derived([...pubkeys].toSorted().slice(0, effectiveLimit))
|
const displayPubkeys = $derived(visiblePubkeys.toSorted().slice(0, effectiveLimit))
|
||||||
const overflowCount = $derived(Math.max(0, pubkeys.length - effectiveLimit))
|
const overflowCount = $derived(Math.max(0, pubkeys.length - effectiveLimit))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {removeUndefined} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {ManagementMethod} from "@welshman/util"
|
import {deriveProfile, displayProfileByPubkey, loadMessagingRelayList} from "@welshman/app"
|
||||||
import {
|
|
||||||
manageRelay,
|
|
||||||
deriveProfile,
|
|
||||||
displayProfileByPubkey,
|
|
||||||
loadMessagingRelayList,
|
|
||||||
} from "@welshman/app"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
import Restart from "@assets/icons/restart.svg?dataurl"
|
import Restart from "@assets/icons/restart.svg?dataurl"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Popover from "@lib/components/Popover.svelte"
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
@@ -26,10 +22,16 @@
|
|||||||
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
||||||
import {deriveUserIsSpaceAdmin, deriveSpaceBannedPubkeyItems, addSpaceMembers} from "@app/members"
|
import {pubkeyLink} from "@app/env"
|
||||||
|
import {
|
||||||
|
deriveUserIsSpaceAdmin,
|
||||||
|
deriveSpaceBannedPubkeyItems,
|
||||||
|
addSpaceMembers,
|
||||||
|
banSpaceMembers,
|
||||||
|
} from "@app/members"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {goToProfile} from "@app/routes"
|
import {goToChat} from "@app/routes"
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
@@ -50,9 +52,9 @@
|
|||||||
|
|
||||||
const showInfo = () => pushModal(EventInfo, {url, event: $profile!.event})
|
const showInfo = () => pushModal(EventInfo, {url, event: $profile!.event})
|
||||||
|
|
||||||
const viewProfile = () => goToProfile(pubkey)
|
const openChat = () => goToChat([pubkey])
|
||||||
|
|
||||||
const toggleMenu = () => {
|
const toggleMenu = (pubkey: string) => {
|
||||||
showMenu = !showMenu
|
showMenu = !showMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,10 +67,7 @@
|
|||||||
title: "Ban User",
|
title: "Ban User",
|
||||||
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
const {error} = await manageRelay(url!, {
|
const error = await banSpaceMembers(url!, [pubkey])
|
||||||
method: ManagementMethod.BanPubkey,
|
|
||||||
params: [pubkey],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
@@ -104,7 +103,7 @@
|
|||||||
<Profile showPubkey avatarSize={14} {pubkey} {url} />
|
<Profile showPubkey avatarSize={14} {pubkey} {url} />
|
||||||
{#if $profile || $userIsAdmin}
|
{#if $profile || $userIsAdmin}
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Button class="btn btn-circle btn-ghost btn-sm" onclick={toggleMenu}>
|
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
|
||||||
<Icon icon={MenuDots} />
|
<Icon icon={MenuDots} />
|
||||||
</Button>
|
</Button>
|
||||||
{#if showMenu}
|
{#if showMenu}
|
||||||
@@ -153,9 +152,13 @@
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button onclick={viewProfile} class="btn btn-primary">
|
<Link external href={pubkeyLink(pubkey)} class="btn btn-neutral">
|
||||||
<Icon icon={UserCircle} />
|
<ImageIcon alt="" src="/coracle.png" />
|
||||||
View Full Profile
|
Open in Coracle
|
||||||
|
</Link>
|
||||||
|
<Button onclick={openChat} class="btn btn-primary">
|
||||||
|
<Icon icon={Letter} />
|
||||||
|
Message
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||||
import MapPoint from "@assets/icons/map-point.svg?dataurl"
|
import MapPoint from "@assets/icons/map-point.svg?dataurl"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -86,24 +85,6 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
{#if !isSignup}
|
{#if !isSignup}
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Website</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon={LinkRound} />
|
|
||||||
<input
|
|
||||||
bind:value={values.profile.website}
|
|
||||||
class="grow"
|
|
||||||
type="text"
|
|
||||||
placeholder="https://" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
A link to your personal site or portfolio.
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
<Field>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Nostr Address</p>
|
<p>Nostr Address</p>
|
||||||
|
|||||||
@@ -5,14 +5,15 @@
|
|||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
|
singleLine?: boolean
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {pubkey, url}: Props = $props()
|
const {pubkey, url, singleLine}: Props = $props()
|
||||||
|
|
||||||
const profile = deriveProfile(pubkey, removeUndefined([url]))
|
const profile = deriveProfile(pubkey, removeUndefined([url]))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $profile}
|
{#if $profile}
|
||||||
<ContentMinimal event={{content: $profile.about || "", tags: []}} />
|
<ContentMinimal event={{content: $profile.about || "", tags: []}} {singleLine} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {nthEq} from "@welshman/lib"
|
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
|
||||||
import {getListTags, getEventTagValues} from "@welshman/util"
|
|
||||||
import {pin, unpin, tagEvent, userPinList, waitForThunkError} from "@welshman/app"
|
|
||||||
import {Router} from "@welshman/router"
|
|
||||||
import Pin from "@assets/icons/pin.svg?dataurl"
|
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
|
||||||
import {publishDelete} from "@app/deletes"
|
|
||||||
import {pushModal} from "@app/modal"
|
|
||||||
import {pushToast} from "@app/toast"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
event: TrustedEvent
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const {event, onClose}: Props = $props()
|
|
||||||
|
|
||||||
const relays = Router.get().Event(event).getUrls()
|
|
||||||
const pinnedIds = $derived(getEventTagValues(getListTags($userPinList)))
|
|
||||||
const isPinned = $derived(pinnedIds.includes(event.id))
|
|
||||||
|
|
||||||
const togglePin = async () => {
|
|
||||||
onClose()
|
|
||||||
|
|
||||||
const thunk = isPinned ? await unpin(event.id) : await pin(tagEvent(event).find(nthEq(0, "e"))!)
|
|
||||||
|
|
||||||
const error = await waitForThunkError(thunk)
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: "Failed to update pinned notes."})
|
|
||||||
} else {
|
|
||||||
pushToast({message: isPinned ? "Note unpinned." : "Note pinned to your profile."})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmDelete = () => {
|
|
||||||
onClose()
|
|
||||||
|
|
||||||
pushModal(Confirm, {
|
|
||||||
title: "Delete Note",
|
|
||||||
message: "Are you sure you want to delete this note?",
|
|
||||||
confirm: async () => {
|
|
||||||
await publishDelete({event, relays, protect: false})
|
|
||||||
|
|
||||||
pushToast({message: "Delete request sent."})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md">
|
|
||||||
<li>
|
|
||||||
<Button onclick={togglePin}>
|
|
||||||
<Icon size={4} icon={Pin} />
|
|
||||||
{isPinned ? "Unpin from profile" : "Pin to profile"}
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Button onclick={confirmDelete} class="text-error">
|
|
||||||
<Icon size={4} icon={TrashBin2} />
|
|
||||||
Delete note
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
@@ -1,382 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
|
||||||
import cx from "classnames"
|
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {compressFile} from "@lib/html"
|
|
||||||
import {displayPubkey} from "@welshman/util"
|
|
||||||
import {
|
|
||||||
pubkey,
|
|
||||||
followLists,
|
|
||||||
deriveProfile,
|
|
||||||
deriveProfileDisplay,
|
|
||||||
deriveUserWotScore,
|
|
||||||
getFollows,
|
|
||||||
follow,
|
|
||||||
unfollow,
|
|
||||||
tagPubkey,
|
|
||||||
} from "@welshman/app"
|
|
||||||
import {clamp} from "@welshman/lib"
|
|
||||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
|
||||||
import QrCode from "@assets/icons/qr-code.svg?dataurl"
|
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
|
||||||
import SquareArrowRight from "@assets/icons/square-arrow-right-up.svg?dataurl"
|
|
||||||
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
|
||||||
import UserPlus from "@assets/icons/user-plus.svg?dataurl"
|
|
||||||
import PenNewSquare from "@assets/icons/pen-new-square.svg?dataurl"
|
|
||||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
|
||||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
|
||||||
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
|
|
||||||
import UsersGroup from "@assets/icons/users-group-rounded.svg?dataurl"
|
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Popover from "@lib/components/Popover.svelte"
|
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
|
||||||
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
|
||||||
import ProfileTrust from "@app/components/ProfileTrust.svelte"
|
|
||||||
import ProfileSharedSpaces from "@app/components/ProfileSharedSpaces.svelte"
|
|
||||||
import ProfilePinnedNotes from "@app/components/ProfilePinnedNotes.svelte"
|
|
||||||
import ProfilePageNotes from "@app/components/ProfilePageNotes.svelte"
|
|
||||||
import ProfilePageSpaces from "@app/components/ProfilePageSpaces.svelte"
|
|
||||||
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
|
||||||
import ProfileQrCode from "@app/components/ProfileQrCode.svelte"
|
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
|
||||||
import {deriveGroupList, getSpaceUrlsFromGroupList, userSpaceUrls} from "@app/groups"
|
|
||||||
import {updateProfile} from "@app/profiles"
|
|
||||||
import {uploadFile} from "@app/uploads"
|
|
||||||
import {pushModal} from "@app/modal"
|
|
||||||
import {clip, pushToast} from "@app/toast"
|
|
||||||
import {goToChat} from "@app/routes"
|
|
||||||
|
|
||||||
type Tab = "about" | "notes" | "spaces"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
pubkey: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const {pubkey: target}: Props = $props()
|
|
||||||
|
|
||||||
const profile = deriveProfile(target)
|
|
||||||
const profileDisplay = deriveProfileDisplay(target)
|
|
||||||
const groupList = deriveGroupList(target)
|
|
||||||
const score = deriveUserWotScore(target)
|
|
||||||
const encodedNpub = nip19.npubEncode(target)
|
|
||||||
const isSelf = $derived($pubkey === target)
|
|
||||||
const isFollowing = $derived.by(() => {
|
|
||||||
void $followLists
|
|
||||||
|
|
||||||
return $pubkey ? getFollows($pubkey).includes(target) : false
|
|
||||||
})
|
|
||||||
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
|
|
||||||
const sharedSpaceUrls = $derived($userSpaceUrls.filter(url => spaceUrls.includes(url)))
|
|
||||||
const displayScore = $derived(Math.round(clamp([0, 100], $score)))
|
|
||||||
const website = $derived($profile?.website?.replace(/^https?:\/\//, ""))
|
|
||||||
const websiteHref = $derived(
|
|
||||||
$profile?.website?.match(/^https?:\/\//)
|
|
||||||
? $profile.website
|
|
||||||
: `https://${$profile?.website || ""}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
let tab = $state<Tab>("about")
|
|
||||||
let showMenu = $state(false)
|
|
||||||
let bannerLoading = $state(false)
|
|
||||||
let bannerInput: HTMLInputElement | undefined = $state()
|
|
||||||
|
|
||||||
const setTab = (next: Tab) => {
|
|
||||||
tab = next
|
|
||||||
}
|
|
||||||
|
|
||||||
const showAboutTab = () => setTab("about")
|
|
||||||
|
|
||||||
const showNotesTab = () => setTab("notes")
|
|
||||||
|
|
||||||
const showSpacesTab = () => setTab("spaces")
|
|
||||||
|
|
||||||
const copyNpub = () => clip(encodedNpub)
|
|
||||||
|
|
||||||
const showQr = () => pushModal(ProfileQrCode, {code: encodedNpub})
|
|
||||||
|
|
||||||
const toggleMenu = () => {
|
|
||||||
showMenu = !showMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const showInfo = () => {
|
|
||||||
closeMenu()
|
|
||||||
pushModal(EventInfo, {event: $profile!.event})
|
|
||||||
}
|
|
||||||
|
|
||||||
const startEdit = () => pushModal(ProfileEdit)
|
|
||||||
|
|
||||||
const openSettings = () => goto("/settings/profile")
|
|
||||||
|
|
||||||
const openSpaces = () => goto("/spaces")
|
|
||||||
|
|
||||||
const openRelaySettings = () => goto("/settings/relays")
|
|
||||||
|
|
||||||
const openChat = () => goToChat([target])
|
|
||||||
|
|
||||||
const toggleFollow = async () => {
|
|
||||||
if (!$pubkey || isSelf) return
|
|
||||||
|
|
||||||
if (isFollowing) {
|
|
||||||
await unfollow(target)
|
|
||||||
} else {
|
|
||||||
await follow(tagPubkey(target))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openBannerPicker = () => bannerInput?.click()
|
|
||||||
|
|
||||||
const onBannerChange = async (e: Event) => {
|
|
||||||
const input = e.target as HTMLInputElement
|
|
||||||
const file = input.files?.[0]
|
|
||||||
|
|
||||||
input.value = ""
|
|
||||||
|
|
||||||
if (!file || !$profile) return
|
|
||||||
|
|
||||||
bannerLoading = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const {result} = await uploadFile(await compressFile(file))
|
|
||||||
|
|
||||||
if (result?.url) {
|
|
||||||
await updateProfile({profile: {...$profile, banner: result.url}})
|
|
||||||
pushToast({message: "Banner updated."})
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
bannerLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start">
|
|
||||||
<div class="min-w-0 flex-1 overflow-hidden border-base-300 bg-alt md:rounded-box md:border">
|
|
||||||
<div class="relative overflow-hidden border-b border-base-300 bg-base-300">
|
|
||||||
{#if $profile?.banner}
|
|
||||||
<img src={$profile.banner} alt="" class="h-28 w-full object-cover sm:h-32 md:h-40" />
|
|
||||||
{:else}
|
|
||||||
<div class="h-28 w-full bg-linear-to-br from-base-300 to-base-100 sm:h-32 md:h-40"></div>
|
|
||||||
{/if}
|
|
||||||
{#if isSelf}
|
|
||||||
<Button
|
|
||||||
class="btn btn-neutral btn-sm absolute top-2 right-2 sm:top-3 sm:right-3"
|
|
||||||
disabled={bannerLoading}
|
|
||||||
onclick={openBannerPicker}>
|
|
||||||
<Icon icon={GallerySend} size={4} />
|
|
||||||
<span class="hidden sm:inline">Change banner</span>
|
|
||||||
</Button>
|
|
||||||
<input
|
|
||||||
bind:this={bannerInput}
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
class="hidden"
|
|
||||||
onchange={onBannerChange} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative border-b border-base-300 px-4 pb-4 sm:px-3 sm:pb-5">
|
|
||||||
<div class="-mt-8 sm:-mt-10">
|
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4">
|
|
||||||
<div class="w-fit shrink-0">
|
|
||||||
{#if isSelf}
|
|
||||||
<Button
|
|
||||||
class="w-fit rounded-full sm:border-4 sm:border-base-200 sm:bg-base-200"
|
|
||||||
onclick={startEdit}>
|
|
||||||
<ProfileCircle pubkey={target} size={16} class="sm:hidden" />
|
|
||||||
<ProfileCircle pubkey={target} size={20} class="hidden sm:block" />
|
|
||||||
</Button>
|
|
||||||
{:else}
|
|
||||||
<div class="w-fit rounded-full sm:border-4 sm:border-base-200 sm:bg-base-200">
|
|
||||||
<ProfileCircle pubkey={target} size={16} class="sm:hidden" />
|
|
||||||
<ProfileCircle pubkey={target} size={20} class="hidden sm:block" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex min-w-0 flex-1 flex-col gap-3 sm:gap-2 sm:pt-14">
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-x-4">
|
|
||||||
<h1 class="min-w-0 text-xl leading-tight font-bold sm:text-2xl">
|
|
||||||
{$profileDisplay}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{#if (isSelf || $pubkey) && $profile}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
{#if isSelf}
|
|
||||||
<Button
|
|
||||||
class="btn btn-primary btn-md flex-1 sm:btn-sm sm:flex-none"
|
|
||||||
onclick={startEdit}>
|
|
||||||
<Icon icon={PenNewSquare} size={4} />
|
|
||||||
Edit profile
|
|
||||||
</Button>
|
|
||||||
{:else}
|
|
||||||
<Button
|
|
||||||
class="btn btn-neutral btn-md flex-1 sm:btn-sm sm:flex-none"
|
|
||||||
onclick={toggleFollow}>
|
|
||||||
<Icon icon={UserPlus} size={4} />
|
|
||||||
{isFollowing ? "Unfollow" : "Follow"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
class="btn btn-primary btn-md flex-1 sm:btn-sm sm:flex-none"
|
|
||||||
onclick={openChat}>
|
|
||||||
<Icon icon={Letter} size={4} />
|
|
||||||
Message
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="relative shrink-0">
|
|
||||||
<Button class="btn btn-circle btn-ghost btn-sm" onclick={toggleMenu}>
|
|
||||||
<Icon icon={MenuDots} />
|
|
||||||
</Button>
|
|
||||||
{#if showMenu}
|
|
||||||
<Popover hideOnClick onClose={closeMenu}>
|
|
||||||
<ul
|
|
||||||
transition:fly
|
|
||||||
class="bg-alt menu absolute right-0 z-popover w-48 gap-1 rounded-box p-2 shadow-md">
|
|
||||||
<li>
|
|
||||||
<Button onclick={showInfo}>
|
|
||||||
<Icon icon={Code2} />
|
|
||||||
User Details
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{#if isSelf}
|
|
||||||
<li>
|
|
||||||
<Button onclick={openSettings}>
|
|
||||||
<Icon icon={Settings} />
|
|
||||||
Account Settings
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-1 text-sm leading-none opacity-75">
|
|
||||||
<span>{displayPubkey(target)}</span>
|
|
||||||
<Button onclick={copyNpub} class="btn btn-ghost btn-xs h-5 min-h-5 w-5 p-0">
|
|
||||||
<Icon size={3} icon={Copy} />
|
|
||||||
</Button>
|
|
||||||
<Button onclick={showQr} class="btn btn-ghost btn-xs h-5 min-h-5 w-5 p-0">
|
|
||||||
<Icon size={3} icon={QrCode} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if website}
|
|
||||||
<Link
|
|
||||||
external
|
|
||||||
href={websiteHref}
|
|
||||||
class="link link-primary row-2 w-fit text-sm font-medium">
|
|
||||||
<Icon icon={LinkRound} size={4} />
|
|
||||||
{website}
|
|
||||||
<Icon icon={SquareArrowRight} size={4} />
|
|
||||||
</Link>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<span class="badge badge-neutral inline-flex h-6 items-center gap-1.5 border-0">
|
|
||||||
<Icon icon={Shield} size={3} />
|
|
||||||
Trust score {displayScore}
|
|
||||||
</span>
|
|
||||||
{#if sharedSpaceUrls.length > 0}
|
|
||||||
<button
|
|
||||||
class="badge badge-neutral inline-flex h-6 items-center gap-1.5 border-0"
|
|
||||||
onclick={showSpacesTab}>
|
|
||||||
<Icon icon={UsersGroup} size={3} />
|
|
||||||
{sharedSpaceUrls.length} shared {sharedSpaceUrls.length === 1
|
|
||||||
? "space"
|
|
||||||
: "spaces"}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="sticky top-0 z-10 border-b border-base-300 bg-base-200/90 px-4 backdrop-blur-sm sm:px-3">
|
|
||||||
<div
|
|
||||||
role="tablist"
|
|
||||||
class="tabs tabs-bordered -mb-px flex w-full justify-between bg-transparent sm:justify-start">
|
|
||||||
<button
|
|
||||||
role="tab"
|
|
||||||
class={cx("tab flex-none px-0 sm:px-4", {"tab-active": tab === "about"})}
|
|
||||||
onclick={showAboutTab}>
|
|
||||||
About
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
role="tab"
|
|
||||||
class={cx("tab flex-none px-0 sm:px-4", {"tab-active": tab === "notes"})}
|
|
||||||
onclick={showNotesTab}>
|
|
||||||
Notes
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
role="tab"
|
|
||||||
class={cx("tab flex-none px-0 sm:px-4", {"tab-active": tab === "spaces"})}
|
|
||||||
onclick={showSpacesTab}>
|
|
||||||
Spaces
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-4 py-3 sm:px-3 sm:py-4">
|
|
||||||
<div class="sm:pl-3">
|
|
||||||
{#if tab === "about"}
|
|
||||||
<div class="col-3 sm:col-4">
|
|
||||||
<ProfileInfo pubkey={target} />
|
|
||||||
<div class="col-3 xl:hidden">
|
|
||||||
<ProfileTrust pubkey={target} />
|
|
||||||
<ProfileSharedSpaces pubkey={target} limit={3} onViewAll={showSpacesTab} />
|
|
||||||
</div>
|
|
||||||
<ProfilePinnedNotes
|
|
||||||
pubkey={target}
|
|
||||||
limit={2}
|
|
||||||
editable={isSelf}
|
|
||||||
onViewAll={showNotesTab} />
|
|
||||||
</div>
|
|
||||||
{:else if tab === "notes"}
|
|
||||||
{#if isSelf}
|
|
||||||
<p class="mb-4 text-sm opacity-75">
|
|
||||||
Notes are public posts on your write relays. Pin notes to highlight them on your
|
|
||||||
profile, or manage relays in
|
|
||||||
<Button class="link link-primary" onclick={openRelaySettings}>relay settings</Button
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
<ProfilePageNotes pubkey={target} editable={isSelf} />
|
|
||||||
{:else}
|
|
||||||
{#if isSelf}
|
|
||||||
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<p class="text-sm opacity-75">Spaces come from your published group list.</p>
|
|
||||||
<Button class="btn btn-neutral btn-sm w-full sm:w-auto" onclick={openSpaces}>
|
|
||||||
Manage spaces
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<ProfilePageSpaces pubkey={target} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<aside class="hidden w-80 shrink-0 xl:block xl:border-l xl:border-base-300 xl:pl-4">
|
|
||||||
<div class="col-3">
|
|
||||||
<ProfileTrust pubkey={target} />
|
|
||||||
<ProfileSharedSpaces pubkey={target} limit={3} onViewAll={showSpacesTab} />
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {onMount} from "svelte"
|
|
||||||
import {sortBy, uniqBy} from "@welshman/lib"
|
|
||||||
import {feedFromFilter} from "@welshman/feeds"
|
|
||||||
import {NOTE, getReplyTags} from "@welshman/util"
|
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
|
||||||
import {makeFeedController} from "@welshman/app"
|
|
||||||
import {createScroller} from "@lib/html"
|
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import NoteItem from "@app/components/NoteItem.svelte"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
pubkey: string
|
|
||||||
editable?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const {pubkey, editable = false}: Props = $props()
|
|
||||||
|
|
||||||
const ctrl = makeFeedController({
|
|
||||||
useWindowing: true,
|
|
||||||
feed: feedFromFilter({kinds: [NOTE], authors: [pubkey]}),
|
|
||||||
onEvent: (event: TrustedEvent) => {
|
|
||||||
if (getReplyTags(event.tags).replies.length === 0) {
|
|
||||||
buffer.push(event)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
let element: Element | undefined = $state()
|
|
||||||
let events: TrustedEvent[] = $state([])
|
|
||||||
let buffer: TrustedEvent[] = []
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const scroller = createScroller({
|
|
||||||
element: element!,
|
|
||||||
delay: 300,
|
|
||||||
threshold: 3000,
|
|
||||||
onScroll: () => {
|
|
||||||
buffer = uniqBy(
|
|
||||||
e => e.id,
|
|
||||||
sortBy(e => -e.created_at, buffer),
|
|
||||||
)
|
|
||||||
|
|
||||||
events = uniqBy(e => e.id, [...events, ...buffer.splice(0, 5)])
|
|
||||||
|
|
||||||
if (buffer.length < 50) {
|
|
||||||
ctrl.load(50)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => scroller.stop()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="col-4" bind:this={element}>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
{#each events as event (event.id)}
|
|
||||||
<div in:fly>
|
|
||||||
<NoteItem {event} {editable} />
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
<p class="center my-12 flex">
|
|
||||||
<Spinner loading />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
|
||||||
import {deriveGroupList, getSpaceUrlsFromGroupList, groupListPubkeysByUrl} from "@app/groups"
|
|
||||||
import {makeSpacePath} from "@app/routes"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
pubkey: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const {pubkey}: Props = $props()
|
|
||||||
|
|
||||||
const groupList = deriveGroupList(pubkey)
|
|
||||||
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="col-2">
|
|
||||||
{#each spaceUrls as url (url)}
|
|
||||||
{@const count = $groupListPubkeysByUrl.get(url)?.size || 0}
|
|
||||||
<div
|
|
||||||
class="card2 card2-sm bg-alt flex flex-col gap-3 border border-base-300 sm:flex-row sm:items-center">
|
|
||||||
<RelayIcon {url} size={10} />
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<RelayName {url} class="font-medium" />
|
|
||||||
<p class="text-sm opacity-75">
|
|
||||||
{#if count >= 1000}
|
|
||||||
{(count / 1000).toFixed(1).replace(/\.0$/, "")}K members
|
|
||||||
{:else}
|
|
||||||
{count} {count === 1 ? "member" : "members"}
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
<p class="ellipsize text-xs opacity-60">{url}</p>
|
|
||||||
</div>
|
|
||||||
<Link class="btn btn-primary btn-sm w-full sm:w-auto" href={makeSpacePath(url)}>
|
|
||||||
Go to space
|
|
||||||
<Icon icon={AltArrowRight} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="card2 bg-alt border border-base-300 text-center">
|
|
||||||
<p class="opacity-75">No spaces found for this user.</p>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {onMount} from "svelte"
|
|
||||||
import {sortBy} from "@welshman/lib"
|
|
||||||
import {getListTags, getEventTagValues} from "@welshman/util"
|
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
|
||||||
import {derivePinList, repository} from "@welshman/app"
|
|
||||||
import {Router} from "@welshman/router"
|
|
||||||
import {load} from "@welshman/net"
|
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import NoteItem from "@app/components/NoteItem.svelte"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
pubkey: string
|
|
||||||
limit?: number
|
|
||||||
onViewAll?: () => void
|
|
||||||
editable?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const {pubkey, limit, onViewAll, editable = false}: Props = $props()
|
|
||||||
|
|
||||||
const pinList = derivePinList(pubkey)
|
|
||||||
const pinnedIds = $derived(getEventTagValues(getListTags($pinList)))
|
|
||||||
const displayIds = $derived(limit ? pinnedIds.slice(0, limit) : pinnedIds)
|
|
||||||
|
|
||||||
const pinnedEvents = $derived.by(() => {
|
|
||||||
return sortBy(
|
|
||||||
e => -pinnedIds.indexOf(e.id),
|
|
||||||
displayIds
|
|
||||||
.map(id => repository.getEvent(id))
|
|
||||||
.filter((event): event is TrustedEvent => Boolean(event)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
let loading = $state(pinnedIds.length > 0)
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (pinnedIds.length === 0) {
|
|
||||||
loading = false
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const missing = pinnedIds.filter(id => !repository.getEvent(id))
|
|
||||||
|
|
||||||
if (missing.length === 0) {
|
|
||||||
loading = false
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
load({
|
|
||||||
relays: Router.get().FromPubkeys([pubkey]).getUrls(),
|
|
||||||
filters: [{ids: missing}],
|
|
||||||
onEvent: () => {
|
|
||||||
loading = !pinnedIds.every(id => repository.getEvent(id))
|
|
||||||
},
|
|
||||||
onClose: () => {
|
|
||||||
loading = false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if pinnedIds.length > 0}
|
|
||||||
<div class="col-4 border-t border-base-300 pt-4">
|
|
||||||
<strong>Pinned notes</strong>
|
|
||||||
{#if loading && pinnedEvents.length === 0}
|
|
||||||
<p class="center flex py-8">
|
|
||||||
<Spinner loading />
|
|
||||||
</p>
|
|
||||||
{:else}
|
|
||||||
<div class="col-2">
|
|
||||||
{#each pinnedEvents as event (event.id)}
|
|
||||||
<div in:fly>
|
|
||||||
<NoteItem {event} {editable} />
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{#if onViewAll && pinnedIds.length > (limit || pinnedIds.length)}
|
|
||||||
<button class="link link-primary row-2 text-sm" onclick={onViewAll}>
|
|
||||||
View all pinned notes
|
|
||||||
<span aria-hidden="true">→</span>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
|
||||||
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 ModalFooter from "@lib/components/ModalFooter.svelte"
|
|
||||||
import QRCode from "@app/components/QRCode.svelte"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
code: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const {code}: Props = $props()
|
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal>
|
|
||||||
<ModalBody>
|
|
||||||
<div class="col-4 items-center text-center">
|
|
||||||
<strong>Profile QR Code</strong>
|
|
||||||
<QRCode {code} class="max-w-64" />
|
|
||||||
<p class="break-all text-sm opacity-75">{code}</p>
|
|
||||||
<p class="text-sm opacity-75">Tap the QR code to copy this npub.</p>
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onclick={back} class="hidden md:btn md:btn-link">
|
|
||||||
<Icon icon={AltArrowLeft} />
|
|
||||||
Go back
|
|
||||||
</Button>
|
|
||||||
<Button onclick={back} class="btn btn-neutral">Close</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import cx from "classnames"
|
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
|
||||||
import UsersGroup from "@assets/icons/users-group-rounded.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
|
||||||
import {
|
|
||||||
groupListPubkeysByUrl,
|
|
||||||
userSpaceUrls,
|
|
||||||
deriveGroupList,
|
|
||||||
getSpaceUrlsFromGroupList,
|
|
||||||
} from "@app/groups"
|
|
||||||
import {makeSpacePath} from "@app/routes"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
pubkey: string
|
|
||||||
limit?: number
|
|
||||||
onViewAll?: () => void
|
|
||||||
class?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const {pubkey, limit, onViewAll, ...props}: Props = $props()
|
|
||||||
|
|
||||||
const groupList = deriveGroupList(pubkey)
|
|
||||||
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
|
|
||||||
const sharedSpaceUrls = $derived($userSpaceUrls.filter(url => spaceUrls.includes(url)))
|
|
||||||
const displayUrls = $derived(limit ? sharedSpaceUrls.slice(0, limit) : sharedSpaceUrls)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class={cx("card2 bg-alt col-3 border border-base-300 max-sm:p-5 sm:col-4", props.class)}>
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
|
||||||
<div class="row-2">
|
|
||||||
<Icon icon={UsersGroup} size={5} />
|
|
||||||
<strong>Shared spaces</strong>
|
|
||||||
</div>
|
|
||||||
<span class="badge badge-neutral">{sharedSpaceUrls.length}</span>
|
|
||||||
</div>
|
|
||||||
{#if displayUrls.length > 0}
|
|
||||||
<div class="col-2 border-t border-base-300 pt-4 sm:pt-4">
|
|
||||||
{#each displayUrls as url (url)}
|
|
||||||
{@const count = $groupListPubkeysByUrl.get(url)?.size || 0}
|
|
||||||
<Link
|
|
||||||
href={makeSpacePath(url)}
|
|
||||||
class="row-2 rounded-box border border-base-300 p-4 transition-colors hover:bg-base-300/30 sm:p-3">
|
|
||||||
<RelayIcon {url} size={8} />
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<RelayName {url} class="ellipsize text-sm font-medium" />
|
|
||||||
<p class="text-xs opacity-75">
|
|
||||||
{#if count >= 1000}
|
|
||||||
{(count / 1000).toFixed(1).replace(/\.0$/, "")}K members
|
|
||||||
{:else}
|
|
||||||
{count} {count === 1 ? "member" : "members"}
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{#if onViewAll && sharedSpaceUrls.length > (limit || sharedSpaceUrls.length)}
|
|
||||||
<button
|
|
||||||
class="link link-primary row-2 border-t border-base-300 pt-4 text-sm max-sm:pt-4"
|
|
||||||
onclick={onViewAll}>
|
|
||||||
View all shared spaces
|
|
||||||
<Icon icon={AltArrowRight} size={4} />
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<p class="border-t border-base-300 pt-4 text-sm opacity-75 max-sm:pt-4">
|
|
||||||
No shared spaces yet.
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {clamp, uniq} from "@welshman/lib"
|
|
||||||
import {
|
|
||||||
pubkey,
|
|
||||||
followLists,
|
|
||||||
deriveUserWotScore,
|
|
||||||
deriveProfileDisplay,
|
|
||||||
deriveFollowList,
|
|
||||||
getFollows,
|
|
||||||
} from "@welshman/app"
|
|
||||||
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
pubkey: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const {pubkey: target}: Props = $props()
|
|
||||||
|
|
||||||
const score = deriveUserWotScore(target)
|
|
||||||
const profileDisplay = deriveProfileDisplay(target)
|
|
||||||
const targetFollowList = deriveFollowList(target)
|
|
||||||
|
|
||||||
const mutualFollows = $derived.by(() => {
|
|
||||||
const viewer = $pubkey
|
|
||||||
void $followLists
|
|
||||||
void $targetFollowList
|
|
||||||
|
|
||||||
if (!viewer) return []
|
|
||||||
|
|
||||||
const viewerFollows = new Set(getFollows(viewer))
|
|
||||||
|
|
||||||
return uniq(
|
|
||||||
getFollows(target).filter(pk => pk !== viewer && pk !== target && viewerFollows.has(pk)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
const displayScore = $derived(Math.round(clamp([0, 100], $score)))
|
|
||||||
const progress = $derived(displayScore)
|
|
||||||
|
|
||||||
const trustMessage = $derived.by(() => {
|
|
||||||
if (displayScore >= 70) return "This user is highly trusted in your network."
|
|
||||||
if (displayScore >= 30) return "This user has some trust in your network."
|
|
||||||
|
|
||||||
return "This user is not well known in your network."
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="card2 bg-alt col-3 border border-base-300 max-sm:p-5 sm:col-4">
|
|
||||||
<div class="row-2">
|
|
||||||
<Icon icon={Shield} size={5} />
|
|
||||||
<strong>Reputation</strong>
|
|
||||||
</div>
|
|
||||||
<div class="col-2 border-t border-base-300 pt-4 sm:pt-4">
|
|
||||||
<div class="flex items-end justify-between gap-2">
|
|
||||||
<span class="text-sm opacity-75">Trust score</span>
|
|
||||||
<span class="text-lg font-semibold">{displayScore} / 100</span>
|
|
||||||
</div>
|
|
||||||
<progress class="progress progress-primary w-full" value={progress} max="100"></progress>
|
|
||||||
<p class="text-sm opacity-75">{trustMessage}</p>
|
|
||||||
</div>
|
|
||||||
{#if mutualFollows.length > 0}
|
|
||||||
<div class="col-2 border-t border-base-300 pt-4 sm:pt-4">
|
|
||||||
<p class="text-sm font-medium">Mutual follows</p>
|
|
||||||
<ProfileCircles pubkeys={mutualFollows} limit={5} />
|
|
||||||
<p class="text-sm opacity-75">
|
|
||||||
{mutualFollows.length}
|
|
||||||
{mutualFollows.length === 1 ? "person" : "people"} you and {$profileDisplay} both follow.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url))
|
const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button class="cv card2 bg-alt shadow-md" onclick={onClick}>
|
<Button class="cv card2" onclick={onClick}>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
{#if h}
|
{#if h}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {THREAD, CLASSIFIED, ZAP_GOAL, EVENT_TIME, POLL} from "@welshman/util"
|
||||||
|
import NoteItem from "@app/components/NoteItem.svelte"
|
||||||
|
import ThreadItem from "@app/components/ThreadItem.svelte"
|
||||||
|
import ClassifiedItem from "@app/components/ClassifiedItem.svelte"
|
||||||
|
import GoalItem from "@app/components/GoalItem.svelte"
|
||||||
|
import CalendarEventItem from "@app/components/CalendarEventItem.svelte"
|
||||||
|
import PollItem from "@app/components/PollItem.svelte"
|
||||||
|
import RecentConversation from "@app/components/RecentConversation.svelte"
|
||||||
|
import type {RecentActivityItem} from "@app/recent"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
item: RecentActivityItem
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, item}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if item.type === "message"}
|
||||||
|
<RecentConversation {url} event={item.event} count={item.count} />
|
||||||
|
{:else if item.event.kind === THREAD}
|
||||||
|
<ThreadItem {url} event={item.event} />
|
||||||
|
{:else if item.event.kind === CLASSIFIED}
|
||||||
|
<ClassifiedItem {url} event={item.event} />
|
||||||
|
{:else if item.event.kind === ZAP_GOAL}
|
||||||
|
<GoalItem {url} event={item.event} />
|
||||||
|
{:else if item.event.kind === EVENT_TIME}
|
||||||
|
<CalendarEventItem {url} event={item.event} />
|
||||||
|
{:else if item.event.kind === POLL}
|
||||||
|
<PollItem {url} event={item.event} />
|
||||||
|
{:else}
|
||||||
|
<NoteItem {url} event={item.event} />
|
||||||
|
{/if}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
import Popover from "@lib/components/Popover.svelte"
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import {deriveUserIsSpaceAdmin} from "@app/members"
|
import {deriveUserIsSpaceAdmin, banSpaceMembers} from "@app/members"
|
||||||
import {publishDelete} from "@app/deletes"
|
import {publishDelete} from "@app/deletes"
|
||||||
import {canEnforceNip70} from "@app/relays"
|
import {canEnforceNip70} from "@app/relays"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
@@ -91,10 +91,7 @@
|
|||||||
title: "Ban User",
|
title: "Ban User",
|
||||||
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
const {error} = await manageRelay(url, {
|
const error = await banSpaceMembers(url, [pubkey], reason)
|
||||||
method: ManagementMethod.BanPubkey,
|
|
||||||
params: [pubkey, reason],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
|
|||||||
+27
-16
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -12,43 +11,55 @@
|
|||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||||
import {addSpaceMembers} from "@app/members"
|
import {addSpaceMembers, assignRole, type SpaceRole} from "@app/members"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string
|
url: string
|
||||||
|
role: SpaceRole
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url}: Props = $props()
|
const {url, role}: Props = $props()
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const addMember = async () => {
|
let loading = $state(false)
|
||||||
|
let pubkeys: string[] = $state([])
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const error = await addSpaceMembers(url, pubkeys)
|
// Ensure they're space members first, then assign the role
|
||||||
|
const memberError = await addSpaceMembers(url, pubkeys)
|
||||||
|
|
||||||
if (error) {
|
if (memberError) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: memberError})
|
||||||
} else {
|
return
|
||||||
pushToast({message: "Members have successfully been added!"})
|
|
||||||
back()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const pubkey of pubkeys) {
|
||||||
|
const error = await assignRole(url, pubkey, role.id)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushToast({message: "Members assigned!"})
|
||||||
|
back()
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let loading = $state(false)
|
|
||||||
let pubkeys: string[] = $state([])
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<ModalTitle>Add Members</ModalTitle>
|
<ModalTitle>Add to {role.label || "Role"}</ModalTitle>
|
||||||
<ModalSubtitle>to {displayRelayUrl(url)}</ModalSubtitle>
|
<ModalSubtitle>Assign members to this role</ModalSubtitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<Field>
|
<Field>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
@@ -64,7 +75,7 @@
|
|||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="btn btn-primary" onclick={addMember} disabled={loading}>
|
<Button class="btn btn-primary" onclick={submit} disabled={loading || pubkeys.length === 0}>
|
||||||
<Spinner {loading}>Save changes</Spinner>
|
<Spinner {loading}>Save changes</Spinner>
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {roleColor, roleColorSoft, type SpaceRole} from "@app/members"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
role: SpaceRole
|
||||||
|
}
|
||||||
|
|
||||||
|
const {role}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="badge min-w-0"
|
||||||
|
style="background-color: {roleColorSoft(role.color)}; border-color: {roleColor(
|
||||||
|
role.color,
|
||||||
|
)}; color: {roleColor(role.color)};">
|
||||||
|
<strong>{role.label || "Untitled Role"}</strong>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {randomId} from "@welshman/lib"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
|
import RoleForm, {type Values} from "@app/components/RoleForm.svelte"
|
||||||
|
import {createRole} from "@app/members"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
|
||||||
|
const onSubmit = async ({label, description, color}: Values) => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = await createRole(url, randomId(), label, description, color, 0)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Role created!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Create Role</ModalTitle>
|
||||||
|
<ModalSubtitle>in <RelayName {url} class="text-primary" /></ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<RoleForm {loading} {onSubmit} />
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
|
import RoleForm, {type Values} from "@app/components/RoleForm.svelte"
|
||||||
|
import {editRole, type SpaceRole} from "@app/members"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
role: SpaceRole
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, role}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
|
||||||
|
const onSubmit = async ({label, description, color}: Values) => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = await editRole(url, role.id, label, description, color, role.order)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Role updated!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Edit Role</ModalTitle>
|
||||||
|
<ModalSubtitle>in <RelayName {url} class="text-primary" /></ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<RoleForm {loading} {onSubmit} initialValues={role} />
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script module lang="ts">
|
||||||
|
import type {SpaceRole} from "@app/members"
|
||||||
|
|
||||||
|
export type Values = Pick<SpaceRole, "label" | "description" | "color">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {roleColor} from "@app/members"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialValues?: Partial<Values>
|
||||||
|
loading?: boolean
|
||||||
|
onSubmit: (values: Values) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {initialValues = {}, loading = false, onSubmit}: Props = $props()
|
||||||
|
|
||||||
|
const values: Values = $state({
|
||||||
|
label: "",
|
||||||
|
description: "",
|
||||||
|
color: Math.floor(Math.random() * 256),
|
||||||
|
...initialValues,
|
||||||
|
})
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const submit = () => onSubmit(values)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Name</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<input
|
||||||
|
bind:value={values.label}
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="Moderator" />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Description</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<textarea bind:value={values.description} class="textarea textarea-bordered w-full" rows="2"
|
||||||
|
></textarea>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Color</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="h-8 w-8 shrink-0 rounded-full border-2 border-base-300"
|
||||||
|
style="background-color: {roleColor(values.color)}">
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="255"
|
||||||
|
bind:value={values.color}
|
||||||
|
class="range range-sm grow"
|
||||||
|
style="color: {roleColor(values.color)}; --range-shdw: {roleColor(values.color)}" />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" onclick={submit} disabled={loading || !values.label}>
|
||||||
|
<Spinner {loading}>Save changes</Spinner>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import RoleBadge from "@app/components/RoleBadge.svelte"
|
||||||
|
import type {SpaceRole} from "@app/members"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
role: SpaceRole
|
||||||
|
}
|
||||||
|
|
||||||
|
const {role}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex min-w-0 flex-col gap-2">
|
||||||
|
<RoleBadge {role} />
|
||||||
|
{#if role.description}
|
||||||
|
<p class="text-sm opacity-70">{role.description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<div class="flex min-w-0 flex-col">
|
<div class="flex min-w-0 flex-col">
|
||||||
<div class="flex min-w-0 items-start gap-2">
|
<div class="flex min-w-0 items-start gap-2">
|
||||||
<RelayIcon {url} size={5} class="shrink-0 rounded-full md:hidden" />
|
<RelayIcon {url} size={5} class="shrink-0 rounded-full md:hidden" />
|
||||||
<div class="hidden shrink-0 md:flex md:items-center">
|
<div class="hidden shrink-0 md:flex md:items-center place-self-center">
|
||||||
{@render leading?.()}
|
{@render leading?.()}
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
|
||||||
import {deriveRelay} from "@welshman/app"
|
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
|
||||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
|
||||||
import ShieldUser from "@assets/icons/shield-user.svg?dataurl"
|
|
||||||
import BillList from "@assets/icons/bill-list.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
|
||||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
|
||||||
import SpaceEdit from "@app/components/SpaceEdit.svelte"
|
|
||||||
import SpaceRelayStatus from "@app/components/SpaceRelayStatus.svelte"
|
|
||||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
|
||||||
import ProfileLatest from "@app/components/ProfileLatest.svelte"
|
|
||||||
import {deriveUserIsSpaceAdmin} from "@app/members"
|
|
||||||
import {pushModal} from "@app/modal"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const {url}: Props = $props()
|
|
||||||
const relay = deriveRelay(url)
|
|
||||||
const owner = $derived($relay?.pubkey)
|
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
|
|
||||||
const startEdit = () => pushModal(SpaceEdit, {url, initialValues: $relay || {url}})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal>
|
|
||||||
<ModalBody>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div class="relative flex gap-4">
|
|
||||||
<div class="relative">
|
|
||||||
<div class="avatar relative">
|
|
||||||
<div
|
|
||||||
class="center flex! h-16 w-16 min-w-16 rounded-full border-2 border-solid border-base-300 bg-base-300">
|
|
||||||
<RelayIcon {url} size={10} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex min-w-0 flex-col gap-1">
|
|
||||||
<h1 class="ellipsize whitespace-nowrap text-2xl font-bold">
|
|
||||||
<RelayName {url} />
|
|
||||||
</h1>
|
|
||||||
<p class="ellipsize text-sm opacity-75">{displayRelayUrl(url)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if $userIsAdmin}
|
|
||||||
<Button class="btn btn-primary" onclick={startEdit}>
|
|
||||||
<Icon icon={Pen} />
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<RelayDescription {url} />
|
|
||||||
{#if $relay?.terms_of_service || $relay?.privacy_policy}
|
|
||||||
<div class="flex gap-3">
|
|
||||||
{#if $relay.terms_of_service}
|
|
||||||
<Link href={$relay.terms_of_service} class="badge badge-neutral flex gap-2">
|
|
||||||
<Icon icon={BillList} size={4} />
|
|
||||||
Terms of Service
|
|
||||||
</Link>
|
|
||||||
{/if}
|
|
||||||
{#if $relay.privacy_policy}
|
|
||||||
<Link href={$relay.privacy_policy} class="badge badge-neutral flex gap-2">
|
|
||||||
<Icon icon={ShieldUser} size={4} />
|
|
||||||
Privacy Policy
|
|
||||||
</Link>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<SpaceRelayStatus {url} />
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
{#if owner}
|
|
||||||
<div class="card2 bg-alt">
|
|
||||||
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
|
|
||||||
<Icon icon={UserRounded} />
|
|
||||||
Latest Updates
|
|
||||||
</h3>
|
|
||||||
<ProfileLatest {url} pubkey={owner}>
|
|
||||||
{#snippet fallback()}
|
|
||||||
<p class="text-sm opacity-60">No recent posts from the relay admin</p>
|
|
||||||
{/snippet}
|
|
||||||
</ProfileLatest>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button class="btn btn-link" onclick={back}>
|
|
||||||
<Icon icon={AltArrowLeft} />
|
|
||||||
Go back
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
<Modal tag="form" onsubmit={preventDefault(trySubmit)}>
|
<Modal tag="form" onsubmit={preventDefault(trySubmit)}>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<ModalTitle>Edit a Space</ModalTitle>
|
<ModalTitle>Edit this Space</ModalTitle>
|
||||||
<ModalSubtitle><span class="text-primary">{displayRelayUrl(url)}</span></ModalSubtitle>
|
<ModalSubtitle><span class="text-primary">{displayRelayUrl(url)}</span></ModalSubtitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Content from "@app/components/Content.svelte"
|
||||||
|
import EditFeaturedContent from "@app/components/EditFeaturedContent.svelte"
|
||||||
|
import SpaceRecentSummary from "@app/components/SpaceRecentSummary.svelte"
|
||||||
|
import {deriveFeaturedContent} from "@app/featured"
|
||||||
|
import {deriveSupportedMethods} from "@app/relays"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const content = deriveFeaturedContent(url)
|
||||||
|
const supportedMethods = deriveSupportedMethods(url)
|
||||||
|
const canEdit = $derived($supportedMethods.some(m => (m as string) === "signevent"))
|
||||||
|
|
||||||
|
const edit = () => pushModal(EditFeaturedContent, {url, initial: $content})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $content.length > 0 || canEdit}
|
||||||
|
<div class="card2 bg-alt flex flex-col gap-3">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<h3 class="flex items-center gap-2 text-lg font-bold">
|
||||||
|
<Icon icon={Bookmark} />
|
||||||
|
Featured
|
||||||
|
</h3>
|
||||||
|
{#if canEdit}
|
||||||
|
<Button class="btn btn-square btn-ghost btn-sm" onclick={edit}>
|
||||||
|
<Icon icon={Pen} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if $content.length === 0}
|
||||||
|
<p class="text-sm opacity-70">No featured content yet.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each $content as value (value)}
|
||||||
|
<Content event={{content: value, tags: []}} {url} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<SpaceRecentSummary {url} />
|
||||||
|
{/if}
|
||||||
@@ -2,29 +2,36 @@
|
|||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {sleep} from "@welshman/lib"
|
import {sleep} from "@welshman/lib"
|
||||||
import {request} from "@welshman/net"
|
import {request} from "@welshman/net"
|
||||||
import {displayRelayUrl, getTagValue, RELAY_INVITE} from "@welshman/util"
|
import {displayRelayUrl, getTagValue, ManagementMethod, RELAY_INVITE} from "@welshman/util"
|
||||||
import {Share} from "@capacitor/share"
|
import {Share} from "@capacitor/share"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
import Upload from "@assets/icons/upload.svg?dataurl"
|
import Upload from "@assets/icons/upload.svg?dataurl"
|
||||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||||
import QRCode from "@app/components/QRCode.svelte"
|
import QRCode from "@app/components/QRCode.svelte"
|
||||||
import {clip} from "@app/toast"
|
import {clip, pushToast} from "@app/toast"
|
||||||
import {PLATFORM_URL} from "@app/env"
|
import {PLATFORM_URL} from "@app/env"
|
||||||
import {deriveRelayAuthError} from "@app/relays"
|
import {deriveRelayAuthError, deriveSupportedMethods} from "@app/relays"
|
||||||
|
import {addSpaceMembers} from "@app/members"
|
||||||
|
|
||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
|
const supportedMethods = deriveSupportedMethods(url)
|
||||||
|
const canAddMembers = $derived($supportedMethods.includes(ManagementMethod.AllowPubkey))
|
||||||
const authError = deriveRelayAuthError(url)
|
const authError = deriveRelayAuthError(url)
|
||||||
|
|
||||||
let networkError = $state(false)
|
let networkError = $state(false)
|
||||||
const isExplicitAuthError = $derived(
|
const isExplicitAuthError = $derived(
|
||||||
$authError &&
|
$authError &&
|
||||||
@@ -54,6 +61,28 @@
|
|||||||
let loading = $state(true)
|
let loading = $state(true)
|
||||||
let invite = $state("")
|
let invite = $state("")
|
||||||
|
|
||||||
|
let adding = $state(false)
|
||||||
|
let pubkeys: string[] = $state([])
|
||||||
|
|
||||||
|
const addMembers = async () => {
|
||||||
|
if (pubkeys.length === 0) return
|
||||||
|
|
||||||
|
adding = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = await addSpaceMembers(url, pubkeys)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Members have successfully been added!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
adding = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const relay = displayRelayUrl(url)
|
const relay = displayRelayUrl(url)
|
||||||
const params = new URLSearchParams({r: relay, c: claim}).toString()
|
const params = new URLSearchParams({r: relay, c: claim}).toString()
|
||||||
@@ -124,7 +153,7 @@
|
|||||||
<div class="flex w-full gap-2">
|
<div class="flex w-full gap-2">
|
||||||
{#if canShare}
|
{#if canShare}
|
||||||
<Button
|
<Button
|
||||||
class="input input-bordered flex shrink-0 w-12 items-center justify-center p-0"
|
class="input input-bordered flex w-12 shrink-0 items-center justify-center p-0"
|
||||||
onclick={shareInvite}>
|
onclick={shareInvite}>
|
||||||
<Icon icon={Upload} />
|
<Icon icon={Upload} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -152,8 +181,32 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if canAddMembers}
|
||||||
|
<Divider>or</Divider>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Add members directly</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<ProfileMultiSelect bind:value={pubkeys} />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
{/if}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-primary grow" onclick={back}>Done</Button>
|
{#if canAddMembers}
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="btn btn-primary"
|
||||||
|
onclick={addMembers}
|
||||||
|
disabled={adding || pubkeys.length === 0}>
|
||||||
|
<Spinner loading={adding}>Save</Spinner>
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button class="btn btn-primary grow" onclick={back}>Done</Button>
|
||||||
|
{/if}
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import {dissoc, maybe} from "@welshman/lib"
|
import {dissoc, maybe} from "@welshman/lib"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import {slideAndFade} from "@lib/transition"
|
import {slideAndFade} from "@lib/transition"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
@@ -22,7 +21,7 @@
|
|||||||
import RelaySummary from "@app/components/RelaySummary.svelte"
|
import RelaySummary from "@app/components/RelaySummary.svelte"
|
||||||
import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte"
|
import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {goToSpace} from "@app/routes"
|
||||||
import {relaysMostlyRestricted} from "@app/policies"
|
import {relaysMostlyRestricted} from "@app/policies"
|
||||||
import {notificationSettings, setSpaceNotifications} from "@app/settings"
|
import {notificationSettings, setSpaceNotifications} from "@app/settings"
|
||||||
import {parseInviteLink} from "@app/invites"
|
import {parseInviteLink} from "@app/invites"
|
||||||
@@ -68,7 +67,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await addSpace(url)
|
await addSpace(url)
|
||||||
await goto(makeSpacePath(url), {replaceState: true})
|
await goToSpace(url)
|
||||||
|
|
||||||
broadcastUserData([url])
|
broadcastUserData([url])
|
||||||
relaysMostlyRestricted.update(dissoc(url))
|
relaysMostlyRestricted.update(dissoc(url))
|
||||||
@@ -124,7 +123,7 @@
|
|||||||
<div class="card2 bg-alt flex flex-col gap-4">
|
<div class="card2 bg-alt flex flex-col gap-4">
|
||||||
<p class="opacity-75">You're about to join:</p>
|
<p class="opacity-75">You're about to join:</p>
|
||||||
<RelaySummary url={inviteData.url} />
|
<RelaySummary url={inviteData.url} />
|
||||||
<SpaceJoinSettings url={inviteData.url} bind:error bind:notifications />
|
<SpaceJoinSettings url={inviteData.url} {error} bind:notifications />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {dissoc, maybe} from "@welshman/lib"
|
import {dissoc, maybe} from "@welshman/lib"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
@@ -22,7 +21,7 @@
|
|||||||
import {notificationSettings} from "@app/settings"
|
import {notificationSettings} from "@app/settings"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {goToSpace} from "@app/routes"
|
||||||
import {Push} from "@app/push"
|
import {Push} from "@app/push"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -56,7 +55,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await addSpace(url)
|
await addSpace(url)
|
||||||
await goto(makeSpacePath(url), {replaceState: true})
|
await goToSpace(url)
|
||||||
|
|
||||||
broadcastUserData([url])
|
broadcastUserData([url])
|
||||||
relaysMostlyRestricted.update(dissoc(url))
|
relaysMostlyRestricted.update(dissoc(url))
|
||||||
@@ -82,7 +81,7 @@
|
|||||||
<Modal tag="form" onsubmit={preventDefault(join)}>
|
<Modal tag="form" onsubmit={preventDefault(join)}>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<RelaySummary {url} />
|
<RelaySummary {url} />
|
||||||
<SpaceJoinSettings {url} bind:error bind:notifications />
|
<SpaceJoinSettings {url} {error} bind:notifications />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
notifications: boolean
|
notifications: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let {url, error = $bindable(), notifications = $bindable()}: Props = $props()
|
let {url, error, notifications = $bindable()}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card2 card2-sm bg-alt">
|
<div class="card2 card2-sm bg-alt">
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {ManagementMethod} from "@welshman/util"
|
||||||
|
import {displayProfileByPubkey} from "@welshman/app"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
|
import UserMinus from "@assets/icons/user-minus.svg?dataurl"
|
||||||
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
||||||
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
|
import SpaceMemberRoles from "@app/components/SpaceMemberRoles.svelte"
|
||||||
|
import RoleBadge from "@app/components/RoleBadge.svelte"
|
||||||
|
import {removeSpaceMembers, banSpaceMembers, type SpaceRole} from "@app/members"
|
||||||
|
import {deriveSupportedMethods} from "@app/relays"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
pubkey: string
|
||||||
|
roles?: SpaceRole[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, pubkey, roles = []}: Props = $props()
|
||||||
|
|
||||||
|
const supportedMethods = deriveSupportedMethods(url)
|
||||||
|
const canUnallow = $derived($supportedMethods.includes(ManagementMethod.UnallowPubkey))
|
||||||
|
const canBan = $derived($supportedMethods.includes(ManagementMethod.BanPubkey))
|
||||||
|
const canAssign = $derived($supportedMethods.some(m => (m as string) === "assignrole"))
|
||||||
|
const canUnassign = $derived($supportedMethods.some(m => (m as string) === "unassignrole"))
|
||||||
|
|
||||||
|
let menuOpen = $state(false)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
const closeMenu = () => (menuOpen = false)
|
||||||
|
|
||||||
|
const openProfile = () => {
|
||||||
|
menuOpen = false
|
||||||
|
pushModal(ProfileDetail, {pubkey, url})
|
||||||
|
}
|
||||||
|
|
||||||
|
const editRoles = () => {
|
||||||
|
menuOpen = false
|
||||||
|
pushModal(SpaceMemberRoles, {url, pubkey})
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeMember = () => {
|
||||||
|
menuOpen = false
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Remove Member",
|
||||||
|
message: `Remove @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
|
confirm: async () => {
|
||||||
|
const error = await removeSpaceMembers(url, [pubkey])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Member has successfully been removed!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const banMember = () => {
|
||||||
|
menuOpen = false
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Ban Member",
|
||||||
|
message: `Ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
|
confirm: async () => {
|
||||||
|
const error = await banSpaceMembers(url, [pubkey])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Member has successfully been banned!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card2 card2-sm relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute inset-0 cursor-pointer rounded-box"
|
||||||
|
aria-label="View {displayProfileByPubkey(pubkey)}'s profile"
|
||||||
|
onclick={openProfile}>
|
||||||
|
</button>
|
||||||
|
<div class="pointer-events-none relative flex items-start justify-between gap-2">
|
||||||
|
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
||||||
|
<Profile {pubkey} {url} inert />
|
||||||
|
{#if roles.length > 0}
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
{#each roles as role (role.id)}
|
||||||
|
<RoleBadge {role} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="line-clamp-1 text-sm opacity-70">
|
||||||
|
<ProfileInfo {pubkey} {url} singleLine />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if canAssign || canUnassign || canUnallow || canBan}
|
||||||
|
<div class="pointer-events-auto relative shrink-0">
|
||||||
|
<Button class="btn btn-square btn-ghost btn-sm" onclick={() => (menuOpen = !menuOpen)}>
|
||||||
|
<Icon icon={MenuDots} />
|
||||||
|
</Button>
|
||||||
|
{#if menuOpen}
|
||||||
|
<Popover hideOnClick onClose={closeMenu}>
|
||||||
|
<ul
|
||||||
|
transition:fly
|
||||||
|
class="menu absolute right-0 z-popover mt-2 w-52 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||||
|
{#if canAssign || canUnassign}
|
||||||
|
<li>
|
||||||
|
<Button onclick={editRoles}>
|
||||||
|
<Icon icon={Pen} />
|
||||||
|
Edit roles
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if canUnallow}
|
||||||
|
<li>
|
||||||
|
<Button onclick={removeMember}>
|
||||||
|
<Icon icon={UserMinus} />
|
||||||
|
Remove member
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if canBan}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={banMember}>
|
||||||
|
<Icon icon={MinusCircle} />
|
||||||
|
Ban member
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {get} from "svelte/store"
|
||||||
|
import {displayProfileByPubkey} from "@welshman/app"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import RoleItem from "@app/components/RoleItem.svelte"
|
||||||
|
import {deriveSpaceRoles, deriveSpaceMemberRoles, assignRole, unassignRole} from "@app/members"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
pubkey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, pubkey}: Props = $props()
|
||||||
|
|
||||||
|
const roles = deriveSpaceRoles(url)
|
||||||
|
const memberRoles = deriveSpaceMemberRoles(url)
|
||||||
|
const initial = new Set(get(memberRoles).get(pubkey) ?? [])
|
||||||
|
|
||||||
|
let selected = $state(new Set(initial))
|
||||||
|
let loading = $state(false)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const toggle = (id: string) => {
|
||||||
|
const next = new Set(selected)
|
||||||
|
|
||||||
|
if (next.has(id)) {
|
||||||
|
next.delete(id)
|
||||||
|
} else {
|
||||||
|
next.add(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
selected = next
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const id of selected) {
|
||||||
|
if (!initial.has(id)) {
|
||||||
|
const error = await assignRole(url, pubkey, id)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of initial) {
|
||||||
|
if (!selected.has(id)) {
|
||||||
|
const error = await unassignRole(url, pubkey, id)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushToast({message: "Roles updated!"})
|
||||||
|
back()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Edit Member</ModalTitle>
|
||||||
|
<ModalSubtitle>
|
||||||
|
Manage roles for <span class="text-primary">@{displayProfileByPubkey(pubkey)}</span>
|
||||||
|
</ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
{#if $roles.length === 0}
|
||||||
|
<div class="card2 bg-base-200 p-4 text-sm opacity-70">This space has no roles yet.</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each $roles as role (role.id)}
|
||||||
|
<label class="card2 card2-sm flex justify-between cursor-pointer gap-3">
|
||||||
|
<RoleItem {role} />
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
checked={selected.has(role.id)}
|
||||||
|
onchange={() => toggle(role.id)} />
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" onclick={submit} disabled={loading}>
|
||||||
|
<Spinner {loading}>Save changes</Spinner>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {ManagementMethod} from "@welshman/util"
|
|
||||||
import {manageRelay, displayProfileByPubkey} from "@welshman/app"
|
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
|
||||||
import UserMinus from "@assets/icons/user-minus.svg?dataurl"
|
|
||||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
|
||||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Popover from "@lib/components/Popover.svelte"
|
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
|
||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
|
||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
|
||||||
import Profile from "@app/components/Profile.svelte"
|
|
||||||
import SpaceMembersAdd from "@app/components/SpaceMembersAdd.svelte"
|
|
||||||
import SpaceMembersBanned from "@app/components/SpaceMembersBanned.svelte"
|
|
||||||
import {
|
|
||||||
deriveSpaceMembers,
|
|
||||||
deriveSpaceBannedPubkeyItems,
|
|
||||||
deriveUserIsSpaceAdmin,
|
|
||||||
} from "@app/members"
|
|
||||||
import {deriveSupportedMethods} from "@app/relays"
|
|
||||||
import {pushModal} from "@app/modal"
|
|
||||||
import {pushToast} from "@app/toast"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const {url}: Props = $props()
|
|
||||||
|
|
||||||
const members = deriveSpaceMembers(url)
|
|
||||||
const bans = deriveSpaceBannedPubkeyItems(url)
|
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
|
||||||
const supportedMethods = deriveSupportedMethods(url)
|
|
||||||
const canBan = $derived($supportedMethods.includes(ManagementMethod.BanPubkey))
|
|
||||||
const canUnallow = $derived($supportedMethods.includes(ManagementMethod.UnallowPubkey))
|
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
|
|
||||||
const toggleMenu = (pubkey: string) => {
|
|
||||||
menuPubkey = menuPubkey === pubkey ? undefined : pubkey
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
menuPubkey = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const showBannedPubkeyItems = () => pushModal(SpaceMembersBanned, {url})
|
|
||||||
|
|
||||||
const addMember = () => pushModal(SpaceMembersAdd, {url})
|
|
||||||
|
|
||||||
const unallowMember = (pubkey: string) =>
|
|
||||||
pushModal(Confirm, {
|
|
||||||
title: "Remove User",
|
|
||||||
message: `Are you sure you want to remove @${displayProfileByPubkey(pubkey)} from the space?`,
|
|
||||||
confirm: async () => {
|
|
||||||
const {error} = await manageRelay(url, {
|
|
||||||
method: ManagementMethod.UnallowPubkey,
|
|
||||||
params: [pubkey],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "User has successfully been removed!"})
|
|
||||||
back()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const banMember = (pubkey: string) =>
|
|
||||||
pushModal(Confirm, {
|
|
||||||
title: "Ban User",
|
|
||||||
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
|
||||||
confirm: async () => {
|
|
||||||
const {error} = await manageRelay(url, {
|
|
||||||
method: ManagementMethod.BanPubkey,
|
|
||||||
params: [pubkey],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pushToast({theme: "error", message: error})
|
|
||||||
} else {
|
|
||||||
pushToast({message: "User has successfully been banned!"})
|
|
||||||
back()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
let menuPubkey = $state<string | undefined>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal>
|
|
||||||
<ModalBody>
|
|
||||||
<ModalHeader>
|
|
||||||
<ModalTitle>Members</ModalTitle>
|
|
||||||
<ModalSubtitle>of <RelayName {url} class="text-primary" /></ModalSubtitle>
|
|
||||||
</ModalHeader>
|
|
||||||
{#if $userIsAdmin}
|
|
||||||
{#if $bans.length > 0}
|
|
||||||
<Button class="btn btn-neutral" onclick={showBannedPubkeyItems}>
|
|
||||||
Banned users ({$bans.length})
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
{#if $members === undefined}
|
|
||||||
<div class="card2 bg-base-200 p-4">
|
|
||||||
<span class="text-error">Member list not available from this space</span>
|
|
||||||
</div>
|
|
||||||
{:else if $members.length === 0}
|
|
||||||
<div class="card2 bg-base-200 p-4">
|
|
||||||
<span class="text-base-content/70">No members yet</span>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
{#each $members as pubkey (pubkey)}
|
|
||||||
<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>
|
|
||||||
<Button class="btn btn-link" onclick={back}>
|
|
||||||
<Icon icon={AltArrowLeft} />
|
|
||||||
Go back
|
|
||||||
</Button>
|
|
||||||
{#if $userIsAdmin}
|
|
||||||
<Button class="btn btn-primary" onclick={addMember}>
|
|
||||||
<Icon icon={AddCircle} />
|
|
||||||
Add members
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
@@ -76,6 +76,9 @@
|
|||||||
<ModalSubtitle>on {displayRelayUrl(url)}</ModalSubtitle>
|
<ModalSubtitle>on {displayRelayUrl(url)}</ModalSubtitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
{#if $bans.length === 0}
|
||||||
|
<div class="card2 bg-base-200 p-4 text-sm opacity-70">No banned users.</div>
|
||||||
|
{/if}
|
||||||
{#each $bans as { pubkey, reason } (pubkey)}
|
{#each $bans as { pubkey, reason } (pubkey)}
|
||||||
<div class="card2 bg-alt relative">
|
<div class="card2 bg-alt relative">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {derived} from "svelte/store"
|
||||||
|
import {RELAY_ADD_MEMBER, RELAY_JOIN, getPubkeyTagValues} from "@welshman/util"
|
||||||
|
import {deriveRelay} from "@welshman/app"
|
||||||
|
import UsersGroup from "@assets/icons/users-group-rounded.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
import {deriveSpaceMembers} from "@app/members"
|
||||||
|
import {deriveEventsForUrl} from "@app/repository"
|
||||||
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const relay = deriveRelay(url)
|
||||||
|
const members = deriveSpaceMembers(url)
|
||||||
|
const memberEvents = deriveEventsForUrl(url, [{kinds: [RELAY_ADD_MEMBER, RELAY_JOIN]}])
|
||||||
|
|
||||||
|
const admins = $derived($relay?.pubkey ? [$relay.pubkey] : [])
|
||||||
|
|
||||||
|
const directoryPath = makeSpacePath(url, "directory")
|
||||||
|
|
||||||
|
// Members sorted by their most recent join/add event, excluding admins.
|
||||||
|
const newMembers = derived(
|
||||||
|
[members, memberEvents, relay],
|
||||||
|
([$members, $memberEvents, $relay]) => {
|
||||||
|
const adminSet = new Set($relay?.pubkey ? [$relay.pubkey] : [])
|
||||||
|
const joinedAt = new Map<string, number>()
|
||||||
|
|
||||||
|
for (const event of $memberEvents) {
|
||||||
|
const pubkeys = event.kind === RELAY_JOIN ? [event.pubkey] : getPubkeyTagValues(event.tags)
|
||||||
|
|
||||||
|
for (const pubkey of pubkeys) {
|
||||||
|
joinedAt.set(pubkey, Math.max(joinedAt.get(pubkey) || 0, event.created_at))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $members
|
||||||
|
.filter(pubkey => !adminSet.has(pubkey))
|
||||||
|
.sort((a, b) => (joinedAt.get(b) || 0) - (joinedAt.get(a) || 0))
|
||||||
|
.slice(0, 5)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card2 bg-alt flex flex-col gap-3">
|
||||||
|
<h3 class="flex items-center gap-2 text-lg font-bold">
|
||||||
|
<Icon icon={UsersGroup} />
|
||||||
|
Members
|
||||||
|
</h3>
|
||||||
|
{#if admins.length > 0}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<p class="text-xs uppercase tracking-wide opacity-60">Admins</p>
|
||||||
|
{#each admins as pubkey (pubkey)}
|
||||||
|
<Profile {pubkey} {url} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if $newMembers.length > 0}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<p class="text-xs uppercase tracking-wide opacity-60">New members</p>
|
||||||
|
{#each $newMembers as pubkey (pubkey)}
|
||||||
|
<Profile {pubkey} {url} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<Link href={directoryPath} class="btn btn-neutral btn-sm">
|
||||||
|
View all members
|
||||||
|
<Icon icon={AltArrowRight} size={4} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||||
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
|
import UsersGroup from "@assets/icons/users-group-rounded.svg?dataurl"
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
import Home from "@assets/icons/home.svg?dataurl"
|
||||||
import Danger from "@assets/icons/danger.svg?dataurl"
|
import Danger from "@assets/icons/danger.svg?dataurl"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
||||||
@@ -29,12 +29,10 @@
|
|||||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
|
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
|
||||||
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
||||||
import SpaceDetail from "@app/components/SpaceDetail.svelte"
|
|
||||||
import SpaceInvite from "@app/components/SpaceInvite.svelte"
|
import SpaceInvite from "@app/components/SpaceInvite.svelte"
|
||||||
import SpaceExit from "@app/components/SpaceExit.svelte"
|
import SpaceExit from "@app/components/SpaceExit.svelte"
|
||||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import SpaceMembers from "@app/components/SpaceMembers.svelte"
|
|
||||||
import SpaceActionItems from "@app/components/SpaceActionItems.svelte"
|
import SpaceActionItems from "@app/components/SpaceActionItems.svelte"
|
||||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||||
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
|
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
|
||||||
@@ -42,7 +40,7 @@
|
|||||||
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
|
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
|
||||||
import {ENABLE_ZAPS} from "@app/env"
|
import {ENABLE_ZAPS} from "@app/env"
|
||||||
import {CONTENT_KINDS} from "@app/content"
|
import {CONTENT_KINDS} from "@app/content"
|
||||||
import {deriveSpaceMembers, deriveUserCanCreateRoom, deriveUserIsSpaceAdmin} from "@app/members"
|
import {deriveUserCanCreateRoom, deriveUserIsSpaceAdmin} from "@app/members"
|
||||||
import {
|
import {
|
||||||
deriveUserRooms,
|
deriveUserRooms,
|
||||||
deriveOtherRooms,
|
deriveOtherRooms,
|
||||||
@@ -70,7 +68,6 @@
|
|||||||
const userRooms = deriveUserRooms(url)
|
const userRooms = deriveUserRooms(url)
|
||||||
const otherRooms = deriveOtherRooms(url)
|
const otherRooms = deriveOtherRooms(url)
|
||||||
const otherVoiceRooms = deriveOtherVoiceRooms(url)
|
const otherVoiceRooms = deriveOtherVoiceRooms(url)
|
||||||
const members = deriveSpaceMembers(url)
|
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
const actionItems = deriveSpaceActionItems(url)
|
const actionItems = deriveSpaceActionItems(url)
|
||||||
|
|
||||||
@@ -97,10 +94,6 @@
|
|||||||
showMenu = !showMenu
|
showMenu = !showMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
const showDetail = () => pushModal(SpaceDetail, {url})
|
|
||||||
|
|
||||||
const showMembers = () => pushModal(SpaceMembers, {url})
|
|
||||||
|
|
||||||
const showActionItems = () => pushModal(SpaceActionItems, {url})
|
const showActionItems = () => pushModal(SpaceActionItems, {url})
|
||||||
|
|
||||||
const canCreateRoom = deriveUserCanCreateRoom(url)
|
const canCreateRoom = deriveUserCanCreateRoom(url)
|
||||||
@@ -164,22 +157,6 @@
|
|||||||
Create Invite
|
Create Invite
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<Button onclick={showDetail}>
|
|
||||||
<Icon icon={RemoteControllerMinimalistic} />
|
|
||||||
Space Information
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Button onclick={showMembers}>
|
|
||||||
<Icon icon={UserRounded} />
|
|
||||||
{#if $members === undefined}
|
|
||||||
View Members
|
|
||||||
{:else}
|
|
||||||
View Members ({$members.length})
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{#if $userIsAdmin}
|
{#if $userIsAdmin}
|
||||||
<li>
|
<li>
|
||||||
<Button onclick={showActionItems}>
|
<Button onclick={showActionItems}>
|
||||||
@@ -230,6 +207,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex min-h-0 flex-1 flex-col gap-1 overflow-auto overflow-x-hidden">
|
<div class="flex min-h-0 flex-1 flex-col gap-1 overflow-auto overflow-x-hidden">
|
||||||
|
<SecondaryNavItem href={makeSpacePath(url, "about")}>
|
||||||
|
<Icon icon={Home} /> Space Details
|
||||||
|
</SecondaryNavItem>
|
||||||
{#if hasNip29($relay)}
|
{#if hasNip29($relay)}
|
||||||
<SecondaryNavItem href={makeSpacePath(url, "recent")}>
|
<SecondaryNavItem href={makeSpacePath(url, "recent")}>
|
||||||
<Icon icon={History} /> Recent Activity
|
<Icon icon={History} /> Recent Activity
|
||||||
@@ -239,6 +219,9 @@
|
|||||||
<Icon icon={ChatRound} /> Chat
|
<Icon icon={ChatRound} /> Chat
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
|
<SecondaryNavItem href={makeSpacePath(url, "directory")}>
|
||||||
|
<Icon icon={UsersGroup} /> Directory
|
||||||
|
</SecondaryNavItem>
|
||||||
{#if ENABLE_ZAPS && $spaceKinds.has(ZAP_GOAL)}
|
{#if ENABLE_ZAPS && $spaceKinds.has(ZAP_GOAL)}
|
||||||
<SecondaryNavItem href={goalsPath}>
|
<SecondaryNavItem href={goalsPath}>
|
||||||
<Icon icon={StarFallMinimalistic} /> Goals
|
<Icon icon={StarFallMinimalistic} /> Goals
|
||||||
@@ -311,8 +294,8 @@
|
|||||||
<div
|
<div
|
||||||
class="flex shrink-0 flex-col gap-2 p-2 pt-0 -mt-4 pb-[calc(var(--saib)+0.25rem)] md:pb-2 z-nav">
|
class="flex shrink-0 flex-col gap-2 p-2 pt-0 -mt-4 pb-[calc(var(--saib)+0.25rem)] md:pb-2 z-nav">
|
||||||
<VoiceWidget />
|
<VoiceWidget />
|
||||||
<Button class="btn btn-neutral btn-sm h-10" onclick={showDetail}>
|
<Link href={makeSpacePath("about")} class="btn btn-neutral btn-sm h-10">
|
||||||
<SocketStatusIndicator {url} />
|
<SocketStatusIndicator {url} />
|
||||||
</Button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import History from "@assets/icons/history.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import RecentItem from "@app/components/RecentItem.svelte"
|
||||||
|
import {deriveRecentActivity} from "@app/recent"
|
||||||
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const recentActivity = deriveRecentActivity(url)
|
||||||
|
const recentPath = makeSpacePath(url, "recent")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card2 bg-alt flex flex-col gap-3">
|
||||||
|
<h3 class="flex items-center gap-2 text-lg font-bold">
|
||||||
|
<Icon icon={History} />
|
||||||
|
Recent Activity
|
||||||
|
</h3>
|
||||||
|
{#if $recentActivity.length === 0}
|
||||||
|
<p class="text-sm opacity-70">No recent activity yet.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each $recentActivity.slice(0, 3) as item (item.event.id)}
|
||||||
|
<RecentItem {url} {item} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<Link href={recentPath} class="btn btn-neutral btn-sm">
|
||||||
|
View all recent activity
|
||||||
|
<Icon icon={AltArrowRight} size={4} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {deriveRelay} from "@welshman/app"
|
|
||||||
import Server from "@assets/icons/server.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
|
||||||
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const {url}: Props = $props()
|
|
||||||
|
|
||||||
const relay = deriveRelay(url)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="card2 bg-alt flex flex-col gap-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h3 class="flex items-center gap-2 text-lg font-semibold">
|
|
||||||
<Icon icon={Server} />
|
|
||||||
Relay Details
|
|
||||||
</h3>
|
|
||||||
<SocketStatusIndicator {url} />
|
|
||||||
</div>
|
|
||||||
{#if $relay}
|
|
||||||
{@const {pubkey, software, version, supported_nips, limitation} = $relay}
|
|
||||||
<div class="flex flex-wrap gap-1">
|
|
||||||
{#if pubkey}
|
|
||||||
<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 text-wrap h-auto">
|
|
||||||
<span class="ellipsize">Contact: {$relay.contact}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if software}
|
|
||||||
<div class="badge badge-neutral text-wrap h-auto">
|
|
||||||
<span class="ellipsize">Software: {software}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if version}
|
|
||||||
<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 text-wrap h-auto">
|
|
||||||
<span class="ellipsize">Supported NIPs: {supported_nips.join(", ")}</span>
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
{#if limitation?.auth_required}
|
|
||||||
<p class="badge badge-warning">
|
|
||||||
<span class="ellipsize">Auth Required</span>
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
{#if limitation?.payment_required}
|
|
||||||
<p class="badge badge-warning">
|
|
||||||
<span class="ellipsize">Payment Required</span>
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
{#if limitation?.min_pow_difficulty}
|
|
||||||
<p class="badge badge-warning text-wrap h-auto">
|
|
||||||
<span class="ellipsize">Min PoW: {limitation?.min_pow_difficulty}</span>
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
|
import TrashBin from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
|
import RoleCreate from "@app/components/RoleCreate.svelte"
|
||||||
|
import RoleEdit from "@app/components/RoleEdit.svelte"
|
||||||
|
import RoleAddMembers from "@app/components/RoleAddMembers.svelte"
|
||||||
|
import RoleItem from "@app/components/RoleItem.svelte"
|
||||||
|
import {deriveSpaceRoles, deleteRole, type SpaceRole} from "@app/members"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const roles = deriveSpaceRoles(url)
|
||||||
|
|
||||||
|
let menuRoleId = $state<string | undefined>()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const closeMenu = () => (menuRoleId = undefined)
|
||||||
|
|
||||||
|
const createRole = () => pushModal(RoleCreate, {url})
|
||||||
|
|
||||||
|
const editRole = (role: SpaceRole) => {
|
||||||
|
menuRoleId = undefined
|
||||||
|
pushModal(RoleEdit, {url, role})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addMembers = (role: SpaceRole) => {
|
||||||
|
menuRoleId = undefined
|
||||||
|
pushModal(RoleAddMembers, {url, role})
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = (role: SpaceRole) => {
|
||||||
|
menuRoleId = undefined
|
||||||
|
pushModal(Confirm, {
|
||||||
|
title: "Delete Role",
|
||||||
|
message: `Delete the "${role.label}" role? Members will keep their space membership.`,
|
||||||
|
confirm: async () => {
|
||||||
|
const error = await deleteRole(url, role.id)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Role deleted!"})
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Manage Roles</ModalTitle>
|
||||||
|
<ModalSubtitle>on <RelayName {url} class="text-primary" /></ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
{#if $roles.length === 0}
|
||||||
|
<div class="card2 bg-base-200 p-4 text-sm opacity-70">
|
||||||
|
No roles yet. Create one to start organizing members.
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each $roles as role (role.id)}
|
||||||
|
<div class="card2 card2-sm bg-alt flex justify-between gap-2">
|
||||||
|
<RoleItem {role} />
|
||||||
|
<div class="relative shrink-0">
|
||||||
|
<Button
|
||||||
|
class="btn btn-square btn-ghost btn-sm"
|
||||||
|
onclick={() => (menuRoleId = menuRoleId === role.id ? undefined : role.id)}>
|
||||||
|
<Icon icon={MenuDots} />
|
||||||
|
</Button>
|
||||||
|
{#if menuRoleId === role.id}
|
||||||
|
<Popover hideOnClick onClose={closeMenu}>
|
||||||
|
<ul
|
||||||
|
transition:fly
|
||||||
|
class="menu absolute right-0 z-popover mt-2 w-52 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||||
|
<li>
|
||||||
|
<Button onclick={() => addMembers(role)}>
|
||||||
|
<Icon icon={AddCircle} />
|
||||||
|
Add members
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Button onclick={() => editRole(role)}>
|
||||||
|
<Icon icon={Pen} />
|
||||||
|
Edit role
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" onclick={() => confirmDelete(role)}>
|
||||||
|
<Icon icon={TrashBin} />
|
||||||
|
Delete role
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" onclick={createRole}>
|
||||||
|
<Icon icon={AddCircle} />
|
||||||
|
Create Role
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
const {url, h, threads}: Props = $props()
|
const {url, h, threads}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="overflow-hidden rounded-box border border-base-content/15 bg-base-100 shadow-sm">
|
<section class="rounded-box border border-base-content/15 bg-base-100 shadow-sm">
|
||||||
<header
|
<header
|
||||||
class="flex items-center justify-between gap-2 border-b border-base-content/15 bg-base-200/70 px-4 py-2.5">
|
class="flex items-center justify-between gap-2 border-b border-base-content/15 bg-base-200/70 px-4 py-2.5">
|
||||||
<h2 class="text-sm font-bold sm:text-base">
|
<h2 class="text-sm font-bold sm:text-base">
|
||||||
@@ -27,14 +27,20 @@
|
|||||||
{threads.length === 1 ? "topic" : "topics"}
|
{threads.length === 1 ? "topic" : "topics"}
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<table class="w-full border-collapse">
|
||||||
class="hidden border-b border-base-content/10 bg-base-200/40 px-4 py-2 text-xs font-bold uppercase tracking-wide opacity-60 sm:grid sm:grid-cols-[1fr_8rem_5rem_8rem] sm:gap-x-4">
|
<thead
|
||||||
<span>Topic</span>
|
class="hidden text-xs font-bold uppercase tracking-wide opacity-60 sm:table-header-group">
|
||||||
<span>Author</span>
|
<tr class="border-b border-base-content/10 bg-base-200/40">
|
||||||
<span class="text-center">Replies</span>
|
<th class="px-4 py-2 text-left font-bold">Topic</th>
|
||||||
<span class="text-right">Last post</span>
|
<th class="w-32 px-4 py-2 text-left font-bold">Author</th>
|
||||||
</div>
|
<th class="w-20 px-4 py-2 text-center font-bold">Replies</th>
|
||||||
{#each threads as event (event.id)}
|
<th class="w-32 px-4 py-2 text-right font-bold">Last post</th>
|
||||||
<ThreadBoardItem {url} {event} />
|
</tr>
|
||||||
{/each}
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each threads as event (event.id)}
|
||||||
|
<ThreadBoardItem {url} {event} />
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import {formatTimestamp, max} from "@welshman/lib"
|
import {formatTimestamp, max} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {COMMENT, getTagValue} from "@welshman/util"
|
import {COMMENT, getTagValue} from "@welshman/util"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
@@ -20,29 +21,28 @@
|
|||||||
const replyCount = $derived($replies.length)
|
const replyCount = $derived($replies.length)
|
||||||
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
|
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
|
||||||
const title = getTagValue("title", event.tags)
|
const title = getTagValue("title", event.tags)
|
||||||
|
const path = makeThreadPath(url, event.id)
|
||||||
|
const goToThread = () => goto(path)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link
|
<tr class="cursor-pointer hover:bg-base-200/40 text-sm" onclick={goToThread}>
|
||||||
href={makeThreadPath(url, event.id)}
|
<td class="px-4 py-2 align-top">
|
||||||
class="grid grid-cols-[1fr_auto] gap-x-3 gap-y-1 border-b border-base-content/10 px-3 py-3 transition-colors hover:bg-base-200/50 sm:grid-cols-[1fr_8rem_5rem_8rem] sm:items-center sm:gap-x-4 sm:px-4">
|
<Link href={path} class="ellipsize font-semibold">
|
||||||
<div class="col-span-2 min-w-0 sm:col-span-1">
|
{title || "Untitled thread"}
|
||||||
<p class="ellipsize text-sm font-bold sm:text-base">{title || "Untitled thread"}</p>
|
</Link>
|
||||||
<p class="ellipsize mt-0.5 text-xs opacity-60 sm:hidden">
|
</td>
|
||||||
by <ProfileName pubkey={event.pubkey} {url} />
|
<td class="px-4 py-2 align-middle">
|
||||||
</p>
|
<div class="flex items-center gap-2">
|
||||||
</div>
|
<ProfileCircle pubkey={event.pubkey} {url} size={5} />
|
||||||
<div class="hidden items-center gap-2 sm:flex">
|
<span class="ellipsize">
|
||||||
<ProfileCircle pubkey={event.pubkey} {url} size={6} />
|
<ProfileName pubkey={event.pubkey} {url} />
|
||||||
<span class="ellipsize text-sm">
|
</span>
|
||||||
<ProfileName pubkey={event.pubkey} {url} />
|
</div>
|
||||||
</span>
|
</td>
|
||||||
</div>
|
<td class="px-4 py-2 align-middle text-right">
|
||||||
<p class="text-right text-xs opacity-75 sm:text-center sm:text-sm">
|
|
||||||
<span class="opacity-60 sm:hidden">Replies · </span>
|
|
||||||
{replyCount}
|
{replyCount}
|
||||||
</p>
|
</td>
|
||||||
<p class="text-right text-xs opacity-75 sm:text-sm">
|
<td class="whitespace-nowrap px-4 py-2 align-middle text-right">
|
||||||
<span class="opacity-60 sm:hidden">Last · </span>
|
|
||||||
{formatTimestamp(lastActive)}
|
{formatTimestamp(lastActive)}
|
||||||
</p>
|
</td>
|
||||||
</Link>
|
</tr>
|
||||||
|
|||||||
@@ -20,9 +20,7 @@
|
|||||||
const h = getTagValue("h", event.tags)
|
const h = getTagValue("h", event.tags)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link
|
<Link class="cv col-2 card2 w-full cursor-pointer" href={makeThreadPath(url, event.id)}>
|
||||||
class="cv col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
|
|
||||||
href={makeThreadPath(url, event.id)}>
|
|
||||||
{#if title}
|
{#if title}
|
||||||
<div class="flex w-full items-center justify-between gap-2">
|
<div class="flex w-full items-center justify-between gap-2">
|
||||||
<p class="text-xl">{title}</p>
|
<p class="text-xl">{title}</p>
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ export const makeCommentFilter = (kinds: number[], extra: Filter = {}) => ({
|
|||||||
...extra,
|
...extra,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const makeDeleteFilter = (kinds: number[], extra: Filter = {}) => ({
|
||||||
|
kinds: [DELETE],
|
||||||
|
"#k": kinds.map(String),
|
||||||
|
...extra,
|
||||||
|
})
|
||||||
|
|
||||||
export const REPOST_KINDS = [REPOST, GENERIC_REPOST]
|
export const REPOST_KINDS = [REPOST, GENERIC_REPOST]
|
||||||
|
|
||||||
export const REACTION_KINDS = [REPORT, DELETE, REACTION]
|
export const REACTION_KINDS = [REPORT, DELETE, REACTION]
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ export const DEFAULT_SPACES = fromCsv(import.meta.env.VITE_DEFAULT_SPACES).map(n
|
|||||||
|
|
||||||
export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
|
export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
|
||||||
|
|
||||||
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
|
export const DUFFLEPUD_URL = "https://dufflepud.coracle.social"
|
||||||
|
|
||||||
export const THUMBNAIL_URL = import.meta.env.VITE_THUMBNAIL_URL
|
export const THUMBNAIL_URL = import.meta.env.VITE_THUMBNAIL_URL
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import {derived} from "svelte/store"
|
||||||
|
import {first, now} from "@welshman/lib"
|
||||||
|
import {APP_DATA, getTagValues} from "@welshman/util"
|
||||||
|
import type {ManagementMethod} from "@welshman/util"
|
||||||
|
import {deriveRelay, manageRelay} from "@welshman/app"
|
||||||
|
import {deriveEventsForUrl} from "@app/repository"
|
||||||
|
|
||||||
|
// NIP-78 app data published by the relay's self key. Each featured entry is a
|
||||||
|
// ["content", <value>] tag (freeform text, intended to be a url or nevent).
|
||||||
|
export const FEATURED_CONTENT_D = "flotilla/featured-content"
|
||||||
|
|
||||||
|
export const deriveFeaturedContent = (url: string) =>
|
||||||
|
derived(
|
||||||
|
[deriveRelay(url), deriveEventsForUrl(url, [{kinds: [APP_DATA], "#d": [FEATURED_CONTENT_D]}])],
|
||||||
|
([$relay, $events]) => {
|
||||||
|
const self = $relay?.self || $relay?.pubkey
|
||||||
|
const event = (self && $events.find(e => e.pubkey === self)) || first($events)
|
||||||
|
|
||||||
|
return getTagValues("content", event?.tags ?? [])
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Publish the featured content list by asking the relay to sign it with its self
|
||||||
|
// key (the unofficial NIP-86 "signevent" method).
|
||||||
|
export const setFeaturedContent = async (
|
||||||
|
url: string,
|
||||||
|
content: string[],
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const template = {
|
||||||
|
kind: APP_DATA,
|
||||||
|
created_at: now(),
|
||||||
|
content: "",
|
||||||
|
tags: [
|
||||||
|
["d", FEATURED_CONTENT_D],
|
||||||
|
...content
|
||||||
|
.map(value => value.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(value => ["content", value]),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: "signevent" as ManagementMethod,
|
||||||
|
params: [template as unknown as string],
|
||||||
|
})
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
ROOM_MEMBERS,
|
ROOM_MEMBERS,
|
||||||
ROOM_REMOVE_MEMBER,
|
ROOM_REMOVE_MEMBER,
|
||||||
getPubkeyTagValues,
|
getPubkeyTagValues,
|
||||||
|
getTags,
|
||||||
getTagValue,
|
getTagValue,
|
||||||
getTagValues,
|
getTagValues,
|
||||||
sortEventsAsc,
|
sortEventsAsc,
|
||||||
@@ -23,11 +24,136 @@ import {first, memoize, sortBy, spec, uniq} from "@welshman/lib"
|
|||||||
import {addRoomMember, manageRelay, pubkey, waitForThunkError} from "@welshman/app"
|
import {addRoomMember, manageRelay, pubkey, waitForThunkError} from "@welshman/app"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import {deriveEventsForUrl, deriveRelaySignedEvents} from "@app/repository"
|
import {deriveEventsForUrl, deriveRelaySignedEvents} from "@app/repository"
|
||||||
|
|
||||||
export const deriveSpaceMembers = (url: string) =>
|
export const deriveSpaceMembers = (url: string) =>
|
||||||
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_MEMBERS]}]), ([event]) =>
|
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_MEMBERS]}]), ([event]) =>
|
||||||
uniq(getTagValues("member", event?.tags ?? [])),
|
uniq(getTagValues("member", event?.tags ?? [])),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const RELAY_ROLE = 33534
|
||||||
|
|
||||||
|
export type SpaceRole = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
description: string
|
||||||
|
color: number
|
||||||
|
order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// hue is 0-255; map to 0-360deg. Saturation/lightness chosen to read on both themes.
|
||||||
|
export const roleColorHue = (color: number) => (((color % 256) + 256) % 256) * (360 / 256)
|
||||||
|
|
||||||
|
export const roleColor = (color: number) => `hsl(${roleColorHue(color)}, 70%, 50%)`
|
||||||
|
|
||||||
|
export const roleColorSoft = (color: number) => `hsl(${roleColorHue(color)}, 70%, 90%)`
|
||||||
|
|
||||||
|
export const deriveSpaceRoles = (url: string) =>
|
||||||
|
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_ROLE]}]), $events => {
|
||||||
|
const roles: SpaceRole[] = []
|
||||||
|
|
||||||
|
for (const event of $events) {
|
||||||
|
const id = getTagValue("d", event.tags)
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
roles.push({
|
||||||
|
id,
|
||||||
|
label: getTagValue("label", event.tags) ?? "",
|
||||||
|
description: getTagValue("description", event.tags) ?? "",
|
||||||
|
color: parseInt(getTagValue("color", event.tags) ?? "0", 10) || 0,
|
||||||
|
order: parseInt(getTagValue("order", event.tags) ?? "0", 10) || 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortBy(r => [r.order, r.label] as [number, string], roles)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Map<pubkey, roleId[]> parsed from EXTRA values on ["member", pubkey, ...roleIds] tags
|
||||||
|
export const deriveSpaceMemberRoles = (url: string) =>
|
||||||
|
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_MEMBERS]}]), ([event]) => {
|
||||||
|
const memberRoles = new Map<string, string[]>()
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
for (const tag of getTags("member", event.tags)) {
|
||||||
|
const pubkey = tag[1]
|
||||||
|
const roleIds = tag.slice(2)
|
||||||
|
|
||||||
|
if (pubkey) {
|
||||||
|
memberRoles.set(pubkey, roleIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return memberRoles
|
||||||
|
})
|
||||||
|
|
||||||
|
export const createRole = async (
|
||||||
|
url: string,
|
||||||
|
id: string,
|
||||||
|
label: string,
|
||||||
|
description: string,
|
||||||
|
color: number,
|
||||||
|
order: number,
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: "createrole" as ManagementMethod,
|
||||||
|
params: [id, label, description, color.toString(), order.toString()],
|
||||||
|
})
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
export const editRole = async (
|
||||||
|
url: string,
|
||||||
|
id: string,
|
||||||
|
label: string,
|
||||||
|
description: string,
|
||||||
|
color: number,
|
||||||
|
order: number,
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: "editrole" as ManagementMethod,
|
||||||
|
params: [id, label, description, color.toString(), order.toString()],
|
||||||
|
})
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteRole = async (url: string, id: string): Promise<string | undefined> => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: "deleterole" as ManagementMethod,
|
||||||
|
params: [id],
|
||||||
|
})
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
export const assignRole = async (
|
||||||
|
url: string,
|
||||||
|
pubkey: string,
|
||||||
|
roleId: string,
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: "assignrole" as ManagementMethod,
|
||||||
|
params: [pubkey, roleId],
|
||||||
|
})
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unassignRole = async (
|
||||||
|
url: string,
|
||||||
|
pubkey: string,
|
||||||
|
roleId: string,
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: "unassignrole" as ManagementMethod,
|
||||||
|
params: [pubkey, roleId],
|
||||||
|
})
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
export const deriveRoomMembers = (url: string, h: string) => {
|
export const deriveRoomMembers = (url: string, h: string) => {
|
||||||
const filters: Filter[] = [{kinds: [ROOM_MEMBERS], "#d": [h]}]
|
const filters: Filter[] = [{kinds: [ROOM_MEMBERS], "#d": [h]}]
|
||||||
|
|
||||||
@@ -292,6 +418,47 @@ export const addSpaceMembers = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const removeSpaceMembers = async (
|
||||||
|
url: string,
|
||||||
|
pubkeys: string[],
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
pubkeys.map(pubkey =>
|
||||||
|
manageRelay(url, {
|
||||||
|
method: ManagementMethod.UnallowPubkey,
|
||||||
|
params: [pubkey],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const {error} of results) {
|
||||||
|
if (error) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const banSpaceMembers = async (
|
||||||
|
url: string,
|
||||||
|
pubkeys: string[],
|
||||||
|
reason = "",
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
pubkeys.map(pubkey =>
|
||||||
|
manageRelay(url, {
|
||||||
|
method: ManagementMethod.BanPubkey,
|
||||||
|
params: reason ? [pubkey, reason] : [pubkey],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const {error} of results) {
|
||||||
|
if (error) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const addRoomMembers = async (
|
export const addRoomMembers = async (
|
||||||
url: string,
|
url: string,
|
||||||
room: PublishedRoomMeta,
|
room: PublishedRoomMeta,
|
||||||
|
|||||||
+1
-1
@@ -99,7 +99,7 @@ export const mostlyRestrictedPolicy = (socket: Socket) => {
|
|||||||
const pending = new Set<string>()
|
const pending = new Set<string>()
|
||||||
|
|
||||||
const updateStatus = (error?: string) => {
|
const updateStatus = (error?: string) => {
|
||||||
if (restricted > total / 2) {
|
if (total > 5 && restricted > total / 2) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return relaysMostlyRestricted.update(assoc(socket.url, error))
|
return relaysMostlyRestricted.update(assoc(socket.url, error))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
import {DM_KINDS, CONTENT_KINDS, makeCommentFilter} from "@app/content"
|
import {DM_KINDS, CONTENT_KINDS, makeCommentFilter} from "@app/content"
|
||||||
import {notificationSettings, shouldNotify, userSettingsValues} from "@app/settings"
|
import {notificationSettings, shouldNotify, userSettingsValues} from "@app/settings"
|
||||||
import {userSpaceUrls} from "@app/groups"
|
import {userSpaceUrls} from "@app/groups"
|
||||||
import {makeSpacePath, getEventPath} from "@app/routes"
|
import {getEventPath, goToSpace} from "@app/routes"
|
||||||
|
|
||||||
export type PushSubscription = {
|
export type PushSubscription = {
|
||||||
key: string
|
key: string
|
||||||
@@ -111,7 +111,7 @@ export const onPushNotificationAction = async (action: ActionPerformed) => {
|
|||||||
if (event) {
|
if (event) {
|
||||||
goto(await getEventPath(event, [relay]))
|
goto(await getEventPath(event, [relay]))
|
||||||
} else {
|
} else {
|
||||||
goto(makeSpacePath(relay))
|
goToSpace(relay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {derived} from "svelte/store"
|
||||||
|
import {groupBy, first, sortBy, uniqBy, ago, MONTH} from "@welshman/lib"
|
||||||
|
import {MESSAGE, COMMENT, getTagValue, getTagValues, getIdAndAddress} from "@welshman/util"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {repository} from "@welshman/app"
|
||||||
|
import {deriveEventsForUrl} from "@app/repository"
|
||||||
|
import {CONTENT_KINDS} from "@app/content"
|
||||||
|
|
||||||
|
export type RecentActivityItem = {
|
||||||
|
type: "message" | "content"
|
||||||
|
event: TrustedEvent
|
||||||
|
count: number
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recent activity for a space: latest message per room plus content with the
|
||||||
|
// most recent activity (post or comment), sorted newest first.
|
||||||
|
export const deriveRecentActivity = (url: string) => {
|
||||||
|
const since = ago(3, MONTH)
|
||||||
|
const messages = deriveEventsForUrl(url, [{kinds: [MESSAGE], since}])
|
||||||
|
const content = deriveEventsForUrl(url, [{kinds: CONTENT_KINDS, since}])
|
||||||
|
const comments = deriveEventsForUrl(url, [{kinds: [COMMENT], since}])
|
||||||
|
|
||||||
|
return derived([messages, content, comments], ([$messages, $content, $comments]) => {
|
||||||
|
const activity: RecentActivityItem[] = []
|
||||||
|
|
||||||
|
const byRoom = groupBy(e => getTagValue("h", e.tags), $messages)
|
||||||
|
for (const roomMessages of byRoom.values()) {
|
||||||
|
const latest = first(roomMessages)
|
||||||
|
if (latest) {
|
||||||
|
activity.push({
|
||||||
|
type: "message",
|
||||||
|
event: latest,
|
||||||
|
count: roomMessages.length,
|
||||||
|
timestamp: latest.created_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestActivityByKey = new Map<string, number>()
|
||||||
|
|
||||||
|
for (const event of $content) {
|
||||||
|
for (const k of getIdAndAddress(event)) {
|
||||||
|
latestActivityByKey.set(k, Math.max(latestActivityByKey.get(k) || 0, event.created_at))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of $comments) {
|
||||||
|
for (const k of getTagValues(["E", "A"], event.tags)) {
|
||||||
|
latestActivityByKey.set(k, Math.max(latestActivityByKey.get(k) || 0, event.created_at))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [address, timestamp] of latestActivityByKey.entries()) {
|
||||||
|
const event = repository.getEvent(address)
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
activity.push({type: "content", event, timestamp, count: 1})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortBy(
|
||||||
|
a => -a.timestamp,
|
||||||
|
uniqBy(a => a.event.id, activity),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
+15
-5
@@ -177,14 +177,24 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
|
|||||||
return `Failed to connect`
|
return `Failed to connect`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some relays are extra slow, remove this when welshman is updated
|
||||||
|
await poll({
|
||||||
|
signal: AbortSignal.timeout(3000),
|
||||||
|
condition: () => socket.auth.status === AuthStatus.Requested,
|
||||||
|
})
|
||||||
|
|
||||||
await socket.auth.attemptAuth(sign)
|
await socket.auth.attemptAuth(sign)
|
||||||
|
|
||||||
|
// Some relays are extra slow, remove this when welshman is updated
|
||||||
|
await poll({
|
||||||
|
signal: AbortSignal.timeout(3000),
|
||||||
|
condition: () => socket.auth.status !== AuthStatus.PendingResponse,
|
||||||
|
})
|
||||||
|
|
||||||
if (![AuthStatus.None, AuthStatus.Ok].includes(socket.auth.status)) {
|
if (![AuthStatus.None, AuthStatus.Ok].includes(socket.auth.status)) {
|
||||||
if (socket.auth.details) {
|
const message = socket.auth.details || last(socket.auth.status.split(":"))
|
||||||
return `Failed to authenticate (${socket.auth.details})`
|
|
||||||
} else {
|
return `Failed to authenticate (${message})`
|
||||||
return `Failed to authenticate (${last(socket.auth.status.split(":"))})`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = await waitForThunkError(publishJoinRequest({url, claim}))
|
const error = await waitForThunkError(publishJoinRequest({url, claim}))
|
||||||
|
|||||||
+12
-14
@@ -44,12 +44,6 @@ export const setupHistory = () =>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Profiles
|
|
||||||
|
|
||||||
export const makeProfilePath = (pubkey: string) => `/people/${nip19.npubEncode(pubkey)}`
|
|
||||||
|
|
||||||
export const goToProfile = (pubkey: string) => goto(makeProfilePath(pubkey))
|
|
||||||
|
|
||||||
// Chat
|
// Chat
|
||||||
|
|
||||||
export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}`
|
export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}`
|
||||||
@@ -85,18 +79,22 @@ export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) =>
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
export const goToSpace = async (url: string) => {
|
export const goToSpace = (url: string) => {
|
||||||
const prevPath = lastPageBySpaceUrl.get(encodeRelay(url))
|
const prevPath = lastPageBySpaceUrl.get(encodeRelay(url))
|
||||||
|
|
||||||
if (prevPath && prevPath !== makeSpacePath(url)) {
|
if (prevPath && prevPath !== makeSpacePath(url)) {
|
||||||
goto(prevPath, {replaceState: true})
|
return goto(prevPath, {replaceState: true})
|
||||||
} else if (!hasNip29(getRelay(url))) {
|
|
||||||
goto(makeSpaceChatPath(url), {replaceState: true})
|
|
||||||
} else if (window.matchMedia(`(min-width: ${theme.screens.md})`).matches) {
|
|
||||||
goto(makeSpacePath(url, "recent"), {replaceState: true})
|
|
||||||
} else {
|
|
||||||
goto(makeSpacePath(url), {replaceState: true})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasNip29(getRelay(url))) {
|
||||||
|
return goto(makeSpaceChatPath(url), {replaceState: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.matchMedia(`(min-width: ${theme.screens.md})`).matches) {
|
||||||
|
return goto(makeSpacePath(url, "about"), {replaceState: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
return goto(makeSpacePath(url), {replaceState: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content types, events
|
// Content types, events
|
||||||
|
|||||||
+6
-18
@@ -20,6 +20,7 @@ import {
|
|||||||
RELAY_REMOVE_MEMBER,
|
RELAY_REMOVE_MEMBER,
|
||||||
MESSAGE,
|
MESSAGE,
|
||||||
POLL_RESPONSE,
|
POLL_RESPONSE,
|
||||||
|
APP_DATA,
|
||||||
isSignedEvent,
|
isSignedEvent,
|
||||||
unionFilters,
|
unionFilters,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
@@ -37,8 +38,6 @@ import {
|
|||||||
loadFollowList,
|
loadFollowList,
|
||||||
loadMuteList,
|
loadMuteList,
|
||||||
loadProfile,
|
loadProfile,
|
||||||
userFollowList,
|
|
||||||
getFollows,
|
|
||||||
repository,
|
repository,
|
||||||
shouldUnwrap,
|
shouldUnwrap,
|
||||||
hasNegentropy,
|
hasNegentropy,
|
||||||
@@ -55,6 +54,8 @@ import {
|
|||||||
} from "@app/groups"
|
} from "@app/groups"
|
||||||
import {decodeRelay} from "@app/relays"
|
import {decodeRelay} from "@app/relays"
|
||||||
import {loadFeedsForPubkey} from "@app/feeds"
|
import {loadFeedsForPubkey} from "@app/feeds"
|
||||||
|
import {RELAY_ROLE} from "@app/members"
|
||||||
|
import {FEATURED_CONTENT_D} from "@app/featured"
|
||||||
import {hasBlossomSupport} from "@app/uploads"
|
import {hasBlossomSupport} from "@app/uploads"
|
||||||
import {LIVEKIT_PARTICIPANTS} from "@app/call/voice"
|
import {LIVEKIT_PARTICIPANTS} from "@app/call/voice"
|
||||||
|
|
||||||
@@ -250,17 +251,6 @@ const syncUserData = () => {
|
|||||||
loadFeedsForPubkey(pubkey)
|
loadFeedsForPubkey(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncFollowNetwork = ($userFollowList: List | undefined) => {
|
|
||||||
const author = $userFollowList?.event?.pubkey
|
|
||||||
|
|
||||||
if (!author) return
|
|
||||||
|
|
||||||
for (const follow of getFollows(author)) {
|
|
||||||
loadFollowList(follow)
|
|
||||||
loadMuteList(follow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribeGroupList = merged([userGroupList]).subscribe(([$userGroupList]) => {
|
const unsubscribeGroupList = merged([userGroupList]).subscribe(([$userGroupList]) => {
|
||||||
syncGroupList($userGroupList)
|
syncGroupList($userGroupList)
|
||||||
})
|
})
|
||||||
@@ -269,13 +259,10 @@ const syncUserData = () => {
|
|||||||
syncRelayList($userRelayList)
|
syncRelayList($userRelayList)
|
||||||
})
|
})
|
||||||
|
|
||||||
const unsubscribeFollowList = userFollowList.subscribe(syncFollowNetwork)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribersByKey.forEach(call)
|
unsubscribersByKey.forEach(call)
|
||||||
unsubscribeGroupList()
|
unsubscribeGroupList()
|
||||||
unsubscribeRelayList()
|
unsubscribeRelayList()
|
||||||
unsubscribeFollowList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +271,7 @@ const syncUserData = () => {
|
|||||||
const syncSpace = (url: string) => {
|
const syncSpace = (url: string) => {
|
||||||
const since = ago(WEEK)
|
const since = ago(WEEK)
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const relayKinds = [RELAY_MEMBERS]
|
const relayKinds = [RELAY_MEMBERS, RELAY_ROLE]
|
||||||
const roomMetaKinds = [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS]
|
const roomMetaKinds = [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS]
|
||||||
const roomDeleteKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE]
|
const roomDeleteKinds = [ROOM_DELETE, ROOM_JOIN, ROOM_LEAVE]
|
||||||
|
|
||||||
@@ -293,8 +280,9 @@ const syncSpace = (url: string) => {
|
|||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
filters: [
|
filters: [
|
||||||
{kinds: [...relayKinds, ...roomMetaKinds, ...roomDeleteKinds, ...CONTENT_KINDS, MESSAGE]},
|
{kinds: [...relayKinds, ...roomMetaKinds, ...roomDeleteKinds, ...CONTENT_KINDS, MESSAGE]},
|
||||||
makeCommentFilter(CONTENT_KINDS, {since}),
|
{kinds: [APP_DATA], "#d": [FEATURED_CONTENT_D]},
|
||||||
{kinds: [...REACTION_KINDS, POLL_RESPONSE], since},
|
{kinds: [...REACTION_KINDS, POLL_RESPONSE], since},
|
||||||
|
makeCommentFilter(CONTENT_KINDS, {since}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+1
-21
@@ -1,6 +1,5 @@
|
|||||||
import {append, identity, uniq} from "@welshman/lib"
|
import {append, identity, uniq} from "@welshman/lib"
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import {repository} from "@welshman/app"
|
||||||
import {repository, displayProfileByPubkey} from "@welshman/app"
|
|
||||||
import {displayPubkey, getTagValue} from "@welshman/util"
|
import {displayPubkey, getTagValue} from "@welshman/util"
|
||||||
import {PLATFORM_NAME} from "@app/env"
|
import {PLATFORM_NAME} from "@app/env"
|
||||||
import {decodeRelay} from "@app/relays"
|
import {decodeRelay} from "@app/relays"
|
||||||
@@ -25,7 +24,6 @@ const staticTitles = new Map<string, string>([
|
|||||||
["/chat", "Messages"],
|
["/chat", "Messages"],
|
||||||
["/join", "Join Space"],
|
["/join", "Join Space"],
|
||||||
["/people", "Find People"],
|
["/people", "Find People"],
|
||||||
["/people/[npub]", "Profile"],
|
|
||||||
["/settings/about", "About"],
|
["/settings/about", "About"],
|
||||||
["/settings/profile", "Profile Settings"],
|
["/settings/profile", "Profile Settings"],
|
||||||
["/settings/content", "Content Settings"],
|
["/settings/content", "Content Settings"],
|
||||||
@@ -122,24 +120,6 @@ export const getPageTitle = ({page, pubkey}: PageTitleContext) => {
|
|||||||
return makeTitle(getChatTitle(page.params.chat, pubkey))
|
return makeTitle(getChatTitle(page.params.chat, pubkey))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (routeId === "/people/[npub]") {
|
|
||||||
try {
|
|
||||||
const decoded = nip19.decode(page.params.npub!)
|
|
||||||
const profilePubkey =
|
|
||||||
decoded.type === "npub"
|
|
||||||
? decoded.data
|
|
||||||
: decoded.type === "nprofile"
|
|
||||||
? decoded.data.pubkey
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
if (profilePubkey) {
|
|
||||||
return makeTitle(displayProfileByPubkey(profilePubkey))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// fall through
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (routeId === "/spaces/[relay]/[h]") {
|
if (routeId === "/spaces/[relay]/[h]") {
|
||||||
return makeTitle(getRoomTitle(page.params))
|
return makeTitle(getRoomTitle(page.params))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="center fixed inset-0 z-modal">
|
<div class="dialog center fixed inset-0 z-modal">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class={cx(
|
class={cx(
|
||||||
"mt-sai mb-sai max-h-screen w-60 min-h-0 shrink-0 flex-col gap-1 bg-base-300 z-nav",
|
"mt-sai mb-sai max-h-screen w-60 min-h-0 shrink-0 flex-col gap-1 bg-base-300 z-nav border-r border-solid border-base-content/5 dark:border-base-content/10",
|
||||||
visible ? "flex" : "hidden md:flex",
|
visible ? "flex" : "hidden md:flex",
|
||||||
props.class,
|
props.class,
|
||||||
)}>
|
)}>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "@src/app.css"
|
import "@src/app.css"
|
||||||
import "@welshman/editor/index.css"
|
import "@welshman/editor/index.css"
|
||||||
import "@capacitor-community/safe-area"
|
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import type {Unsubscriber} from "svelte/store"
|
import type {Unsubscriber} from "svelte/store"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
|
|||||||
@@ -8,21 +8,13 @@
|
|||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import {goToEvent, makeProfilePath} from "@app/routes"
|
import {goToEvent} from "@app/routes"
|
||||||
|
|
||||||
const {bech32} = $page.params as MakeNonOptional<typeof $page.params>
|
const {bech32} = $page.params as MakeNonOptional<typeof $page.params>
|
||||||
|
|
||||||
const attemptToNavigate = async () => {
|
const attemptToNavigate = async () => {
|
||||||
const {type, data} = nip19.decode(bech32) as any
|
const {type, data} = nip19.decode(bech32) as any
|
||||||
|
|
||||||
if (type === "npub") {
|
|
||||||
return goto(makeProfilePath(data), {replaceState: true})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "nprofile") {
|
|
||||||
return goto(makeProfilePath(data.pubkey), {replaceState: true})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!["nevent", "naddr"].includes(type) && data.relays.length > 0) {
|
if (!["nevent", "naddr"].includes(type) && data.relays.length > 0) {
|
||||||
return goto("/", {replaceState: true})
|
return goto("/", {replaceState: true})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {onMount} from "svelte"
|
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
|
||||||
import {page} from "$app/stores"
|
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import type {MakeNonOptional} from "@welshman/lib"
|
|
||||||
import {
|
|
||||||
loadProfile,
|
|
||||||
loadRelayList,
|
|
||||||
loadFollowList,
|
|
||||||
loadMessagingRelayList,
|
|
||||||
loadPinList,
|
|
||||||
} from "@welshman/app"
|
|
||||||
import {load} from "@welshman/net"
|
|
||||||
import {Router} from "@welshman/router"
|
|
||||||
import {ROOMS, NOTE} from "@welshman/util"
|
|
||||||
import Page from "@lib/components/Page.svelte"
|
|
||||||
import PageContent from "@lib/components/PageContent.svelte"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
|
||||||
import ProfilePage from "@app/components/ProfilePage.svelte"
|
|
||||||
import {loadGroupList} from "@app/groups"
|
|
||||||
|
|
||||||
const {npub} = $page.params as MakeNonOptional<typeof $page.params>
|
|
||||||
|
|
||||||
let pubkey = $state<string | undefined>()
|
|
||||||
let ready = $state(false)
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
const decoded = nip19.decode(npub)
|
|
||||||
|
|
||||||
if (decoded.type === "npub") {
|
|
||||||
pubkey = decoded.data
|
|
||||||
} else if (decoded.type === "nprofile") {
|
|
||||||
pubkey = decoded.data.pubkey
|
|
||||||
} else {
|
|
||||||
goto("/people", {replaceState: true})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadProfile(pubkey)
|
|
||||||
await loadRelayList(pubkey)
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
loadFollowList(pubkey),
|
|
||||||
loadPinList(pubkey),
|
|
||||||
loadGroupList(pubkey),
|
|
||||||
loadMessagingRelayList(pubkey),
|
|
||||||
])
|
|
||||||
|
|
||||||
load({
|
|
||||||
relays: Router.get().FromPubkeys([pubkey]).getUrls(),
|
|
||||||
filters: [
|
|
||||||
{authors: [pubkey], kinds: [ROOMS]},
|
|
||||||
{authors: [pubkey], kinds: [NOTE], limit: 1},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
ready = true
|
|
||||||
} catch {
|
|
||||||
goto("/people", {replaceState: true})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Page>
|
|
||||||
<PageContent class="p-0 md:p-4">
|
|
||||||
{#if ready && pubkey}
|
|
||||||
<ProfilePage {pubkey} />
|
|
||||||
{:else}
|
|
||||||
<p class="center flex py-20">
|
|
||||||
<Spinner loading />
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
@@ -248,7 +248,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageBar>
|
</PageBar>
|
||||||
<PageContent class="flex flex-col gap-2 p-2">
|
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 p-4">
|
||||||
<div class="flex flex-col gap-2" bind:this={element}>
|
<div class="flex flex-col gap-2" bind:this={element}>
|
||||||
{#each PLATFORM_RELAYS as url (url)}
|
{#each PLATFORM_RELAYS as url (url)}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
const url = decodeRelay($page.params.relay!)
|
const url = decodeRelay($page.params.relay!)
|
||||||
const md = parseFloat(theme.screens.md) * 16
|
const md = parseFloat(theme.screens.md) * 16
|
||||||
|
|
||||||
let width = $state(0)
|
let width = $state(window.innerWidth)
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (width > md) {
|
if (width > md) {
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {page} from "$app/stores"
|
||||||
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
|
import {deriveRelay} from "@welshman/app"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
|
import ShieldUser from "@assets/icons/shield-user.svg?dataurl"
|
||||||
|
import BillList from "@assets/icons/bill-list.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
|
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||||
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
|
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||||
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
|
import SpaceEdit from "@app/components/SpaceEdit.svelte"
|
||||||
|
import SpaceMembersSummary from "@app/components/SpaceMembersSummary.svelte"
|
||||||
|
import SpaceFeaturedContent from "@app/components/SpaceFeaturedContent.svelte"
|
||||||
|
import {deriveUserIsSpaceAdmin} from "@app/members"
|
||||||
|
import {decodeRelay} from "@app/relays"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
const url = decodeRelay($page.params.relay!)
|
||||||
|
const relay = deriveRelay(url)
|
||||||
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
|
||||||
|
const startEdit = () => pushModal(SpaceEdit, {url, initialValues: $relay || {url}})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PageContent class="flex flex-col gap-4 p-4">
|
||||||
|
<div class="card2 bg-alt flex flex-col gap-4">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="relative flex gap-4">
|
||||||
|
<div class="relative">
|
||||||
|
<RelayIcon {url} size={14} class="rounded-full" />
|
||||||
|
</div>
|
||||||
|
<div class="flex min-w-0 flex-col">
|
||||||
|
<h1 class="ellipsize whitespace-nowrap">
|
||||||
|
<RelayName {url} class="text-2xl font-bold" />
|
||||||
|
</h1>
|
||||||
|
<p class="ellipsize text-sm text-primary">{displayRelayUrl(url)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if $userIsAdmin}
|
||||||
|
<Button class="btn btn-primary" onclick={startEdit}>
|
||||||
|
<Icon icon={Pen} />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<RelayDescription {url} />
|
||||||
|
{#if $relay?.terms_of_service || $relay?.privacy_policy}
|
||||||
|
<div class="flex gap-3">
|
||||||
|
{#if $relay.terms_of_service}
|
||||||
|
<Link href={$relay.terms_of_service} class="badge badge-neutral flex gap-2">
|
||||||
|
<Icon icon={BillList} size={4} />
|
||||||
|
Terms of Service
|
||||||
|
</Link>
|
||||||
|
{/if}
|
||||||
|
{#if $relay.privacy_policy}
|
||||||
|
<Link href={$relay.privacy_policy} class="badge badge-neutral flex gap-2">
|
||||||
|
<Icon icon={ShieldUser} size={4} />
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if $relay}
|
||||||
|
{@const {pubkey, software, version, supported_nips, limitation} = $relay}
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
{#if pubkey}
|
||||||
|
<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 text-wrap h-auto">
|
||||||
|
<span class="ellipsize">Contact: {$relay.contact}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if software}
|
||||||
|
<div class="badge badge-neutral text-wrap h-auto">
|
||||||
|
<span class="ellipsize">Software: {software}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if version}
|
||||||
|
<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 text-wrap h-auto">
|
||||||
|
<span class="ellipsize">Supported NIPs: {supported_nips.join(", ")}</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{#if limitation?.auth_required}
|
||||||
|
<p class="badge badge-warning">
|
||||||
|
<span class="ellipsize">Auth Required</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{#if limitation?.payment_required}
|
||||||
|
<p class="badge badge-warning">
|
||||||
|
<span class="ellipsize">Payment Required</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{#if limitation?.min_pow_difficulty}
|
||||||
|
<p class="badge badge-warning text-wrap h-auto">
|
||||||
|
<span class="ellipsize">Min PoW: {limitation?.min_pow_difficulty}</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||||
|
<div class="lg:col-span-2 flex flex-col gap-4">
|
||||||
|
<SpaceFeaturedContent {url} />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<SpaceMembersSummary {url} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageContent>
|
||||||
@@ -126,9 +126,9 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</SpaceBar>
|
</SpaceBar>
|
||||||
|
|
||||||
<PageContent bind:element class="flex flex-col gap-2 p-2">
|
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:px-4">
|
||||||
{#each items as { event, dateDisplay, isFirstFutureEvent }, i (event.id)}
|
{#each items as { event, dateDisplay, isFirstFutureEvent }, i (event.id)}
|
||||||
<div class={"calendar-event-" + event.id}>
|
<div class="flex flex-col gap-2 calendar-event-{event.id}">
|
||||||
{#if isFirstFutureEvent}
|
{#if isFirstFutureEvent}
|
||||||
<div class="flex items-center gap-2 p-2">
|
<div class="flex items-center gap-2 p-2">
|
||||||
<div class="h-px grow bg-primary"></div>
|
<div class="h-px grow bg-primary"></div>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</SpaceBar>
|
</SpaceBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col gap-3 p-2">
|
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
|
||||||
{#if $event}
|
{#if $event}
|
||||||
<div class="card2 bg-alt col-3 z-feature">
|
<div class="card2 bg-alt col-3 z-feature">
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</SpaceBar>
|
</SpaceBar>
|
||||||
|
|
||||||
<PageContent bind:element class="flex flex-col gap-2 p-2">
|
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
|
||||||
{#each items as event (event.id)}
|
{#each items as event (event.id)}
|
||||||
<div in:fly>
|
<div in:fly>
|
||||||
<ClassifiedItem {url} event={$state.snapshot(event)} />
|
<ClassifiedItem {url} event={$state.snapshot(event)} />
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</SpaceBar>
|
</SpaceBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col gap-2 p-2">
|
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
|
||||||
{#if $event}
|
{#if $event}
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {derived} from "svelte/store"
|
||||||
|
import {page} from "$app/stores"
|
||||||
|
import {displayProfileByPubkey} from "@welshman/app"
|
||||||
|
import UsersGroup from "@assets/icons/users-group-rounded.svg?dataurl"
|
||||||
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
|
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||||
|
import SpaceMember from "@app/components/SpaceMember.svelte"
|
||||||
|
import SpaceInvite from "@app/components/SpaceInvite.svelte"
|
||||||
|
import SpaceRoles from "@app/components/SpaceRoles.svelte"
|
||||||
|
import SpaceMembersBanned from "@app/components/SpaceMembersBanned.svelte"
|
||||||
|
import {
|
||||||
|
deriveSpaceRoles,
|
||||||
|
deriveSpaceMembers,
|
||||||
|
deriveSpaceMemberRoles,
|
||||||
|
deriveUserIsSpaceAdmin,
|
||||||
|
type SpaceRole,
|
||||||
|
} from "@app/members"
|
||||||
|
import {decodeRelay} from "@app/relays"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
const url = decodeRelay($page.params.relay!)
|
||||||
|
const roles = deriveSpaceRoles(url)
|
||||||
|
const members = deriveSpaceMembers(url)
|
||||||
|
const memberRoles = deriveSpaceMemberRoles(url)
|
||||||
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
|
||||||
|
// Each member with their resolved roles (sorted by order).
|
||||||
|
const memberList = derived([members, memberRoles, roles], ([$members, $memberRoles, $roles]) => {
|
||||||
|
const byId = new Map($roles.map(role => [role.id, role]))
|
||||||
|
|
||||||
|
return $members.map(pubkey => ({
|
||||||
|
pubkey,
|
||||||
|
roleList: ($memberRoles.get(pubkey) ?? [])
|
||||||
|
.map(id => byId.get(id))
|
||||||
|
.filter((role): role is SpaceRole => Boolean(role)),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
let menuOpen = $state(false)
|
||||||
|
|
||||||
|
const inviteMembers = () => {
|
||||||
|
menuOpen = false
|
||||||
|
pushModal(SpaceInvite, {url})
|
||||||
|
}
|
||||||
|
|
||||||
|
const manageRoles = () => {
|
||||||
|
menuOpen = false
|
||||||
|
pushModal(SpaceRoles, {url})
|
||||||
|
}
|
||||||
|
|
||||||
|
const bannedMembers = () => {
|
||||||
|
menuOpen = false
|
||||||
|
pushModal(SpaceMembersBanned, {url})
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-place search: filter member cards by member info, and keep role sections
|
||||||
|
// whose name matches the term even when their members don't.
|
||||||
|
let term = $state("")
|
||||||
|
|
||||||
|
const matchesTerm = (pubkey: string, t: string) =>
|
||||||
|
displayProfileByPubkey(pubkey).toLowerCase().includes(t) || pubkey.toLowerCase().includes(t)
|
||||||
|
|
||||||
|
// In-place search: match by member info or by the name of any role they hold.
|
||||||
|
const visibleMembers = $derived.by(() => {
|
||||||
|
const t = term.trim().toLowerCase()
|
||||||
|
|
||||||
|
if (!t) return $memberList
|
||||||
|
|
||||||
|
return $memberList.filter(
|
||||||
|
({pubkey, roleList}) =>
|
||||||
|
matchesTerm(pubkey, t) || roleList.some(role => role.label.toLowerCase().includes(t)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SpaceBar>
|
||||||
|
{#snippet leading()}
|
||||||
|
<Icon icon={UsersGroup} />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
|
<strong>Members</strong>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet action()}
|
||||||
|
<button class="btn btn-primary btn-sm" onclick={inviteMembers}>
|
||||||
|
<Icon icon={AddCircle} />
|
||||||
|
Invite people
|
||||||
|
</button>
|
||||||
|
{#if $userIsAdmin}
|
||||||
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
class="btn btn-neutral btn-sm btn-square"
|
||||||
|
aria-label="More options"
|
||||||
|
onclick={() => (menuOpen = !menuOpen)}>
|
||||||
|
<Icon size={4} icon={MenuDots} />
|
||||||
|
</button>
|
||||||
|
{#if menuOpen}
|
||||||
|
<Popover hideOnClick onClose={() => (menuOpen = false)}>
|
||||||
|
<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 onclick={manageRoles}>
|
||||||
|
<Icon icon={UsersGroup} />
|
||||||
|
Manage Roles
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Button onclick={bannedMembers}>
|
||||||
|
<Icon icon={MinusCircle} />
|
||||||
|
Banned Members
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</SpaceBar>
|
||||||
|
|
||||||
|
<PageContent class="flex flex-col gap-4 p-4">
|
||||||
|
<div class="card2 bg-alt flex flex-col gap-2">
|
||||||
|
<label class="input input-sm input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon size={4} icon={Magnifier} />
|
||||||
|
<input
|
||||||
|
bind:value={term}
|
||||||
|
class="min-w-0 grow"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search people or roles..." />
|
||||||
|
</label>
|
||||||
|
{#if visibleMembers.length === 0}
|
||||||
|
<p class="flex flex-col items-center py-20 text-center">No members found.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="grid grid-cols-1 gap-2 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{#each visibleMembers as { pubkey, roleList } (pubkey)}
|
||||||
|
<SpaceMember {url} {pubkey} roles={roleList} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</PageContent>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</SpaceBar>
|
</SpaceBar>
|
||||||
|
|
||||||
<PageContent bind:element class="flex flex-col gap-2 p-2">
|
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
|
||||||
{#each items as event (event.id)}
|
{#each items as event (event.id)}
|
||||||
<div in:fly>
|
<div in:fly>
|
||||||
<GoalItem {url} event={$state.snapshot(event)} />
|
<GoalItem {url} event={$state.snapshot(event)} />
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</SpaceBar>
|
</SpaceBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col gap-2 p-2">
|
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
|
||||||
{#if $event}
|
{#if $event}
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</SpaceBar>
|
</SpaceBar>
|
||||||
|
|
||||||
<PageContent bind:element class="flex flex-col gap-2 p-2">
|
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
|
||||||
{#each items as event (event.id)}
|
{#each items as event (event.id)}
|
||||||
<div in:fly>
|
<div in:fly>
|
||||||
<PollItem {url} event={$state.snapshot(event)} />
|
<PollItem {url} event={$state.snapshot(event)} />
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</SpaceBar>
|
</SpaceBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col gap-2 p-2">
|
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
|
||||||
{#if $event}
|
{#if $event}
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
||||||
|
|||||||
@@ -1,38 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {tick, onMount} from "svelte"
|
import {tick, onMount} from "svelte"
|
||||||
import {derived} from "svelte/store"
|
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import {
|
import {formatTimestampAsDate, groupBy, now, MINUTE, HOUR, DAY, WEEK, uniqBy} from "@welshman/lib"
|
||||||
formatTimestampAsDate,
|
|
||||||
groupBy,
|
|
||||||
ago,
|
|
||||||
now,
|
|
||||||
MONTH,
|
|
||||||
MINUTE,
|
|
||||||
HOUR,
|
|
||||||
DAY,
|
|
||||||
WEEK,
|
|
||||||
first,
|
|
||||||
sortBy,
|
|
||||||
uniqBy,
|
|
||||||
} from "@welshman/lib"
|
|
||||||
import {request} from "@welshman/net"
|
import {request} from "@welshman/net"
|
||||||
import {
|
import {MESSAGE, getTagValue, sortEventsDesc} from "@welshman/util"
|
||||||
MESSAGE,
|
|
||||||
THREAD,
|
|
||||||
CLASSIFIED,
|
|
||||||
ZAP_GOAL,
|
|
||||||
EVENT_TIME,
|
|
||||||
COMMENT,
|
|
||||||
POLL,
|
|
||||||
getTagValue,
|
|
||||||
getTagValues,
|
|
||||||
getIdAndAddress,
|
|
||||||
sortEventsDesc,
|
|
||||||
} from "@welshman/util"
|
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {repository} from "@welshman/app"
|
|
||||||
import History from "@assets/icons/history.svg?dataurl"
|
import History from "@assets/icons/history.svg?dataurl"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
@@ -42,76 +15,15 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import PageContent from "@lib/components/PageContent.svelte"
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||||
import NoteItem from "@app/components/NoteItem.svelte"
|
import RecentItem from "@app/components/RecentItem.svelte"
|
||||||
import ThreadItem from "@app/components/ThreadItem.svelte"
|
|
||||||
import ClassifiedItem from "@app/components/ClassifiedItem.svelte"
|
|
||||||
import GoalItem from "@app/components/GoalItem.svelte"
|
|
||||||
import CalendarEventItem from "@app/components/CalendarEventItem.svelte"
|
|
||||||
import PollItem from "@app/components/PollItem.svelte"
|
|
||||||
import RecentConversation from "@app/components/RecentConversation.svelte"
|
|
||||||
import {decodeRelay} from "@app/relays"
|
import {decodeRelay} from "@app/relays"
|
||||||
import {deriveEventsForUrl} from "@app/repository"
|
|
||||||
import {CONTENT_KINDS} from "@app/content"
|
import {CONTENT_KINDS} from "@app/content"
|
||||||
|
import {deriveRecentActivity} from "@app/recent"
|
||||||
import {goToEvent} from "@app/routes"
|
import {goToEvent} from "@app/routes"
|
||||||
|
|
||||||
const url = decodeRelay($page.params.relay!)
|
const url = decodeRelay($page.params.relay!)
|
||||||
const since = ago(3, MONTH)
|
|
||||||
|
|
||||||
const messages = deriveEventsForUrl(url, [{kinds: [MESSAGE], since}])
|
const recentActivity = deriveRecentActivity(url)
|
||||||
const content = deriveEventsForUrl(url, [{kinds: CONTENT_KINDS, since}])
|
|
||||||
const comments = deriveEventsForUrl(url, [{kinds: [COMMENT], since}])
|
|
||||||
|
|
||||||
const recentActivity = derived(
|
|
||||||
[messages, content, comments],
|
|
||||||
([$messages, $content, $comments]) => {
|
|
||||||
const activity: Array<{
|
|
||||||
type: "message" | "content"
|
|
||||||
event: TrustedEvent
|
|
||||||
count: number
|
|
||||||
timestamp: number
|
|
||||||
}> = []
|
|
||||||
|
|
||||||
const byRoom = groupBy(e => getTagValue("h", e.tags), $messages)
|
|
||||||
for (const roomMessages of byRoom.values()) {
|
|
||||||
const latest = first(roomMessages)
|
|
||||||
if (latest) {
|
|
||||||
activity.push({
|
|
||||||
type: "message",
|
|
||||||
event: latest,
|
|
||||||
count: roomMessages.length,
|
|
||||||
timestamp: latest.created_at,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestActivityByKey = new Map<string, number>()
|
|
||||||
|
|
||||||
for (const event of $content) {
|
|
||||||
for (const k of getIdAndAddress(event)) {
|
|
||||||
latestActivityByKey.set(k, Math.max(latestActivityByKey.get(k) || 0, event.created_at))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const event of $comments) {
|
|
||||||
for (const k of getTagValues(["E", "A"], event.tags)) {
|
|
||||||
latestActivityByKey.set(k, Math.max(latestActivityByKey.get(k) || 0, event.created_at))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [address, timestamp] of latestActivityByKey.entries()) {
|
|
||||||
const event = repository.getEvent(address)
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
activity.push({type: "content", event, timestamp, count: 1})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sortBy(
|
|
||||||
a => -a.timestamp,
|
|
||||||
uniqBy(a => a.event.id, activity),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
let term = $state("")
|
let term = $state("")
|
||||||
let showSearch = $state(false)
|
let showSearch = $state(false)
|
||||||
@@ -294,26 +206,12 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</SpaceBar>
|
</SpaceBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col gap-2 p-2" bind:element>
|
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4" bind:element>
|
||||||
{#if $recentActivity.length === 0}
|
{#if $recentActivity.length === 0}
|
||||||
<p class="flex flex-col items-center py-20 text-center">No recent activity found!</p>
|
<p class="flex flex-col items-center py-20 text-center">No recent activity found!</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#each $recentActivity.slice(0, limit) as { type, event, count = 0 } (event.id)}
|
{#each $recentActivity.slice(0, limit) as item (item.event.id)}
|
||||||
{#if type === "message"}
|
<RecentItem {url} {item} />
|
||||||
<RecentConversation {url} {event} {count} />
|
|
||||||
{:else if event.kind === THREAD}
|
|
||||||
<ThreadItem {url} {event} />
|
|
||||||
{:else if event.kind === CLASSIFIED}
|
|
||||||
<ClassifiedItem {url} {event} />
|
|
||||||
{:else if event.kind === ZAP_GOAL}
|
|
||||||
<GoalItem {url} {event} />
|
|
||||||
{:else if event.kind === EVENT_TIME}
|
|
||||||
<CalendarEventItem {url} {event} />
|
|
||||||
{:else if event.kind === POLL}
|
|
||||||
<PollItem {url} {event} />
|
|
||||||
{:else}
|
|
||||||
<NoteItem {url} {event} />
|
|
||||||
{/if}
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</SpaceBar>
|
</SpaceBar>
|
||||||
|
|
||||||
<PageContent bind:element class="flex flex-col gap-4 p-2">
|
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
|
||||||
{#each threadFeed.boards as [h, threads] (h || "general")}
|
{#each threadFeed.boards as [h, threads] (h || "general")}
|
||||||
<ThreadBoard {url} {h} {threads} />
|
<ThreadBoard {url} {h} {threads} />
|
||||||
{/each}
|
{/each}
|
||||||
@@ -93,8 +93,6 @@
|
|||||||
Looking for threads...
|
Looking for threads...
|
||||||
{:else if threadFeed.items.length === 0}
|
{:else if threadFeed.items.length === 0}
|
||||||
No threads found.
|
No threads found.
|
||||||
{:else}
|
|
||||||
That's all!
|
|
||||||
{/if}
|
{/if}
|
||||||
</Spinner>
|
</Spinner>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page>
|
<Page>
|
||||||
<PageContent class="flex flex-col items-center gap-2 p-2">
|
<PageContent class="flex flex-col items-center gap-2 p-2 sm:gap-4 sm:p-4">
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<div>Choose your Hosting Plan</div>
|
<div>Choose your Hosting Plan</div>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
<div class="flex w-full max-w-lg flex-col gap-4 lg:max-w-4xl">
|
<div class="flex w-full max-w-lg flex-col gap-4 lg:max-w-4xl">
|
||||||
<div class="grid grid-cols-1 gap-2 lg:grid-cols-2">
|
<div class="grid grid-cols-1 gap-2 lg:grid-cols-2">
|
||||||
<div class="card2 bg-alt flex flex-col gap-5">
|
<div class="card2 flex flex-col gap-5">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="bg-primary/20 flex h-10 w-10 items-center justify-center rounded-md">
|
<div class="bg-primary/20 flex h-10 w-10 items-center justify-center rounded-md">
|
||||||
<Icon icon={CloudCheck} class="text-primary" />
|
<Icon icon={CloudCheck} class="text-primary" />
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<Icon icon={ArrowRight} />
|
<Icon icon={ArrowRight} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div class="card2 bg-alt border-primary flex flex-col gap-5 border">
|
<div class="card2 border-primary flex flex-col gap-5 border">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<img alt="Coracle Logo" src="/coracle.png" class="h-10 w-10" />
|
<img alt="Coracle Logo" src="/coracle.png" class="h-10 w-10" />
|
||||||
|
|||||||
Reference in New Issue
Block a user