Compare commits

...

60 Commits

Author SHA1 Message Date
Jon Staab 27d9d4fff1 Update changelog 2025-02-13 16:32:34 -08:00
Jon Staab c089812363 Show spinner when joining room 2025-02-13 15:34:14 -08:00
Jon Staab 07dd1e97dc Fix long-running subscriptions clogging things up 2025-02-13 14:50:03 -08:00
Jon Staab 7f6a1bff34 Re-work threads page, fix some iphone bugs 2025-02-13 10:52:00 -08:00
Jon Staab 7d1310722a Factor out calendar event component, render calendar event notes better 2025-02-11 16:00:14 -08:00
Jon Staab cb57710654 Clean up quotes/depth 2025-02-11 15:37:55 -08:00
Jon Staab c74c116667 Fix page bar margin #112 2025-02-11 15:16:24 -08:00
Jon Staab 0ba55f2387 Attempt to fix new messages button #114 2025-02-11 11:49:17 -08:00
Jon Staab 622214713b replace state when navigating from space menu 2025-02-11 11:42:49 -08:00
Jon Staab d8cf48381b Build before other stuff 2025-02-06 12:52:42 -08:00
Jon Staab 7dc7b5abeb Bump android version 2025-02-06 12:43:18 -08:00
Jon Staab 324db6a9e8 Bump welshman 2025-02-06 12:25:55 -08:00
Jon Staab 466541caf5 Fix marker in chat 2025-02-06 11:39:51 -08:00
Jon Staab 19f657e348 Make sharing indicator nicer 2025-02-06 11:33:01 -08:00
Jon Staab 98a0511b34 Update changelog 2025-02-06 11:17:32 -08:00
Jon Staab 0ec620dff9 Make calendar event detail nice 2025-02-06 11:12:15 -08:00
Jon Staab 1301c2c74f Rename ThreadReply to EventReply 2025-02-06 10:24:31 -08:00
Jon Staab 7848859153 Make event menu/share generic 2025-02-06 10:14:29 -08:00
Jon Staab 2d67a9bcf6 Add shared EventActions component 2025-02-06 10:00:25 -08:00
Jon Staab a7e9318819 Add shared EventActivity component 2025-02-06 09:53:50 -08:00
Jon Staab d66371d573 Break out a few shared sub-components 2025-02-06 09:39:23 -08:00
Jon Staab 5684d1a9cf Add calendar actions, menus, etc 2025-02-06 09:29:30 -08:00
Jon Staab fa4bc6894f Fix a couple calendar bugs 2025-02-06 08:55:01 -08:00
Jon Staab 72919cb1c2 Derive all the things 2025-02-06 08:50:50 -08:00
Jon Staab 6a3a02bc34 Handle scrolling on calendar 2025-02-05 17:05:41 -08:00
Jon Staab db69c56f57 Add makeCalendarFeed 2025-02-05 16:26:22 -08:00
Jon Staab a0c6e46184 Use unix days instead of time hashes 2025-02-05 15:15:50 -08:00
Jon Staab 65aabf5feb Rework datetime input 2025-02-05 15:05:58 -08:00
Jon Staab 131cc99c47 Flesh out EventItem 2025-02-05 13:02:51 -08:00
Jon Staab 5909b593ab Fix bugs, add timehash 2025-02-05 10:47:56 -08:00
Jon Staab f0b2b7c8b3 Re-work datetime input 2025-02-05 08:57:31 -08:00
Jon Staab 24a7fa4174 Add calendar to navigation 2025-02-05 08:55:39 -08:00
Jon Staab 3f2813b63b Add missing editor content 2025-02-05 08:54:04 -08:00
Jon Staab 3e214881a3 Fix warning, hide images in quotes 2025-02-05 08:53:26 -08:00
Jon Staab af171bd2c9 Move EditorContent to editor directory 2025-02-05 08:17:51 -08:00
Jon Staab 565ccb399a Remove editor, use welshman editor again 2025-02-04 20:29:12 -08:00
Jon Staab fd99866b1e Replace svelte components with node views 2025-02-04 20:01:36 -08:00
Jon Staab 506276f594 Fix suggestions component 2025-02-04 19:39:26 -08:00
Jon Staab d4df23545d Re-write suggestions 2025-02-04 19:00:48 -08:00
Jon Staab e53d2eb8da Small bugs/copy changes 2025-02-04 14:21:21 -08:00
Jon Staab 22cbb9fe1c Handle thunks in feeds 2025-02-04 14:06:05 -08:00
Jon Staab fedc99b0f0 Create new EditorContent component 2025-02-03 20:57:47 -08:00
Jon Staab 7d4ba6c806 Use snapshots in some places 2025-02-03 20:43:18 -08:00
Jon Staab a0e97d5e5b Finish svelte 5 migration 2025-02-03 19:28:29 -08:00
Jon Staab 24045a7e2a Fix more stuff, particularly event handlers 2025-02-03 17:21:46 -08:00
Jon Staab 8d3433b167 Migrate more stuff 2025-02-03 16:37:14 -08:00
Jon Staab 0f705c459a Fix some small issues 2025-02-03 15:50:19 -08:00
Jon Staab 08ee07d157 Fix some type errors 2025-02-03 15:40:00 -08:00
Jon Staab cfbff94b4c Fix self-closing tags 2025-02-03 15:01:42 -08:00
Jon Staab 34477e8ea6 Upgrade to svelte 5 2025-02-03 14:36:09 -08:00
Jon Staab eab0ea4eef Upload sourcemaps by hash 2025-02-03 12:37:17 -08:00
Jon Staab 8ec4d9c548 Update lockfile 2025-02-03 09:12:14 -08:00
Jon Staab 9defe20f91 Fix signer plugin 2025-02-03 09:01:41 -08:00
Jon Staab 614cdcdf53 Fix p-tagging dms 2025-02-03 08:59:45 -08:00
Jon Staab bcd94ee75e Make ios with notches prettier 2025-01-31 17:40:14 -08:00
Jon Staab def6de321c Some ios stuff 2025-01-31 17:09:46 -08:00
Jon Staab 6c7c533637 Fix QR code on iphone 2025-01-31 14:14:42 -08:00
Jon Staab f0207b35d0 Update capacitor, get ios running 2025-01-31 13:14:29 -08:00
Jon Staab 858e04d7fa tiny change 2025-01-31 09:36:46 -08:00
Jon Staab b7dcb77378 Rename njump -> nstart 2025-01-29 07:52:24 -08:00
216 changed files with 5645 additions and 3065 deletions
+3
View File
@@ -1,3 +1,6 @@
--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
+2
View File
@@ -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
+50 -15
View File
@@ -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,53 @@ 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/
# IDEs and editors
.idea/
.vscode/
# OS generated
.DS_Store
Thumbs.db
+1
View File
@@ -0,0 +1 @@
lts/jod
+19
View File
@@ -1,5 +1,24 @@
# Changelog # Changelog
# 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 # 0.2.6
* Add reply to long-press menu * Add reply to long-press menu
+2 -2
View File
@@ -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 6 versionCode 9
versionName "0.2.6" versionName "0.2.7"
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 -2
View File
@@ -2,8 +2,8 @@
android { android {
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_21
} }
} }
+1 -1
View File
@@ -8,7 +8,7 @@
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"
+1 -1
View File
@@ -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
Binary file not shown.
+1 -1
View File
@@ -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
+13 -9
View File
@@ -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" \
+12 -10
View File
@@ -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
+10 -10
View File
@@ -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'
} }
+6 -1
View File
@@ -11,7 +11,12 @@ const config: CapacitorConfig = {
SplashScreen: { SplashScreen: {
androidSplashResourceName: "splash" androidSplashResourceName: "splash"
} }
} },
// 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;
+13
View File
@@ -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
+420
View File
@@ -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 = 2;
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.8;
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 = 2;
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.8;
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 */;
}
+49
View File
@@ -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)
}
}
Binary file not shown.

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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

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>
+19
View File
@@ -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>
+53
View File
@@ -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>
+25
View File
@@ -0,0 +1,25 @@
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 '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
+1109 -960
View File
File diff suppressed because it is too large Load Diff
+22 -24
View File
@@ -1,49 +1,47 @@
{ {
"name": "flotilla", "name": "flotilla",
"version": "0.2.6", "version": "0.2.8",
"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",
"@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 +50,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.42",
"@welshman/content": "~0.0.16", "@welshman/content": "~0.0.18",
"@welshman/dvm": "~0.0.14", "@welshman/dvm": "~0.0.14",
"@welshman/editor": "~0.0.10", "@welshman/editor": "~0.0.15",
"@welshman/feeds": "~0.0.30", "@welshman/feeds": "~0.0.30",
"@welshman/lib": "~0.0.38", "@welshman/lib": "~0.0.41",
"@welshman/net": "~0.0.46", "@welshman/net": "~0.0.47",
"@welshman/signer": "~0.0.20", "@welshman/signer": "~0.0.20",
"@welshman/store": "~0.0.15", "@welshman/store": "~0.0.16",
"@welshman/util": "~0.0.59", "@welshman/util": "~0.0.61",
"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 +67,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"
Executable
+15
View File
@@ -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
+70 -3
View File
@@ -2,6 +2,8 @@
@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 +40,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 +54,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,
@@ -236,11 +294,20 @@
/* 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 */
+1 -1
View File
@@ -2,7 +2,7 @@
<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="viewport-fit=cover, width=device-width, initial-scale=1.0" />
<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}" />
+8 -1
View File
@@ -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}
+19 -21
View File
@@ -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>
+18 -6
View File
@@ -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} hideMedia 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>
+16 -12
View File
@@ -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())
@@ -61,16 +66,16 @@
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}
@@ -96,11 +101,10 @@
</div> </div>
<button <button
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all" class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
class:group-hover:opacity-100={!isMobile} class:group-hover:opacity-100={!isMobile}>
on:click|stopPropagation>
<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}
@@ -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 -6
View File
@@ -7,9 +7,7 @@
import ConfirmDelete from "@app/components/ConfirmDelete.svelte" import ConfirmDelete from "@app/components/ConfirmDelete.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()
@@ -29,21 +27,21 @@
<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
@@ -9,9 +9,7 @@
import {publishReaction} from "@app/commands" import {publishReaction} from "@app/commands"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
export let url const {url, event, reply} = $props()
export let event
export let reply
const onEmoji = (emoji: NativeEmoji) => { const onEmoji = (emoji: NativeEmoji) => {
history.back() history.back()
@@ -31,20 +29,20 @@
</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={sendReply}> <Button class="btn btn-neutral w-full" onclick={sendReply}>
<Icon size={4} icon="reply" /> <Icon size={4} icon="reply" />
Send Reply Send Reply
</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>
{#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 -2
View File
@@ -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}
+89 -73
View File
@@ -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 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
$: { const elements = $derived.by(() => {
elements = [] const elements = []
let previousDate let previousDate
let previousPubkey let previousPubkey
@@ -102,8 +100,8 @@
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
@@ -118,50 +116,54 @@
<div class="relative flex h-full w-full flex-col"> <div class="relative flex h-full w-full flex-col">
{#if others.length > 0} {#if others.length > 0}
<PageBar> <PageBar>
<div slot="title" class="flex flex-col gap-1 sm:flex-row sm:gap-2"> {#snippet title()}
{#if others.length === 1} <div class="flex flex-col gap-1 sm:flex-row sm:gap-2">
{@const pubkey = others[0]} {#if others.length === 1}
{@const onClick = () => pushModal(ProfileDetail, {pubkey})} {@const pubkey = others[0]}
<Button on:click={onClick} class="row-2"> {@const onClick = () => pushModal(ProfileDetail, {pubkey})}
<ProfileCircle {pubkey} size={5} /> <Button onclick={onClick} class="row-2">
<ProfileName {pubkey} /> <ProfileCircle {pubkey} size={5} />
</Button> <ProfileName {pubkey} />
{:else} </Button>
<div class="flex items-center gap-2"> {:else}
<ProfileCircles pubkeys={others} size={5} /> <div class="flex items-center gap-2">
<p class="overflow-hidden text-ellipsis whitespace-nowrap"> <ProfileCircles pubkeys={others} size={5} />
<ProfileName pubkey={others[0]} /> <p class="overflow-hidden text-ellipsis whitespace-nowrap">
and <ProfileName pubkey={others[0]} />
{#if others.length === 2} and
<ProfileName pubkey={others[1]} /> {#if others.length === 2}
{:else} <ProfileName pubkey={others[1]} />
{others.length - 1} {:else}
{others.length > 2 ? "others" : "other"} {others.length - 1}
{/if} {others.length > 2 ? "others" : "other"}
</p> {/if}
</div> </p>
{#if others.length > 2} </div>
<Button on:click={showMembers} class="btn btn-link hidden sm:block" {#if others.length > 2}
>Show all members</Button> <Button onclick={showMembers} class="btn btn-link hidden sm:block"
>Show all members</Button>
{/if}
{/if} {/if}
{/if} </div>
</div> {/snippet}
<div slot="action"> {#snippet action()}
{#if remove($pubkey, $missingInboxes).length > 0} <div>
{@const count = remove($pubkey, $missingInboxes).length} {#if remove($pubkey, missingInboxes).length > 0}
{@const label = count > 1 ? "inboxes are" : "inbox is"} {@const count = remove($pubkey, missingInboxes).length}
<div {@const label = count > 1 ? "inboxes are" : "inbox is"}
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer" <div
data-tip="{count} {label} not configured."> class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
<Icon icon="danger" /> data-tip="{count} {label} not configured.">
{count} <Icon icon="danger" />
</div> {count}
{/if} </div>
</div> {/if}
</div>
{/snippet}
</PageBar> </PageBar>
{/if} {/if}
<div class="-mt-2 flex flex-grow flex-col-reverse overflow-auto py-2"> <div class="-mt-2 flex flex-grow flex-col-reverse overflow-auto py-2">
{#if $missingInboxes.includes(assertNotNil($pubkey))} {#if missingInboxes.includes($pubkey!)}
<div class="py-12"> <div class="py-12">
<div class="card2 col-2 m-auto max-w-md items-center text-center"> <div class="card2 col-2 m-auto max-w-md items-center text-center">
<p class="row-2 text-lg text-error"> <p class="row-2 text-lg text-error">
@@ -175,6 +177,20 @@
</p> </p>
</div> </div>
</div> </div>
{:else if missingInboxes.length > 0}
<div class="py-12">
<div class="card2 col-2 m-auto max-w-md items-center text-center">
<p class="row-2 text-lg text-error">
<Icon icon="danger" />
{missingInboxes.length}
{missingInboxes.length > 1 ? "inboxes are" : "inbox is"} not configured.
</p>
<p>
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
sure everyone in this conversation has set up their inbox relays.
</p>
</div>
</div>
{/if} {/if}
{#each elements as { type, id, value, showPubkey } (id)} {#each elements as { type, id, value, showPubkey } (id)}
{#if type === "date"} {#if type === "date"}
@@ -192,11 +208,11 @@
End of message history End of message history
{/if} {/if}
</Spinner> </Spinner>
<slot name="info" /> {@render info?.()}
</p> </p>
</div> </div>
{#if parent} {#if parent}
<ChatComposeParent event={parent} clear={clearParent} /> <ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
{/if} {/if}
<ChatCompose bind:this={compose} {onSubmit} /> <ChatCompose bind:this={compose} {onSubmit} />
</div> </div>
+11 -6
View File
@@ -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>
+15 -10
View File
@@ -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>
+2 -2
View File
@@ -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>
+15 -14
View File
@@ -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?: any
export let showPubkey = false pubkeys: string[]
showPubkey?: boolean
}
const {event, replyTo = undefined, 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
@@ -49,14 +53,14 @@
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}
@@ -86,7 +90,7 @@
type="button" type="button"
class="opacity-0 transition-all" class="opacity-0 transition-all"
class:group-hover:opacity-100={!isMobile} class:group-hover:opacity-100={!isMobile}
on:click={togglePopover}> onclick={togglePopover}>
<Icon icon="menu-dots" size={4} /> <Icon icon="menu-dots" size={4} />
</button> </button>
</Tippy> </Tippy>
@@ -97,16 +101,13 @@
{#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>
@@ -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})
+3 -6
View File
@@ -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>
@@ -8,8 +8,7 @@
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {clip} from "@app/toast" import {clip} from "@app/toast"
export let event const {event, pubkeys} = $props()
export let pubkeys
const onEmoji = (emoji: NativeEmoji) => { const onEmoji = (emoji: NativeEmoji) => {
history.back() history.back()
@@ -27,15 +26,15 @@
</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={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>
+12 -7
View File
@@ -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>
+4 -3
View File
@@ -3,11 +3,12 @@
import {publishDelete} from "@app/commands" import {publishDelete} from "@app/commands"
import {clearModals} from "@app/modal" import {clearModals} from "@app/modal"
export let url const {url, event} = $props()
export let event
const confirm = async () => { const confirm = async () => {
await publishDelete({event, relays: [url]}) const snapshot = $state.snapshot(event)
await publishDelete({event: snapshot, relays: [url]})
clearModals() clearModals()
} }
+44 -31
View File
@@ -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 -2
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
export let value const {value, isBlock} = $props()
export let isBlock
</script> </script>
<code <code
+12 -4
View File
@@ -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 rounded-box" /> <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 -2
View File
@@ -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>
+3 -2
View File
@@ -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>
+2 -2
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
export let value const {value} = $props()
</script> </script>
{#each value as _} {#each value as _}
+16 -11
View File
@@ -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">
+2 -2
View File
@@ -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 -1
View File
@@ -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">
+4 -5
View File
@@ -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>
+45
View File
@@ -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>
+31
View File
@@ -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>
-122
View File
@@ -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>
+33 -21
View File
@@ -7,7 +7,7 @@
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 const {event} = $props()
const relays = ctx.app.router.Event(event).getUrls() const relays = ctx.app.router.Event(event).getUrls()
const nevent1 = nip19.neventEncode({...event, relays}) const nevent1 = nip19.neventEncode({...event, relays})
@@ -20,36 +20,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>
-24
View File
@@ -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 ConfirmDelete from "@app/components/ConfirmDelete.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
@@ -27,7 +36,7 @@
const share = () => { const share = () => {
onClick() onClick()
pushModal(ThreadShare, {url, event}) pushModal(EventShare, {url, event})
} }
const showDelete = () => { const showDelete = () => {
@@ -39,28 +48,28 @@
<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>
+40 -24
View File
@@ -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>
+9 -6
View File
@@ -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>
+4 -2
View File
@@ -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>
+4 -2
View File
@@ -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>
+6 -4
View File
@@ -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>
+4 -2
View File
@@ -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>
+4 -2
View File
@@ -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>
+20 -8
View File
@@ -20,18 +20,30 @@
<h1 class="heading">Welcome to {PLATFORM_NAME}!</h1> <h1 class="heading">Welcome to {PLATFORM_NAME}!</h1>
<p class="text-center">The chat app built for self-hosted communities.</p> <p class="text-center">The chat app built for self-hosted communities.</p>
</div> </div>
<Button on:click={logIn}> <Button onclick={logIn}>
<CardButton class="!btn-primary"> <CardButton class="!btn-primary">
<div slot="icon"><Icon icon="login-2" size={7} /></div> {#snippet icon()}
<div slot="title">Log in</div> <div><Icon icon="login-2" size={7} /></div>
<div slot="info">If you've been here before, you know the drill.</div> {/snippet}
{#snippet title()}
<div>Log in</div>
{/snippet}
{#snippet info()}
<div>If you've been here before, you know the drill.</div>
{/snippet}
</CardButton> </CardButton>
</Button> </Button>
<Button on:click={signUp}> <Button onclick={signUp}>
<CardButton> <CardButton>
<div slot="icon"><Icon icon="add-circle" size={7} /></div> {#snippet icon()}
<div slot="title">Create an account</div> <div><Icon icon="add-circle" size={7} /></div>
<div slot="info">Just a few questions and you'll be on your way.</div> {/snippet}
{#snippet title()}
<div>Create an account</div>
{/snippet}
{#snippet info()}
<div>Just a few questions and you'll be on your way.</div>
{/snippet}
</CardButton> </CardButton>
</Button> </Button>
<p class="text-center text-xs opacity-75"> <p class="text-center text-xs opacity-75">
+52 -50
View File
@@ -16,20 +16,13 @@
import {loadUserData} from "@app/commands" import {loadUserData} from "@app/commands"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
let signers: any[] = $state([])
let loading: string | undefined = $state()
const disabled = $derived(loading ? true : undefined)
const signUp = () => pushModal(SignUp) const signUp = () => pushModal(SignUp)
const withLoading =
(s: string, cb: (...args: any[]) => any) =>
async (...args: any[]) => {
loading = s
try {
await cb(...args)
} finally {
loading = undefined
}
}
const onSuccess = async (session: Session, relays: string[] = []) => { const onSuccess = async (session: Session, relays: string[] = []) => {
await loadUserData(session.pubkey, {relays}) await loadUserData(session.pubkey, {relays})
@@ -39,41 +32,50 @@
clearModals() clearModals()
} }
const loginWithNip07 = withLoading("nip07", async () => { const loginWithNip07 = async () => {
const pubkey = await getNip07()?.getPublicKey() loading = "nip07"
if (pubkey) { try {
await onSuccess({method: "nip07", pubkey}) const pubkey = await getNip07()?.getPublicKey()
} else {
pushToast({ if (pubkey) {
theme: "error", await onSuccess({method: "nip07", pubkey})
message: "Something went wrong! Please try again.", } else {
}) pushToast({
theme: "error",
message: "Something went wrong! Please try again.",
})
}
} finally {
loading = undefined
} }
}) }
const loginWithNip55 = withLoading("nip55", async (app: any) => { const loginWithNip55 = async (app: any) => {
const signer = new Nip55Signer(app.packageName) loading = "nip55"
const pubkey = await signer.getPubkey()
if (pubkey) { try {
await onSuccess({method: "nip55", pubkey, signer: app.packageName}) const signer = new Nip55Signer(app.packageName)
} else { const pubkey = await signer.getPubkey()
pushToast({
theme: "error", if (pubkey) {
message: "Something went wrong! Please try again.", await onSuccess({method: "nip55", pubkey, signer: app.packageName})
}) } else {
pushToast({
theme: "error",
message: "Something went wrong! Please try again.",
})
}
} finally {
loading = undefined
} }
}) }
const loginWithPassword = () => pushModal(LogInPassword) const loginWithPassword = () => pushModal(LogInPassword)
const loginWithBunker = () => pushModal(LogInBunker) const loginWithBunker = () => pushModal(LogInBunker)
let signers: any[] = [] const hasSigner = $derived(getNip07() || signers.length > 0)
let loading: string | undefined
$: hasSigner = getNip07() || signers.length > 0
onMount(async () => { onMount(async () => {
if (Capacitor.isNativePlatform()) { if (Capacitor.isNativePlatform()) {
@@ -86,13 +88,13 @@
<h1 class="heading">Log in with Nostr</h1> <h1 class="heading">Log in with Nostr</h1>
<p class="m-auto max-w-sm text-center"> <p class="m-auto max-w-sm text-center">
{PLATFORM_NAME} is built using the {PLATFORM_NAME} is built using the
<Button class="link" on:click={() => pushModal(InfoNostr)}>nostr protocol</Button>, which allows <Button class="link" onclick={() => pushModal(InfoNostr)}>nostr protocol</Button>, which allows
you to own your social identity. you to own your social identity.
</p> </p>
{#if getNip07()} {#if getNip07()}
<Button disabled={loading} on:click={loginWithNip07} class="btn btn-primary"> <Button {disabled} onclick={loginWithNip07} class="btn btn-primary">
{#if loading === "nip07"} {#if loading === "nip07"}
<span class="loading loading-spinner mr-3" /> <span class="loading loading-spinner mr-3"></span>
{:else} {:else}
<Icon icon="widget" /> <Icon icon="widget" />
{/if} {/if}
@@ -100,9 +102,9 @@
</Button> </Button>
{/if} {/if}
{#each signers as app} {#each signers as app}
<Button disabled={loading} class="btn btn-primary" on:click={() => loginWithNip55(app)}> <Button {disabled} class="btn btn-primary" onclick={() => loginWithNip55(app)}>
{#if loading === "nip55"} {#if loading === "nip55"}
<span class="loading loading-spinner mr-3" /> <span class="loading loading-spinner mr-3"></span>
{:else} {:else}
<img src={app.iconUrl} alt={app.name} width="20" height="20" /> <img src={app.iconUrl} alt={app.name} width="20" height="20" />
{/if} {/if}
@@ -110,9 +112,9 @@
</Button> </Button>
{/each} {/each}
{#if BURROW_URL && !hasSigner} {#if BURROW_URL && !hasSigner}
<Button disabled={loading} on:click={loginWithPassword} class="btn btn-primary"> <Button {disabled} onclick={loginWithPassword} class="btn btn-primary">
{#if loading === "password"} {#if loading === "password"}
<span class="loading loading-spinner mr-3" /> <span class="loading loading-spinner mr-3"></span>
{:else} {:else}
<Icon icon="key" /> <Icon icon="key" />
{/if} {/if}
@@ -120,16 +122,16 @@
</Button> </Button>
{/if} {/if}
<Button <Button
disabled={loading} onclick={loginWithBunker}
on:click={loginWithBunker} {disabled}
class="btn {hasSigner || BURROW_URL ? 'btn-neutral' : 'btn-primary'}"> class="btn {hasSigner || BURROW_URL ? 'btn-neutral' : 'btn-primary'}">
<Icon icon="cpu" /> <Icon icon="cpu" />
Log in with Remote Signer Log in with Remote Signer
</Button> </Button>
{#if BURROW_URL && hasSigner} {#if BURROW_URL && hasSigner}
<Button disabled={loading} on:click={loginWithPassword} class="btn"> <Button {disabled} onclick={loginWithPassword} class="btn">
{#if loading === "password"} {#if loading === "password"}
<span class="loading loading-spinner mr-3" /> <span class="loading loading-spinner mr-3"></span>
{:else} {:else}
<Icon icon="key" /> <Icon icon="key" />
{/if} {/if}
@@ -139,7 +141,7 @@
{#if !hasSigner || !BURROW_URL} {#if !hasSigner || !BURROW_URL}
<Link <Link
external external
disabled={loading} {disabled}
href="https://nostrapps.com#signers" href="https://nostrapps.com#signers"
class="btn {hasSigner || BURROW_URL ? '' : 'btn-neutral'}"> class="btn {hasSigner || BURROW_URL ? '' : 'btn-neutral'}">
<Icon icon="compass" /> <Icon icon="compass" />
@@ -148,6 +150,6 @@
{/if} {/if}
<div class="text-sm"> <div class="text-sm">
Need an account? Need an account?
<Button class="link" on:click={signUp}>Register instead</Button> <Button class="link" onclick={signUp}>Register instead</Button>
</div> </div>
</div> </div>
+33 -24
View File
@@ -2,6 +2,7 @@
import {onMount, onDestroy} from "svelte" import {onMount, onDestroy} from "svelte"
import {Nip46Broker, getPubkey, makeSecret} from "@welshman/signer" import {Nip46Broker, getPubkey, makeSecret} from "@welshman/signer"
import {addSession} from "@welshman/app" import {addSession} from "@welshman/app"
import {preventDefault} from "@lib/html"
import {slideAndFade} from "@lib/transition" import {slideAndFade} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -26,7 +27,7 @@
const back = () => history.back() const back = () => history.back()
const onSubmit = async () => { const onSubmit = async () => {
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(input) const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(bunker)
if (loading) { if (loading) {
return return
@@ -59,18 +60,18 @@
clearModals() clearModals()
} }
let url = "" let url = $state("")
let input = "" let bunker = $state("")
let loading = false let loading = $state(false)
$: { $effect(() => {
// For testing and for play store reviewers // For testing and for play store reviewers
if (input === "reviewkey") { if (bunker === "reviewkey") {
const secret = makeSecret() const secret = makeSecret()
addSession({method: "nip01", secret, pubkey: getPubkey(secret)}) addSession({method: "nip01", secret, pubkey: getPubkey(secret)})
} }
} })
onMount(async () => { onMount(async () => {
url = await broker.makeNostrconnectUrl({ url = await broker.makeNostrconnectUrl({
@@ -121,35 +122,43 @@
}) })
</script> </script>
<form class="column gap-4" on:submit|preventDefault={onSubmit}> <form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
<ModalHeader> <ModalHeader>
<div slot="title">Log In</div> {#snippet title()}
<div slot="info"> <div>Log In</div>
Connect your signer by scanning the QR code below or pasting a bunker link. {/snippet}
</div> {#snippet info()}
<div>Connect your signer by scanning the QR code below or pasting a bunker link.</div>
{/snippet}
</ModalHeader> </ModalHeader>
{#if !loading && url} {#if !loading && url}
<div class="w-xs m-auto" out:slideAndFade> <div class="flex justify-center" out:slideAndFade>
<QRCode code={url} /> <QRCode code={url} />
</div> </div>
{/if} {/if}
<Field> <Field>
<p slot="label">Bunker Link*</p> {#snippet label()}
<label class="input input-bordered flex w-full items-center gap-2" slot="input"> <p>Bunker Link*</p>
<Icon icon="cpu" /> {/snippet}
<input disabled={loading} bind:value={input} class="grow" placeholder="bunker://" /> {#snippet input()}
</label> <label class="input input-bordered flex w-full items-center gap-2">
<p slot="info"> <Icon icon="cpu" />
A login link provided by a nostr signing app. <input disabled={loading} bind:value={bunker} class="grow" placeholder="bunker://" />
<Button class="link" on:click={() => pushModal(InfoBunker)}>What is a bunker link?</Button> </label>
</p> {/snippet}
{#snippet info()}
<p>
A login link provided by a nostr signing app.
<Button class="link" onclick={() => pushModal(InfoBunker)}>What is a bunker link?</Button>
</p>
{/snippet}
</Field> </Field>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" on:click={back} disabled={loading}> <Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon="alt-arrow-left" /> <Icon icon="alt-arrow-left" />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary" disabled={loading || !input}> <Button type="submit" class="btn btn-primary" disabled={loading || !bunker}>
<Spinner {loading}>Next</Spinner> <Spinner {loading}>Next</Spinner>
<Icon icon="alt-arrow-right" /> <Icon icon="alt-arrow-right" />
</Button> </Button>
+35 -18
View File
@@ -4,6 +4,7 @@
import {Nip46Broker, makeSecret} from "@welshman/signer" import {Nip46Broker, makeSecret} from "@welshman/signer"
import {normalizeRelayUrl} from "@welshman/util" import {normalizeRelayUrl} from "@welshman/util"
import {addSession} from "@welshman/app" import {addSession} from "@welshman/app"
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 FieldInline from "@lib/components/FieldInline.svelte" import FieldInline from "@lib/components/FieldInline.svelte"
@@ -17,7 +18,11 @@
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {NIP46_PERMS, BURROW_URL, PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO} from "@app/state" import {NIP46_PERMS, BURROW_URL, PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO} from "@app/state"
export let email = "" interface Props {
email?: string
}
let {email = $bindable("")}: Props = $props()
const clientSecret = makeSecret() const clientSecret = makeSecret()
@@ -50,8 +55,8 @@
} }
let url = "" let url = ""
let password = "" let password = $state("")
let loading = false let loading = $state(false)
onMount(async () => { onMount(async () => {
url = await broker.makeNostrconnectUrl({ url = await broker.makeNostrconnectUrl({
@@ -100,32 +105,44 @@
}) })
</script> </script>
<form class="column gap-4" on:submit|preventDefault={onSubmit}> <form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
<ModalHeader> <ModalHeader>
<div slot="title">Log In</div> {#snippet title()}
<div slot="info">Log in using your email and password</div> <div>Log In</div>
{/snippet}
{#snippet info()}
<div>Log in using your email and password</div>
{/snippet}
</ModalHeader> </ModalHeader>
<FieldInline> <FieldInline>
<p slot="label">Email</p> {#snippet label()}
<label class="input input-bordered flex w-full items-center gap-2" slot="input"> <p>Email</p>
<Icon icon="user-rounded" /> {/snippet}
<input bind:value={email} /> {#snippet input()}
</label> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="user-rounded" />
<input bind:value={email} />
</label>
{/snippet}
</FieldInline> </FieldInline>
<FieldInline> <FieldInline>
<p slot="label">Password</p> {#snippet label()}
<label class="input input-bordered flex w-full items-center gap-2" slot="input"> <p>Password</p>
<Icon icon="key" /> {/snippet}
<input bind:value={password} type="password" /> {#snippet input()}
</label> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="key" />
<input bind:value={password} type="password" />
</label>
{/snippet}
</FieldInline> </FieldInline>
<p class="text-sm"> <p class="text-sm">
Your email and password only work to log in to {PLATFORM_NAME}. To use your key on other nostr Your email and password only work to log in to {PLATFORM_NAME}. To use your key on other nostr
applications, visit your settings page. <Button class="link" on:click={startReset} applications, visit your settings page. <Button class="link" onclick={startReset}
>Forgot your password?</Button> >Forgot your password?</Button>
</p> </p>
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" on:click={back} disabled={loading}> <Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon="alt-arrow-left" /> <Icon icon="alt-arrow-left" />
Go back Go back
</Button> </Button>
+7 -4
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
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"
@@ -19,16 +20,18 @@
} }
} }
let loading = false let loading = $state(false)
</script> </script>
<form class="column gap-4" on:submit|preventDefault={doLogout}> <form class="column gap-4" onsubmit={preventDefault(doLogout)}>
<ModalHeader> <ModalHeader>
<div slot="title">Are you sure you want<br />to log out?</div> {#snippet title()}
<div>Are you sure you want<br />to log out?</div>
{/snippet}
</ModalHeader> </ModalHeader>
<p class="text-center">Your local database will be cleared.</p> <p class="text-center">Your local database will be cleared.</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>
+37 -13
View File
@@ -13,33 +13,57 @@
<div class="column menu gap-2"> <div class="column menu gap-2">
<Link replaceState href="/settings/profile"> <Link replaceState href="/settings/profile">
<CardButton> <CardButton>
<div slot="icon"><Icon icon="user-rounded" size={7} /></div> {#snippet icon()}
<div slot="title">Profile</div> <div><Icon icon="user-rounded" size={7} /></div>
<div slot="info">Customize your user profile</div> {/snippet}
{#snippet title()}
<div>Profile</div>
{/snippet}
{#snippet info()}
<div>Customize your user profile</div>
{/snippet}
</CardButton> </CardButton>
</Link> </Link>
<Link replaceState href="/settings/relays"> <Link replaceState href="/settings/relays">
<CardButton> <CardButton>
<div slot="icon"><Icon icon="server" size={7} /></div> {#snippet icon()}
<div slot="title">Relays</div> <div><Icon icon="server" size={7} /></div>
<div slot="info">Control how {PLATFORM_NAME} talks to the network</div> {/snippet}
{#snippet title()}
<div>Relays</div>
{/snippet}
{#snippet info()}
<div>Control how {PLATFORM_NAME} talks to the network</div>
{/snippet}
</CardButton> </CardButton>
</Link> </Link>
<Link replaceState href="/settings"> <Link replaceState href="/settings">
<CardButton> <CardButton>
<div slot="icon"><Icon icon="settings" size={7} /></div> {#snippet icon()}
<div slot="title">Settings</div> <div><Icon icon="settings" size={7} /></div>
<div slot="info">Get into the details about how {PLATFORM_NAME} works</div> {/snippet}
{#snippet title()}
<div>Settings</div>
{/snippet}
{#snippet info()}
<div>Get into the details about how {PLATFORM_NAME} works</div>
{/snippet}
</CardButton> </CardButton>
</Link> </Link>
<Link replaceState href="/settings/about"> <Link replaceState href="/settings/about">
<CardButton> <CardButton>
<div slot="icon"><Icon icon="code-2" size={7} /></div> {#snippet icon()}
<div slot="title">About</div> <div><Icon icon="code-2" size={7} /></div>
<div slot="info">Learn about {PLATFORM_NAME} and support the developer</div> {/snippet}
{#snippet title()}
<div>About</div>
{/snippet}
{#snippet info()}
<div>Learn about {PLATFORM_NAME} and support the developer</div>
{/snippet}
</CardButton> </CardButton>
</Link> </Link>
<Button on:click={logout} class="btn btn-neutral"> <Button onclick={logout} class="btn btn-neutral">
<Icon icon="exit" /> Log Out <Icon icon="exit" /> Log Out
</Button> </Button>
</div> </div>
+31 -19
View File
@@ -26,9 +26,10 @@
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {makeSpacePath} from "@app/routes" import {makeSpacePath} from "@app/routes"
export let url const {url} = $props()
const threadsPath = makeSpacePath(url, "threads") const threadsPath = makeSpacePath(url, "threads")
const calendarPath = makeSpacePath(url, "calendar")
const userRooms = deriveUserRooms(url) const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url) const otherRooms = deriveOtherRooms(url)
@@ -55,14 +56,16 @@
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState}) const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
let showMenu = false let showMenu = $state(false)
let replaceState = false let replaceState = $state(false)
let element: Element let element: Element | undefined = $state()
$: members = $memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey) const members = $derived(
$memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey),
)
onMount(async () => { onMount(() => {
replaceState = Boolean(element.closest(".drawer")) replaceState = Boolean(element?.closest(".drawer"))
pullConservatively({relays: [url], filters: [{kinds: [GROUP_META]}]}) pullConservatively({relays: [url], filters: [{kinds: [GROUP_META]}]})
}) })
</script> </script>
@@ -70,7 +73,7 @@
<div bind:this={element}> <div bind:this={element}>
<SecondaryNavSection class="max-h-screen"> <SecondaryNavSection class="max-h-screen">
<div> <div>
<SecondaryNavItem class="w-full !justify-between" on:click={openMenu}> <SecondaryNavItem class="w-full !justify-between" onclick={openMenu}>
<strong class="ellipsize">{displayRelayUrl(url)}</strong> <strong class="ellipsize">{displayRelayUrl(url)}</strong>
<Icon icon="alt-arrow-down" /> <Icon icon="alt-arrow-down" />
</SecondaryNavItem> </SecondaryNavItem>
@@ -80,25 +83,25 @@
transition:fly transition:fly
class="menu absolute z-popover mt-2 w-full rounded-box bg-base-100 p-2 shadow-xl"> class="menu absolute z-popover mt-2 w-full rounded-box bg-base-100 p-2 shadow-xl">
<li> <li>
<Button on:click={showMembers}> <Button onclick={showMembers}>
<Icon icon="user-rounded" /> <Icon icon="user-rounded" />
View Members ({members.length}) View Members ({members.length})
</Button> </Button>
</li> </li>
<li> <li>
<Button on:click={createInvite}> <Button onclick={createInvite}>
<Icon icon="link-round" /> <Icon icon="link-round" />
Create Invite Create Invite
</Button> </Button>
</li> </li>
<li> <li>
{#if $userRoomsByUrl.has(url)} {#if $userRoomsByUrl.has(url)}
<Button on:click={leaveSpace} class="text-error"> <Button onclick={leaveSpace} class="text-error">
<Icon icon="exit" /> <Icon icon="exit" />
Leave Space Leave Space
</Button> </Button>
{:else} {:else}
<Button on:click={joinSpace} class="bg-primary text-primary-content"> <Button onclick={joinSpace} class="bg-primary text-primary-content">
<Icon icon="login-2" /> <Icon icon="login-2" />
Join Space Join Space
</Button> </Button>
@@ -109,19 +112,28 @@
{/if} {/if}
</div> </div>
<div class="flex min-h-0 flex-col gap-1 overflow-auto"> <div class="flex min-h-0 flex-col gap-1 overflow-auto">
<SecondaryNavItem href={makeSpacePath(url)}> <SecondaryNavItem {replaceState} href={makeSpacePath(url)}>
<Icon icon="home-smile" /> Home <Icon icon="home-smile" /> Home
</SecondaryNavItem> </SecondaryNavItem>
<SecondaryNavItem href={threadsPath} notification={$notifications.has(threadsPath)}> <SecondaryNavItem
{replaceState}
href={threadsPath}
notification={$notifications.has(threadsPath)}>
<Icon icon="notes-minimalistic" /> Threads <Icon icon="notes-minimalistic" /> Threads
</SecondaryNavItem> </SecondaryNavItem>
<div class="h-2" /> <SecondaryNavItem
{replaceState}
href={calendarPath}
notification={$notifications.has(calendarPath)}>
<Icon icon="calendar-minimalistic" /> Calendar
</SecondaryNavItem>
<div class="h-2"></div>
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader> <SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
{#each $userRooms as room, i (room)} {#each $userRooms as room, i (room)}
<MenuSpaceRoomItem notify {url} {room} /> <MenuSpaceRoomItem {replaceState} notify {url} {room} />
{/each} {/each}
{#if $otherRooms.length > 0} {#if $otherRooms.length > 0}
<div class="h-2" /> <div class="h-2"></div>
<SecondaryNavHeader> <SecondaryNavHeader>
{#if $userRooms.length > 0} {#if $userRooms.length > 0}
Other Rooms Other Rooms
@@ -131,9 +143,9 @@
</SecondaryNavHeader> </SecondaryNavHeader>
{/if} {/if}
{#each $otherRooms as room, i (room)} {#each $otherRooms as room, i (room)}
<MenuSpaceRoomItem {url} {room} /> <MenuSpaceRoomItem {replaceState} {url} {room} />
{/each} {/each}
<SecondaryNavItem on:click={addRoom}> <SecondaryNavItem {replaceState} onclick={addRoom}>
<Icon icon="add-circle" /> <Icon icon="add-circle" />
Create room Create room
</SecondaryNavItem> </SecondaryNavItem>
+3 -3
View File
@@ -6,16 +6,16 @@
import {makeSpacePath} from "@app/routes" import {makeSpacePath} from "@app/routes"
import {pushDrawer} from "@app/modal" import {pushDrawer} from "@app/modal"
export let url const {url} = $props()
const path = makeSpacePath(url) const path = makeSpacePath(url)
const openMenu = () => pushDrawer(MenuSpace, {url}) const openMenu = () => pushDrawer(MenuSpace, {url})
</script> </script>
<Button on:click={openMenu} class="btn btn-neutral btn-sm relative md:hidden"> <Button onclick={openMenu} class="btn btn-neutral btn-sm relative md:hidden">
<Icon icon="menu-dots" /> <Icon icon="menu-dots" />
{#if $notifications.has(path)} {#if $notifications.has(path)}
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-primary" /> <div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-primary"></div>
{/if} {/if}
</Button> </Button>
+12 -4
View File
@@ -6,15 +6,23 @@
import {deriveChannel, channelIsLocked} from "@app/state" import {deriveChannel, channelIsLocked} from "@app/state"
import {notifications} from "@app/notifications" import {notifications} from "@app/notifications"
export let url interface Props {
export let room url: any
export let notify = false room: any
notify?: boolean
replaceState?: boolean
}
const {url, room, notify = false, replaceState = false}: Props = $props()
const path = makeRoomPath(url, room) const path = makeRoomPath(url, room)
const channel = deriveChannel(url, room) const channel = deriveChannel(url, room)
</script> </script>
<SecondaryNavItem href={path} notification={notify ? $notifications.has(path) : false}> <SecondaryNavItem
href={path}
{replaceState}
notification={notify ? $notifications.has(path) : false}>
{#if channelIsLocked($channel)} {#if channelIsLocked($channel)}
<Icon icon="lock" size={4} /> <Icon icon="lock" size={4} />
{:else} {:else}

Some files were not shown because too many files have changed in this diff Show More