Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e7e439e3f | |||
| 3368cba1be | |||
| 4f0579bb7f | |||
| 08e80262a4 | |||
| e10b83bed8 | |||
| fa17c398ca | |||
| e0840f24dd | |||
| 8e38271534 | |||
| 86928fc12c | |||
| e15fb3ce9c | |||
| bf1ab5f0ee | |||
| 59568f95f1 | |||
| 75d52e7e17 | |||
| bdb5d3dfaa | |||
| c387b65460 | |||
| 01c4219922 | |||
| 9ca4440038 | |||
| d6cc414f41 | |||
| 7ccb2949a9 | |||
| 8d4e657af5 | |||
| 4886650dfa | |||
| e36e6093e9 | |||
| edd6e5c8fc | |||
| be7a42d951 | |||
| af91fe129b | |||
| 6fcf0e7f12 | |||
| b6defe59a8 | |||
| f618e4e1f3 | |||
| 5253980cdc |
@@ -1,6 +1,8 @@
|
|||||||
VITE_DEFAULT_PUBKEYS=fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
|
VITE_DEFAULT_PUBKEYS=fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
|
||||||
VITE_BURROW_URL=
|
VITE_BURROW_URL=
|
||||||
VITE_PLATFORM_URL=https://flotilla.social
|
VITE_PLATFORM_URL=https://flotilla.social
|
||||||
|
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||||
|
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||||
VITE_PLATFORM_NAME=Flotilla
|
VITE_PLATFORM_NAME=Flotilla
|
||||||
VITE_PLATFORM_LOGO=static/flotilla.png
|
VITE_PLATFORM_LOGO=static/flotilla.png
|
||||||
VITE_PLATFORM_RELAY=
|
VITE_PLATFORM_RELAY=
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
src/assets
|
src/assets
|
||||||
android
|
|
||||||
build
|
build
|
||||||
|
.idea
|
||||||
|
.gradle
|
||||||
|
*.png
|
||||||
|
*.ttf
|
||||||
|
gradlew*
|
||||||
|
_app
|
||||||
|
release
|
||||||
|
android/capacitor-cordova-android-plugins
|
||||||
|
android/app/src/androidTest
|
||||||
|
android/app/src/test
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ Thumbs.db
|
|||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
# Android
|
||||||
|
.idea
|
||||||
|
|
||||||
# Generated assets
|
# Generated assets
|
||||||
static/favicon.ico
|
static/favicon.ico
|
||||||
static/pwa-64x64.png
|
static/pwa-64x64.png
|
||||||
|
|||||||
@@ -1,5 +1,33 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 0.2.5
|
||||||
|
|
||||||
|
* Improve room and data loading
|
||||||
|
* Use @welshman/editor
|
||||||
|
* Drop support for legacy event kinds
|
||||||
|
* Add support for back button navigation on android
|
||||||
|
* Remove note to self page (still available via chat)
|
||||||
|
* Improve chat conversation search
|
||||||
|
* Change how reply UI works
|
||||||
|
|
||||||
|
# 0.2.4
|
||||||
|
|
||||||
|
* Update icons
|
||||||
|
|
||||||
|
# 0.2.3
|
||||||
|
|
||||||
|
* Add NIP 56 reports for messages and threads
|
||||||
|
* Add ToS and privacy policy
|
||||||
|
* Add avatar fallback icons
|
||||||
|
* Add mark as read to chats
|
||||||
|
* Add send button to chat compose
|
||||||
|
* Accommodate onion URLs
|
||||||
|
* Improve loading and notifications
|
||||||
|
|
||||||
|
# 0.2.2
|
||||||
|
|
||||||
|
* Fix bug with sending messages
|
||||||
|
|
||||||
# 0.2.1
|
# 0.2.1
|
||||||
|
|
||||||
* Improve performance, as well as scrolling and loading
|
* Improve performance, as well as scrolling and loading
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ If you would like to be interoperable with Flotilla, please check out this draft
|
|||||||
|
|
||||||
# Deploy
|
# Deploy
|
||||||
|
|
||||||
To run your own Flotilla, it's as simple as `npm run build`, then serve the `build` directory.
|
To run your own Flotilla, it's as simple as:
|
||||||
|
|
||||||
|
- `npm install`
|
||||||
|
- `npm run build`
|
||||||
|
- `npx serve build`
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "social.flotilla"
|
applicationId "social.flotilla"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 5
|
||||||
versionName "0.2.2"
|
versionName "0.2.5"
|
||||||
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.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ android {
|
|||||||
|
|
||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':capacitor-app')
|
||||||
implementation project(':nostr-signer-capacitor-plugin')
|
implementation project(':nostr-signer-capacitor-plugin')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 531 B After Width: | Height: | Size: 899 B |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 916 B After Width: | Height: | Size: 705 B |
|
Before Width: | Height: | Size: 277 B After Width: | Height: | Size: 329 B |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 350 B After Width: | Height: | Size: 550 B |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 697 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 5.3 KiB |
@@ -1,9 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
@@ -19,4 +17,4 @@
|
|||||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
<item name="android:background">@drawable/splash</item>
|
<item name="android:background">@drawable/splash</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
classpath 'com.android.tools.build:gradle:8.8.0'
|
||||||
classpath 'com.google.gms:google-services:4.4.0'
|
classpath 'com.google.gms:google-services:4.4.0'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|||||||
@@ -2,5 +2,8 @@
|
|||||||
include ':capacitor-android'
|
include ':capacitor-android'
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
|
include ':capacitor-app'
|
||||||
|
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||||
|
|
||||||
include ':nostr-signer-capacitor-plugin'
|
include ':nostr-signer-capacitor-plugin'
|
||||||
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/nostr-signer-capacitor-plugin/android')
|
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/nostr-signer-capacitor-plugin/android')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -16,7 +16,6 @@ eval "$temp_env"
|
|||||||
|
|
||||||
if [[ $VITE_PLATFORM_LOGO =~ ^https://* ]]; then
|
if [[ $VITE_PLATFORM_LOGO =~ ^https://* ]]; then
|
||||||
curl $VITE_PLATFORM_LOGO > static/logo.png
|
curl $VITE_PLATFORM_LOGO > static/logo.png
|
||||||
cp static/logo.png assets/logo.png
|
|
||||||
export VITE_PLATFORM_LOGO=static/logo.png
|
export VITE_PLATFORM_LOGO=static/logo.png
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -28,3 +27,10 @@ perl -i -pe"s|{DESCRIPTION}|$VITE_PLATFORM_DESCRIPTION|g" build/index.html
|
|||||||
perl -i -pe"s|{ACCENT}|$VITE_PLATFORM_ACCENT|g" build/index.html
|
perl -i -pe"s|{ACCENT}|$VITE_PLATFORM_ACCENT|g" build/index.html
|
||||||
perl -i -pe"s|{NAME}|$VITE_PLATFORM_NAME|g" build/index.html
|
perl -i -pe"s|{NAME}|$VITE_PLATFORM_NAME|g" build/index.html
|
||||||
perl -i -pe"s|{URL}|$VITE_PLATFORM_URL|g" build/index.html
|
perl -i -pe"s|{URL}|$VITE_PLATFORM_URL|g" build/index.html
|
||||||
|
|
||||||
|
npx cap sync
|
||||||
|
npx @capacitor/assets generate \
|
||||||
|
--iconBackgroundColor '#eeeeee' \
|
||||||
|
--iconBackgroundColorDark '#222222' \
|
||||||
|
--splashBackgroundColor '#ffffff' \
|
||||||
|
--splashBackgroundColorDark '#191E24'
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ const config: CapacitorConfig = {
|
|||||||
appId: 'social.flotilla',
|
appId: 'social.flotilla',
|
||||||
appName: 'Flotilla',
|
appName: 'Flotilla',
|
||||||
webDir: 'build'
|
webDir: 'build'
|
||||||
|
server: {
|
||||||
|
androidScheme: "https"
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
SplashScreen: {
|
SplashScreen: {
|
||||||
androidSplashResourceName: "splash"
|
androidSplashResourceName: "splash"
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.2.2",
|
"version": "0.2.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "./build.sh",
|
"build": "./build.sh",
|
||||||
"sourcemaps": "sentry-cli --url https://glitchtip.coracle.social --auth-token $GLITCHTIP_AUTH_TOKEN --api-key $VITE_GLITCHTIP_API_KEY sourcemaps --org coracle --project flotilla --release $(cat package.json|jq -r '.version') upload --url-prefix /_app/immutable/ build/_app/immutable",
|
"sourcemaps": "sentry-cli --url https://glitchtip.coracle.social --auth-token $GLITCHTIP_AUTH_TOKEN --api-key $VITE_GLITCHTIP_API_KEY sourcemaps --org coracle --project flotilla --release $(cat package.json|jq -r '.version') upload --url-prefix /_app/immutable/ build/_app/immutable",
|
||||||
"release:android": "cap sync && cap build android --androidreleasetype APK --signing-type apksigner",
|
"release:android": "cap build android --androidreleasetype APK --signing-type apksigner",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check src && eslint src",
|
"lint": "prettier --check src && eslint src",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@capacitor/core": "^7.0.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
"@sentry/cli": "^2.40.0",
|
"@sentry/cli": "^2.40.0",
|
||||||
@@ -37,38 +40,28 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^6.1.2",
|
"@capacitor/android": "^7.0.1",
|
||||||
"@capacitor/cli": "^6.1.2",
|
"@capacitor/app": "^7.0.0",
|
||||||
"@capacitor/core": "^6.1.2",
|
"@capacitor/cli": "^6.2.0",
|
||||||
|
"@capacitor/core": "^7.0.1",
|
||||||
"@noble/curves": "^1.5.0",
|
"@noble/curves": "^1.5.0",
|
||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
"@sentry/browser": "^8.35.0",
|
"@sentry/browser": "^8.35.0",
|
||||||
"@sveltejs/adapter-static": "^3.0.4",
|
"@sveltejs/adapter-static": "^3.0.4",
|
||||||
"@tiptap/extension-code": "^2.6.6",
|
|
||||||
"@tiptap/extension-code-block": "^2.6.6",
|
|
||||||
"@tiptap/extension-document": "^2.6.6",
|
|
||||||
"@tiptap/extension-dropcursor": "^2.6.6",
|
|
||||||
"@tiptap/extension-gapcursor": "^2.6.6",
|
|
||||||
"@tiptap/extension-hard-break": "^2.6.6",
|
|
||||||
"@tiptap/extension-history": "^2.6.6",
|
|
||||||
"@tiptap/extension-paragraph": "^2.6.6",
|
|
||||||
"@tiptap/extension-placeholder": "^2.9.1",
|
|
||||||
"@tiptap/extension-text": "^2.6.6",
|
|
||||||
"@tiptap/suggestion": "^2.6.4",
|
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.6",
|
"@vite-pwa/sveltekit": "^0.6.6",
|
||||||
"@welshman/app": "~0.0.36",
|
"@welshman/app": "~0.0.41",
|
||||||
"@welshman/content": "~0.0.15",
|
"@welshman/content": "~0.0.15",
|
||||||
"@welshman/dvm": "~0.0.13",
|
"@welshman/dvm": "~0.0.14",
|
||||||
"@welshman/editor": "~0.0.4",
|
"@welshman/editor": "~0.0.8",
|
||||||
"@welshman/feeds": "~0.0.30",
|
"@welshman/feeds": "~0.0.30",
|
||||||
"@welshman/lib": "~0.0.37",
|
"@welshman/lib": "~0.0.38",
|
||||||
"@welshman/net": "~0.0.45",
|
"@welshman/net": "~0.0.46",
|
||||||
"@welshman/signer": "~0.0.19",
|
"@welshman/signer": "~0.0.20",
|
||||||
"@welshman/store": "~0.0.15",
|
"@welshman/store": "~0.0.15",
|
||||||
"@welshman/util": "~0.0.55",
|
"@welshman/util": "~0.0.59",
|
||||||
"daisyui": "^4.12.10",
|
"daisyui": "^4.12.10",
|
||||||
"date-picker-svelte": "^2.13.0",
|
"date-picker-svelte": "^2.13.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
@@ -76,11 +69,9 @@
|
|||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"husky": "^9.1.6",
|
"husky": "^9.1.6",
|
||||||
"idb": "^8.0.0",
|
"idb": "^8.0.0",
|
||||||
"nostr-editor": "^0.0.3",
|
|
||||||
"nostr-signer-capacitor-plugin": "^0.0.3",
|
"nostr-signer-capacitor-plugin": "^0.0.3",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4"
|
||||||
"svelte-tiptap": "^1.1.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,15 +40,14 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: Lato;
|
font-family: Lato;
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme] {
|
|
||||||
--base-100: oklch(var(--b1));
|
--base-100: oklch(var(--b1));
|
||||||
--base-200: oklch(var(--b2));
|
--base-200: oklch(var(--b2));
|
||||||
--base-300: oklch(var(--b3));
|
--base-300: oklch(var(--b3));
|
||||||
--base-content: oklch(var(--bc));
|
--base-content: oklch(var(--bc));
|
||||||
--primary: oklch(var(--p));
|
--primary: oklch(var(--p));
|
||||||
|
--primary-content: oklch(var(--pc));
|
||||||
--secondary: oklch(var(--s));
|
--secondary: oklch(var(--s));
|
||||||
|
--secondary-content: oklch(var(--sc));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-alt,
|
.bg-alt,
|
||||||
@@ -120,6 +119,16 @@
|
|||||||
@apply overflow-hidden text-ellipsis;
|
@apply overflow-hidden text-ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-tip]::before {
|
||||||
|
@apply ellipsize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
[data-tip]::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.content-padding-x {
|
.content-padding-x {
|
||||||
@apply px-4 sm:px-8 md:px-12;
|
@apply px-4 sm:px-8 md:px-12;
|
||||||
}
|
}
|
||||||
@@ -239,3 +248,19 @@ emoji-picker {
|
|||||||
--input-font-color: var(--base-content);
|
--input-font-color: var(--base-content);
|
||||||
--outline-color: var(--base-100);
|
--outline-color: var(--base-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* tiptap */
|
||||||
|
|
||||||
|
.tiptap {
|
||||||
|
--tiptap-object-bg: var(--base-100);
|
||||||
|
--tiptap-object-fg: var(--base-content);
|
||||||
|
--tiptap-active-bg: var(--primary);
|
||||||
|
--tiptap-active-fg: var(--primary-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-suggestions {
|
||||||
|
--tiptap-object-bg: var(--base-100);
|
||||||
|
--tiptap-object-fg: var(--base-content);
|
||||||
|
--tiptap-active-bg: var(--base-300);
|
||||||
|
--tiptap-active-fg: var(--base-content);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import {ctx, sample, uniq, sleep, chunk, equals} from "@welshman/lib"
|
import {ctx, sample, uniq, sleep, chunk, equals} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
DELETE,
|
DELETE,
|
||||||
|
REPORT,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
INBOX_RELAYS,
|
INBOX_RELAYS,
|
||||||
RELAYS,
|
RELAYS,
|
||||||
@@ -26,8 +28,9 @@ import {
|
|||||||
getRelayTags,
|
getRelayTags,
|
||||||
isShareableRelayUrl,
|
isShareableRelayUrl,
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
|
toNostrURI,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, EventTemplate, List} from "@welshman/util"
|
import type {TrustedEvent, EventContent, EventTemplate, List} from "@welshman/util"
|
||||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
||||||
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
|
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
|
||||||
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
|
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
|
||||||
@@ -45,7 +48,7 @@ import {
|
|||||||
loadFollows,
|
loadFollows,
|
||||||
loadMutes,
|
loadMutes,
|
||||||
tagEvent,
|
tagEvent,
|
||||||
tagReactionTo,
|
tagEventForReaction,
|
||||||
getRelayUrls,
|
getRelayUrls,
|
||||||
userRelaySelections,
|
userRelaySelections,
|
||||||
userInboxRelaySelections,
|
userInboxRelaySelections,
|
||||||
@@ -54,6 +57,8 @@ import {
|
|||||||
addSession,
|
addSession,
|
||||||
clearStorage,
|
clearStorage,
|
||||||
dropSession,
|
dropSession,
|
||||||
|
tagEventForComment,
|
||||||
|
tagEventForQuote,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import type {Thunk} from "@welshman/app"
|
import type {Thunk} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
@@ -94,6 +99,22 @@ export const getThunkError = async (thunk: Thunk) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const prependParent = (parent: TrustedEvent | undefined, {content, tags}: EventContent) => {
|
||||||
|
if (parent) {
|
||||||
|
const nevent = nip19.neventEncode({
|
||||||
|
id: parent.id,
|
||||||
|
kind: parent.kind,
|
||||||
|
author: parent.pubkey,
|
||||||
|
relays: ctx.app.router.Event(parent).limit(3).getUrls(),
|
||||||
|
})
|
||||||
|
|
||||||
|
tags = [...tags, tagEventForQuote(parent)]
|
||||||
|
content = toNostrURI(nevent) + "\n\n" + content
|
||||||
|
}
|
||||||
|
|
||||||
|
return {content, tags}
|
||||||
|
}
|
||||||
|
|
||||||
// Log in
|
// Log in
|
||||||
|
|
||||||
export const loginWithNip46 = async ({
|
export const loginWithNip46 = async ({
|
||||||
@@ -376,7 +397,6 @@ export const checkRelayAuth = async (url: string, timeout = 3000) => {
|
|||||||
|
|
||||||
export const attemptRelayAccess = async (url: string, claim = "") => {
|
export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||||
const checks = [
|
const checks = [
|
||||||
() => checkRelayProfile(url),
|
|
||||||
() => checkRelayConnection(url),
|
() => checkRelayConnection(url),
|
||||||
() => checkRelayAccess(url, claim),
|
() => checkRelayAccess(url, claim),
|
||||||
() => checkRelayAuth(url),
|
() => checkRelayAuth(url),
|
||||||
@@ -430,13 +450,36 @@ export const makeDelete = ({event}: {event: TrustedEvent}) => {
|
|||||||
export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) =>
|
export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) =>
|
||||||
publishThunk({event: makeDelete({event}), relays})
|
publishThunk({event: makeDelete({event}), relays})
|
||||||
|
|
||||||
|
export type ReportParams = {
|
||||||
|
event: TrustedEvent
|
||||||
|
content: string
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeReport = ({event, reason, content}: ReportParams) => {
|
||||||
|
const tags = [
|
||||||
|
["p", event.pubkey],
|
||||||
|
["e", event.id, reason],
|
||||||
|
]
|
||||||
|
|
||||||
|
return createEvent(REPORT, {content, tags})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const publishReport = ({
|
||||||
|
relays,
|
||||||
|
event,
|
||||||
|
reason,
|
||||||
|
content,
|
||||||
|
}: ReportParams & {relays: string[]}) =>
|
||||||
|
publishThunk({event: makeReport({event, reason, content}), relays})
|
||||||
|
|
||||||
export type ReactionParams = {
|
export type ReactionParams = {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeReaction = ({event, content}: ReactionParams) => {
|
export const makeReaction = ({event, content}: ReactionParams) => {
|
||||||
const tags = [["k", String(event.kind)], ...tagReactionTo(event)]
|
const tags = tagEventForReaction(event)
|
||||||
const groupTag = getTag("h", event.tags)
|
const groupTag = getTag("h", event.tags)
|
||||||
|
|
||||||
if (groupTag) {
|
if (groupTag) {
|
||||||
@@ -450,37 +493,14 @@ export const makeReaction = ({event, content}: ReactionParams) => {
|
|||||||
export const publishReaction = ({relays, ...params}: ReactionParams & {relays: string[]}) =>
|
export const publishReaction = ({relays, ...params}: ReactionParams & {relays: string[]}) =>
|
||||||
publishThunk({event: makeReaction(params), relays})
|
publishThunk({event: makeReaction(params), relays})
|
||||||
|
|
||||||
export type ReplyParams = {
|
export type CommentParams = {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
content: string
|
content: string
|
||||||
tags?: string[][]
|
tags?: string[][]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeComment = ({event, content, tags = []}: ReplyParams) => {
|
export const makeComment = ({event, content, tags = []}: CommentParams) =>
|
||||||
const seenRoots = new Set<string>()
|
createEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event)]})
|
||||||
|
|
||||||
for (const [raw, ...tag] of event.tags.filter(t => t[0].match(/^(k|e|a|i)$/i))) {
|
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
||||||
const T = raw.toUpperCase()
|
|
||||||
const t = raw.toLowerCase()
|
|
||||||
|
|
||||||
if (seenRoots.has(T)) {
|
|
||||||
tags.push([t, ...tag])
|
|
||||||
} else {
|
|
||||||
tags.push([T, ...tag])
|
|
||||||
seenRoots.add(T)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seenRoots.size === 0) {
|
|
||||||
tags.push(["K", String(event.kind)])
|
|
||||||
tags.push(["E", event.id])
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.push(["k", String(event.kind)])
|
|
||||||
tags.push(["e", event.id])
|
|
||||||
|
|
||||||
return createEvent(COMMENT, {content, tags})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const publishComment = ({relays, ...params}: ReplyParams & {relays: string[]}) =>
|
|
||||||
publishThunk({event: makeComment(params), relays})
|
publishThunk({event: makeComment(params), relays})
|
||||||
|
|||||||
@@ -1,35 +1,37 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
|
import {EditorContent} from "svelte-tiptap"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import {getEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
|
|
||||||
export let onSubmit: any
|
export let onSubmit: any
|
||||||
export let content = ""
|
export let content = ""
|
||||||
export let editor: ReturnType<typeof getEditor> | undefined = undefined
|
|
||||||
|
export const focus = () => $editor.chain().focus().run()
|
||||||
|
|
||||||
const uploading = writable(false)
|
const uploading = writable(false)
|
||||||
|
|
||||||
let element: HTMLElement
|
|
||||||
|
|
||||||
const uploadFiles = () => $editor!.chain().selectFiles().run()
|
const uploadFiles = () => $editor!.chain().selectFiles().run()
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if ($uploading) return
|
if ($uploading) return
|
||||||
|
|
||||||
onSubmit({
|
const content = $editor!.getText({blockSeparator: "\n"}).trim()
|
||||||
content: $editor!.getText({blockSeparator: "\n"}).trim(),
|
const tags = $editor!.storage.nostr.getEditorTags()
|
||||||
tags: $editor!.storage.nostr.getEditorTags(),
|
|
||||||
})
|
if (!content) return
|
||||||
|
|
||||||
|
onSubmit({content, tags})
|
||||||
|
|
||||||
$editor!.chain().clearContent().run()
|
$editor!.chain().clearContent().run()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
const editor = makeEditor({autofocus: !isMobile, submit, uploading, aggressive: true})
|
||||||
editor = getEditor({autofocus: !isMobile, aggressive: true, element, submit, uploading})
|
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
$editor!.chain().setContent(content).run()
|
$editor!.chain().setContent(content).run()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -49,6 +51,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<div class="chat-editor flex-grow overflow-hidden">
|
<div class="chat-editor flex-grow overflow-hidden">
|
||||||
<div bind:this={element} />
|
<EditorContent editor={$editor} />
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
||||||
|
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
||||||
|
disabled={$uploading}
|
||||||
|
on:click={submit}>
|
||||||
|
<Icon icon="plain" />
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {displayProfileByPubkey} from "@welshman/app"
|
||||||
|
import {slide} from "@lib/transition"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Content from "@app/components/Content.svelte"
|
||||||
|
|
||||||
|
export let event: TrustedEvent
|
||||||
|
export let clear: () => void
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
|
||||||
|
transition:slide>
|
||||||
|
<p class="text-primary">Replying to @{displayProfileByPubkey(event.pubkey)}</p>
|
||||||
|
{#key event.id}
|
||||||
|
<Content {event} minLength={100} maxLength={300} expandMode="disabled" />
|
||||||
|
{/key}
|
||||||
|
<Button class="absolute right-2 top-2 cursor-pointer" on:click={clear}>
|
||||||
|
<Icon icon="close-circle" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {hash} from "@welshman/lib"
|
import {hash} from "@welshman/lib"
|
||||||
|
import {now} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
thunks,
|
thunks,
|
||||||
|
pubkey,
|
||||||
deriveProfile,
|
deriveProfile,
|
||||||
deriveProfileDisplay,
|
deriveProfileDisplay,
|
||||||
|
formatTimestampAsDate,
|
||||||
formatTimestampAsTime,
|
formatTimestampAsTime,
|
||||||
pubkey,
|
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import LongPress from "@lib/components/LongPress.svelte"
|
import LongPress from "@lib/components/LongPress.svelte"
|
||||||
@@ -31,6 +33,7 @@
|
|||||||
export let inert = false
|
export let inert = false
|
||||||
|
|
||||||
const thunk = $thunks[event.id]
|
const thunk = $thunks[event.id]
|
||||||
|
const today = formatTimestampAsDate(now())
|
||||||
const profile = deriveProfile(event.pubkey)
|
const profile = deriveProfile(event.pubkey)
|
||||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||||
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
||||||
@@ -70,7 +73,14 @@
|
|||||||
<Button on:click={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
<Button on:click={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
||||||
{$profileDisplay}
|
{$profileDisplay}
|
||||||
</Button>
|
</Button>
|
||||||
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
|
<span class="text-xs opacity-50">
|
||||||
|
{#if formatTimestampAsDate(event.created_at) === today}
|
||||||
|
Today
|
||||||
|
{:else}
|
||||||
|
{formatTimestampAsDate(event.created_at)}
|
||||||
|
{/if}
|
||||||
|
at {formatTimestampAsTime(event.created_at)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
@@ -82,7 +92,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-2 ml-10 mt-1">
|
<div class="row-2 ml-10 mt-1">
|
||||||
<ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-right" />
|
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
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 EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
|
import EventReport from "@app/components/EventReport.svelte"
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
@@ -10,6 +11,11 @@
|
|||||||
export let event
|
export let event
|
||||||
export let onClick
|
export let onClick
|
||||||
|
|
||||||
|
const report = () => {
|
||||||
|
onClick()
|
||||||
|
pushModal(EventReport, {url, event})
|
||||||
|
}
|
||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(EventInfo, {event})
|
pushModal(EventInfo, {event})
|
||||||
@@ -35,5 +41,12 @@
|
|||||||
Delete Message
|
Delete Message
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" on:click={report}>
|
||||||
|
<Icon size={4} icon="danger" />
|
||||||
|
Report Content
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -10,19 +10,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import type {Readable} from "svelte/store"
|
import {int, nthNe, MINUTE, sortBy, remove} from "@welshman/lib"
|
||||||
import type {Editor} from "svelte-tiptap"
|
|
||||||
import {nip19} from "nostr-tools"
|
|
||||||
import {int, nthNe, MINUTE, sortBy, remove, ctx} from "@welshman/lib"
|
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {createEvent, DIRECT_MESSAGE, INBOX_RELAYS} from "@welshman/util"
|
import {createEvent, DIRECT_MESSAGE, INBOX_RELAYS} from "@welshman/util"
|
||||||
import {
|
import {pubkey, formatTimestampAsDate, inboxRelaySelectionsByPubkey, load} from "@welshman/app"
|
||||||
pubkey,
|
|
||||||
formatTimestampAsDate,
|
|
||||||
inboxRelaySelectionsByPubkey,
|
|
||||||
load,
|
|
||||||
tagPubkey,
|
|
||||||
} from "@welshman/app"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
@@ -36,9 +27,10 @@
|
|||||||
import ProfileList from "@app/components/ProfileList.svelte"
|
import ProfileList from "@app/components/ProfileList.svelte"
|
||||||
import ChatMessage from "@app/components/ChatMessage.svelte"
|
import ChatMessage from "@app/components/ChatMessage.svelte"
|
||||||
import ChatCompose from "@app/components/ChannelCompose.svelte"
|
import ChatCompose from "@app/components/ChannelCompose.svelte"
|
||||||
|
import ChatComposeParent from "@app/components/ChannelComposeParent.svelte"
|
||||||
import {userSettingValues, deriveChat, splitChatId, PLATFORM_NAME} from "@app/state"
|
import {userSettingValues, deriveChat, splitChatId, PLATFORM_NAME} from "@app/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {sendWrapped} from "@app/commands"
|
import {sendWrapped, prependParent} from "@app/commands"
|
||||||
|
|
||||||
export let id
|
export let id
|
||||||
|
|
||||||
@@ -57,28 +49,31 @@
|
|||||||
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
|
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
|
||||||
|
|
||||||
const replyTo = (event: TrustedEvent) => {
|
const replyTo = (event: TrustedEvent) => {
|
||||||
const relays = ctx.app.router.Event(event).getUrls()
|
parent = event
|
||||||
const nevent = nip19.neventEncode({...event, relays})
|
compose.focus()
|
||||||
|
|
||||||
$editor.commands.insertNEvent({nevent})
|
|
||||||
$editor.commands.insertContent("\n")
|
|
||||||
$editor.commands.focus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async ({content, ...params}: EventContent) => {
|
const clearParent = () => {
|
||||||
// Remove p tags since they result in forking the conversation
|
parent = undefined
|
||||||
const tags = [...params.tags.filter(nthNe(0, "p")), ...remove($pubkey!, pubkeys).map(tagPubkey)]
|
}
|
||||||
|
|
||||||
|
const onSubmit = async ({content, tags}: EventContent) => {
|
||||||
await sendWrapped({
|
await sendWrapped({
|
||||||
pubkeys,
|
pubkeys,
|
||||||
template: createEvent(DIRECT_MESSAGE, {content, tags}),
|
template: createEvent(
|
||||||
|
DIRECT_MESSAGE,
|
||||||
|
prependParent(parent, {content, tags: tags.filter(nthNe(0, "p"))}),
|
||||||
|
),
|
||||||
delay: $userSettingValues.send_delay,
|
delay: $userSettingValues.send_delay,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
clearParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
let loading = true
|
let loading = true
|
||||||
let editor: Readable<Editor>
|
let parent: TrustedEvent | undefined
|
||||||
let elements: Element[] = []
|
let elements: Element[] = []
|
||||||
|
let compose: ChatCompose
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
elements = []
|
elements = []
|
||||||
@@ -200,5 +195,8 @@
|
|||||||
<slot name="info" />
|
<slot name="info" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ChatCompose bind:editor {onSubmit} />
|
{#if parent}
|
||||||
|
<ChatComposeParent event={parent} clear={clearParent} />
|
||||||
|
{/if}
|
||||||
|
<ChatCompose bind:this={compose} {onSubmit} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,10 +41,10 @@
|
|||||||
<form class="column gap-4" on:submit|preventDefault={submit}>
|
<form class="column gap-4" on:submit|preventDefault={submit}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">Enable Messages</div>
|
<div slot="title">Enable Messages</div>
|
||||||
<div slot="info">Do you want to enable notes and direct messages?</div>
|
<div slot="info">Do you want to enable direct messages?</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>
|
<p>
|
||||||
By default, notes and direct messages are disabled, since loading them requires
|
By default, direct messages are disabled, since loading them requires
|
||||||
{PLATFORM_NAME} to download and decrypt a lot of data.
|
{PLATFORM_NAME} to download and decrypt a lot of data.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -34,7 +34,10 @@
|
|||||||
<div class="flex flex-col justify-start gap-1">
|
<div class="flex flex-col justify-start gap-1">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<div class="flex min-w-0 items-center gap-2">
|
<div class="flex min-w-0 items-center gap-2">
|
||||||
{#if others.length === 1}
|
{#if others.length === 0}
|
||||||
|
<ProfileCircle pubkey={$pubkey} size={5} />
|
||||||
|
Note to self
|
||||||
|
{:else if others.length === 1}
|
||||||
<ProfileCircle pubkey={others[0]} size={5} />
|
<ProfileCircle pubkey={others[0]} size={5} />
|
||||||
<ProfileName pubkey={others[0]} />
|
<ProfileName pubkey={others[0]} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ChatStart from "@app/components/ChatStart.svelte"
|
||||||
|
import {setChecked} from "@app/notifications"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
|
||||||
|
|
||||||
|
const markAsRead = () => {
|
||||||
|
setChecked("/chat/*")
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="col-2">
|
||||||
|
<Button class="btn btn-primary" on:click={startChat}>
|
||||||
|
<Icon size={4} icon="add-circle" />
|
||||||
|
Start chat
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-neutral" on:click={markAsRead}>
|
||||||
|
<Icon size={4} icon="check-circle" />
|
||||||
|
Mark all read
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
@@ -112,7 +112,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
|
<span class="whitespace-nowrap text-xs opacity-50"
|
||||||
|
>{formatTimestampAsTime(event.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
mediaLength: hideMedia ? 20 : 200,
|
mediaLength: hideMedia ? 20 : 200,
|
||||||
})
|
})
|
||||||
|
|
||||||
$: hasEllipsis = shortContent.find(isEllipsis)
|
$: hasEllipsis = shortContent.some(isEllipsis)
|
||||||
$: expandInline = hasEllipsis && expandMode === "inline"
|
$: expandInline = hasEllipsis && expandMode === "inline"
|
||||||
$: expandBlock = hasEllipsis && expandMode === "block"
|
$: expandBlock = hasEllipsis && expandMode === "block"
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
|
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link external href={url} class="my-2 inline-block">
|
<Link external href={url} class="my-2 block">
|
||||||
<div class="overflow-hidden rounded-box leading-[0]">
|
<div class="overflow-hidden rounded-box leading-[0]">
|
||||||
{#if url.match(/\.(mov|webm|mp4)$/)}
|
{#if url.match(/\.(mov|webm|mp4)$/)}
|
||||||
<video controls src={url} class="max-h-96 object-contain object-center">
|
<video controls src={url} class="max-h-96 object-contain object-center">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {EditorContent} from "svelte-tiptap"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {randomId} from "@welshman/lib"
|
import {randomId} from "@welshman/lib"
|
||||||
import {createEvent, EVENT_TIME} from "@welshman/util"
|
import {createEvent, EVENT_TIME} from "@welshman/util"
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
||||||
import {PROTECTED} from "@app/state"
|
import {PROTECTED} from "@app/state"
|
||||||
import {getEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
@@ -54,16 +54,12 @@
|
|||||||
history.back()
|
history.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
let element: HTMLElement
|
const editor = makeEditor({submit, uploading})
|
||||||
let editor: ReturnType<typeof getEditor>
|
|
||||||
let title = ""
|
let title = ""
|
||||||
let location = ""
|
let location = ""
|
||||||
let start: Date
|
let start: Date
|
||||||
let end: Date
|
let end: Date
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
editor = getEditor({submit, element, uploading})
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" on:submit|preventDefault={submit}>
|
<form class="column gap-4" on:submit|preventDefault={submit}>
|
||||||
@@ -83,7 +79,7 @@
|
|||||||
slot="input"
|
slot="input"
|
||||||
class="relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100">
|
class="relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100">
|
||||||
<div class="input-editor flex-grow overflow-hidden">
|
<div class="input-editor flex-grow overflow-hidden">
|
||||||
<div bind:this={element} />
|
<EditorContent editor={$editor} />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<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 ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {publishReport} from "@app/commands"
|
||||||
|
|
||||||
|
export let url
|
||||||
|
export let event
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const confirm = async () => {
|
||||||
|
if (!reason) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please select a reason for your report.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
await publishReport({event, reason: reason.toLowerCase(), content, relays: [url]})
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
history.back()
|
||||||
|
|
||||||
|
return pushToast({message: "Your report has been sent!"})
|
||||||
|
}
|
||||||
|
|
||||||
|
let reason = ""
|
||||||
|
let content = ""
|
||||||
|
let loading = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" on:submit|preventDefault={confirm}>
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">Report Content</div>
|
||||||
|
<div slot="info">Flag inappropriate content.</div>
|
||||||
|
</ModalHeader>
|
||||||
|
<Field>
|
||||||
|
<p slot="label">Reason*</p>
|
||||||
|
<select slot="input" class="select select-bordered" bind:value={reason}>
|
||||||
|
<option disabled selected>Choose a reason</option>
|
||||||
|
<option>Nudity</option>
|
||||||
|
<option>Malware</option>
|
||||||
|
<option>Profanity</option>
|
||||||
|
<option>Illegal</option>
|
||||||
|
<option>Spam</option>
|
||||||
|
<option>Impersonation</option>
|
||||||
|
<option>Other</option>
|
||||||
|
</select>
|
||||||
|
<p slot="info">Please select a reason for your report.</p>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<p slot="label">Details</p>
|
||||||
|
<textarea slot="input" class="textarea textarea-bordered" bind:value={content} />
|
||||||
|
<p slot="info">Please provide any additional details relevant to your report.</p>
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" on:click={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Send Report</Spinner>
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {getTag, REPORT} from "@welshman/util"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {deriveEvents} from "@welshman/store"
|
||||||
|
import {pubkey, repository} from "@welshman/app"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
import {publishDelete} from "@app/commands"
|
||||||
|
|
||||||
|
export let url
|
||||||
|
export let event
|
||||||
|
|
||||||
|
const reports = deriveEvents(repository, {
|
||||||
|
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
||||||
|
})
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const deleteReport = (report: TrustedEvent) => {
|
||||||
|
publishDelete({event: report, relays: [url]})
|
||||||
|
|
||||||
|
if ($reports.length === 0) {
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getReason = (tags: string[][]) => getTag("e", tags)?.[2] || "other"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">Report Details</div>
|
||||||
|
<div slot="info">All reports for this event are shown below.</div>
|
||||||
|
</ModalHeader>
|
||||||
|
{#each $reports as report (report.id)}
|
||||||
|
{@const reason = getReason(report.tags)}
|
||||||
|
{@const remove = () => deleteReport(report)}
|
||||||
|
<div class="column gap-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<Profile pubkey={report.pubkey} />
|
||||||
|
<span>Reported this event as "{reason}"</span>
|
||||||
|
</div>
|
||||||
|
{#if report.pubkey === $pubkey}
|
||||||
|
<Button class="btn-default btn" on:click={remove}>Delete Report</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if report.content}
|
||||||
|
<p>"{report.content}"</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<Button class="btn btn-primary" on:click={back}>Got it</Button>
|
||||||
|
</div>
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Dialog from "@lib/components/Dialog.svelte"
|
import Dialog from "@lib/components/Dialog.svelte"
|
||||||
import CardButton from "@lib/components/CardButton.svelte"
|
import CardButton from "@lib/components/CardButton.svelte"
|
||||||
import LogIn from "@app/components/LogIn.svelte"
|
import LogIn from "@app/components/LogIn.svelte"
|
||||||
import SignUp from "@app/components/SignUp.svelte"
|
import SignUp from "@app/components/SignUp.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME} from "@app/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
const logIn = () => pushModal(LogIn)
|
const logIn = () => pushModal(LogIn)
|
||||||
@@ -33,5 +34,10 @@
|
|||||||
<div slot="info">Just a few questions and you'll be on your way.</div>
|
<div slot="info">Just a few questions and you'll be on your way.</div>
|
||||||
</CardButton>
|
</CardButton>
|
||||||
</Button>
|
</Button>
|
||||||
|
<p class="text-center text-xs opacity-75">
|
||||||
|
By using {PLATFORM_NAME}, you consent to our
|
||||||
|
<Link external class="link" href={PLATFORM_TERMS}>Terms of Service</Link> and
|
||||||
|
<Link external class="link" href={PLATFORM_PRIVACY}>Privacy Policy</Link>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount, onDestroy} from "svelte"
|
import {onMount, onDestroy} from "svelte"
|
||||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
import {Nip46Broker, getPubkey, makeSecret} from "@welshman/signer"
|
||||||
import {addSession} from "@welshman/app"
|
import {addSession} from "@welshman/app"
|
||||||
import {slideAndFade} from "@lib/transition"
|
import {slideAndFade} from "@lib/transition"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
const {signerPubkey, connectSecret, relays} = broker.parseBunkerUrl(input)
|
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(input)
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return
|
return
|
||||||
@@ -63,6 +63,15 @@
|
|||||||
let input = ""
|
let input = ""
|
||||||
let loading = false
|
let loading = false
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// For testing and for play store reviewers
|
||||||
|
if (input === "reviewkey") {
|
||||||
|
const secret = makeSecret()
|
||||||
|
|
||||||
|
addSession({method: "nip01", secret, pubkey: getPubkey(secret)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
url = await broker.makeNostrconnectUrl({
|
url = await broker.makeNostrconnectUrl({
|
||||||
perms: NIP46_PERMS,
|
perms: NIP46_PERMS,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<SecondaryNavSection class="max-h-screen">
|
<SecondaryNavSection class="max-h-screen">
|
||||||
<div>
|
<div>
|
||||||
<SecondaryNavItem class="w-full !justify-between" on:click={openMenu}>
|
<SecondaryNavItem class="w-full !justify-between" on:click={openMenu}>
|
||||||
<strong>{displayRelayUrl(url)}</strong>
|
<strong class="ellipsize">{displayRelayUrl(url)}</strong>
|
||||||
<Icon icon="alt-arrow-down" />
|
<Icon icon="alt-arrow-down" />
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{#if showMenu}
|
{#if showMenu}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<NoteCard {event} class="card2 bg-alt">
|
<NoteCard {event} class="card2 bg-alt">
|
||||||
<Content {event} expandMode="inline" />
|
<Content {event} expandMode="inline" />
|
||||||
<div class="flex w-full justify-between gap-2">
|
<div class="flex w-full justify-between gap-2">
|
||||||
<ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-right">
|
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right">
|
||||||
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
|
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
|
||||||
<Icon icon="smile-circle" size={4} />
|
<Icon icon="smile-circle" size={4} />
|
||||||
</EmojiButton>
|
</EmojiButton>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {first, sortBy, ctx} from "@welshman/lib"
|
import {ctx} from "@welshman/lib"
|
||||||
import {getAncestorTags} from "@welshman/util"
|
|
||||||
import type {Filter} from "@welshman/util"
|
import type {Filter} from "@welshman/util"
|
||||||
import {deriveEvents} from "@welshman/store"
|
import {deriveEvents} from "@welshman/store"
|
||||||
import {repository, load, loadRelaySelections, formatTimestampRelative} from "@welshman/app"
|
import {repository, load, loadRelaySelections, formatTimestampRelative} from "@welshman/app"
|
||||||
@@ -13,11 +12,9 @@
|
|||||||
|
|
||||||
export let pubkey
|
export let pubkey
|
||||||
|
|
||||||
const filters: Filter[] = [{authors: [pubkey]}]
|
const filters: Filter[] = [{authors: [pubkey], limit: 1}]
|
||||||
const events = deriveEvents(repository, {filters})
|
const events = deriveEvents(repository, {filters})
|
||||||
|
|
||||||
$: roots = $events.filter(e => getAncestorTags(e.tags).replies.length === 0)
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Make sure we have their relay selections before we load their posts
|
// Make sure we have their relay selections before we load their posts
|
||||||
await loadRelaySelections(pubkey)
|
await loadRelaySelections(pubkey)
|
||||||
@@ -39,10 +36,9 @@
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<ProfileInfo {pubkey} />
|
<ProfileInfo {pubkey} />
|
||||||
{#if roots.length > 0}
|
{#if $events.length > 0}
|
||||||
{@const event = first(sortBy(e => -e.created_at, roots))}
|
|
||||||
<div class="bg-alt badge badge-neutral border-none">
|
<div class="bg-alt badge badge-neutral border-none">
|
||||||
Last active {formatTimestampRelative(event.created_at)}
|
Last active {formatTimestampRelative($events[0].created_at)}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Link class="btn btn-primary sm:hidden" href={makeChatPath([pubkey])}>
|
<Link class="btn btn-primary sm:hidden" href={makeChatPath([pubkey])}>
|
||||||
|
|||||||
@@ -21,8 +21,6 @@
|
|||||||
|
|
||||||
const showSettingsMenu = () => pushModal(MenuSettings)
|
const showSettingsMenu = () => pushModal(MenuSettings)
|
||||||
|
|
||||||
const openNotes = () => ($canDecrypt ? goto("/notes") : pushModal(ChatEnable, {next: "/notes"}))
|
|
||||||
|
|
||||||
const openChat = () => ($canDecrypt ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
|
const openChat = () => ($canDecrypt ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
|
||||||
|
|
||||||
$: spaceUrls = Array.from($userRoomsByUrl.keys())
|
$: spaceUrls = Array.from($userRoomsByUrl.keys())
|
||||||
@@ -58,9 +56,6 @@
|
|||||||
class="tooltip-right">
|
class="tooltip-right">
|
||||||
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
|
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem title="Notes" on:click={openNotes} class="tooltip-right">
|
|
||||||
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
|
|
||||||
</PrimaryNavItem>
|
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
title="Messages"
|
title="Messages"
|
||||||
on:click={openChat}
|
on:click={openChat}
|
||||||
@@ -81,11 +76,8 @@
|
|||||||
class="border-top fixed bottom-0 left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
class="border-top fixed bottom-0 left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
||||||
<div class="content-padding-x content-sizing flex justify-between px-2">
|
<div class="content-padding-x content-sizing flex justify-between px-2">
|
||||||
<div class="flex gap-2 sm:gap-8">
|
<div class="flex gap-2 sm:gap-8">
|
||||||
<PrimaryNavItem title="Search" href="/people">
|
<PrimaryNavItem title="Home" href="/home">
|
||||||
<Avatar icon="magnifer" class="!h-10 !w-10" />
|
<Avatar icon="home-smile" class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
|
||||||
<PrimaryNavItem title="Notes" on:click={openNotes}>
|
|
||||||
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
|
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
title="Messages"
|
title="Messages"
|
||||||
@@ -98,7 +90,7 @@
|
|||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
<PrimaryNavItem title="Settings" on:click={showSettingsMenu}>
|
<PrimaryNavItem title="Settings" on:click={showSettingsMenu}>
|
||||||
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
|
<Avatar icon="settings" src={$userProfile?.picture} class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {sortBy, uniqBy} from "@welshman/lib"
|
import {sortBy, uniqBy} from "@welshman/lib"
|
||||||
import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
||||||
import {NOTE, getAncestorTags} from "@welshman/util"
|
import {NOTE, getReplyTags} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {createFeedController} from "@welshman/app"
|
import {createFeedController} from "@welshman/app"
|
||||||
import {createScroller} from "@lib/html"
|
import {createScroller} from "@lib/html"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
feedFromFilter({kinds: [NOTE], authors: [pubkey]}),
|
feedFromFilter({kinds: [NOTE], authors: [pubkey]}),
|
||||||
),
|
),
|
||||||
onEvent: (event: TrustedEvent) => {
|
onEvent: (event: TrustedEvent) => {
|
||||||
if (getAncestorTags(event.tags).replies.length === 0) {
|
if (getReplyTags(event.tags).replies.length === 0) {
|
||||||
buffer.push(event)
|
buffer.push(event)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {SvelteComponent} from "svelte"
|
import type {SvelteComponent} from "svelte"
|
||||||
|
import {derived} from "svelte/store"
|
||||||
import {type Instance} from "tippy.js"
|
import {type Instance} from "tippy.js"
|
||||||
import {append, remove, uniq} from "@welshman/lib"
|
import {append, remove, uniq} from "@welshman/lib"
|
||||||
import {profileSearch} from "@welshman/app"
|
import {profileSearch} from "@welshman/app"
|
||||||
@@ -20,6 +21,8 @@
|
|||||||
let popover: Instance
|
let popover: Instance
|
||||||
let instance: SvelteComponent
|
let instance: SvelteComponent
|
||||||
|
|
||||||
|
const search = derived(profileSearch, $profileSearch => $profileSearch.searchValues)
|
||||||
|
|
||||||
const selectPubkey = (pubkey: string) => {
|
const selectPubkey = (pubkey: string) => {
|
||||||
term = ""
|
term = ""
|
||||||
popover.hide()
|
popover.hide()
|
||||||
@@ -76,8 +79,8 @@
|
|||||||
component={Suggestions}
|
component={Suggestions}
|
||||||
props={{
|
props={{
|
||||||
term,
|
term,
|
||||||
|
search,
|
||||||
select: selectPubkey,
|
select: selectPubkey,
|
||||||
search: profileSearch,
|
|
||||||
component: ProfileSuggestion,
|
component: ProfileSuggestion,
|
||||||
class: "rounded-box",
|
class: "rounded-box",
|
||||||
style: `left: 4px; width: ${input?.clientWidth + 12}px`,
|
style: `left: 4px; width: ${input?.clientWidth + 12}px`,
|
||||||
|
|||||||
@@ -1,45 +1,69 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {groupBy, uniqBy, batch} from "@welshman/lib"
|
import {groupBy, uniq, uniqBy, batch} from "@welshman/lib"
|
||||||
import {REACTION, DELETE} from "@welshman/util"
|
import {REACTION, getTag, REPORT, DELETE} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {deriveEvents} from "@welshman/store"
|
import {deriveEvents} from "@welshman/store"
|
||||||
import {pubkey, repository, load, displayProfileByPubkey} from "@welshman/app"
|
import {pubkey, repository, load, displayProfileByPubkey} from "@welshman/app"
|
||||||
import {displayList} from "@lib/util"
|
import {displayList} from "@lib/util"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import EventReportDetails from "@app/components/EventReportDetails.svelte"
|
||||||
import {displayReaction} from "@app/state"
|
import {displayReaction} from "@app/state"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let event
|
export let event
|
||||||
export let onReactionClick
|
export let onReactionClick
|
||||||
export let relays: string[] = []
|
export let url = ""
|
||||||
export let reactionClass = ""
|
export let reactionClass = ""
|
||||||
export let noTooltip = false
|
export let noTooltip = false
|
||||||
|
|
||||||
|
const reports = deriveEvents(repository, {
|
||||||
|
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
||||||
|
})
|
||||||
|
|
||||||
const reactions = deriveEvents(repository, {
|
const reactions = deriveEvents(repository, {
|
||||||
filters: [{kinds: [REACTION], "#e": [event.id]}],
|
filters: [{kinds: [REACTION], "#e": [event.id]}],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const onReportClick = () => pushModal(EventReportDetails, {url, event})
|
||||||
|
|
||||||
|
$: reportReasons = uniq($reports.map(e => getTag("e", e.tags)?.[2]))
|
||||||
|
|
||||||
$: groupedReactions = groupBy(
|
$: groupedReactions = groupBy(
|
||||||
e => e.content,
|
e => e.content,
|
||||||
uniqBy(e => e.pubkey + e.content, $reactions),
|
uniqBy(e => e.pubkey + e.content, $reactions),
|
||||||
)
|
)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
load({
|
if (url) {
|
||||||
relays,
|
load({
|
||||||
filters: [{kinds: [REACTION, DELETE], "#e": [event.id]}],
|
relays: [url],
|
||||||
onEvent: batch(300, (events: TrustedEvent[]) => {
|
filters: [{kinds: [REACTION, REPORT, DELETE], "#e": [event.id]}],
|
||||||
load({
|
onEvent: batch(300, (events: TrustedEvent[]) => {
|
||||||
relays,
|
load({
|
||||||
filters: [{kinds: [DELETE], "#e": events.map(e => e.id)}],
|
relays: [url],
|
||||||
})
|
filters: [{kinds: [DELETE], "#e": events.map(e => e.id)}],
|
||||||
}),
|
})
|
||||||
})
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $reactions.length > 0}
|
{#if $reactions.length > 0 || $reports.length > 0}
|
||||||
<div class="flex min-w-0 flex-wrap gap-2">
|
<div class="flex min-w-0 flex-wrap gap-2">
|
||||||
|
{#if url && $reports.length > 0}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-tip="{`This content has been reported as "${displayList(reportReasons)}".`}}"
|
||||||
|
class="btn btn-error btn-xs tooltip-right flex items-center gap-1 rounded-full"
|
||||||
|
class:tooltip={!noTooltip && !isMobile}
|
||||||
|
on:click|preventDefault|stopPropagation={onReportClick}>
|
||||||
|
<Icon icon="danger" />
|
||||||
|
<span>{$reports.length}</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{#each groupedReactions.entries() as [content, events]}
|
{#each groupedReactions.entries() as [content, events]}
|
||||||
{@const pubkeys = events.map(e => e.pubkey)}
|
{@const pubkeys = events.map(e => e.pubkey)}
|
||||||
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||||
<ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-left" />
|
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-left" />
|
||||||
{#if $deleted}
|
{#if $deleted}
|
||||||
<div class="btn btn-error btn-xs rounded-full">Deleted</div>
|
<div class="btn btn-error btn-xs rounded-full">Deleted</div>
|
||||||
{:else if thunk}
|
{:else if thunk}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
|
import {EditorContent} from "svelte-tiptap"
|
||||||
import {createEvent, THREAD} from "@welshman/util"
|
import {createEvent, THREAD} from "@welshman/util"
|
||||||
import {publishThunk} from "@welshman/app"
|
import {publishThunk} from "@welshman/app"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {GENERAL, tagRoom, PROTECTED} from "@app/state"
|
import {GENERAL, tagRoom, PROTECTED} from "@app/state"
|
||||||
import {getEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
@@ -53,13 +53,9 @@
|
|||||||
history.back()
|
history.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
let title: string
|
const editor = makeEditor({submit, uploading, placeholder: "What's on your mind?"})
|
||||||
let element: HTMLElement
|
|
||||||
let editor: ReturnType<typeof getEditor>
|
|
||||||
|
|
||||||
onMount(() => {
|
let title: string
|
||||||
editor = getEditor({submit, element, uploading, placeholder: "What's on your mind?"})
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" on:submit|preventDefault={submit}>
|
<form class="column gap-4" on:submit|preventDefault={submit}>
|
||||||
@@ -83,7 +79,7 @@
|
|||||||
<Field>
|
<Field>
|
||||||
<p slot="label">Message*</p>
|
<p slot="label">Message*</p>
|
||||||
<div slot="input" class="note-editor flex-grow overflow-hidden">
|
<div slot="input" class="note-editor flex-grow overflow-hidden">
|
||||||
<div bind:this={element} />
|
<EditorContent editor={$editor} />
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
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 EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
|
import EventReport from "@app/components/EventReport.svelte"
|
||||||
import ThreadShare from "@app/components/ThreadShare.svelte"
|
import ThreadShare from "@app/components/ThreadShare.svelte"
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
@@ -14,6 +15,11 @@
|
|||||||
|
|
||||||
const isRoot = event.kind !== COMMENT
|
const isRoot = event.kind !== COMMENT
|
||||||
|
|
||||||
|
const report = () => {
|
||||||
|
onClick()
|
||||||
|
pushModal(EventReport, {url, event})
|
||||||
|
}
|
||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(EventInfo, {event})
|
pushModal(EventInfo, {event})
|
||||||
@@ -52,5 +58,12 @@
|
|||||||
Delete Message
|
Delete Message
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" on:click={report}>
|
||||||
|
<Icon size={4} icon="danger" />
|
||||||
|
Report Content
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
|
import {EditorContent} from "svelte-tiptap"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import {fly, slideAndFade} from "@lib/transition"
|
import {fly, slideAndFade} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {publishComment} from "@app/commands"
|
import {publishComment} from "@app/commands"
|
||||||
import {tagRoom, GENERAL, PROTECTED} from "@app/state"
|
import {tagRoom, GENERAL, PROTECTED} from "@app/state"
|
||||||
import {getEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
@@ -34,12 +34,7 @@
|
|||||||
onSubmit(publishComment({event, content, tags, relays: [url]}))
|
onSubmit(publishComment({event, content, tags, relays: [url]}))
|
||||||
}
|
}
|
||||||
|
|
||||||
let editor: ReturnType<typeof getEditor>
|
const editor = makeEditor({submit, uploading, autofocus: !isMobile})
|
||||||
let element: HTMLElement
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
editor = getEditor({element, submit, uploading, autofocus: !isMobile})
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
@@ -49,7 +44,7 @@
|
|||||||
class="card2 sticky bottom-2 z-feature mx-2 mt-4 bg-neutral">
|
class="card2 sticky bottom-2 z-feature mx-2 mt-4 bg-neutral">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="note-editor flex-grow overflow-hidden">
|
<div class="note-editor flex-grow overflow-hidden">
|
||||||
<div bind:this={element} />
|
<EditorContent editor={$editor} />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
|
|||||||
@@ -42,9 +42,9 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-end px-1 text-xs {$$props.class}">
|
{#if isFailure && failure}
|
||||||
{#if isFailure && failure}
|
{@const [url, {message, status}] = failure}
|
||||||
{@const [url, {message, status}] = failure}
|
<div class="flex justify-end px-1 text-xs {$$props.class}">
|
||||||
<Tippy
|
<Tippy
|
||||||
class="flex items-center {$$props.class}"
|
class="flex items-center {$$props.class}"
|
||||||
component={ThunkStatusDetail}
|
component={ThunkStatusDetail}
|
||||||
@@ -55,7 +55,9 @@
|
|||||||
<span>Failed to send!</span>
|
<span>Failed to send!</span>
|
||||||
</span>
|
</span>
|
||||||
</Tippy>
|
</Tippy>
|
||||||
{:else if canCancel || isPending}
|
</div>
|
||||||
|
{:else if canCancel || isPending}
|
||||||
|
<div class="flex justify-end px-1 text-xs {$$props.class}">
|
||||||
<span class="flex items-center gap-1 {$$props.class}">
|
<span class="flex items-center gap-1 {$$props.class}">
|
||||||
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
|
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
|
||||||
<span class="opacity-50">Sending...</span>
|
<span class="opacity-50">Sending...</span>
|
||||||
@@ -63,5 +65,5 @@
|
|||||||
<Button class="link" on:click={abort}>Cancel</Button>
|
<Button class="link" on:click={abort}>Cancel</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|||||||
@@ -25,12 +25,11 @@ export const signWithAssert = async (template: StampedEvent) => {
|
|||||||
return event!
|
return event!
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getEditor = ({
|
export const makeEditor = ({
|
||||||
aggressive = false,
|
aggressive = false,
|
||||||
autofocus = false,
|
autofocus = false,
|
||||||
charCount,
|
charCount,
|
||||||
content = "",
|
content = "",
|
||||||
element,
|
|
||||||
placeholder = "",
|
placeholder = "",
|
||||||
submit,
|
submit,
|
||||||
uploading,
|
uploading,
|
||||||
@@ -40,14 +39,12 @@ export const getEditor = ({
|
|||||||
autofocus?: boolean
|
autofocus?: boolean
|
||||||
charCount?: Writable<number>
|
charCount?: Writable<number>
|
||||||
content?: string
|
content?: string
|
||||||
element: HTMLElement
|
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
submit: () => void
|
submit: () => void
|
||||||
uploading?: Writable<boolean>
|
uploading?: Writable<boolean>
|
||||||
wordCount?: Writable<number>
|
wordCount?: Writable<number>
|
||||||
}) =>
|
}) =>
|
||||||
createEditor({
|
createEditor({
|
||||||
element,
|
|
||||||
content,
|
content,
|
||||||
autofocus,
|
autofocus,
|
||||||
extensions: [
|
extensions: [
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {synced, throttled} from "@welshman/store"
|
import {synced, throttled} from "@welshman/store"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
import {prop, identity, now} from "@welshman/lib"
|
import {prop, spec, identity, now, groupBy} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {MESSAGE} from "@welshman/util"
|
import {MESSAGE, THREAD, COMMENT, getTagValue} from "@welshman/util"
|
||||||
import {makeSpacePath, makeChatPath, makeThreadPath, makeRoomPath} from "@app/routes"
|
import {makeSpacePath, makeChatPath, makeThreadPath, makeRoomPath} from "@app/routes"
|
||||||
import {
|
import {chats, getUrlsForEvent, userRoomsByUrl, repositoryStore} from "@app/state"
|
||||||
THREAD_FILTER,
|
|
||||||
COMMENT_FILTER,
|
|
||||||
chats,
|
|
||||||
getUrlsForEvent,
|
|
||||||
userRoomsByUrl,
|
|
||||||
repositoryStore,
|
|
||||||
} from "@app/state"
|
|
||||||
|
|
||||||
// Checked state
|
// Checked state
|
||||||
|
|
||||||
@@ -36,7 +29,10 @@ export const notifications = derived(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [entryPath, ts] of Object.entries($checked)) {
|
for (const [entryPath, ts] of Object.entries($checked)) {
|
||||||
const isMatch = entryPath === "*" || entryPath.startsWith(path)
|
const isMatch =
|
||||||
|
entryPath === "*" ||
|
||||||
|
entryPath.startsWith(path) ||
|
||||||
|
(entryPath === "/chat/*" && path.startsWith("/chat/"))
|
||||||
|
|
||||||
if (isMatch && ts > latestEvent.created_at) {
|
if (isMatch && ts > latestEvent.created_at) {
|
||||||
return false
|
return false
|
||||||
@@ -57,19 +53,35 @@ export const notifications = derived(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allThreadEvents = $repository.query([THREAD_FILTER, COMMENT_FILTER])
|
const allThreadEvents = $repository.query([
|
||||||
|
{kinds: [THREAD]},
|
||||||
|
{kinds: [COMMENT], "#K": [String(THREAD)]},
|
||||||
|
])
|
||||||
const allMessageEvents = $repository.query([{kinds: [MESSAGE]}])
|
const allMessageEvents = $repository.query([{kinds: [MESSAGE]}])
|
||||||
|
|
||||||
for (const [url, rooms] of $userRoomsByUrl.entries()) {
|
for (const [url, rooms] of $userRoomsByUrl.entries()) {
|
||||||
const spacePath = makeSpacePath(url)
|
const spacePath = makeSpacePath(url)
|
||||||
const threadPath = makeThreadPath(url)
|
const threadPath = makeThreadPath(url)
|
||||||
const latestEvent = allThreadEvents.find(e => $getUrlsForEvent(e.id).includes(url))
|
const threadEvents = allThreadEvents.filter(e => $getUrlsForEvent(e.id).includes(url))
|
||||||
|
|
||||||
if (hasNotification(threadPath, latestEvent)) {
|
if (hasNotification(threadPath, threadEvents[0])) {
|
||||||
paths.add(spacePath)
|
paths.add(spacePath)
|
||||||
paths.add(threadPath)
|
paths.add(threadPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commentsByThreadId = groupBy(
|
||||||
|
e => getTagValue("E", e.tags),
|
||||||
|
threadEvents.filter(spec({kind: COMMENT})),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const [threadId, [comment]] of commentsByThreadId.entries()) {
|
||||||
|
const threadItemPath = makeThreadPath(url, threadId)
|
||||||
|
|
||||||
|
if (hasNotification(threadItemPath, comment)) {
|
||||||
|
paths.add(threadItemPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const room of rooms) {
|
for (const room of rooms) {
|
||||||
const roomPath = makeRoomPath(url, room)
|
const roomPath = makeRoomPath(url, room)
|
||||||
const latestEvent = allMessageEvents.find(
|
const latestEvent = allMessageEvents.find(
|
||||||
|
|||||||