Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75905e4652 | |||
| d07b9cde5f | |||
| d8a9cc5a7e | |||
| 863d11352f | |||
| b4cc770cdf | |||
| 901e56a625 | |||
| 479fed34f7 | |||
| 81d7b08aed | |||
| a582b1ea73 | |||
| 1c0b2a09df | |||
| 3a42a1b560 | |||
| db203bf00d | |||
| ffb36af734 | |||
| b399fa8dcc | |||
| 5bba5959f7 | |||
| 2ad65e394e | |||
| 345b20bf5d | |||
| b9fb251b32 | |||
| dd9a9c0df2 | |||
| 115b5f9fbe | |||
| 3ad7dcfeb4 | |||
| 60d107aed2 | |||
| 08d8d45ecb | |||
| c40e8ce1a7 | |||
| 993bf8d2e6 | |||
| c3c65c3970 | |||
| a5b868cd56 | |||
| 8fcc56a408 | |||
| c8dfbc936b | |||
| f1e76a1ed1 | |||
| 6ecc3e6770 | |||
| b05c408977 | |||
| e484c3cb00 | |||
| 69d0e11ba4 | |||
| 27d9d4fff1 | |||
| c089812363 | |||
| 07dd1e97dc | |||
| 7f6a1bff34 | |||
| 7d1310722a | |||
| cb57710654 | |||
| c74c116667 | |||
| 0ba55f2387 | |||
| 622214713b | |||
| d8cf48381b | |||
| 7dc7b5abeb | |||
| 324db6a9e8 | |||
| 466541caf5 | |||
| 19f657e348 | |||
| 98a0511b34 | |||
| 0ec620dff9 | |||
| 1301c2c74f | |||
| 7848859153 | |||
| 2d67a9bcf6 | |||
| a7e9318819 | |||
| d66371d573 | |||
| 5684d1a9cf | |||
| fa4bc6894f | |||
| 72919cb1c2 | |||
| 6a3a02bc34 | |||
| db69c56f57 | |||
| a0c6e46184 | |||
| 65aabf5feb | |||
| 131cc99c47 | |||
| 5909b593ab | |||
| f0b2b7c8b3 | |||
| 24a7fa4174 | |||
| 3f2813b63b | |||
| 3e214881a3 | |||
| af171bd2c9 | |||
| 565ccb399a | |||
| fd99866b1e | |||
| 506276f594 | |||
| d4df23545d | |||
| e53d2eb8da | |||
| 22cbb9fe1c | |||
| fedc99b0f0 | |||
| 7d4ba6c806 | |||
| a0e97d5e5b | |||
| 24045a7e2a | |||
| 8d3433b167 | |||
| 0f705c459a | |||
| 08ee07d157 | |||
| cfbff94b4c | |||
| 34477e8ea6 | |||
| eab0ea4eef | |||
| 8ec4d9c548 | |||
| 9defe20f91 | |||
| 614cdcdf53 | |||
| bcd94ee75e | |||
| def6de321c | |||
| 6c7c533637 | |||
| f0207b35d0 | |||
| 858e04d7fa | |||
| b7dcb77378 | |||
| cee6c3c164 | |||
| 06d0ae2798 | |||
| b129ef4242 | |||
| 48a45f3a3a | |||
| ce1fb396e3 | |||
| e95c57bcb7 | |||
| 414f5a5ace | |||
| a331d24bb1 | |||
| fb53e53411 |
@@ -1,3 +1,8 @@
|
|||||||
--ignore-dir=.svelte-kit
|
--ignore-dir=.svelte-kit
|
||||||
--ignore-dir=android
|
--ignore-dir=android
|
||||||
--ignore-dir=build
|
--ignore-dir=build
|
||||||
|
--ignore-dir=ios/DerivedData
|
||||||
|
--ignore-dir=ios/App/App/public
|
||||||
|
--ignore-file=match:.svg
|
||||||
|
--ignore-file=match:package-lock.json
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
VITE_DEFAULT_PUBKEYS=fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
|
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||||
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_TERMS=https://flotilla.social/terms
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ build
|
|||||||
gradlew*
|
gradlew*
|
||||||
_app
|
_app
|
||||||
release
|
release
|
||||||
|
ios/DerivedData/
|
||||||
|
ios/App/Pods/
|
||||||
android/capacitor-cordova-android-plugins
|
android/capacitor-cordova-android-plugins
|
||||||
android/app/src/androidTest
|
android/app/src/androidTest
|
||||||
android/app/src/test
|
android/app/src/test
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
node_modules
|
|
||||||
|
|
||||||
# Output
|
|
||||||
.output
|
|
||||||
.vercel
|
|
||||||
/.svelte-kit
|
|
||||||
/build
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Env
|
# Env
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
@@ -17,9 +5,6 @@ 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
|
||||||
@@ -29,3 +14,55 @@ static/apple-touch-icon-180x180.png
|
|||||||
static/maskable-icon-512x512.png
|
static/maskable-icon-512x512.png
|
||||||
src/assets/icons/*.webp
|
src/assets/icons/*.webp
|
||||||
manifest.webmanifest
|
manifest.webmanifest
|
||||||
|
|
||||||
|
# Capacitor
|
||||||
|
ios/App/public/
|
||||||
|
ios/App/Pods/
|
||||||
|
ios/App/Podfile.lock
|
||||||
|
ios/DerivedData/
|
||||||
|
android/app/src/main/assets/public/
|
||||||
|
|
||||||
|
# Web/JavaScript
|
||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
.svelte-kit/
|
||||||
|
|
||||||
|
# iOS
|
||||||
|
ios/App/App/public
|
||||||
|
ios/DerivedData
|
||||||
|
xcuserdata/
|
||||||
|
*.xcworkspace/
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
*.pbxuser
|
||||||
|
*.xccheckout
|
||||||
|
*.moved-aside
|
||||||
|
*.hmap
|
||||||
|
*.ipa
|
||||||
|
*.xcuserstate
|
||||||
|
*.dSYM.zip
|
||||||
|
*.dSYM
|
||||||
|
|
||||||
|
# Android
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
*.ap_
|
||||||
|
*.dex
|
||||||
|
*.class
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
out/
|
||||||
|
.gradle/
|
||||||
|
local.properties
|
||||||
|
proguard/
|
||||||
|
google-services.json
|
||||||
|
GoogleService-Info.plist
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# OS generated
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|||||||
@@ -1,5 +1,62 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 1.0.0
|
||||||
|
|
||||||
|
* Add alerts via Anchor
|
||||||
|
|
||||||
|
# 0.2.12
|
||||||
|
|
||||||
|
* Fix keyboard covering chat input
|
||||||
|
* Fix thread replies
|
||||||
|
* Make error reporting and analytics optional
|
||||||
|
* Replace long press with tap target
|
||||||
|
* Fix time input
|
||||||
|
* Fix nevent hints for url-specific stuff
|
||||||
|
* Fix confirm and reactions on mobile
|
||||||
|
* Add reply to chat on mobile
|
||||||
|
* Fix profile suggestions
|
||||||
|
|
||||||
|
# 0.2.11
|
||||||
|
|
||||||
|
* Add in-app signup flow on ios
|
||||||
|
* Add profile deletion
|
||||||
|
|
||||||
|
# 0.2.10
|
||||||
|
|
||||||
|
* Improve space discovery
|
||||||
|
|
||||||
|
# 0.2.9
|
||||||
|
|
||||||
|
* Add NIP 01 signup flow on mobile
|
||||||
|
|
||||||
|
# 0.2.8
|
||||||
|
|
||||||
|
* Show spinner when joining a room
|
||||||
|
* Reduce self-rate limiting of REQs
|
||||||
|
* Fix disabled signers link
|
||||||
|
* Prepare for iOS release
|
||||||
|
* Improve threads and calendar pages
|
||||||
|
* Improve quote rendering and new messages button
|
||||||
|
|
||||||
|
# 0.2.7
|
||||||
|
|
||||||
|
* Add calendar events
|
||||||
|
* Migrate to svelte 5 (fixes some bugs, probably introduces others)
|
||||||
|
* Migrate to new welshman editor
|
||||||
|
* Make reply indicator nicer
|
||||||
|
* Make share indicator nicer
|
||||||
|
* Improve feed loading
|
||||||
|
* Show marker for last activity in chat
|
||||||
|
|
||||||
|
# 0.2.6
|
||||||
|
|
||||||
|
* Add reply to long-press menu
|
||||||
|
* Fix @-mentions
|
||||||
|
* Replace nsec.app signup with njump.me
|
||||||
|
* Add new messages button in rooms
|
||||||
|
* Add media server settings
|
||||||
|
* Add build hash to about page
|
||||||
|
|
||||||
# 0.2.5
|
# 0.2.5
|
||||||
|
|
||||||
* Improve room and data loading
|
* Improve room and data loading
|
||||||
|
|||||||
@@ -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 5
|
versionCode 13
|
||||||
versionName "0.2.5"
|
versionName "0.2.13"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(':capacitor-app')
|
||||||
|
implementation project(':capacitor-keyboard')
|
||||||
implementation project(':nostr-signer-capacitor-plugin')
|
implementation project(':nostr-signer-capacitor-plugin')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
<activity
|
<activity
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/title_activity_main"
|
android:label="@string/title_activity_main"
|
||||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.8.0'
|
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.2'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
@@ -5,5 +5,8 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
|
|||||||
include ':capacitor-app'
|
include ':capacitor-app'
|
||||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||||
|
|
||||||
|
include ':capacitor-keyboard'
|
||||||
|
project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/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.10.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
@@ -55,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@@ -83,7 +85,9 @@ done
|
|||||||
# This is normally unused
|
# This is normally unused
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
|
' "$PWD" ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
@@ -201,11 +205,11 @@ fi
|
|||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
# Collect all arguments for the java command:
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
# and any embedded shellness will be escaped.
|
||||||
# double quotes to make sure that they get re-expanded; and
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
|||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
ext {
|
ext {
|
||||||
minSdkVersion = 22
|
minSdkVersion = 23
|
||||||
compileSdkVersion = 34
|
compileSdkVersion = 35
|
||||||
targetSdkVersion = 34
|
targetSdkVersion = 35
|
||||||
androidxActivityVersion = '1.8.0'
|
androidxActivityVersion = '1.9.2'
|
||||||
androidxAppCompatVersion = '1.6.1'
|
androidxAppCompatVersion = '1.7.0'
|
||||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||||
androidxCoreVersion = '1.12.0'
|
androidxCoreVersion = '1.15.0'
|
||||||
androidxFragmentVersion = '1.6.2'
|
androidxFragmentVersion = '1.8.4'
|
||||||
coreSplashScreenVersion = '1.0.1'
|
coreSplashScreenVersion = '1.0.1'
|
||||||
androidxWebkitVersion = '1.9.0'
|
androidxWebkitVersion = '1.12.1'
|
||||||
junitVersion = '4.13.2'
|
junitVersion = '4.13.2'
|
||||||
androidxJunitVersion = '1.1.5'
|
androidxJunitVersion = '1.2.1'
|
||||||
androidxEspressoCoreVersion = '3.5.1'
|
androidxEspressoCoreVersion = '3.6.1'
|
||||||
cordovaAndroidVersion = '10.1.1'
|
cordovaAndroidVersion = '10.1.1'
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,10 @@ fi
|
|||||||
# https://stackoverflow.com/a/69127685/1467342
|
# https://stackoverflow.com/a/69127685/1467342
|
||||||
eval "$temp_env"
|
eval "$temp_env"
|
||||||
|
|
||||||
|
if [[ -z $VITE_BUILD_HASH ]]; then
|
||||||
|
export VITE_BUILD_HASH=$(git rev-parse --short HEAD)
|
||||||
|
fi
|
||||||
|
|
||||||
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
|
||||||
export VITE_PLATFORM_LOGO=static/logo.png
|
export VITE_PLATFORM_LOGO=static/logo.png
|
||||||
|
|||||||
@@ -10,8 +10,17 @@ const config: CapacitorConfig = {
|
|||||||
plugins: {
|
plugins: {
|
||||||
SplashScreen: {
|
SplashScreen: {
|
||||||
androidSplashResourceName: "splash"
|
androidSplashResourceName: "splash"
|
||||||
}
|
},
|
||||||
}
|
Keyboard: {
|
||||||
|
style: "DARK",
|
||||||
|
resizeOnFullScreen: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
|
||||||
|
// server: {
|
||||||
|
// url: "http://192.168.1.250:1847",
|
||||||
|
// cleartext: true
|
||||||
|
// },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
App/build
|
||||||
|
App/Pods
|
||||||
|
App/output
|
||||||
|
App/App/public
|
||||||
|
DerivedData
|
||||||
|
xcuserdata
|
||||||
|
|
||||||
|
# Cordova plugins for Capacitor
|
||||||
|
capacitor-cordova-ios-plugins
|
||||||
|
|
||||||
|
# Generated Config files
|
||||||
|
App/App/capacitor.config.json
|
||||||
|
App/App/config.xml
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 48;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||||
|
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||||
|
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||||
|
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||||
|
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||||
|
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||||
|
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||||
|
AC8D5382B9575A9124613C5D /* Pods_Flotilla_Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA42C7E3CF3FFF7A17A3A729 /* Pods_Flotilla_Chat.framework */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||||
|
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||||
|
504EC3041FED79650016851F /* Flotilla Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Flotilla Chat.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||||
|
7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
DA42C7E3CF3FFF7A17A3A729 /* Pods_Flotilla_Chat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Flotilla_Chat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
504EC3011FED79650016851F /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
AC8D5382B9575A9124613C5D /* Pods_Flotilla_Chat.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DA42C7E3CF3FFF7A17A3A729 /* Pods_Flotilla_Chat.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
504EC2FB1FED79650016851F = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
504EC3061FED79650016851F /* App */,
|
||||||
|
504EC3051FED79650016851F /* Products */,
|
||||||
|
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||||
|
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
504EC3051FED79650016851F /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
504EC3041FED79650016851F /* Flotilla Chat.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
504EC3061FED79650016851F /* App */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||||
|
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||||
|
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||||
|
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||||
|
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
||||||
|
504EC3131FED79650016851F /* Info.plist */,
|
||||||
|
2FAD9762203C412B000D30F8 /* config.xml */,
|
||||||
|
50B271D01FEDC1A000F3C39B /* public */,
|
||||||
|
);
|
||||||
|
path = App;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
7F8756D8B27F46E3366F6CEA /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
||||||
|
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
||||||
|
7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */,
|
||||||
|
1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
504EC3031FED79650016851F /* Flotilla Chat */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Flotilla Chat" */;
|
||||||
|
buildPhases = (
|
||||||
|
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
504EC3001FED79650016851F /* Sources */,
|
||||||
|
504EC3011FED79650016851F /* Frameworks */,
|
||||||
|
504EC3021FED79650016851F /* Resources */,
|
||||||
|
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = "Flotilla Chat";
|
||||||
|
productName = App;
|
||||||
|
productReference = 504EC3041FED79650016851F /* Flotilla Chat.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
504EC2FC1FED79650016851F /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
LastSwiftUpdateCheck = 920;
|
||||||
|
LastUpgradeCheck = 920;
|
||||||
|
TargetAttributes = {
|
||||||
|
504EC3031FED79650016851F = {
|
||||||
|
CreatedOnToolsVersion = 9.2;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||||
|
compatibilityVersion = "Xcode 8.0";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 504EC2FB1FED79650016851F;
|
||||||
|
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
504EC3031FED79650016851F /* Flotilla Chat */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
504EC3021FED79650016851F /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||||
|
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||||
|
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||||
|
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||||
|
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||||
|
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Flotilla Chat-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
504EC3001FED79650016851F /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
504EC30C1FED79650016851F /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
504EC3111FED79650016851F /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
504EC3141FED79650016851F /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
504EC3151FED79650016851F /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
504EC3171FED79650016851F /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
|
MARKETING_VERSION = 0.2.13;
|
||||||
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
504EC3181FED79650016851F /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
|
MARKETING_VERSION = 0.2.13;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
504EC3141FED79650016851F /* Debug */,
|
||||||
|
504EC3151FED79650016851F /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Flotilla Chat" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
504EC3171FED79650016851F /* Debug */,
|
||||||
|
504EC3181FED79650016851F /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import UIKit
|
||||||
|
import Capacitor
|
||||||
|
|
||||||
|
@UIApplicationMain
|
||||||
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
|
var window: UIWindow?
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
// Override point for customization after application launch.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationWillResignActive(_ application: UIApplication) {
|
||||||
|
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||||
|
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||||
|
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||||
|
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||||
|
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
|
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationWillTerminate(_ application: UIApplication) {
|
||||||
|
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||||
|
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||||
|
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||||
|
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||||
|
// Called when the app was launched with an activity, including Universal Links.
|
||||||
|
// Feel free to add additional processing here, but if you want the App API to support
|
||||||
|
// tracking app url opens, make sure to keep this call
|
||||||
|
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"size": "1024x1024",
|
||||||
|
"filename": "AppIcon-512@2x.png",
|
||||||
|
"platform": "ios"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"author": "xcode",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"filename": "Default@1x~universal~anyany.png",
|
||||||
|
"scale": "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"filename": "Default@2x~universal~anyany.png",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"filename": "Default@3x~universal~anyany.png",
|
||||||
|
"scale": "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "1x",
|
||||||
|
"filename": "Default@1x~universal~anyany-dark.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "2x",
|
||||||
|
"filename": "Default@2x~universal~anyany-dark.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "3x",
|
||||||
|
"filename": "Default@3x~universal~anyany-dark.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
</imageView>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="Splash" width="1366" height="1366"/>
|
||||||
|
<systemColor name="systemBackgroundColor">
|
||||||
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<device id="retina4_7" orientation="portrait">
|
||||||
|
<adaptation id="fullscreen"/>
|
||||||
|
</device>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Bridge View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Flotilla</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>armv7</string>
|
||||||
|
</array>
|
||||||
|
<key>UIStatusBarStyle</key>
|
||||||
|
<string></string>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<true/>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||||
|
|
||||||
|
platform :ios, '14.0'
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
# workaround to avoid Xcode caching of Pods that requires
|
||||||
|
# Product -> Clean Build Folder after new Cordova plugins installed
|
||||||
|
# Requires CocoaPods 1.6 or newer
|
||||||
|
install! 'cocoapods', :disable_input_output_paths => true
|
||||||
|
|
||||||
|
def capacitor_pods
|
||||||
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
|
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||||
|
pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
|
||||||
|
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/nostr-signer-capacitor-plugin'
|
||||||
|
end
|
||||||
|
|
||||||
|
target 'Flotilla Chat' do
|
||||||
|
capacitor_pods
|
||||||
|
# Add your Pods here
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
assertDeploymentTarget(installer)
|
||||||
|
end
|
||||||
@@ -1,49 +1,48 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.2.5",
|
"version": "0.2.13",
|
||||||
"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": "./build.sh && ./sourcemaps.sh",
|
||||||
"release:android": "cap build android --androidreleasetype APK --signing-type apksigner",
|
"release:android": "./build.sh && 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",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.5.27",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
"@types/eslint": "^9.6.0",
|
"@types/eslint": "^9.6.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.36.0",
|
"eslint-plugin-svelte": "^2.45.1",
|
||||||
"globals": "^15.0.0",
|
"globals": "^15.0.0",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.40",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"prettier-plugin-svelte": "^3.1.2",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"svelte": "^4.2.7",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^3.6.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tailwindcss": "^3.4.7",
|
"tailwindcss": "^3.4.7",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.5.0",
|
||||||
"typescript-eslint": "^8.0.0",
|
"typescript-eslint": "^8.0.0",
|
||||||
"vite": "^5.0.3"
|
"vite": "^5.4.4"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^7.0.1",
|
"@capacitor/android": "^7.0.0",
|
||||||
"@capacitor/app": "^7.0.0",
|
"@capacitor/app": "^7.0.0",
|
||||||
"@capacitor/cli": "^6.2.0",
|
"@capacitor/cli": "^7.0.0",
|
||||||
"@capacitor/core": "^7.0.1",
|
"@capacitor/core": "^7.0.1",
|
||||||
|
"@capacitor/ios": "^7.0.0",
|
||||||
|
"@capacitor/keyboard": "^7.0.0",
|
||||||
"@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",
|
||||||
@@ -52,16 +51,16 @@
|
|||||||
"@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.41",
|
"@welshman/app": "^0.0.43",
|
||||||
"@welshman/content": "~0.0.15",
|
"@welshman/content": "^0.1.0",
|
||||||
"@welshman/dvm": "~0.0.14",
|
"@welshman/dvm": "^0.0.15",
|
||||||
"@welshman/editor": "~0.0.8",
|
"@welshman/editor": "^0.1.0",
|
||||||
"@welshman/feeds": "~0.0.30",
|
"@welshman/feeds": "^0.1.0",
|
||||||
"@welshman/lib": "~0.0.38",
|
"@welshman/lib": "^0.1.0",
|
||||||
"@welshman/net": "~0.0.46",
|
"@welshman/net": "^0.0.49",
|
||||||
"@welshman/signer": "~0.0.20",
|
"@welshman/signer": "^0.1.0",
|
||||||
"@welshman/store": "~0.0.15",
|
"@welshman/store": "^0.1.0",
|
||||||
"@welshman/util": "~0.0.59",
|
"@welshman/util": "^0.1.0",
|
||||||
"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",
|
||||||
@@ -69,7 +68,7 @@
|
|||||||
"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-signer-capacitor-plugin": "^0.0.3",
|
"nostr-signer-capacitor-plugin": "coracle-social/nostr-signer-capacitor-plugin#9fbe4f8",
|
||||||
"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"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
hash=$(find build -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum | awk '{print $1}')
|
||||||
|
|
||||||
|
sentry-cli \
|
||||||
|
--url https://glitchtip.coracle.social \
|
||||||
|
--auth-token $GLITCHTIP_AUTH_TOKEN \
|
||||||
|
--api-key $VITE_GLITCHTIP_API_KEY \
|
||||||
|
sourcemaps \
|
||||||
|
--org coracle \
|
||||||
|
--project flotilla \
|
||||||
|
--release $hash \
|
||||||
|
upload \
|
||||||
|
--url-prefix /_app/immutable/ \
|
||||||
|
build/_app/immutable
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
|
@import "@welshman/editor/index.css";
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Fonts */
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Satoshis";
|
font-family: "Satoshis";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -38,6 +42,8 @@
|
|||||||
url("/fonts/Italic.ttf") format("truetype");
|
url("/fonts/Italic.ttf") format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* root */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: Lato;
|
font-family: Lato;
|
||||||
--base-100: oklch(var(--b1));
|
--base-100: oklch(var(--b1));
|
||||||
@@ -50,6 +56,60 @@
|
|||||||
--secondary-content: oklch(var(--sc));
|
--secondary-content: oklch(var(--sc));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root,
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
@apply bg-base-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ios */
|
||||||
|
|
||||||
|
.sait {
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sair {
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
.saib {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sail {
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
}
|
||||||
|
|
||||||
|
.saix {
|
||||||
|
@apply sail sair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saiy {
|
||||||
|
@apply sait saib;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sai {
|
||||||
|
@apply saiy saix;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-sai {
|
||||||
|
top: env(safe-area-inset-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-sai {
|
||||||
|
right: env(safe-area-inset-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-sai {
|
||||||
|
bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-sai {
|
||||||
|
left: env(safe-area-inset-left);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* utilities */
|
||||||
|
|
||||||
.bg-alt,
|
.bg-alt,
|
||||||
.bg-alt .bg-alt .bg-alt,
|
.bg-alt .bg-alt .bg-alt,
|
||||||
.hover\:bg-alt:hover,
|
.hover\:bg-alt:hover,
|
||||||
@@ -185,54 +245,71 @@
|
|||||||
@apply -m-1 min-h-12 p-1;
|
@apply -m-1 min-h-12 p-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap[contenteditable="true"] {
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap {
|
||||||
@apply max-h-[350px] overflow-y-auto p-2 px-4;
|
@apply max-h-[350px] overflow-y-auto p-2 px-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-editor .tiptap[contenteditable="true"] {
|
.tiptap p.is-editor-empty:first-child::before {
|
||||||
@apply rounded-box bg-base-300;
|
opacity: 40%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-editor .tiptap[contenteditable="true"] {
|
.chat-editor .tiptap {
|
||||||
@apply input input-bordered h-auto p-[.65rem];
|
@apply rounded-box bg-base-300 pr-12;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-editor .tiptap[contenteditable="true"] {
|
.note-editor .tiptap {
|
||||||
|
--tiptap-object-bg: var(--base-200);
|
||||||
@apply input input-bordered h-auto min-h-32 rounded-box p-[.65rem] pb-6;
|
@apply input input-bordered h-auto min-h-32 rounded-box p-[.65rem] pb-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap pre code {
|
.input-editor .tiptap {
|
||||||
@apply link-content block w-full;
|
--tiptap-object-bg: var(--base-200);
|
||||||
|
@apply input input-bordered h-auto p-[.65rem];
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap p code {
|
/* link-content, based on tiptap */
|
||||||
@apply link-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-content,
|
.link-content {
|
||||||
.tiptap [tag] {
|
max-width: 100%;
|
||||||
@apply max-w-full overflow-hidden text-ellipsis whitespace-nowrap rounded bg-neutral px-1 text-neutral-content no-underline;
|
overflow: hidden;
|
||||||
}
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
.link-content.link-content-selected {
|
border-radius: 3px;
|
||||||
@apply bg-primary text-primary-content;
|
padding: 0 0.25rem;
|
||||||
}
|
background-color: var(--base-100);
|
||||||
|
color: var(--base-content);
|
||||||
.tiptap p.is-editor-empty:first-child::before {
|
|
||||||
content: attr(data-placeholder);
|
|
||||||
float: left;
|
|
||||||
height: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* date input */
|
/* date input */
|
||||||
|
|
||||||
.date-time-field {
|
.picker {
|
||||||
@apply input input-bordered rounded px-0;
|
--date-picker-foreground: var(--base-content);
|
||||||
|
--date-picker-background: var(--base-300);
|
||||||
|
--date-picker-highlight-border: var(--primary);
|
||||||
|
--date-picker-selected-color: var(--primary-content);
|
||||||
|
--date-picker-selected-background: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-time-field {
|
||||||
|
@apply input input-bordered rounded-lg px-0;
|
||||||
|
}
|
||||||
|
|
||||||
.date-time-field input {
|
.date-time-field input {
|
||||||
@apply !h-full !w-full !border-none !bg-inherit !text-inherit;
|
@apply !h-full !w-full !rounded-lg !border-none !bg-inherit !px-4 !text-inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* emoji picker */
|
/* emoji picker */
|
||||||
@@ -249,18 +326,32 @@ emoji-picker {
|
|||||||
--outline-color: var(--base-100);
|
--outline-color: var(--base-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tiptap */
|
/* progress */
|
||||||
|
|
||||||
.tiptap {
|
progress[value]::-webkit-progress-value {
|
||||||
--tiptap-object-bg: var(--base-100);
|
transition: width 0.5s;
|
||||||
--tiptap-object-fg: var(--base-content);
|
|
||||||
--tiptap-active-bg: var(--primary);
|
|
||||||
--tiptap-active-fg: var(--primary-content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap-suggestions {
|
/* content width for fixed elements */
|
||||||
--tiptap-object-bg: var(--base-100);
|
|
||||||
--tiptap-object-fg: var(--base-content);
|
.cw {
|
||||||
--tiptap-active-bg: var(--base-300);
|
@apply w-full md:w-[calc(100%-18.5rem)];
|
||||||
--tiptap-active-fg: var(--base-content);
|
}
|
||||||
|
|
||||||
|
/* chat view */
|
||||||
|
|
||||||
|
.chat__page-bar {
|
||||||
|
@apply sait cw !fixed top-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat__messages {
|
||||||
|
@apply saib cw fixed top-12 flex h-[calc(100%-6rem)] flex-col-reverse overflow-y-auto overflow-x-hidden md:h-[calc(100%-2.5rem)];
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat__compose {
|
||||||
|
@apply saib cw fixed bottom-14 md:bottom-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat__scroll-down {
|
||||||
|
@apply saib fixed bottom-28 right-4 md:bottom-16;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||||
<meta name="theme-color" content="{ACCENT}" />
|
<meta name="theme-color" content="{ACCENT}" />
|
||||||
<meta name="description" content="{DESCRIPTION}" />
|
<meta name="description" content="{DESCRIPTION}" />
|
||||||
<meta name="og:url" content="{URL}" />
|
<meta name="og:url" content="{URL}" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint prefer-rest-params: 0 */
|
/* eslint prefer-rest-params: 0 */
|
||||||
|
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
|
import {getSetting} from "@app/state"
|
||||||
|
|
||||||
const w = window as any
|
const w = window as any
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ w.plausible =
|
|||||||
|
|
||||||
export const setupAnalytics = () => {
|
export const setupAnalytics = () => {
|
||||||
page.subscribe($page => {
|
page.subscribe($page => {
|
||||||
if ($page.route) {
|
if ($page.route && getSetting("report_usage")) {
|
||||||
w.plausible("pageview", {u: $page.route.id})
|
w.plausible("pageview", {u: $page.route.id})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as nip19 from "nostr-tools/nip19"
|
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, randomId, uniq, equals} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
DELETE,
|
DELETE,
|
||||||
REPORT,
|
REPORT,
|
||||||
@@ -26,12 +26,11 @@ import {
|
|||||||
getTag,
|
getTag,
|
||||||
getListTags,
|
getListTags,
|
||||||
getRelayTags,
|
getRelayTags,
|
||||||
isShareableRelayUrl,
|
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
toNostrURI,
|
toNostrURI,
|
||||||
|
unionFilters,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, EventContent, EventTemplate, List} from "@welshman/util"
|
import type {TrustedEvent, Filter, EventContent, EventTemplate} from "@welshman/util"
|
||||||
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"
|
||||||
import {
|
import {
|
||||||
@@ -40,13 +39,9 @@ import {
|
|||||||
repository,
|
repository,
|
||||||
publishThunk,
|
publishThunk,
|
||||||
publishThunks,
|
publishThunks,
|
||||||
loadProfile,
|
|
||||||
loadInboxRelaySelections,
|
|
||||||
profilesByPubkey,
|
profilesByPubkey,
|
||||||
relaySelectionsByPubkey,
|
relaySelectionsByPubkey,
|
||||||
getWriteRelayUrls,
|
getWriteRelayUrls,
|
||||||
loadFollows,
|
|
||||||
loadMutes,
|
|
||||||
tagEvent,
|
tagEvent,
|
||||||
tagEventForReaction,
|
tagEventForReaction,
|
||||||
getRelayUrls,
|
getRelayUrls,
|
||||||
@@ -67,11 +62,12 @@ import {
|
|||||||
userMembership,
|
userMembership,
|
||||||
INDEXER_RELAYS,
|
INDEXER_RELAYS,
|
||||||
NIP46_PERMS,
|
NIP46_PERMS,
|
||||||
loadMembership,
|
ALERT,
|
||||||
loadSettings,
|
NOTIFIER_PUBKEY,
|
||||||
getDefaultPubkeys,
|
NOTIFIER_RELAY,
|
||||||
userRoomsByUrl,
|
userRoomsByUrl,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
|
import {loadUserData} from "@app/requests"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
@@ -161,47 +157,6 @@ export const logout = async () => {
|
|||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loaders
|
|
||||||
|
|
||||||
export const loadUserData = (
|
|
||||||
pubkey: string,
|
|
||||||
request: Partial<SubscribeRequestWithHandlers> = {},
|
|
||||||
) => {
|
|
||||||
const promise = Promise.race([
|
|
||||||
sleep(3000),
|
|
||||||
Promise.all([
|
|
||||||
loadInboxRelaySelections(pubkey, request),
|
|
||||||
loadMembership(pubkey, request),
|
|
||||||
loadSettings(pubkey, request),
|
|
||||||
loadProfile(pubkey, request),
|
|
||||||
loadFollows(pubkey, request),
|
|
||||||
loadMutes(pubkey, request),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
|
|
||||||
// Load followed profiles slowly in the background without clogging other stuff up. Only use a single
|
|
||||||
// indexer relay to avoid too many redundant validations, which slow things down and eat bandwidth
|
|
||||||
promise.then(async () => {
|
|
||||||
for (const pubkeys of chunk(50, getDefaultPubkeys())) {
|
|
||||||
const relays = sample(1, INDEXER_RELAYS)
|
|
||||||
|
|
||||||
await sleep(1000)
|
|
||||||
|
|
||||||
for (const pubkey of pubkeys) {
|
|
||||||
loadMembership(pubkey, {relays})
|
|
||||||
loadProfile(pubkey, {relays})
|
|
||||||
loadFollows(pubkey, {relays})
|
|
||||||
loadMutes(pubkey, {relays})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
export const discoverRelays = (lists: List[]) =>
|
|
||||||
Promise.all(uniq(lists.flatMap(getRelayUrls)).filter(isShareableRelayUrl).map(loadRelay))
|
|
||||||
|
|
||||||
// Synchronization
|
// Synchronization
|
||||||
|
|
||||||
export const broadcastUserData = async (relays: string[]) => {
|
export const broadcastUserData = async (relays: string[]) => {
|
||||||
@@ -376,6 +331,7 @@ export const checkRelayConnection = async (url: string) => {
|
|||||||
const connection = ctx.net.pool.get(url)
|
const connection = ctx.net.pool.get(url)
|
||||||
|
|
||||||
await connection.socket.open()
|
await connection.socket.open()
|
||||||
|
await connection.socket.wait(3000)
|
||||||
|
|
||||||
if (connection.socket.status !== SocketStatus.Open) {
|
if (connection.socket.status !== SocketStatus.Open) {
|
||||||
return `Failed to connect`
|
return `Failed to connect`
|
||||||
@@ -504,3 +460,35 @@ export const makeComment = ({event, content, tags = []}: CommentParams) =>
|
|||||||
|
|
||||||
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
||||||
publishThunk({event: makeComment(params), relays})
|
publishThunk({event: makeComment(params), relays})
|
||||||
|
|
||||||
|
export type AlertParams = {
|
||||||
|
cron: string
|
||||||
|
email: string
|
||||||
|
relay: string
|
||||||
|
handler: string
|
||||||
|
filters: Filter[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeAlert = async ({cron, email, handler, relay, filters}: AlertParams) =>
|
||||||
|
createEvent(ALERT, {
|
||||||
|
content: await signer
|
||||||
|
.get()
|
||||||
|
.nip44.encrypt(
|
||||||
|
NOTIFIER_PUBKEY,
|
||||||
|
JSON.stringify([
|
||||||
|
["cron", cron],
|
||||||
|
["email", email],
|
||||||
|
["relay", relay],
|
||||||
|
["handler", handler],
|
||||||
|
["channel", "email"],
|
||||||
|
...unionFilters(filters).map(filter => ["filter", JSON.stringify(filter)]),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
tags: [
|
||||||
|
["d", randomId()],
|
||||||
|
["p", NOTIFIER_PUBKEY],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const publishAlert = async (params: AlertParams) =>
|
||||||
|
publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Capacitor} from "@capacitor/core"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import {randomInt} from "@welshman/lib"
|
||||||
|
import {displayRelayUrl, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
||||||
|
import type {Filter} from "@welshman/util"
|
||||||
|
import {pubkey} from "@welshman/app"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {getMembershipUrls, getMembershipRoomsByUrl, userMembership} from "@app/state"
|
||||||
|
import {loadAlertStatuses} from "@app/requests"
|
||||||
|
import {publishAlert} from "@app/commands"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
const handler = Capacitor.isNativePlatform()
|
||||||
|
? "https://app.flotilla.social"
|
||||||
|
: window.location.origin
|
||||||
|
|
||||||
|
const timezone = new Date()
|
||||||
|
.toString()
|
||||||
|
.match(/GMT[^\s]+/)![0]
|
||||||
|
.slice(3)
|
||||||
|
const timezoneOffset = parseInt(timezone) / 100
|
||||||
|
const minute = randomInt(0, 59)
|
||||||
|
const hour = (17 - timezoneOffset) % 24
|
||||||
|
const WEEKLY = `0 ${minute} ${hour} * * 1`
|
||||||
|
const DAILY = `0 ${minute} ${hour} * * *`
|
||||||
|
|
||||||
|
let loading = false
|
||||||
|
let cron = WEEKLY
|
||||||
|
let email = ""
|
||||||
|
let relay = ""
|
||||||
|
let notifyThreads = true
|
||||||
|
let notifyCalendar = true
|
||||||
|
let notifyChat = false
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!email.includes("@")) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please provide an email address",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relay) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please select a space",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notifyThreads && !notifyCalendar && !notifyChat) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please select something to be notified about",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters: Filter[] = []
|
||||||
|
|
||||||
|
if (notifyThreads) {
|
||||||
|
filters.push({kinds: [THREAD]})
|
||||||
|
filters.push({kinds: [COMMENT], "#k": [String(THREAD)]})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyCalendar) {
|
||||||
|
filters.push({kinds: [EVENT_TIME]})
|
||||||
|
filters.push({kinds: [COMMENT], "#k": [String(EVENT_TIME)]})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyChat) {
|
||||||
|
filters.push({kinds: [MESSAGE], "#h": getMembershipRoomsByUrl(relay, $userMembership)})
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await publishAlert({cron, email, relay, handler, filters})
|
||||||
|
await loadAlertStatuses($pubkey!)
|
||||||
|
|
||||||
|
pushToast({message: "Your alert has been successfully created!"})
|
||||||
|
back()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
Add an Alert
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Email Address*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<input placeholder="email@example.com" bind:value={email} />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Frequency*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<select bind:value={cron} class="select select-bordered">
|
||||||
|
<option value={WEEKLY}>Weekly</option>
|
||||||
|
<option value={DAILY}>Daily</option>
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Space*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<select bind:value={relay} class="select select-bordered">
|
||||||
|
<option value="" disabled selected>Choose a space URL</option>
|
||||||
|
{#each getMembershipUrls($userMembership) as url (url)}
|
||||||
|
<option value={url}>{displayRelayUrl(url)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Notifications*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="flex items-center justify-end gap-4">
|
||||||
|
<span class="flex gap-3">
|
||||||
|
<input type="checkbox" class="checkbox" bind:checked={notifyThreads} />
|
||||||
|
Threads
|
||||||
|
</span>
|
||||||
|
<span class="flex gap-3">
|
||||||
|
<input type="checkbox" class="checkbox" bind:checked={notifyCalendar} />
|
||||||
|
Calendar
|
||||||
|
</span>
|
||||||
|
<span class="flex gap-3">
|
||||||
|
<input type="checkbox" class="checkbox" bind:checked={notifyChat} />
|
||||||
|
Chat
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Confirm</Spinner>
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
|
import type {Alert} from "@app/state"
|
||||||
|
import {NOTIFIER_RELAY} from "@app/state"
|
||||||
|
import {publishDelete} from "@app/commands"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
alert: Alert
|
||||||
|
}
|
||||||
|
|
||||||
|
const {alert}: Props = $props()
|
||||||
|
|
||||||
|
const confirm = () => {
|
||||||
|
publishDelete({event: alert.event, relays: [NOTIFIER_RELAY]})
|
||||||
|
pushToast({message: "Your alert has been deleted!"})
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Confirm {confirm} message="You'll no longer receive messages for this alert." />
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {parseJson, nthEq} from "@welshman/lib"
|
||||||
|
import {
|
||||||
|
getAddress,
|
||||||
|
getTagValue,
|
||||||
|
getTagValues,
|
||||||
|
displayRelayUrl,
|
||||||
|
EVENT_TIME,
|
||||||
|
MESSAGE,
|
||||||
|
THREAD,
|
||||||
|
} from "@welshman/util"
|
||||||
|
import {displayList} from "@lib/util"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import AlertDelete from "@app/components/AlertDelete.svelte"
|
||||||
|
import type {Alert} from "@app/state"
|
||||||
|
import {alertStatuses} from "@app/state"
|
||||||
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
alert: Alert
|
||||||
|
}
|
||||||
|
|
||||||
|
const {alert}: Props = $props()
|
||||||
|
|
||||||
|
const address = $derived(getAddress(alert.event))
|
||||||
|
const status = $derived($alertStatuses.find(s => s.event.tags.some(nthEq(1, address))))
|
||||||
|
const cron = $derived(getTagValue("cron", alert.tags))
|
||||||
|
const channel = $derived(getTagValue("channel", alert.tags))
|
||||||
|
const relay = $derived(getTagValue("relay", alert.tags)!)
|
||||||
|
const filters = $derived(getTagValues("filter", alert.tags).map(parseJson))
|
||||||
|
const types = $derived.by(() => {
|
||||||
|
const t: string[] = []
|
||||||
|
|
||||||
|
if (filters.some(f => f.kinds?.includes(THREAD))) t.push("threads")
|
||||||
|
if (filters.some(f => f.kinds?.includes(EVENT_TIME))) t.push("calendar events")
|
||||||
|
if (filters.some(f => f.kinds?.includes(MESSAGE))) t.push("chat")
|
||||||
|
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
|
||||||
|
const startDelete = () => pushModal(AlertDelete, {alert})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
<Button class="py-1" onclick={startDelete}>
|
||||||
|
<Icon icon="trash-bin-2" />
|
||||||
|
</Button>
|
||||||
|
<div class="flex-inline gap-1">
|
||||||
|
{cron?.endsWith("1") ? "Weekly" : "Daily"} alert for
|
||||||
|
{displayList(types)} on
|
||||||
|
<Link class="link" href={makeSpacePath(relay)}>
|
||||||
|
{displayRelayUrl(relay)}
|
||||||
|
</Link>, sent via {channel}.
|
||||||
|
</div>
|
||||||
|
{#if status}
|
||||||
|
{@const statusText = getTagValue("status", status.tags) || "error"}
|
||||||
|
{#if statusText === "ok"}
|
||||||
|
<span
|
||||||
|
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content px-3 py-1 text-sm"
|
||||||
|
data-tip={getTagValue("message", status.tags)}>
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
{:else if statusText === "pending"}
|
||||||
|
<span
|
||||||
|
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content border-yellow-500 px-3 py-1 text-sm text-yellow-500"
|
||||||
|
data-tip={getTagValue("message", status.tags)}>
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span
|
||||||
|
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
||||||
|
data-tip={getTagValue("message", status.tags)}>
|
||||||
|
{statusText.replace("-", " ").replace(/^(.)/, x => x.toUpperCase())}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<span
|
||||||
|
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
||||||
|
data-tip="The notification server did not respond to your request.">
|
||||||
|
Inactive
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||||
|
import AlertItem from "@app/components/AlertItem.svelte"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
import {alerts} from "@app/state"
|
||||||
|
|
||||||
|
const startAlert = () => pushModal(AlertAdd)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<strong class="flex items-center gap-3">
|
||||||
|
<Icon icon="inbox" />
|
||||||
|
Alerts
|
||||||
|
</strong>
|
||||||
|
<Button class="btn btn-primary btn-sm" onclick={startAlert}>
|
||||||
|
<Icon icon="add-circle" />
|
||||||
|
Add Alert
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
{#each $alerts as alert (alert.event.id)}
|
||||||
|
<AlertItem {alert} />
|
||||||
|
{:else}
|
||||||
|
<p class="text-center opacity-75 py-12">No alerts found</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type {Snippet} from "svelte"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
import Landing from "@app/components/Landing.svelte"
|
import Landing from "@app/components/Landing.svelte"
|
||||||
@@ -9,6 +10,12 @@
|
|||||||
import {BURROW_URL} from "@app/state"
|
import {BURROW_URL} from "@app/state"
|
||||||
import {modals, pushModal} from "@app/modal"
|
import {modals, pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: Snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
const {children}: Props = $props()
|
||||||
|
|
||||||
if (BURROW_URL && !$pubkey) {
|
if (BURROW_URL && !$pubkey) {
|
||||||
if ($page.url.pathname === "/confirm-email") {
|
if ($page.url.pathname === "/confirm-email") {
|
||||||
pushModal(EmailConfirm, {
|
pushModal(EmailConfirm, {
|
||||||
@@ -29,7 +36,7 @@
|
|||||||
<div class="flex h-screen overflow-hidden">
|
<div class="flex h-screen overflow-hidden">
|
||||||
{#if $pubkey}
|
{#if $pubkey}
|
||||||
<PrimaryNav>
|
<PrimaryNav>
|
||||||
<slot />
|
{@render children?.()}
|
||||||
</PrimaryNav>
|
</PrimaryNav>
|
||||||
{:else if !$modals[$page.url.hash.slice(1)]}
|
{:else if !$modals[$page.url.hash.slice(1)]}
|
||||||
<Landing />
|
<Landing />
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {pubkey} from "@welshman/app"
|
||||||
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
|
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||||
|
import EventActivity from "@app/components/EventActivity.svelte"
|
||||||
|
import EventActions from "@app/components/EventActions.svelte"
|
||||||
|
import {publishDelete, publishReaction} from "@app/commands"
|
||||||
|
import {makeCalendarPath} from "@app/routes"
|
||||||
|
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
event,
|
||||||
|
showActivity = false,
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
showActivity?: boolean
|
||||||
|
} = $props()
|
||||||
|
|
||||||
|
const path = makeCalendarPath(url, event.id)
|
||||||
|
|
||||||
|
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
||||||
|
const reaction = events.find(e => e.pubkey === $pubkey)
|
||||||
|
|
||||||
|
if (reaction) {
|
||||||
|
publishDelete({relays: [url], event: reaction})
|
||||||
|
} else {
|
||||||
|
publishReaction({event, content, relays: [url]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||||
|
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-left" />
|
||||||
|
<ThunkStatusOrDeleted {event} />
|
||||||
|
{#if showActivity}
|
||||||
|
<EventActivity {url} {path} {event} />
|
||||||
|
{/if}
|
||||||
|
<EventActions {url} {event} noun="Event" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {writable} from "svelte/store"
|
||||||
|
import {randomId, HOUR} from "@welshman/lib"
|
||||||
|
import {createEvent, EVENT_TIME} from "@welshman/util"
|
||||||
|
import {publishThunk} from "@welshman/app"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import {daysBetween} from "@lib/util"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
||||||
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
|
import {PROTECTED, GENERAL, tagRoom} from "@app/state"
|
||||||
|
import {makeEditor} from "@app/editor"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
const {url} = $props()
|
||||||
|
|
||||||
|
const uploading = writable(false)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
if ($uploading) return
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please provide a title.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!start || !end) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please provide start and end times.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start >= end) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "End time must be later than start time.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = createEvent(EVENT_TIME, {
|
||||||
|
content: editor.getText({blockSeparator: "\n"}).trim(),
|
||||||
|
tags: [
|
||||||
|
["d", randomId()],
|
||||||
|
["title", title],
|
||||||
|
["location", location],
|
||||||
|
["start", start.toString()],
|
||||||
|
["end", end.toString()],
|
||||||
|
...daysBetween(start, end).map(D => ["D", D.toString()]),
|
||||||
|
...editor.storage.nostr.getEditorTags(),
|
||||||
|
tagRoom(GENERAL, url),
|
||||||
|
PROTECTED,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
pushToast({message: "Your event has been published!"})
|
||||||
|
publishThunk({event, relays: [url]})
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = makeEditor({submit, uploading})
|
||||||
|
|
||||||
|
let title = $state("")
|
||||||
|
let location = $state("")
|
||||||
|
let start: number | undefined = $state()
|
||||||
|
let end: number | undefined = $state()
|
||||||
|
let endDirty = false
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!endDirty && start) {
|
||||||
|
end = start + HOUR
|
||||||
|
} else if (end) {
|
||||||
|
endDirty = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Create an Event</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Invite other group members to events online or in real life.</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Title*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<input bind:value={title} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Summary</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div 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">
|
||||||
|
<EditorContent {editor} />
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
data-tip="Add an image"
|
||||||
|
class="center btn tooltip"
|
||||||
|
onclick={() => editor.chain().selectFiles().run()}>
|
||||||
|
{#if $uploading}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon="gallery-send" />
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
Start*
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<DateTimeInput bind:value={start} />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
End*
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<DateTimeInput bind:value={end} />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Location (optional)</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="map-point" />
|
||||||
|
<input bind:value={location} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={$uploading}>
|
||||||
|
<Spinner loading={$uploading}>Create Event</Spinner>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {fromPairs} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {LOCALE, secondsToDate} from "@welshman/app"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {event}: Props = $props()
|
||||||
|
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||||
|
const startDate = $derived(secondsToDate(parseInt(meta.start)))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex h-14 w-14 flex-col items-center justify-center gap-1 rounded-box border border-solid border-base-content p-2 sm:h-24 sm:w-24">
|
||||||
|
<span class="sm:text-lg">{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</span>
|
||||||
|
<span class="sm:text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {fromPairs} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import {formatTimestamp, formatTimestampAsDate, formatTimestampAsTime} from "@welshman/app"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {event}: Props = $props()
|
||||||
|
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||||
|
const start = $derived(parseInt(meta.start))
|
||||||
|
const end = $derived(parseInt(meta.end))
|
||||||
|
const startDateDisplay = $derived(formatTimestampAsDate(start))
|
||||||
|
const endDateDisplay = $derived(formatTimestampAsDate(end))
|
||||||
|
const isSingleDay = $derived(startDateDisplay === endDateDisplay)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p class="text-xl">{meta.title || meta.name}</p>
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<Icon icon="clock-circle" size={4} />
|
||||||
|
{formatTimestampAsTime(start)} — {isSingleDay ? formatTimestampAsTime(end) : formatTimestamp(end)}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||||
|
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||||
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
|
import {makeCalendarPath} from "@app/routes"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Link class="col-3 card2 bg-alt w-full cursor-pointer" href={makeCalendarPath(url, event.id)}>
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<CalendarEventHeader {event} />
|
||||||
|
</div>
|
||||||
|
<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">
|
||||||
|
Posted by <ProfileLink pubkey={event.pubkey} />
|
||||||
|
</span>
|
||||||
|
<CalendarEventActions showActivity {url} {event} />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {fromPairs} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {event}: Props = $props()
|
||||||
|
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Posted by <ProfileLink pubkey={event.pubkey} />
|
||||||
|
</span>
|
||||||
|
{#if meta.location}
|
||||||
|
<span>•</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<Icon icon="map-point" size={4} />
|
||||||
|
{meta.location}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
@@ -1,49 +1,47 @@
|
|||||||
<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, preventDefault} 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 EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
|
|
||||||
export let onSubmit: any
|
interface Props {
|
||||||
export let content = ""
|
onSubmit: any
|
||||||
|
}
|
||||||
|
|
||||||
export const focus = () => $editor.chain().focus().run()
|
const {onSubmit}: Props = $props()
|
||||||
|
|
||||||
|
const autofocus = !isMobile
|
||||||
|
|
||||||
const uploading = writable(false)
|
const uploading = writable(false)
|
||||||
|
|
||||||
const uploadFiles = () => $editor!.chain().selectFiles().run()
|
export const focus = () => editor.chain().focus().run()
|
||||||
|
|
||||||
|
const uploadFiles = () => editor.chain().selectFiles().run()
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if ($uploading) return
|
if ($uploading) return
|
||||||
|
|
||||||
const content = $editor!.getText({blockSeparator: "\n"}).trim()
|
const content = editor.getText({blockSeparator: "\n"}).trim()
|
||||||
const tags = $editor!.storage.nostr.getEditorTags()
|
const tags = editor.storage.nostr.getEditorTags()
|
||||||
|
|
||||||
if (!content) return
|
if (!content) return
|
||||||
|
|
||||||
onSubmit({content, tags})
|
onSubmit({content, tags})
|
||||||
|
|
||||||
$editor!.chain().clearContent().run()
|
editor.chain().clearContent().run()
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor = makeEditor({autofocus: !isMobile, submit, uploading, aggressive: true})
|
const editor = makeEditor({autofocus, submit, uploading, aggressive: true})
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
$editor!.chain().setContent(content).run()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||||
class="relative z-feature flex gap-2 p-2"
|
|
||||||
on:submit|preventDefault={$uploading ? undefined : submit}>
|
|
||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
class="center tooltip tooltip-right h-10 w-10 min-w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
|
class="center tooltip tooltip-right h-10 w-10 min-w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
|
||||||
disabled={$uploading}
|
disabled={$uploading}
|
||||||
on:click={uploadFiles}>
|
onclick={uploadFiles}>
|
||||||
{#if $uploading}
|
{#if $uploading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -51,13 +49,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<div class="chat-editor flex-grow overflow-hidden">
|
<div class="chat-editor flex-grow overflow-hidden">
|
||||||
<EditorContent editor={$editor} />
|
<EditorContent {editor} />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
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"
|
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
||||||
disabled={$uploading}
|
disabled={$uploading}
|
||||||
on:click={submit}>
|
onclick={submit}>
|
||||||
<Icon icon="plain" />
|
<Icon icon="plain" />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -4,20 +4,32 @@
|
|||||||
import {slide} from "@lib/transition"
|
import {slide} 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 Button from "@lib/components/Button.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import NoteContent from "@app/components/NoteContent.svelte"
|
||||||
|
|
||||||
export let event: TrustedEvent
|
const {
|
||||||
export let clear: () => void
|
verb,
|
||||||
|
event,
|
||||||
|
clear,
|
||||||
|
}: {
|
||||||
|
verb: string
|
||||||
|
event: TrustedEvent
|
||||||
|
clear: () => void
|
||||||
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
|
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
|
||||||
transition:slide>
|
transition:slide>
|
||||||
<p class="text-primary">Replying to @{displayProfileByPubkey(event.pubkey)}</p>
|
<p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||||
{#key event.id}
|
{#key event.id}
|
||||||
<Content {event} minLength={100} maxLength={300} expandMode="disabled" />
|
<NoteContent
|
||||||
|
{event}
|
||||||
|
hideMediaAtDepth={0}
|
||||||
|
minLength={100}
|
||||||
|
maxLength={300}
|
||||||
|
expandMode="disabled" />
|
||||||
{/key}
|
{/key}
|
||||||
<Button class="absolute right-2 top-2 cursor-pointer" on:click={clear}>
|
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||||
<Icon icon="close-circle" />
|
<Icon icon="close-circle" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
formatTimestampAsTime,
|
formatTimestampAsTime,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import LongPress from "@lib/components/LongPress.svelte"
|
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
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"
|
||||||
@@ -26,11 +26,16 @@
|
|||||||
import {publishDelete, publishReaction} from "@app/commands"
|
import {publishDelete, publishReaction} from "@app/commands"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let url, room
|
interface Props {
|
||||||
export let event: TrustedEvent
|
url: any
|
||||||
export let replyTo: any = undefined
|
room: any
|
||||||
export let showPubkey = false
|
event: TrustedEvent
|
||||||
export let inert = false
|
replyTo?: any
|
||||||
|
showPubkey?: boolean
|
||||||
|
inert?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, room, event, replyTo = undefined, showPubkey = false, inert = false}: Props = $props()
|
||||||
|
|
||||||
const thunk = $thunks[event.id]
|
const thunk = $thunks[event.id]
|
||||||
const today = formatTimestampAsDate(now())
|
const today = formatTimestampAsDate(now())
|
||||||
@@ -40,7 +45,7 @@
|
|||||||
|
|
||||||
const reply = () => replyTo(event)
|
const reply = () => replyTo(event)
|
||||||
|
|
||||||
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event})
|
const onTap = () => pushModal(ChannelMessageMenuMobile, {url, event, reply})
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
|
|
||||||
@@ -55,22 +60,22 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LongPress
|
<TapTarget
|
||||||
data-event={event.id}
|
data-event={event.id}
|
||||||
onLongPress={inert ? null : onLongPress}
|
onTap={inert ? null : onTap}
|
||||||
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left">
|
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left">
|
||||||
<div class="flex w-full gap-3 overflow-auto">
|
<div class="flex w-full gap-3 overflow-auto">
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<Button on:click={openProfile} class="flex items-start">
|
<Button onclick={openProfile} class="flex items-start">
|
||||||
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={8} />
|
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={8} />
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="w-8 min-w-8 max-w-8" />
|
<div class="w-8 min-w-8 max-w-8"></div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="min-w-0 flex-grow pr-1">
|
<div class="min-w-0 flex-grow pr-1">
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button on:click={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
||||||
{$profileDisplay}
|
{$profileDisplay}
|
||||||
</Button>
|
</Button>
|
||||||
<span class="text-xs opacity-50">
|
<span class="text-xs opacity-50">
|
||||||
@@ -84,7 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<Content {event} quoteProps={{minimal: true, relays: [url]}} />
|
<Content {event} relays={[url]} />
|
||||||
{#if thunk}
|
{#if thunk}
|
||||||
<ThunkStatus {thunk} class="mt-2" />
|
<ThunkStatus {thunk} class="mt-2" />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -94,16 +99,17 @@
|
|||||||
<div class="row-2 ml-10 mt-1">
|
<div class="row-2 ml-10 mt-1">
|
||||||
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
|
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
|
||||||
</div>
|
</div>
|
||||||
<button
|
{#if !isMobile}
|
||||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
<button
|
||||||
class:group-hover:opacity-100={!isMobile}
|
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||||
on:click|stopPropagation>
|
class:group-hover:opacity-100={!isMobile}>
|
||||||
<ChannelMessageEmojiButton {url} {room} {event} />
|
<ChannelMessageEmojiButton {url} {room} {event} />
|
||||||
{#if replyTo}
|
{#if replyTo}
|
||||||
<Button class="btn join-item btn-xs" on:click={reply}>
|
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||||
<Icon icon="reply" size={4} />
|
<Icon icon="reply" size={4} />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<ChannelMessageMenuButton {url} {event} />
|
<ChannelMessageMenuButton {url} {event} />
|
||||||
</button>
|
</button>
|
||||||
</LongPress>
|
{/if}
|
||||||
|
</TapTarget>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import {publishReaction} from "@app/commands"
|
import {publishReaction} from "@app/commands"
|
||||||
|
|
||||||
export let url, room, event
|
const {url, room, event} = $props()
|
||||||
|
|
||||||
// Tell svelte-check to shut up
|
// Tell svelte-check to shut up
|
||||||
noop(room)
|
noop(room)
|
||||||
|
|||||||
@@ -4,12 +4,10 @@
|
|||||||
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 EventReport from "@app/components/EventReport.svelte"
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let url
|
const {url, event, onClick} = $props()
|
||||||
export let event
|
|
||||||
export let onClick
|
|
||||||
|
|
||||||
const report = () => {
|
const report = () => {
|
||||||
onClick()
|
onClick()
|
||||||
@@ -18,32 +16,32 @@
|
|||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(EventInfo, {event})
|
pushModal(EventInfo, {url, event})
|
||||||
}
|
}
|
||||||
|
|
||||||
const showDelete = () => {
|
const showDelete = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(ConfirmDelete, {url, event})
|
pushModal(EventDeleteConfirm, {url, event})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl">
|
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl">
|
||||||
<li>
|
<li>
|
||||||
<Button on:click={showInfo}>
|
<Button onclick={showInfo}>
|
||||||
<Icon size={4} icon="code-2" />
|
<Icon size={4} icon="code-2" />
|
||||||
Message Details
|
Message Details
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{#if event.pubkey === $pubkey}
|
{#if event.pubkey === $pubkey}
|
||||||
<li>
|
<li>
|
||||||
<Button on:click={showDelete} class="text-error">
|
<Button onclick={showDelete} class="text-error">
|
||||||
<Icon size={4} icon="trash-bin-2" />
|
<Icon size={4} icon="trash-bin-2" />
|
||||||
Delete Message
|
Delete Message
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li>
|
<li>
|
||||||
<Button class="text-error" on:click={report}>
|
<Button class="text-error" onclick={report}>
|
||||||
<Icon size={4} icon="danger" />
|
<Icon size={4} icon="danger" />
|
||||||
Report Content
|
Report Content
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,27 +6,29 @@
|
|||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
import ChannelMessageMenu from "@app/components/ChannelMessageMenu.svelte"
|
import ChannelMessageMenu from "@app/components/ChannelMessageMenu.svelte"
|
||||||
|
|
||||||
export let url, event
|
const {url, event} = $props()
|
||||||
|
|
||||||
const open = () => popover.show()
|
const open = () => popover?.show()
|
||||||
|
|
||||||
const onClick = () => popover.hide()
|
const onClick = () => popover?.hide()
|
||||||
|
|
||||||
const onMouseMove = ({clientX, clientY}: any) => {
|
const onMouseMove = ({clientX, clientY}: any) => {
|
||||||
const {x, y, width, height} = popover.popper.getBoundingClientRect()
|
if (popover) {
|
||||||
|
const {x, y, width, height} = popover.popper.getBoundingClientRect()
|
||||||
|
|
||||||
if (!between([x, x + width], clientX) || !between([y, y + height + 30], clientY)) {
|
if (!between([x, x + width], clientX) || !between([y, y + height + 30], clientY)) {
|
||||||
popover.hide()
|
popover.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let popover: Instance
|
let popover: Instance | undefined = $state()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document on:mousemove={onMouseMove} />
|
<svelte:document onmousemove={onMouseMove} />
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<Button class="btn join-item btn-xs" on:click={open}>
|
<Button class="btn join-item btn-xs" onclick={open}>
|
||||||
<Icon icon="menu-dots" size={4} />
|
<Icon icon="menu-dots" size={4} />
|
||||||
</Button>
|
</Button>
|
||||||
<Tippy
|
<Tippy
|
||||||
|
|||||||
@@ -1,40 +1,55 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
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 EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {publishReaction} from "@app/commands"
|
import {publishReaction} from "@app/commands"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let url
|
type Props = {
|
||||||
export let event
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
reply: () => void
|
||||||
|
}
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) => {
|
const {url, event, reply}: Props = $props()
|
||||||
|
|
||||||
|
const onEmoji = ((event: TrustedEvent, url: string, emoji: NativeEmoji) => {
|
||||||
history.back()
|
history.back()
|
||||||
publishReaction({event, relays: [url], content: emoji.unicode})
|
publishReaction({event, relays: [url], content: emoji.unicode})
|
||||||
}
|
}).bind(undefined, event, url)
|
||||||
|
|
||||||
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
||||||
|
|
||||||
const showInfo = () => pushModal(EventInfo, {event}, {replaceState: true})
|
const sendReply = () => {
|
||||||
|
history.back()
|
||||||
|
reply()
|
||||||
|
}
|
||||||
|
|
||||||
const showDelete = () => pushModal(ConfirmDelete, {url, event})
|
const showInfo = () => pushModal(EventInfo, {url, event}, {replaceState: true})
|
||||||
|
|
||||||
|
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<Button class="btn btn-primary w-full" on:click={showEmojiPicker}>
|
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
||||||
<Icon size={4} icon="smile-circle" />
|
<Icon size={4} icon="smile-circle" />
|
||||||
Send Reaction
|
Send Reaction
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="btn btn-neutral" on:click={showInfo}>
|
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
||||||
|
<Icon size={4} icon="reply" />
|
||||||
|
Send Reply
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-neutral" onclick={showInfo}>
|
||||||
<Icon size={4} icon="code-2" />
|
<Icon size={4} icon="code-2" />
|
||||||
Message Details
|
Message Details
|
||||||
</Button>
|
</Button>
|
||||||
{#if event.pubkey === $pubkey}
|
{#if event.pubkey === $pubkey}
|
||||||
<Button class="btn btn-neutral text-error" on:click={showDelete}>
|
<Button class="btn btn-neutral text-error" onclick={showDelete}>
|
||||||
<Icon size={4} icon="trash-bin-2" />
|
<Icon size={4} icon="trash-bin-2" />
|
||||||
Delete Message
|
Delete Message
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {GENERAL, channelsById, makeChannelId} from "@app/state"
|
import {GENERAL, channelsById, makeChannelId} from "@app/state"
|
||||||
|
|
||||||
export let url
|
const {url, room} = $props()
|
||||||
export let room
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if room === GENERAL}
|
{#if room === GENERAL}
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
<script lang="ts" context="module">
|
|
||||||
type Element = {
|
|
||||||
id: string
|
|
||||||
type: "date" | "note"
|
|
||||||
value: string | TrustedEvent
|
|
||||||
showPubkey: boolean
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type {Snippet} from "svelte"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {derived} from "svelte/store"
|
|
||||||
import {int, nthNe, MINUTE, sortBy, remove} from "@welshman/lib"
|
import {int, nthNe, MINUTE, sortBy, remove} 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 {pubkey, formatTimestampAsDate, inboxRelaySelectionsByPubkey, load} from "@welshman/app"
|
import {
|
||||||
|
pubkey,
|
||||||
|
tagPubkey,
|
||||||
|
formatTimestampAsDate,
|
||||||
|
inboxRelaySelectionsByPubkey,
|
||||||
|
load,
|
||||||
|
} 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"
|
||||||
@@ -32,51 +29,52 @@
|
|||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {sendWrapped, prependParent} from "@app/commands"
|
import {sendWrapped, prependParent} from "@app/commands"
|
||||||
|
|
||||||
export let id
|
const {
|
||||||
|
id,
|
||||||
|
info,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
info?: Snippet
|
||||||
|
} = $props()
|
||||||
|
|
||||||
const chat = deriveChat(id)
|
const chat = deriveChat(id)
|
||||||
const pubkeys = splitChatId(id)
|
const pubkeys = splitChatId(id)
|
||||||
const others = remove($pubkey!, pubkeys)
|
const others = remove($pubkey!, pubkeys)
|
||||||
const missingInboxes = derived(inboxRelaySelectionsByPubkey, $m =>
|
const missingInboxes = $derived(pubkeys.filter(pk => !$inboxRelaySelectionsByPubkey.has(pk)))
|
||||||
pubkeys.filter(pk => !$m.has(pk)),
|
|
||||||
)
|
|
||||||
|
|
||||||
const assertEvent = (e: any) => e as TrustedEvent
|
|
||||||
|
|
||||||
const assertNotNil = <T,>(x: T | undefined) => x!
|
|
||||||
|
|
||||||
const showMembers = () =>
|
const showMembers = () =>
|
||||||
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) => {
|
||||||
parent = event
|
parent = event
|
||||||
compose.focus()
|
compose?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearParent = () => {
|
const clearParent = () => {
|
||||||
parent = undefined
|
parent = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async ({content, tags}: EventContent) => {
|
const onSubmit = async (params: EventContent) => {
|
||||||
|
// Remove p tags since they result in forking the conversation
|
||||||
|
const tags = [...params.tags.filter(nthNe(0, "p")), ...remove($pubkey!, pubkeys).map(tagPubkey)]
|
||||||
|
|
||||||
await sendWrapped({
|
await sendWrapped({
|
||||||
pubkeys,
|
pubkeys,
|
||||||
template: createEvent(
|
template: createEvent(DIRECT_MESSAGE, prependParent(parent, {...params, tags})),
|
||||||
DIRECT_MESSAGE,
|
|
||||||
prependParent(parent, {content, tags: tags.filter(nthNe(0, "p"))}),
|
|
||||||
),
|
|
||||||
delay: $userSettingValues.send_delay,
|
delay: $userSettingValues.send_delay,
|
||||||
})
|
})
|
||||||
|
|
||||||
clearParent()
|
clearParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
let loading = true
|
let loading = $state(true)
|
||||||
let parent: TrustedEvent | undefined
|
let compose: ChatCompose | undefined = $state()
|
||||||
let elements: Element[] = []
|
let parent: TrustedEvent | undefined = $state()
|
||||||
let compose: ChatCompose
|
let chatCompose: HTMLElement | undefined = $state()
|
||||||
|
let dynamicPadding: HTMLElement | undefined = $state()
|
||||||
|
|
||||||
$: {
|
const elements = $derived.by(() => {
|
||||||
elements = []
|
const elements = []
|
||||||
|
|
||||||
let previousDate
|
let previousDate
|
||||||
let previousPubkey
|
let previousPubkey
|
||||||
@@ -102,12 +100,22 @@
|
|||||||
previousCreatedAt = created_at
|
previousCreatedAt = created_at
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.reverse()
|
return elements.reverse()
|
||||||
}
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Don't use loadInboxRelaySelection because we want to force reload
|
// Don't use loadInboxRelaySelection because we want to force reload
|
||||||
load({filters: [{kinds: [INBOX_RELAYS], authors: others}]})
|
load({filters: [{kinds: [INBOX_RELAYS], authors: others}]})
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
dynamicPadding!.style.minHeight = `${chatCompose!.offsetHeight}px`
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(chatCompose!)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(chatCompose!)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -115,14 +123,14 @@
|
|||||||
}, 5000)
|
}, 5000)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative flex h-full w-full flex-col">
|
{#if others.length > 0}
|
||||||
{#if others.length > 0}
|
<PageBar class="chat__page-bar">
|
||||||
<PageBar>
|
{#snippet title()}
|
||||||
<div slot="title" class="flex flex-col gap-1 sm:flex-row sm:gap-2">
|
<div class="flex flex-col gap-1 sm:flex-row sm:gap-2">
|
||||||
{#if others.length === 1}
|
{#if others.length === 1}
|
||||||
{@const pubkey = others[0]}
|
{@const pubkey = others[0]}
|
||||||
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
|
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
|
||||||
<Button on:click={onClick} class="row-2">
|
<Button onclick={onClick} class="row-2">
|
||||||
<ProfileCircle {pubkey} size={5} />
|
<ProfileCircle {pubkey} size={5} />
|
||||||
<ProfileName {pubkey} />
|
<ProfileName {pubkey} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -141,14 +149,16 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{#if others.length > 2}
|
{#if others.length > 2}
|
||||||
<Button on:click={showMembers} class="btn btn-link hidden sm:block"
|
<Button onclick={showMembers} class="btn btn-link hidden sm:block"
|
||||||
>Show all members</Button>
|
>Show all members</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div slot="action">
|
{/snippet}
|
||||||
{#if remove($pubkey, $missingInboxes).length > 0}
|
{#snippet action()}
|
||||||
{@const count = remove($pubkey, $missingInboxes).length}
|
<div>
|
||||||
|
{#if remove($pubkey, missingInboxes).length > 0}
|
||||||
|
{@const count = remove($pubkey, missingInboxes).length}
|
||||||
{@const label = count > 1 ? "inboxes are" : "inbox is"}
|
{@const label = count > 1 ? "inboxes are" : "inbox is"}
|
||||||
<div
|
<div
|
||||||
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
|
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
|
||||||
@@ -158,45 +168,68 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</PageBar>
|
{/snippet}
|
||||||
{/if}
|
</PageBar>
|
||||||
<div class="-mt-2 flex flex-grow flex-col-reverse overflow-auto py-2">
|
{/if}
|
||||||
{#if $missingInboxes.includes(assertNotNil($pubkey))}
|
|
||||||
<div class="py-12">
|
<div class="chat__messages scroll-container">
|
||||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
<div bind:this={dynamicPadding}></div>
|
||||||
<p class="row-2 text-lg text-error">
|
{#if missingInboxes.includes($pubkey!)}
|
||||||
<Icon icon="danger" />
|
<div class="py-12">
|
||||||
Your inbox is not configured.
|
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||||
</p>
|
<p class="row-2 text-lg text-error">
|
||||||
<p>
|
<Icon icon="danger" />
|
||||||
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
|
Your inbox is not configured.
|
||||||
your <Link class="link" href="/settings/relays">relay settings page</Link> to set up your
|
</p>
|
||||||
inbox.
|
<p>
|
||||||
</p>
|
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
|
||||||
</div>
|
your <Link class="link" href="/settings/relays">relay settings page</Link> to set up your inbox.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
{#each elements as { type, id, value, showPubkey } (id)}
|
{:else if missingInboxes.length > 0}
|
||||||
{#if type === "date"}
|
<div class="py-12">
|
||||||
<Divider>{value}</Divider>
|
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||||
{:else}
|
<p class="row-2 text-lg text-error">
|
||||||
<ChatMessage event={assertEvent(value)} {pubkeys} {showPubkey} {replyTo} />
|
<Icon icon="danger" />
|
||||||
{/if}
|
{missingInboxes.length}
|
||||||
{/each}
|
{missingInboxes.length > 1 ? "inboxes are" : "inbox is"} not configured.
|
||||||
<p
|
</p>
|
||||||
class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
<p>
|
||||||
<Spinner {loading}>
|
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
|
||||||
{#if loading}
|
sure everyone in this conversation has set up their inbox relays.
|
||||||
Looking for messages...
|
</p>
|
||||||
{:else}
|
</div>
|
||||||
End of message history
|
</div>
|
||||||
{/if}
|
|
||||||
</Spinner>
|
|
||||||
<slot name="info" />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{#if parent}
|
|
||||||
<ChatComposeParent event={parent} clear={clearParent} />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{#each elements as { type, id, value, showPubkey } (id)}
|
||||||
|
{#if type === "date"}
|
||||||
|
<Divider>{value}</Divider>
|
||||||
|
{:else}
|
||||||
|
<ChatMessage
|
||||||
|
event={$state.snapshot(value as TrustedEvent)}
|
||||||
|
{pubkeys}
|
||||||
|
{showPubkey}
|
||||||
|
{replyTo} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
||||||
|
<Spinner {loading}>
|
||||||
|
{#if loading}
|
||||||
|
Looking for messages...
|
||||||
|
{:else}
|
||||||
|
End of message history
|
||||||
|
{/if}
|
||||||
|
</Spinner>
|
||||||
|
{@render info?.()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
||||||
|
<div>
|
||||||
|
{#if parent}
|
||||||
|
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<ChatCompose bind:this={compose} {onSubmit} />
|
<ChatCompose bind:this={compose} {onSubmit} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {WRAP} from "@welshman/util"
|
import {WRAP} from "@welshman/util"
|
||||||
import {repository} from "@welshman/app"
|
import {repository} from "@welshman/app"
|
||||||
|
import {preventDefault} 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 Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
@@ -10,9 +11,9 @@
|
|||||||
import {canDecrypt, PLATFORM_NAME, ensureUnwrapped} from "@app/state"
|
import {canDecrypt, PLATFORM_NAME, ensureUnwrapped} from "@app/state"
|
||||||
import {clearModals} from "@app/modal"
|
import {clearModals} from "@app/modal"
|
||||||
|
|
||||||
export let next
|
const {next} = $props()
|
||||||
|
|
||||||
let loading = false
|
let loading = $state(false)
|
||||||
|
|
||||||
const enableChat = async () => {
|
const enableChat = async () => {
|
||||||
canDecrypt.set(true)
|
canDecrypt.set(true)
|
||||||
@@ -38,10 +39,14 @@
|
|||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" on:submit|preventDefault={submit}>
|
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">Enable Messages</div>
|
{#snippet title()}
|
||||||
<div slot="info">Do you want to enable direct messages?</div>
|
<div>Enable Messages</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Do you want to enable direct messages?</div>
|
||||||
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>
|
<p>
|
||||||
By default, direct messages are disabled, since loading them requires
|
By default, direct messages are disabled, since loading them requires
|
||||||
@@ -52,7 +57,7 @@
|
|||||||
to decrypt data.
|
to decrypt data.
|
||||||
</p>
|
</p>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" on:click={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Icon icon="alt-arrow-left" />
|
<Icon icon="alt-arrow-left" />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -12,13 +12,18 @@
|
|||||||
import {makeChatPath} from "@app/routes"
|
import {makeChatPath} from "@app/routes"
|
||||||
import {notifications} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
|
|
||||||
export let id: string
|
interface Props {
|
||||||
export let pubkeys: string[]
|
id: string
|
||||||
export let messages: TrustedEvent[]
|
pubkeys: string[]
|
||||||
|
messages: TrustedEvent[]
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
const others = remove($pubkey!, pubkeys)
|
const {...props}: Props = $props()
|
||||||
const active = $page.params.chat === id
|
|
||||||
const path = makeChatPath(pubkeys)
|
const others = remove($pubkey!, props.pubkeys)
|
||||||
|
const active = $page.params.chat === props.id
|
||||||
|
const path = makeChatPath(props.pubkeys)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
for (const pk of others) {
|
for (const pk of others) {
|
||||||
@@ -27,9 +32,9 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link class="flex flex-col justify-start gap-1" href={makeChatPath(pubkeys)}>
|
<Link class="flex flex-col justify-start gap-1" href={makeChatPath(props.pubkeys)}>
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer border-t border-solid border-base-100 px-6 py-2 transition-colors hover:bg-base-100 {$$props.class}"
|
class="cursor-pointer border-t border-solid border-base-100 px-6 py-2 transition-colors hover:bg-base-100 {props.class}"
|
||||||
class:bg-base-100={active}>
|
class:bg-base-100={active}>
|
||||||
<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">
|
||||||
@@ -50,11 +55,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if !active && $notifications.has(path)}
|
{#if !active && $notifications.has(path)}
|
||||||
<div class="h-2 w-2 rounded-full bg-primary" transition:fade />
|
<div class="h-2 w-2 rounded-full bg-primary" transition:fade></div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
||||||
{messages[0].content}
|
{props.messages[0].content}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<Button class="btn btn-primary" on:click={startChat}>
|
<Button class="btn btn-primary" onclick={startChat}>
|
||||||
<Icon size={4} icon="add-circle" />
|
<Icon size={4} icon="add-circle" />
|
||||||
Start chat
|
Start chat
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="btn btn-neutral" on:click={markAsRead}>
|
<Button class="btn btn-neutral" onclick={markAsRead}>
|
||||||
<Icon size={4} icon="check-circle" />
|
<Icon size={4} icon="check-circle" />
|
||||||
Mark all read
|
Mark all read
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
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 Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
import LongPress from "@lib/components/LongPress.svelte"
|
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
@@ -25,10 +25,14 @@
|
|||||||
import {makeDelete, makeReaction, sendWrapped} from "@app/commands"
|
import {makeDelete, makeReaction, sendWrapped} from "@app/commands"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let event: TrustedEvent
|
interface Props {
|
||||||
export let replyTo: any = undefined
|
event: TrustedEvent
|
||||||
export let pubkeys: string[]
|
replyTo: (event: TrustedEvent) => void
|
||||||
export let showPubkey = false
|
pubkeys: string[]
|
||||||
|
showPubkey?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
|
||||||
|
|
||||||
const thunk = $thunks[event.id]
|
const thunk = $thunks[event.id]
|
||||||
const isOwn = event.pubkey === $pubkey
|
const isOwn = event.pubkey === $pubkey
|
||||||
@@ -36,6 +40,8 @@
|
|||||||
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]
|
||||||
|
|
||||||
|
const reply = () => replyTo(event)
|
||||||
|
|
||||||
const onReactionClick = async (content: string, events: TrustedEvent[]) => {
|
const onReactionClick = async (content: string, events: TrustedEvent[]) => {
|
||||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
const reaction = events.find(e => e.pubkey === $pubkey)
|
||||||
const template = reaction ? makeDelete({event: reaction}) : makeReaction({event, content})
|
const template = reaction ? makeDelete({event: reaction}) : makeReaction({event, content})
|
||||||
@@ -45,18 +51,18 @@
|
|||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
|
|
||||||
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys})
|
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply})
|
||||||
|
|
||||||
const togglePopover = () => {
|
const togglePopover = () => {
|
||||||
if (popoverIsVisible) {
|
if (popoverIsVisible) {
|
||||||
popover.hide()
|
popover?.hide()
|
||||||
} else {
|
} else {
|
||||||
popover.show()
|
popover?.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let popover: Instance
|
let popover: Instance | undefined = $state()
|
||||||
let popoverIsVisible = false
|
let popoverIsVisible = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if thunk}
|
{#if thunk}
|
||||||
@@ -68,45 +74,44 @@
|
|||||||
class:chat-start={!isOwn}
|
class:chat-start={!isOwn}
|
||||||
class:flex-row-reverse={!isOwn}
|
class:flex-row-reverse={!isOwn}
|
||||||
class:chat-end={isOwn}>
|
class:chat-end={isOwn}>
|
||||||
<Tippy
|
{#if !isMobile}
|
||||||
bind:popover
|
<Tippy
|
||||||
component={ChatMessageMenu}
|
bind:popover
|
||||||
props={{event, pubkeys, popover, replyTo}}
|
component={ChatMessageMenu}
|
||||||
params={{
|
props={{event, pubkeys, popover, replyTo}}
|
||||||
interactive: true,
|
params={{
|
||||||
trigger: "manual",
|
interactive: true,
|
||||||
onShow() {
|
trigger: "manual",
|
||||||
popoverIsVisible = true
|
onShow() {
|
||||||
},
|
popoverIsVisible = true
|
||||||
onHidden() {
|
},
|
||||||
popoverIsVisible = false
|
onHidden() {
|
||||||
},
|
popoverIsVisible = false
|
||||||
}}>
|
},
|
||||||
<button
|
}}>
|
||||||
type="button"
|
<button
|
||||||
class="opacity-0 transition-all"
|
type="button"
|
||||||
class:group-hover:opacity-100={!isMobile}
|
class="opacity-0 transition-all"
|
||||||
on:click={togglePopover}>
|
class:group-hover:opacity-100={!isMobile}
|
||||||
<Icon icon="menu-dots" size={4} />
|
onclick={togglePopover}>
|
||||||
</button>
|
<Icon icon="menu-dots" size={4} />
|
||||||
</Tippy>
|
</button>
|
||||||
|
</Tippy>
|
||||||
|
{/if}
|
||||||
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
||||||
<LongPress
|
<TapTarget
|
||||||
class="bg-alt chat-bubble mx-1 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
||||||
onLongPress={showMobileMenu}>
|
onTap={showMobileMenu}>
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if !isOwn}
|
{#if !isOwn}
|
||||||
<Button on:click={openProfile} class="flex items-center gap-1">
|
<Button onclick={openProfile} class="flex items-center gap-1">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={$profile?.picture}
|
src={$profile?.picture}
|
||||||
class="border border-solid border-base-content"
|
class="border border-solid border-base-content"
|
||||||
size={4} />
|
size={4} />
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button
|
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
||||||
on:click={openProfile}
|
|
||||||
class="text-sm font-bold"
|
|
||||||
style="color: {colorValue}">
|
|
||||||
{$profileDisplay}
|
{$profileDisplay}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,7 +124,7 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<Content showEntire {event} />
|
<Content showEntire {event} />
|
||||||
</div>
|
</div>
|
||||||
</LongPress>
|
</TapTarget>
|
||||||
<div class="row-2 z-feature -mt-1 ml-4">
|
<div class="row-2 z-feature -mt-1 ml-4">
|
||||||
<ReactionSummary {event} {onReactionClick} noTooltip />
|
<ReactionSummary {event} {onReactionClick} noTooltip />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,8 +5,12 @@
|
|||||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||||
import {makeReaction, sendWrapped} from "@app/commands"
|
import {makeReaction, sendWrapped} from "@app/commands"
|
||||||
|
|
||||||
export let event: TrustedEvent
|
interface Props {
|
||||||
export let pubkeys: string[]
|
event: TrustedEvent
|
||||||
|
pubkeys: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const {event, pubkeys}: Props = $props()
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) =>
|
const onEmoji = (emoji: NativeEmoji) =>
|
||||||
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
|
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
|
||||||
|
|||||||
@@ -5,10 +5,7 @@
|
|||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let event
|
const {event, pubkeys, popover, replyTo} = $props()
|
||||||
export let pubkeys
|
|
||||||
export let popover
|
|
||||||
export let replyTo
|
|
||||||
|
|
||||||
const reply = () => replyTo(event)
|
const reply = () => replyTo(event)
|
||||||
|
|
||||||
@@ -21,11 +18,11 @@
|
|||||||
<div class="join border border-solid border-neutral text-xs">
|
<div class="join border border-solid border-neutral text-xs">
|
||||||
<ChatMessageEmojiButton {event} {pubkeys} />
|
<ChatMessageEmojiButton {event} {pubkeys} />
|
||||||
{#if replyTo}
|
{#if replyTo}
|
||||||
<Button class="btn join-item btn-xs" on:click={reply}>
|
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||||
<Icon size={4} icon="reply" />
|
<Icon size={4} icon="reply" />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button class="btn join-item btn-xs" on:click={showInfo}>
|
<Button class="btn join-item btn-xs" onclick={showInfo}>
|
||||||
<Icon size={4} icon="code-2" />
|
<Icon size={4} icon="code-2" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
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 EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||||
@@ -8,16 +9,26 @@
|
|||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {clip} from "@app/toast"
|
import {clip} from "@app/toast"
|
||||||
|
|
||||||
export let event
|
type Props = {
|
||||||
export let pubkeys
|
pubkeys: string[]
|
||||||
|
event: TrustedEvent
|
||||||
const onEmoji = (emoji: NativeEmoji) => {
|
reply: () => void
|
||||||
history.back()
|
|
||||||
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {event, pubkeys, reply}: Props = $props()
|
||||||
|
|
||||||
|
const onEmoji = ((event: TrustedEvent, emoji: NativeEmoji) => {
|
||||||
|
history.back()
|
||||||
|
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
|
||||||
|
}).bind(undefined, event)
|
||||||
|
|
||||||
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
||||||
|
|
||||||
|
const sendReply = () => {
|
||||||
|
history.back()
|
||||||
|
reply()
|
||||||
|
}
|
||||||
|
|
||||||
const copyText = () => {
|
const copyText = () => {
|
||||||
history.back()
|
history.back()
|
||||||
clip(event.content)
|
clip(event.content)
|
||||||
@@ -27,15 +38,19 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<Button class="btn btn-primary w-full" on:click={showEmojiPicker}>
|
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
||||||
<Icon size={4} icon="smile-circle" />
|
<Icon size={4} icon="smile-circle" />
|
||||||
Send Reaction
|
Send Reaction
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="btn btn-neutral w-full" on:click={copyText}>
|
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
||||||
|
<Icon size={4} icon="reply" />
|
||||||
|
Send Reply
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-neutral w-full" onclick={copyText}>
|
||||||
<Icon size={4} icon="copy" />
|
<Icon size={4} icon="copy" />
|
||||||
Copy Text
|
Copy Text
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="btn btn-neutral" on:click={showInfo}>
|
<Button class="btn btn-neutral" onclick={showInfo}>
|
||||||
<Icon size={4} icon="code-2" />
|
<Icon size={4} icon="code-2" />
|
||||||
Message Details
|
Message Details
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
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"
|
||||||
@@ -13,21 +14,25 @@
|
|||||||
|
|
||||||
const onSubmit = () => goto(makeChatPath([...pubkeys, $pubkey!]))
|
const onSubmit = () => goto(makeChatPath([...pubkeys, $pubkey!]))
|
||||||
|
|
||||||
let pubkeys: string[] = []
|
let pubkeys: string[] = $state([])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" on:submit|preventDefault={onSubmit}>
|
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">Start a Chat</div>
|
{#snippet title()}
|
||||||
<div slot="info">Create an encrypted chat room for private conversations.</div>
|
<div>Start a Chat</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Create an encrypted chat room for private conversations.</div>
|
||||||
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<Field>
|
<Field>
|
||||||
<div slot="input">
|
{#snippet input()}
|
||||||
<ProfileMultiSelect autofocus bind:value={pubkeys} />
|
<ProfileMultiSelect autofocus bind:value={pubkeys} />
|
||||||
</div>
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" on:click={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Icon icon="alt-arrow-left" />
|
<Icon icon="alt-arrow-left" />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
isAddress,
|
isAddress,
|
||||||
isNewline,
|
isNewline,
|
||||||
} from "@welshman/content"
|
} from "@welshman/content"
|
||||||
|
import {preventDefault, stopPropagation} from "@lib/html"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
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"
|
||||||
@@ -30,14 +31,27 @@
|
|||||||
import ContentMention from "@app/components/ContentMention.svelte"
|
import ContentMention from "@app/components/ContentMention.svelte"
|
||||||
import {entityLink, userSettingValues} from "@app/state"
|
import {entityLink, userSettingValues} from "@app/state"
|
||||||
|
|
||||||
export let event
|
interface Props {
|
||||||
export let minLength = 500
|
event: any
|
||||||
export let maxLength = 700
|
minLength?: number
|
||||||
export let showEntire = false
|
maxLength?: number
|
||||||
export let hideMedia = false
|
showEntire?: boolean
|
||||||
export let expandMode = "block"
|
hideMediaAtDepth?: number
|
||||||
export let quoteProps: Record<string, any> = {}
|
expandMode?: string
|
||||||
export let depth = 0
|
relays?: string[]
|
||||||
|
depth?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
event,
|
||||||
|
minLength = 500,
|
||||||
|
maxLength = 700,
|
||||||
|
showEntire = $bindable(false),
|
||||||
|
hideMediaAtDepth = 1,
|
||||||
|
expandMode = "block",
|
||||||
|
relays = [],
|
||||||
|
depth = 0,
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
const fullContent = parse(event)
|
const fullContent = parse(event)
|
||||||
|
|
||||||
@@ -48,13 +62,13 @@
|
|||||||
const isBlock = (i: number) => {
|
const isBlock = (i: number) => {
|
||||||
const parsed = fullContent[i]
|
const parsed = fullContent[i]
|
||||||
|
|
||||||
if (!parsed || hideMedia) return false
|
if (!parsed || hideMediaAtDepth <= depth) return false
|
||||||
|
|
||||||
if (isLink(parsed) && $userSettingValues.show_media && isStartOrEnd(i)) {
|
if (isLink(parsed) && $userSettingValues.show_media && isStartOrEnd(i)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i) && depth < 1) {
|
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,20 +96,23 @@
|
|||||||
warning = null
|
warning = null
|
||||||
}
|
}
|
||||||
|
|
||||||
let warning =
|
let warning = $state(
|
||||||
$userSettingValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1]
|
$userSettingValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
|
||||||
|
)
|
||||||
|
|
||||||
$: shortContent = showEntire
|
const shortContent = $derived(
|
||||||
? fullContent
|
showEntire
|
||||||
: truncate(fullContent, {
|
? fullContent
|
||||||
minLength,
|
: truncate(fullContent, {
|
||||||
maxLength,
|
minLength,
|
||||||
mediaLength: hideMedia ? 20 : 200,
|
maxLength,
|
||||||
})
|
mediaLength: hideMediaAtDepth <= depth ? 20 : 200,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
$: hasEllipsis = shortContent.some(isEllipsis)
|
const hasEllipsis = $derived(shortContent.some(isEllipsis))
|
||||||
$: expandInline = hasEllipsis && expandMode === "inline"
|
const expandInline = $derived(hasEllipsis && expandMode === "inline")
|
||||||
$: expandBlock = hasEllipsis && expandMode === "block"
|
const expandBlock = $derived(hasEllipsis && expandMode === "block")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@@ -104,7 +121,7 @@
|
|||||||
<Icon icon="danger" />
|
<Icon icon="danger" />
|
||||||
<p>
|
<p>
|
||||||
This note has been flagged by the author as "{warning}".<br />
|
This note has been flagged by the author as "{warning}".<br />
|
||||||
<Button class="link" on:click={ignoreWarning}>Show anyway</Button>
|
<Button class="link" onclick={ignoreWarning}>Show anyway</Button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -112,8 +129,8 @@
|
|||||||
class="overflow-hidden text-ellipsis break-words"
|
class="overflow-hidden text-ellipsis break-words"
|
||||||
style={expandBlock ? "mask-image: linear-gradient(0deg, transparent 0px, black 100px)" : ""}>
|
style={expandBlock ? "mask-image: linear-gradient(0deg, transparent 0px, black 100px)" : ""}>
|
||||||
{#each shortContent as parsed, i}
|
{#each shortContent as parsed, i}
|
||||||
{#if isNewline(parsed)}
|
{#if isNewline(parsed) && !isBlock(i - 1)}
|
||||||
<ContentNewline value={parsed.value.slice(isBlock(i - 1) ? 1 : 0)} />
|
<ContentNewline value={parsed.value} />
|
||||||
{:else if isTopic(parsed)}
|
{:else if isTopic(parsed)}
|
||||||
<ContentTopic value={parsed.value} />
|
<ContentTopic value={parsed.value} />
|
||||||
{:else if isCode(parsed)}
|
{:else if isCode(parsed)}
|
||||||
@@ -132,11 +149,7 @@
|
|||||||
<ContentMention value={parsed.value} />
|
<ContentMention value={parsed.value} />
|
||||||
{:else if isEvent(parsed) || isAddress(parsed)}
|
{:else if isEvent(parsed) || isAddress(parsed)}
|
||||||
{#if isBlock(i)}
|
{#if isBlock(i)}
|
||||||
<ContentQuote {...quoteProps} value={parsed.value} {depth} {event}>
|
<ContentQuote {depth} {relays} {hideMediaAtDepth} value={parsed.value} {event} />
|
||||||
<div slot="note-content" let:event>
|
|
||||||
<svelte:self {quoteProps} {hideMedia} {event} depth={depth + 1} />
|
|
||||||
</div>
|
|
||||||
</ContentQuote>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Link
|
<Link
|
||||||
external
|
external
|
||||||
@@ -158,7 +171,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-neutral"
|
class="btn btn-neutral"
|
||||||
on:click|stopPropagation|preventDefault={expand}>
|
onclick={stopPropagation(preventDefault(expand))}>
|
||||||
See more
|
See more
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let value
|
const {value, isBlock} = $props()
|
||||||
export let isBlock
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<code
|
<code
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {ellipsize, postJson} from "@welshman/lib"
|
import {ellipsize, postJson} from "@welshman/lib"
|
||||||
import {dufflepud, imgproxy} from "@app/state"
|
import {dufflepud, imgproxy} from "@app/state"
|
||||||
|
import {preventDefault, stopPropagation} from "@lib/html"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let value
|
const {value} = $props()
|
||||||
|
|
||||||
|
let hideImage = $state(false)
|
||||||
|
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
|
|
||||||
@@ -19,6 +22,10 @@
|
|||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onError = () => {
|
||||||
|
hideImage = true
|
||||||
|
}
|
||||||
|
|
||||||
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
|
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -29,19 +36,20 @@
|
|||||||
<track kind="captions" />
|
<track kind="captions" />
|
||||||
</video>
|
</video>
|
||||||
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
||||||
<button type="button" on:click|stopPropagation|preventDefault={expand}>
|
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
|
||||||
<img alt="Link preview" src={imgproxy(url)} class="m-auto max-h-96" />
|
<img alt="Link preview" src={imgproxy(url)} class="m-auto max-h-96 rounded-box" />
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
{#await loadPreview()}
|
{#await loadPreview()}
|
||||||
<div class="center my-12 w-full">
|
<div class="center my-12 w-full">
|
||||||
<span class="loading loading-spinner" />
|
<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="bg-alt flex max-w-xl flex-col leading-normal">
|
||||||
{#if preview.image}
|
{#if preview.image && !hideImage}
|
||||||
<img
|
<img
|
||||||
alt="Link preview"
|
alt="Link preview"
|
||||||
|
onerror={onError}
|
||||||
src={imgproxy(preview.image)}
|
src={imgproxy(preview.image)}
|
||||||
class="bg-alt max-h-72 object-contain object-center" />
|
class="bg-alt max-h-72 object-contain object-center" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import {imgproxy} from "@app/state"
|
import {imgproxy} from "@app/state"
|
||||||
|
|
||||||
export let url
|
const {url} = $props()
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button class="m-auto h-screen w-screen cursor-pointer p-4" on:click={back}>
|
<Button class="m-auto h-screen w-screen cursor-pointer p-4" onclick={back}>
|
||||||
<img alt="" src={imgproxy(url)} class="m-auto max-h-full max-w-full rounded-box" />
|
<img alt="" src={imgproxy(url)} class="m-auto max-h-full max-w-full rounded-box" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {displayUrl} from "@welshman/lib"
|
import {displayUrl} from "@welshman/lib"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
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 ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let value
|
const {value} = $props()
|
||||||
|
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
|
|
||||||
{#if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
{#if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
||||||
<!-- Use a real link so people can copy the href -->
|
<!-- Use a real link so people can copy the href -->
|
||||||
<a href={url} class="link-content whitespace-nowrap" on:click|preventDefault={expand}>
|
<a href={url} class="link-content whitespace-nowrap" onclick={preventDefault(expand)}>
|
||||||
<Icon icon="link-round" size={3} class="inline-block" />
|
<Icon icon="link-round" size={3} class="inline-block" />
|
||||||
{displayUrl(url)}
|
{displayUrl(url)}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let value
|
const {value} = $props()
|
||||||
|
|
||||||
const profile = deriveProfile(value.pubkey)
|
const profile = deriveProfile(value.pubkey)
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button on:click={openProfile} class="link-content">
|
<Button onclick={openProfile} class="link-content">
|
||||||
@{displayProfile($profile)}
|
@{displayProfile($profile)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let value
|
const {value} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each value as _}
|
{#each value as _}
|
||||||
|
|||||||
@@ -3,18 +3,15 @@
|
|||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {ctx, nthEq} from "@welshman/lib"
|
import {ctx, nthEq} from "@welshman/lib"
|
||||||
import {tracker, repository} from "@welshman/app"
|
import {tracker, repository} from "@welshman/app"
|
||||||
import {Address, DIRECT_MESSAGE, MESSAGE, THREAD} from "@welshman/util"
|
import {Address, DIRECT_MESSAGE, MESSAGE, THREAD, EVENT_TIME} from "@welshman/util"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import NoteCard from "@app/components/NoteCard.svelte"
|
import NoteCard from "@app/components/NoteCard.svelte"
|
||||||
|
import NoteContent from "@app/components/NoteContent.svelte"
|
||||||
import {deriveEvent, entityLink, ROOM} from "@app/state"
|
import {deriveEvent, entityLink, ROOM} from "@app/state"
|
||||||
import {makeThreadPath, makeRoomPath} from "@app/routes"
|
import {makeThreadPath, makeCalendarPath, makeRoomPath} from "@app/routes"
|
||||||
|
|
||||||
export let value
|
const {value, event, depth, hideMediaAtDepth, relays = []} = $props()
|
||||||
export let event
|
|
||||||
export let depth = 0
|
|
||||||
export let relays: string[] = []
|
|
||||||
export let minimal = false
|
|
||||||
|
|
||||||
const {id, identifier, kind, pubkey, relays: relayHints = []} = value
|
const {id, identifier, kind, pubkey, relays: relayHints = []} = value
|
||||||
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
|
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
|
||||||
@@ -60,7 +57,7 @@
|
|||||||
return Boolean(event)
|
return Boolean(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClick = (e: Event) => {
|
const onclick = () => {
|
||||||
if ($quote) {
|
if ($quote) {
|
||||||
if ($quote.kind === DIRECT_MESSAGE) {
|
if ($quote.kind === DIRECT_MESSAGE) {
|
||||||
return scrollToEvent($quote.id)
|
return scrollToEvent($quote.id)
|
||||||
@@ -74,6 +71,10 @@
|
|||||||
return goto(makeThreadPath(url, $quote.id))
|
return goto(makeThreadPath(url, $quote.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($quote.kind === EVENT_TIME) {
|
||||||
|
return goto(makeCalendarPath(url, $quote.id))
|
||||||
|
}
|
||||||
|
|
||||||
if ($quote.kind === MESSAGE) {
|
if ($quote.kind === MESSAGE) {
|
||||||
return scrollToEvent($quote.id) || openMessage(url, room, $quote.id)
|
return scrollToEvent($quote.id) || openMessage(url, room, $quote.id)
|
||||||
}
|
}
|
||||||
@@ -86,6 +87,10 @@
|
|||||||
return goto(makeThreadPath(url, id))
|
return goto(makeThreadPath(url, id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parseInt(kind) === EVENT_TIME) {
|
||||||
|
return goto(makeCalendarPath(url, id))
|
||||||
|
}
|
||||||
|
|
||||||
if (parseInt(kind) === MESSAGE) {
|
if (parseInt(kind) === MESSAGE) {
|
||||||
return scrollToEvent(id) || openMessage(url, room, id)
|
return scrollToEvent(id) || openMessage(url, room, id)
|
||||||
}
|
}
|
||||||
@@ -97,10 +102,10 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button class="my-2 block max-w-full text-left" on:click={onClick}>
|
<Button class="my-2 block max-w-full text-left" {onclick}>
|
||||||
{#if $quote}
|
{#if $quote}
|
||||||
<NoteCard {minimal} event={$quote} class="bg-alt rounded-box p-4">
|
<NoteCard event={$quote} class="bg-alt rounded-box p-4">
|
||||||
<slot name="note-content" event={$quote} {depth} />
|
<NoteContent {hideMediaAtDepth} {relays} event={$quote} depth={depth + 1} />
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="rounded-box p-4">
|
<div class="rounded-box p-4">
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import {clip} from "@app/toast"
|
import {clip} from "@app/toast"
|
||||||
|
|
||||||
export let value
|
const {value} = $props()
|
||||||
|
|
||||||
const copy = () => clip(value)
|
const copy = () => clip(value)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button on:click={copy} class="link-content">
|
<Button onclick={copy} class="link-content">
|
||||||
<Icon icon="bolt" size={3} class="inline-block translate-y-px" />
|
<Icon icon="bolt" size={3} class="inline-block translate-y-px" />
|
||||||
{value.slice(0, 16)}...
|
{value.slice(0, 16)}...
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let value
|
const {value} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="link-content">
|
<span class="link-content">
|
||||||
|
|||||||
@@ -7,15 +7,14 @@
|
|||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {BURROW_URL} from "@app/state"
|
import {BURROW_URL} from "@app/state"
|
||||||
|
|
||||||
export let email
|
const {email, confirm_token} = $props()
|
||||||
export let confirm_token
|
|
||||||
|
|
||||||
const login = () => {
|
const login = () => {
|
||||||
pushModal(LogInPassword, {email}, {path: "/"})
|
pushModal(LogInPassword, {email}, {path: "/"})
|
||||||
}
|
}
|
||||||
|
|
||||||
let error: string
|
let error = $state("")
|
||||||
let loading = true
|
let loading = $state(true)
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const [res] = await Promise.all([
|
const [res] = await Promise.all([
|
||||||
@@ -49,5 +48,5 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Spinner>
|
</Spinner>
|
||||||
</p>
|
</p>
|
||||||
<Button class="btn btn-primary" on:click={login} disabled={loading}>Continue to Login</Button>
|
<Button class="btn btn-primary" onclick={login} disabled={loading}>Continue to Login</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {Instance} from "tippy.js"
|
||||||
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||||
|
import EventMenu from "@app/components/EventMenu.svelte"
|
||||||
|
import {publishReaction} from "@app/commands"
|
||||||
|
|
||||||
|
const {
|
||||||
|
url,
|
||||||
|
noun,
|
||||||
|
event,
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
noun: string
|
||||||
|
event: TrustedEvent
|
||||||
|
} = $props()
|
||||||
|
|
||||||
|
const showPopover = () => popover?.show()
|
||||||
|
|
||||||
|
const hidePopover = () => popover?.hide()
|
||||||
|
|
||||||
|
const onEmoji = (emoji: NativeEmoji) =>
|
||||||
|
publishReaction({event, content: emoji.unicode, relays: [url]})
|
||||||
|
|
||||||
|
let popover: Instance | undefined = $state()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button class="join rounded-full">
|
||||||
|
<EmojiButton {onEmoji} class="btn join-item btn-neutral btn-xs">
|
||||||
|
<Icon icon="smile-circle" size={4} />
|
||||||
|
</EmojiButton>
|
||||||
|
<Tippy
|
||||||
|
bind:popover
|
||||||
|
component={EventMenu}
|
||||||
|
props={{url, noun, event, onClick: hidePopover}}
|
||||||
|
params={{trigger: "manual", interactive: true}}>
|
||||||
|
<Button class="btn join-item btn-neutral btn-xs" onclick={showPopover}>
|
||||||
|
<Icon icon="menu-dots" size={4} />
|
||||||
|
</Button>
|
||||||
|
</Tippy>
|
||||||
|
</Button>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {max} from "@welshman/lib"
|
||||||
|
import {COMMENT} from "@welshman/util"
|
||||||
|
import {deriveEvents} from "@welshman/store"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {formatTimestampRelative, repository, load} from "@welshman/app"
|
||||||
|
import {notifications} from "@app/notifications"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
|
||||||
|
const {url, path, event}: {url: string; path: string; event: TrustedEvent} = $props()
|
||||||
|
|
||||||
|
const filters = [{kinds: [COMMENT], "#E": [event.id]}]
|
||||||
|
const replies = deriveEvents(repository, {filters})
|
||||||
|
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
load({relays: [url], filters})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full">
|
||||||
|
<Icon icon="reply" />
|
||||||
|
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
|
||||||
|
</div>
|
||||||
|
<div class="btn btn-neutral btn-xs relative hidden rounded-full sm:flex">
|
||||||
|
{#if $notifications.has(path)}
|
||||||
|
<div class="h-2 w-2 rounded-full bg-primary"></div>
|
||||||
|
{/if}
|
||||||
|
Active {formatTimestampRelative(lastActive)}
|
||||||
|
</div>
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {EditorContent} from "svelte-tiptap"
|
|
||||||
import {writable} from "svelte/store"
|
|
||||||
import {randomId} from "@welshman/lib"
|
|
||||||
import {createEvent, EVENT_TIME} from "@welshman/util"
|
|
||||||
import {publishThunk, dateToSeconds} from "@welshman/app"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Field from "@lib/components/Field.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
|
||||||
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
|
||||||
import {PROTECTED} from "@app/state"
|
|
||||||
import {makeEditor} from "@app/editor"
|
|
||||||
import {pushToast} from "@app/toast"
|
|
||||||
|
|
||||||
export let url
|
|
||||||
|
|
||||||
const uploading = writable(false)
|
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
|
|
||||||
const submit = () => {
|
|
||||||
if ($uploading) return
|
|
||||||
|
|
||||||
if (!title) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Please provide a title.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!start || !end) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Please provide start and end times.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = createEvent(EVENT_TIME, {
|
|
||||||
content: $editor.getText({blockSeparator: "\n"}).trim(),
|
|
||||||
tags: [
|
|
||||||
["d", randomId()],
|
|
||||||
["title", title],
|
|
||||||
["location", location],
|
|
||||||
["start", dateToSeconds(start).toString()],
|
|
||||||
["end", dateToSeconds(end).toString()],
|
|
||||||
...$editor.storage.nostr.getEditorTags(),
|
|
||||||
PROTECTED,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
publishThunk({event, relays: [url]})
|
|
||||||
history.back()
|
|
||||||
}
|
|
||||||
|
|
||||||
const editor = makeEditor({submit, uploading})
|
|
||||||
|
|
||||||
let title = ""
|
|
||||||
let location = ""
|
|
||||||
let start: Date
|
|
||||||
let end: Date
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<form class="column gap-4" on:submit|preventDefault={submit}>
|
|
||||||
<ModalHeader>
|
|
||||||
<div slot="title">Create an Event</div>
|
|
||||||
<div slot="info">Invite other group members to events online or in real life.</div>
|
|
||||||
</ModalHeader>
|
|
||||||
<Field>
|
|
||||||
<p slot="label">Title*</p>
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
|
||||||
<input bind:value={title} class="grow" type="text" />
|
|
||||||
</label>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<p slot="label">Summary</p>
|
|
||||||
<div
|
|
||||||
slot="input"
|
|
||||||
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">
|
|
||||||
<EditorContent editor={$editor} />
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
data-tip="Add an image"
|
|
||||||
class="center btn tooltip"
|
|
||||||
on:click={() => $editor.chain().selectFiles().run()}>
|
|
||||||
{#if $uploading}
|
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
|
||||||
{:else}
|
|
||||||
<Icon icon="gallery-send" />
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<div slot="input" class="grid grid-cols-2 gap-2">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<strong>Start</strong>
|
|
||||||
<DateTimeInput bind:value={start} />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<strong>End</strong>
|
|
||||||
<DateTimeInput bind:value={end} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<p slot="label">Location (optional)</p>
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
|
||||||
<Icon icon="map-point" />
|
|
||||||
<input bind:value={location} class="grow" type="text" />
|
|
||||||
</label>
|
|
||||||
</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">Create Event</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</form>
|
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import {publishDelete} from "@app/commands"
|
import {publishDelete} from "@app/commands"
|
||||||
import {clearModals} from "@app/modal"
|
import {clearModals} from "@app/modal"
|
||||||
|
|
||||||
export let url
|
type Props = {
|
||||||
export let event
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
const confirm = async () => {
|
const confirm = async () => {
|
||||||
await publishDelete({event, relays: [url]})
|
await publishDelete({event, relays: [url]})
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {nip19} from "nostr-tools"
|
import {nip19} from "nostr-tools"
|
||||||
import {ctx} from "@welshman/lib"
|
import {ctx} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import {clip} from "@app/toast"
|
import {clip} from "@app/toast"
|
||||||
|
|
||||||
export let event
|
type Props = {
|
||||||
|
url?: string
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
const relays = ctx.app.router.Event(event).getUrls()
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
|
const relays = url ? [url] : ctx.app.router.Event(event).getUrls()
|
||||||
const nevent1 = nip19.neventEncode({...event, relays})
|
const nevent1 = nip19.neventEncode({...event, relays})
|
||||||
const npub1 = nip19.npubEncode(event.pubkey)
|
const npub1 = nip19.npubEncode(event.pubkey)
|
||||||
const json = JSON.stringify(event, null, 2)
|
const json = JSON.stringify(event, null, 2)
|
||||||
@@ -20,36 +26,48 @@
|
|||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">Event Details</div>
|
{#snippet title()}
|
||||||
<div slot="info">The full details of this event are shown below.</div>
|
<div>Event Details</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>The full details of this event are shown below.</div>
|
||||||
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
<p slot="label">Event Link</p>
|
{#snippet label()}
|
||||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
<p>Event Link</p>
|
||||||
<Icon icon="file" />
|
{/snippet}
|
||||||
<input type="text" class="ellipsize min-w-0 grow" value={nevent1} />
|
{#snippet input()}
|
||||||
<Button on:click={copyLink} class="flex items-center">
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
<Icon icon="copy" />
|
<Icon icon="file" />
|
||||||
</Button>
|
<input type="text" class="ellipsize min-w-0 grow" value={nevent1} />
|
||||||
</label>
|
<Button onclick={copyLink} class="flex items-center">
|
||||||
|
<Icon icon="copy" />
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
<p slot="label">Author Pubkey</p>
|
{#snippet label()}
|
||||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
<p>Author Pubkey</p>
|
||||||
<Icon icon="user-circle" />
|
{/snippet}
|
||||||
<input type="text" class="ellipsize min-w-0 grow" value={npub1} />
|
{#snippet input()}
|
||||||
<Button on:click={copyPubkey} class="flex items-center">
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
<Icon icon="copy" />
|
<Icon icon="user-circle" />
|
||||||
</Button>
|
<input type="text" class="ellipsize min-w-0 grow" value={npub1} />
|
||||||
</label>
|
<Button onclick={copyPubkey} class="flex items-center">
|
||||||
|
<Icon icon="copy" />
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<pre class="card2 card2-sm bg-alt overflow-auto text-xs"><code>{json}</code></pre>
|
<pre class="card2 card2-sm bg-alt overflow-auto text-xs"><code>{json}</code></pre>
|
||||||
<p class="absolute right-2 top-2 flex flex-grow items-center justify-between">
|
<p class="absolute right-2 top-2 flex flex-grow items-center justify-between">
|
||||||
<Button on:click={copyJson} class="btn btn-neutral btn-sm flex items-center">
|
<Button onclick={copyJson} class="btn btn-neutral btn-sm flex items-center">
|
||||||
<Icon icon="copy" /> Copy
|
<Icon icon="copy" /> Copy
|
||||||
</Button>
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
|
<Button class="btn btn-primary" onclick={() => history.back()}>Got it</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {fromPairs} from "@welshman/lib"
|
|
||||||
import {formatTimestamp, formatTimestampAsDate, formatTimestampAsTime} from "@welshman/app"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
|
|
||||||
export let event
|
|
||||||
|
|
||||||
$: meta = fromPairs(event.tags) as Record<string, string>
|
|
||||||
$: end = parseInt(meta.end)
|
|
||||||
$: start = parseInt(meta.start)
|
|
||||||
$: startDateDisplay = formatTimestampAsDate(start)
|
|
||||||
$: endDateDisplay = formatTimestampAsDate(end)
|
|
||||||
$: isSingleDay = startDateDisplay === endDateDisplay
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="card2 flex items-center justify-between gap-2">
|
|
||||||
<span>{meta.title || meta.name}</span>
|
|
||||||
<div class="flex items-center gap-2 text-sm">
|
|
||||||
<Icon icon="clock-circle" size={4} />
|
|
||||||
{formatTimestampAsTime(start)} — {isSingleDay
|
|
||||||
? formatTimestampAsTime(end)
|
|
||||||
: formatTimestamp(end)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,17 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {COMMENT} from "@welshman/util"
|
import {COMMENT} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
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 EventReport from "@app/components/EventReport.svelte"
|
||||||
import ThreadShare from "@app/components/ThreadShare.svelte"
|
import EventShare from "@app/components/EventShare.svelte"
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let url
|
const {
|
||||||
export let event
|
url,
|
||||||
export let onClick
|
noun,
|
||||||
|
event,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
noun: string
|
||||||
|
event: TrustedEvent
|
||||||
|
onClick: () => void
|
||||||
|
} = $props()
|
||||||
|
|
||||||
const isRoot = event.kind !== COMMENT
|
const isRoot = event.kind !== COMMENT
|
||||||
|
|
||||||
@@ -22,45 +31,45 @@
|
|||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(EventInfo, {event})
|
pushModal(EventInfo, {url, event})
|
||||||
}
|
}
|
||||||
|
|
||||||
const share = () => {
|
const share = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(ThreadShare, {url, event})
|
pushModal(EventShare, {url, event})
|
||||||
}
|
}
|
||||||
|
|
||||||
const showDelete = () => {
|
const showDelete = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(ConfirmDelete, {url, event})
|
pushModal(EventDeleteConfirm, {url, event})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl">
|
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl">
|
||||||
{#if isRoot}
|
{#if isRoot}
|
||||||
<li>
|
<li>
|
||||||
<Button on:click={share}>
|
<Button onclick={share}>
|
||||||
<Icon size={4} icon="share-circle" />
|
<Icon size={4} icon="share-circle" />
|
||||||
Share to Chat
|
Share to Chat
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
<li>
|
<li>
|
||||||
<Button on:click={showInfo}>
|
<Button onclick={showInfo}>
|
||||||
<Icon size={4} icon="code-2" />
|
<Icon size={4} icon="code-2" />
|
||||||
Message Details
|
{noun} Details
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{#if event.pubkey === $pubkey}
|
{#if event.pubkey === $pubkey}
|
||||||
<li>
|
<li>
|
||||||
<Button on:click={showDelete} class="text-error">
|
<Button onclick={showDelete} class="text-error">
|
||||||
<Icon size={4} icon="trash-bin-2" />
|
<Icon size={4} icon="trash-bin-2" />
|
||||||
Delete Message
|
Delete {noun}
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li>
|
<li>
|
||||||
<Button class="text-error" on:click={report}>
|
<Button class="text-error" onclick={report}>
|
||||||
<Icon size={4} icon="danger" />
|
<Icon size={4} icon="danger" />
|
||||||
Report Content
|
Report Content
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1,28 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {EditorContent} from "svelte-tiptap"
|
import {isMobile, preventDefault} 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"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import EditorContent from "@app/editor/EditorContent.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 {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
export let url
|
const {url, event, onClose, onSubmit} = $props()
|
||||||
export let event
|
|
||||||
export let onClose
|
|
||||||
export let onSubmit
|
|
||||||
|
|
||||||
const uploading = writable(false)
|
const uploading = writable(false)
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if ($uploading) return
|
if ($uploading) return
|
||||||
|
|
||||||
const content = $editor.getText({blockSeparator: "\n"}).trim()
|
const content = editor.getText({blockSeparator: "\n"}).trim()
|
||||||
const tags = [...$editor.storage.nostr.getEditorTags(), tagRoom(GENERAL, url), PROTECTED]
|
const tags = [...editor.storage.nostr.getEditorTags(), tagRoom(GENERAL, url), PROTECTED]
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -40,16 +37,16 @@
|
|||||||
<form
|
<form
|
||||||
in:fly
|
in:fly
|
||||||
out:slideAndFade
|
out:slideAndFade
|
||||||
on:submit|preventDefault={submit}
|
onsubmit={preventDefault(submit)}
|
||||||
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">
|
||||||
<EditorContent editor={$editor} />
|
<EditorContent {editor} />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
data-tip="Add an image"
|
data-tip="Add an image"
|
||||||
class="tooltip tooltip-left absolute bottom-1 right-2"
|
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||||
on:click={$editor.commands.selectFiles}>
|
onclick={editor.commands.selectFiles}>
|
||||||
{#if $uploading}
|
{#if $uploading}
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -58,7 +55,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" on:click={onClose}>Cancel</Button>
|
<Button class="btn btn-link" onclick={onClose}>Cancel</Button>
|
||||||
<Button type="submit" class="btn btn-primary">Post Reply</Button>
|
<Button type="submit" class="btn btn-primary">Post Reply</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</form>
|
</form>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
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"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
@@ -8,8 +9,7 @@
|
|||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {publishReport} from "@app/commands"
|
import {publishReport} from "@app/commands"
|
||||||
|
|
||||||
export let url
|
const {url, event} = $props()
|
||||||
export let event
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
@@ -31,37 +31,53 @@
|
|||||||
return pushToast({message: "Your report has been sent!"})
|
return pushToast({message: "Your report has been sent!"})
|
||||||
}
|
}
|
||||||
|
|
||||||
let reason = ""
|
let reason = $state("")
|
||||||
let content = ""
|
let content = $state("")
|
||||||
let loading = false
|
let loading = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" on:submit|preventDefault={confirm}>
|
<form class="column gap-4" onsubmit={preventDefault(confirm)}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">Report Content</div>
|
{#snippet title()}
|
||||||
<div slot="info">Flag inappropriate content.</div>
|
<div>Report Content</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Flag inappropriate content.</div>
|
||||||
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<Field>
|
<Field>
|
||||||
<p slot="label">Reason*</p>
|
{#snippet label()}
|
||||||
<select slot="input" class="select select-bordered" bind:value={reason}>
|
<p>Reason*</p>
|
||||||
<option disabled selected>Choose a reason</option>
|
{/snippet}
|
||||||
<option>Nudity</option>
|
{#snippet input()}
|
||||||
<option>Malware</option>
|
<select class="select select-bordered" bind:value={reason}>
|
||||||
<option>Profanity</option>
|
<option disabled selected>Choose a reason</option>
|
||||||
<option>Illegal</option>
|
<option>Nudity</option>
|
||||||
<option>Spam</option>
|
<option>Malware</option>
|
||||||
<option>Impersonation</option>
|
<option>Profanity</option>
|
||||||
<option>Other</option>
|
<option>Illegal</option>
|
||||||
</select>
|
<option>Spam</option>
|
||||||
<p slot="info">Please select a reason for your report.</p>
|
<option>Impersonation</option>
|
||||||
|
<option>Other</option>
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>Please select a reason for your report.</p>
|
||||||
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<p slot="label">Details</p>
|
{#snippet label()}
|
||||||
<textarea slot="input" class="textarea textarea-bordered" bind:value={content} />
|
<p>Details</p>
|
||||||
<p slot="info">Please provide any additional details relevant to your report.</p>
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<textarea class="textarea textarea-bordered" bind:value={content}></textarea>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>Please provide any additional details relevant to your report.</p>
|
||||||
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" on:click={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Icon icon="alt-arrow-left" />
|
<Icon icon="alt-arrow-left" />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import {publishDelete} from "@app/commands"
|
import {publishDelete} from "@app/commands"
|
||||||
|
|
||||||
export let url
|
const {url, event} = $props()
|
||||||
export let event
|
|
||||||
|
|
||||||
const reports = deriveEvents(repository, {
|
const reports = deriveEvents(repository, {
|
||||||
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
||||||
@@ -30,8 +29,12 @@
|
|||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">Report Details</div>
|
{#snippet title()}
|
||||||
<div slot="info">All reports for this event are shown below.</div>
|
<div>Report Details</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>All reports for this event are shown below.</div>
|
||||||
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
{#each $reports as report (report.id)}
|
{#each $reports as report (report.id)}
|
||||||
{@const reason = getReason(report.tags)}
|
{@const reason = getReason(report.tags)}
|
||||||
@@ -43,7 +46,7 @@
|
|||||||
<span>Reported this event as "{reason}"</span>
|
<span>Reported this event as "{reason}"</span>
|
||||||
</div>
|
</div>
|
||||||
{#if report.pubkey === $pubkey}
|
{#if report.pubkey === $pubkey}
|
||||||
<Button class="btn-default btn" on:click={remove}>Delete Report</Button>
|
<Button class="btn-default btn" onclick={remove}>Delete Report</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if report.content}
|
{#if report.content}
|
||||||
@@ -51,5 +54,5 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<Button class="btn btn-primary" on:click={back}>Got it</Button>
|
<Button class="btn btn-primary" onclick={back}>Got it</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {nip19} from "nostr-tools"
|
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {ctx} from "@welshman/lib"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {toNostrURI} from "@welshman/util"
|
import {preventDefault} 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 ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
@@ -12,16 +11,12 @@
|
|||||||
import {makeRoomPath} from "@app/routes"
|
import {makeRoomPath} from "@app/routes"
|
||||||
import {setKey} from "@app/implicit"
|
import {setKey} from "@app/implicit"
|
||||||
|
|
||||||
export let url
|
const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props()
|
||||||
export let event
|
|
||||||
|
|
||||||
const relays = ctx.app.router.Event(event).getUrls()
|
|
||||||
const nevent = nip19.neventEncode({id: event.id, relays})
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
setKey("content", toNostrURI(nevent))
|
setKey("share", event)
|
||||||
goto(makeRoomPath(url, selection), {replaceState: true})
|
goto(makeRoomPath(url, selection), {replaceState: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,13 +24,17 @@
|
|||||||
selection = room === selection ? "" : room
|
selection = room === selection ? "" : room
|
||||||
}
|
}
|
||||||
|
|
||||||
let selection = ""
|
let selection = $state("")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" on:submit|preventDefault={onSubmit}>
|
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">Share Thread</div>
|
{#snippet title()}
|
||||||
<div slot="info">Which room would you like to share this thread to?</div>
|
<div>Share {noun}</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Which room would you like to share this event to?</div>
|
||||||
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
{#each $channelsByUrl.get(url) || [] as channel (channel.room)}
|
{#each $channelsByUrl.get(url) || [] as channel (channel.room)}
|
||||||
@@ -44,18 +43,18 @@
|
|||||||
class="btn"
|
class="btn"
|
||||||
class:btn-neutral={selection !== channel.room}
|
class:btn-neutral={selection !== channel.room}
|
||||||
class:btn-primary={selection === channel.room}
|
class:btn-primary={selection === channel.room}
|
||||||
on:click={() => toggleRoom(channel.room)}>
|
onclick={() => toggleRoom(channel.room)}>
|
||||||
#<ChannelName {...channel} />
|
#<ChannelName {...channel} />
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" on:click={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Icon icon="alt-arrow-left" />
|
<Icon icon="alt-arrow-left" />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary" disabled={!selection}>
|
<Button type="submit" class="btn btn-primary" disabled={!selection}>
|
||||||
Share Thread
|
Share {noun}
|
||||||
<Icon icon="alt-arrow-right" />
|
<Icon icon="alt-arrow-right" />
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
@@ -7,7 +7,9 @@
|
|||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">What is a bunker link?</div>
|
{#snippet title()}
|
||||||
|
<div>What is a bunker link?</div>
|
||||||
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>
|
<p>
|
||||||
<Link external class="link" href="https://nostr.com/">Nostr</Link> uses "keys" instead of passwords
|
<Link external class="link" href="https://nostr.com/">Nostr</Link> uses "keys" instead of passwords
|
||||||
@@ -33,5 +35,5 @@
|
|||||||
href="https://nostrapps.com#signers">nostrapps.com</Link
|
href="https://nostrapps.com#signers">nostrapps.com</Link
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
|
<Button class="btn btn-primary" onclick={() => history.back()}>Got it</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">What is a nostr address?</div>
|
{#snippet title()}
|
||||||
|
<div>What is a nostr address?</div>
|
||||||
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>
|
<p>
|
||||||
{PLATFORM_NAME} hosts spaces on the <Link external href="https://nostr.com/" class="underline"
|
{PLATFORM_NAME} hosts spaces on the <Link external href="https://nostr.com/" class="underline"
|
||||||
@@ -23,5 +25,5 @@
|
|||||||
class="underline">nostr.how</Link
|
class="underline">nostr.how</Link
|
||||||
>.
|
>.
|
||||||
</p>
|
</p>
|
||||||
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
|
<Button class="btn btn-primary" onclick={() => history.back()}>Got it</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">What is a private key?</div>
|
{#snippet title()}
|
||||||
|
<div>What is a private key?</div>
|
||||||
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>
|
<p>
|
||||||
Most online services keep track of users by giving them a username and password. This gives the
|
Most online services keep track of users by giving them a username and password. This gives the
|
||||||
@@ -36,16 +38,16 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>If you'd like to switch to self-custody, please click below to get started.</p>
|
<p>If you'd like to switch to self-custody, please click below to get started.</p>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" on:click={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Icon icon="alt-arrow-left" />
|
<Icon icon="alt-arrow-left" />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="btn btn-primary" on:click={startEject}>
|
<Button class="btn btn-primary" onclick={startEject}>
|
||||||
<Icon icon="check-circle" />
|
<Icon icon="check-circle" />
|
||||||
I want to hold my own keys
|
I want to hold my own keys
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
{:else}
|
{:else}
|
||||||
<Button class="btn btn-primary" on:click={back}>Got it</Button>
|
<Button class="btn btn-primary" onclick={back}>Got it</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">What is nostr?</div>
|
{#snippet title()}
|
||||||
|
<div>What is nostr?</div>
|
||||||
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>
|
<p>
|
||||||
<Link external href="https://nostr.com/" class="link">Nostr</Link> is way to build social apps that
|
<Link external href="https://nostr.com/" class="link">Nostr</Link> is way to build social apps that
|
||||||
@@ -24,5 +26,5 @@
|
|||||||
To learn more about how to manage your keys, or to set up an account, try
|
To learn more about how to manage your keys, or to set up an account, try
|
||||||
<Link external class="link" href="https://nosta.me/">nosta.me</Link>.
|
<Link external class="link" href="https://nosta.me/">nosta.me</Link>.
|
||||||
</p>
|
</p>
|
||||||
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
|
<Button class="btn btn-primary" onclick={() => history.back()}>Got it</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<div slot="title">What is a relay?</div>
|
{#snippet title()}
|
||||||
|
<div>What is a relay?</div>
|
||||||
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>
|
<p>
|
||||||
{PLATFORM_NAME} hosts spaces on the <Link external href="https://nostr.com/" class="underline"
|
{PLATFORM_NAME} hosts spaces on the <Link external href="https://nostr.com/" class="underline"
|
||||||
@@ -20,5 +22,5 @@
|
|||||||
Different relays have different policies for access control and content retention. Be sure to
|
Different relays have different policies for access control and content retention. Be sure to
|
||||||
double check that you have access to the relays you try to use by visiting their website.
|
double check that you have access to the relays you try to use by visiting their website.
|
||||||
</p>
|
</p>
|
||||||
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
|
<Button class="btn btn-primary" onclick={() => history.back()}>Got it</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||