Compare commits

..

62 Commits

Author SHA1 Message Date
Jon Staab e484c3cb00 Bump versions 2025-02-14 10:56:00 -08:00
Jon Staab 69d0e11ba4 Add nip 01 login flow to mobile 2025-02-14 10:53:36 -08:00
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
220 changed files with 5914 additions and 3089 deletions
+3
View File
@@ -1,3 +1,6 @@
--ignore-dir=.svelte-kit
--ignore-dir=android
--ignore-dir=build
--ignore-dir=ios/DerivedData
--ignore-dir=ios/App/App/public
+2
View File
@@ -7,6 +7,8 @@ build
gradlew*
_app
release
ios/DerivedData/
ios/App/Pods/
android/capacitor-cordova-android-plugins
android/app/src/androidTest
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.local
@@ -17,9 +5,6 @@ Thumbs.db
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Android
.idea
# Generated assets
static/favicon.ico
static/pwa-64x64.png
@@ -29,3 +14,53 @@ static/apple-touch-icon-180x180.png
static/maskable-icon-512x512.png
src/assets/icons/*.webp
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
+23
View File
@@ -1,5 +1,28 @@
# Changelog
# 0.2.9
* Add NIP 01 signup flow on mobile
# 0.2.8
* Show spinner when joining a room
* Reduce self-rate limiting of REQs
* Fix disabled signers link
* Prepare for iOS release
* Improve threads and calendar pages
* Improve quote rendering and new messages button
# 0.2.7
* Add calendar events
* Migrate to svelte 5 (fixes some bugs, probably introduces others)
* Migrate to new welshman editor
* Make reply indicator nicer
* Make share indicator nicer
* Improve feed loading
* Show marker for last activity in chat
# 0.2.6
* Add reply to long-press menu
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "social.flotilla"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 6
versionName "0.2.6"
versionCode 10
versionName "0.2.9"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// 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 {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
android:supportsRtl="true"
android:theme="@style/AppTheme">
<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:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
+1 -1
View File
@@ -8,7 +8,7 @@ buildscript {
}
dependencies {
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
// in the individual module build.gradle files
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+13 -9
View File
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (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.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,7 +85,9 @@ done
# This is normally unused
# shellcheck disable=SC2034
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.
MAX_FD=maximum
@@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# 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 ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# 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" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
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.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-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 limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
+10 -10
View File
@@ -1,16 +1,16 @@
ext {
minSdkVersion = 22
compileSdkVersion = 34
targetSdkVersion = 34
androidxActivityVersion = '1.8.0'
androidxAppCompatVersion = '1.6.1'
minSdkVersion = 23
compileSdkVersion = 35
targetSdkVersion = 35
androidxActivityVersion = '1.9.2'
androidxAppCompatVersion = '1.7.0'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.12.0'
androidxFragmentVersion = '1.6.2'
androidxCoreVersion = '1.15.0'
androidxFragmentVersion = '1.8.4'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.9.0'
androidxWebkitVersion = '1.12.1'
junitVersion = '4.13.2'
androidxJunitVersion = '1.1.5'
androidxEspressoCoreVersion = '3.5.1'
androidxJunitVersion = '1.2.1'
androidxEspressoCoreVersion = '3.6.1'
cordovaAndroidVersion = '10.1.1'
}
+6 -1
View File
@@ -11,7 +11,12 @@ const config: CapacitorConfig = {
SplashScreen: {
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;
+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 = 3;
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.9;
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 = 3;
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.9;
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",
"version": "0.2.6",
"version": "0.2.9",
"private": true,
"scripts": {
"dev": "vite dev",
"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",
"release:android": "cap build android --androidreleasetype APK --signing-type apksigner",
"sourcemaps": "./build.sh && ./sourcemaps.sh",
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check src && eslint src",
"format": "prettier --write src",
"prepare": "husky"
},
"overrides": {
"@capacitor/core": "^7.0.1"
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@sentry/cli": "^2.40.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@sveltejs/kit": "^2.5.27",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/eslint": "^9.6.0",
"autoprefixer": "^10.4.19",
"classnames": "^2.5.1",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"eslint-plugin-svelte": "^2.45.1",
"globals": "^15.0.0",
"postcss": "^8.4.40",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"prettier-plugin-svelte": "^3.2.6",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^3.4.7",
"typescript": "^5.0.0",
"typescript": "^5.5.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.0.3"
"vite": "^5.4.4"
},
"type": "module",
"dependencies": {
"@capacitor/android": "^7.0.1",
"@capacitor/android": "^7.0.0",
"@capacitor/app": "^7.0.0",
"@capacitor/cli": "^6.2.0",
"@capacitor/cli": "^7.0.0",
"@capacitor/core": "^7.0.1",
"@capacitor/ios": "^7.0.0",
"@noble/curves": "^1.5.0",
"@noble/hashes": "^1.4.0",
"@poppanator/sveltekit-svg": "^4.2.1",
@@ -52,16 +50,16 @@
"@types/qrcode": "^1.5.5",
"@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.6",
"@welshman/app": "~0.0.41",
"@welshman/content": "~0.0.16",
"@welshman/app": "~0.0.42",
"@welshman/content": "~0.0.18",
"@welshman/dvm": "~0.0.14",
"@welshman/editor": "~0.0.10",
"@welshman/editor": "~0.0.15",
"@welshman/feeds": "~0.0.30",
"@welshman/lib": "~0.0.38",
"@welshman/net": "~0.0.46",
"@welshman/lib": "~0.0.41",
"@welshman/net": "~0.0.47",
"@welshman/signer": "~0.0.20",
"@welshman/store": "~0.0.15",
"@welshman/util": "~0.0.59",
"@welshman/store": "~0.0.16",
"@welshman/util": "~0.0.61",
"daisyui": "^4.12.10",
"date-picker-svelte": "^2.13.0",
"dotenv": "^16.4.5",
@@ -69,7 +67,7 @@
"fuse.js": "^7.0.0",
"husky": "^9.1.6",
"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",
"prettier-plugin-tailwindcss": "^0.6.5",
"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 utilities;
/* Fonts */
@font-face {
font-family: "Satoshis";
font-style: normal;
@@ -38,6 +40,8 @@
url("/fonts/Italic.ttf") format("truetype");
}
/* root */
:root {
font-family: Lato;
--base-100: oklch(var(--b1));
@@ -50,6 +54,60 @@
--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,
.hover\:bg-alt:hover,
@@ -236,11 +294,20 @@
/* date input */
.date-time-field {
@apply input input-bordered rounded px-0;
.picker {
--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 {
@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 */
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<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="description" content="{DESCRIPTION}" />
<meta name="og:url" content="{URL}" />
+8 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {page} from "$app/stores"
import {pubkey} from "@welshman/app"
import Landing from "@app/components/Landing.svelte"
@@ -9,6 +10,12 @@
import {BURROW_URL} from "@app/state"
import {modals, pushModal} from "@app/modal"
interface Props {
children: Snippet
}
const {children}: Props = $props()
if (BURROW_URL && !$pubkey) {
if ($page.url.pathname === "/confirm-email") {
pushModal(EmailConfirm, {
@@ -29,7 +36,7 @@
<div class="flex h-screen overflow-hidden">
{#if $pubkey}
<PrimaryNav>
<slot />
{@render children?.()}
</PrimaryNav>
{:else if !$modals[$page.url.hash.slice(1)]}
<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">
import {onMount} from "svelte"
import {writable} from "svelte/store"
import {EditorContent} from "svelte-tiptap"
import {isMobile} from "@lib/html"
import {isMobile, preventDefault} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {makeEditor} from "@app/editor"
export let onSubmit: any
export let content = ""
interface Props {
onSubmit: any
}
export const focus = () => $editor.chain().focus().run()
const {onSubmit}: Props = $props()
const autofocus = !isMobile
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 = () => {
if ($uploading) return
const content = $editor!.getText({blockSeparator: "\n"}).trim()
const tags = $editor!.storage.nostr.getEditorTags()
const content = editor.getText({blockSeparator: "\n"}).trim()
const tags = editor.storage.nostr.getEditorTags()
if (!content) return
onSubmit({content, tags})
$editor!.chain().clearContent().run()
editor.chain().clearContent().run()
}
const editor = makeEditor({autofocus: !isMobile, submit, uploading, aggressive: true})
onMount(() => {
$editor!.chain().setContent(content).run()
})
const editor = makeEditor({autofocus, submit, uploading, aggressive: true})
</script>
<form
class="relative z-feature flex gap-2 p-2"
on:submit|preventDefault={$uploading ? undefined : submit}>
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
<Button
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"
disabled={$uploading}
on:click={uploadFiles}>
onclick={uploadFiles}>
{#if $uploading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
@@ -51,13 +49,13 @@
{/if}
</Button>
<div class="chat-editor flex-grow overflow-hidden">
<EditorContent editor={$editor} />
<EditorContent {editor} />
</div>
<Button
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
disabled={$uploading}
on:click={submit}>
onclick={submit}>
<Icon icon="plain" />
</Button>
</form>
+18 -6
View File
@@ -4,20 +4,32 @@
import {slide} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Content from "@app/components/Content.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
export let event: TrustedEvent
export let clear: () => void
const {
verb,
event,
clear,
}: {
verb: string
event: TrustedEvent
clear: () => void
} = $props()
</script>
<div
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
transition:slide>
<p class="text-primary">Replying to @{displayProfileByPubkey(event.pubkey)}</p>
<p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
{#key event.id}
<Content {event} hideMedia minLength={100} maxLength={300} expandMode="disabled" />
<NoteContent
{event}
hideMediaAtDepth={0}
minLength={100}
maxLength={300}
expandMode="disabled" />
{/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" />
</Button>
</div>
+16 -12
View File
@@ -26,11 +26,16 @@
import {publishDelete, publishReaction} from "@app/commands"
import {pushModal} from "@app/modal"
export let url, room
export let event: TrustedEvent
export let replyTo: any = undefined
export let showPubkey = false
export let inert = false
interface Props {
url: any
room: any
event: TrustedEvent
replyTo?: any
showPubkey?: boolean
inert?: boolean
}
const {url, room, event, replyTo = undefined, showPubkey = false, inert = false}: Props = $props()
const thunk = $thunks[event.id]
const today = formatTimestampAsDate(now())
@@ -61,16 +66,16 @@
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">
{#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} />
</Button>
{:else}
<div class="w-8 min-w-8 max-w-8" />
<div class="w-8 min-w-8 max-w-8"></div>
{/if}
<div class="min-w-0 flex-grow pr-1">
{#if showPubkey}
<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}
</Button>
<span class="text-xs opacity-50">
@@ -84,7 +89,7 @@
</div>
{/if}
<div class="text-sm">
<Content {event} quoteProps={{minimal: true, relays: [url]}} />
<Content {event} relays={[url]} />
{#if thunk}
<ThunkStatus {thunk} class="mt-2" />
{/if}
@@ -96,11 +101,10 @@
</div>
<button
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
class:group-hover:opacity-100={!isMobile}
on:click|stopPropagation>
class:group-hover:opacity-100={!isMobile}>
<ChannelMessageEmojiButton {url} {room} {event} />
{#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} />
</Button>
{/if}
@@ -5,7 +5,7 @@
import Icon from "@lib/components/Icon.svelte"
import {publishReaction} from "@app/commands"
export let url, room, event
const {url, room, event} = $props()
// Tell svelte-check to shut up
noop(room)
+4 -6
View File
@@ -7,9 +7,7 @@
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
import {pushModal} from "@app/modal"
export let url
export let event
export let onClick
const {url, event, onClick} = $props()
const report = () => {
onClick()
@@ -29,21 +27,21 @@
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl">
<li>
<Button on:click={showInfo}>
<Button onclick={showInfo}>
<Icon size={4} icon="code-2" />
Message Details
</Button>
</li>
{#if event.pubkey === $pubkey}
<li>
<Button on:click={showDelete} class="text-error">
<Button onclick={showDelete} class="text-error">
<Icon size={4} icon="trash-bin-2" />
Delete Message
</Button>
</li>
{:else}
<li>
<Button class="text-error" on:click={report}>
<Button class="text-error" onclick={report}>
<Icon size={4} icon="danger" />
Report Content
</Button>
@@ -6,27 +6,29 @@
import Tippy from "@lib/components/Tippy.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 {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)) {
popover.hide()
if (!between([x, x + width], clientX) || !between([y, y + height + 30], clientY)) {
popover.hide()
}
}
}
let popover: Instance
let popover: Instance | undefined = $state()
</script>
<svelte:document on:mousemove={onMouseMove} />
<svelte:document onmousemove={onMouseMove} />
<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} />
</Button>
<Tippy
@@ -9,9 +9,7 @@
import {publishReaction} from "@app/commands"
import {pushModal} from "@app/modal"
export let url
export let event
export let reply
const {url, event, reply} = $props()
const onEmoji = (emoji: NativeEmoji) => {
history.back()
@@ -31,20 +29,20 @@
</script>
<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" />
Send Reaction
</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" />
Send Reply
</Button>
<Button class="btn btn-neutral" on:click={showInfo}>
<Button class="btn btn-neutral" onclick={showInfo}>
<Icon size={4} icon="code-2" />
Message Details
</Button>
{#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" />
Delete Message
</Button>
+1 -2
View File
@@ -1,8 +1,7 @@
<script lang="ts">
import {GENERAL, channelsById, makeChannelId} from "@app/state"
export let url
export let room
const {url, room} = $props()
</script>
{#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">
import type {Snippet} from "svelte"
import {onMount} from "svelte"
import {derived} from "svelte/store"
import {int, nthNe, MINUTE, sortBy, remove} from "@welshman/lib"
import type {TrustedEvent, EventContent} 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 Link from "@lib/components/Link.svelte"
import Spinner from "@lib/components/Spinner.svelte"
@@ -32,51 +29,52 @@
import {pushModal} from "@app/modal"
import {sendWrapped, prependParent} from "@app/commands"
export let id
const {
id,
info,
}: {
id: string
info?: Snippet
} = $props()
const chat = deriveChat(id)
const pubkeys = splitChatId(id)
const others = remove($pubkey!, pubkeys)
const missingInboxes = derived(inboxRelaySelectionsByPubkey, $m =>
pubkeys.filter(pk => !$m.has(pk)),
)
const missingInboxes = $derived(pubkeys.filter(pk => !$inboxRelaySelectionsByPubkey.has(pk)))
const assertEvent = (e: any) => e as TrustedEvent
const assertNotNil = <T,>(x: T | undefined) => x!
const showMembers = () =>
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
const replyTo = (event: TrustedEvent) => {
parent = event
compose.focus()
compose?.focus()
}
const clearParent = () => {
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({
pubkeys,
template: createEvent(
DIRECT_MESSAGE,
prependParent(parent, {content, tags: tags.filter(nthNe(0, "p"))}),
),
template: createEvent(DIRECT_MESSAGE, prependParent(parent, {...params, tags})),
delay: $userSettingValues.send_delay,
})
clearParent()
}
let loading = true
let parent: TrustedEvent | undefined
let elements: Element[] = []
let compose: ChatCompose
let loading = $state(true)
let compose: ChatCompose | undefined = $state()
let parent: TrustedEvent | undefined = $state()
$: {
elements = []
const elements = $derived.by(() => {
const elements = []
let previousDate
let previousPubkey
@@ -102,8 +100,8 @@
previousCreatedAt = created_at
}
elements.reverse()
}
return elements.reverse()
})
onMount(() => {
// Don't use loadInboxRelaySelection because we want to force reload
@@ -118,50 +116,54 @@
<div class="relative flex h-full w-full flex-col">
{#if others.length > 0}
<PageBar>
<div slot="title" class="flex flex-col gap-1 sm:flex-row sm:gap-2">
{#if others.length === 1}
{@const pubkey = others[0]}
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
<Button on:click={onClick} class="row-2">
<ProfileCircle {pubkey} size={5} />
<ProfileName {pubkey} />
</Button>
{:else}
<div class="flex items-center gap-2">
<ProfileCircles pubkeys={others} size={5} />
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
<ProfileName pubkey={others[0]} />
and
{#if others.length === 2}
<ProfileName pubkey={others[1]} />
{:else}
{others.length - 1}
{others.length > 2 ? "others" : "other"}
{/if}
</p>
</div>
{#if others.length > 2}
<Button on:click={showMembers} class="btn btn-link hidden sm:block"
>Show all members</Button>
{#snippet title()}
<div class="flex flex-col gap-1 sm:flex-row sm:gap-2">
{#if others.length === 1}
{@const pubkey = others[0]}
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
<Button onclick={onClick} class="row-2">
<ProfileCircle {pubkey} size={5} />
<ProfileName {pubkey} />
</Button>
{:else}
<div class="flex items-center gap-2">
<ProfileCircles pubkeys={others} size={5} />
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
<ProfileName pubkey={others[0]} />
and
{#if others.length === 2}
<ProfileName pubkey={others[1]} />
{:else}
{others.length - 1}
{others.length > 2 ? "others" : "other"}
{/if}
</p>
</div>
{#if others.length > 2}
<Button onclick={showMembers} class="btn btn-link hidden sm:block"
>Show all members</Button>
{/if}
{/if}
{/if}
</div>
<div slot="action">
{#if remove($pubkey, $missingInboxes).length > 0}
{@const count = remove($pubkey, $missingInboxes).length}
{@const label = count > 1 ? "inboxes are" : "inbox is"}
<div
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
data-tip="{count} {label} not configured.">
<Icon icon="danger" />
{count}
</div>
{/if}
</div>
</div>
{/snippet}
{#snippet action()}
<div>
{#if remove($pubkey, missingInboxes).length > 0}
{@const count = remove($pubkey, missingInboxes).length}
{@const label = count > 1 ? "inboxes are" : "inbox is"}
<div
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
data-tip="{count} {label} not configured.">
<Icon icon="danger" />
{count}
</div>
{/if}
</div>
{/snippet}
</PageBar>
{/if}
<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="card2 col-2 m-auto max-w-md items-center text-center">
<p class="row-2 text-lg text-error">
@@ -175,6 +177,20 @@
</p>
</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}
{#each elements as { type, id, value, showPubkey } (id)}
{#if type === "date"}
@@ -192,11 +208,11 @@
End of message history
{/if}
</Spinner>
<slot name="info" />
{@render info?.()}
</p>
</div>
{#if parent}
<ChatComposeParent event={parent} clear={clearParent} />
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
{/if}
<ChatCompose bind:this={compose} {onSubmit} />
</div>
+11 -6
View File
@@ -2,6 +2,7 @@
import {goto} from "$app/navigation"
import {WRAP} from "@welshman/util"
import {repository} from "@welshman/app"
import {preventDefault} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
@@ -10,9 +11,9 @@
import {canDecrypt, PLATFORM_NAME, ensureUnwrapped} from "@app/state"
import {clearModals} from "@app/modal"
export let next
const {next} = $props()
let loading = false
let loading = $state(false)
const enableChat = async () => {
canDecrypt.set(true)
@@ -38,10 +39,14 @@
const back = () => history.back()
</script>
<form class="column gap-4" on:submit|preventDefault={submit}>
<form class="column gap-4" onsubmit={preventDefault(submit)}>
<ModalHeader>
<div slot="title">Enable Messages</div>
<div slot="info">Do you want to enable direct messages?</div>
{#snippet title()}
<div>Enable Messages</div>
{/snippet}
{#snippet info()}
<div>Do you want to enable direct messages?</div>
{/snippet}
</ModalHeader>
<p>
By default, direct messages are disabled, since loading them requires
@@ -52,7 +57,7 @@
to decrypt data.
</p>
<ModalFooter>
<Button class="btn btn-link" on:click={back}>
<Button class="btn btn-link" onclick={back}>
<Icon icon="alt-arrow-left" />
Go back
</Button>
+15 -10
View File
@@ -12,13 +12,18 @@
import {makeChatPath} from "@app/routes"
import {notifications} from "@app/notifications"
export let id: string
export let pubkeys: string[]
export let messages: TrustedEvent[]
interface Props {
id: string
pubkeys: string[]
messages: TrustedEvent[]
[key: string]: any
}
const others = remove($pubkey!, pubkeys)
const active = $page.params.chat === id
const path = makeChatPath(pubkeys)
const {...props}: Props = $props()
const others = remove($pubkey!, props.pubkeys)
const active = $page.params.chat === props.id
const path = makeChatPath(props.pubkeys)
onMount(() => {
for (const pk of others) {
@@ -27,9 +32,9 @@
})
</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
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}>
<div class="flex flex-col justify-start gap-1">
<div class="flex items-center justify-between gap-2">
@@ -50,11 +55,11 @@
{/if}
</div>
{#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}
</div>
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
{messages[0].content}
{props.messages[0].content}
</p>
</div>
</div>
+2 -2
View File
@@ -14,11 +14,11 @@
</script>
<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" />
Start chat
</Button>
<Button class="btn btn-neutral" on:click={markAsRead}>
<Button class="btn btn-neutral" onclick={markAsRead}>
<Icon size={4} icon="check-circle" />
Mark all read
</Button>
+15 -14
View File
@@ -25,10 +25,14 @@
import {makeDelete, makeReaction, sendWrapped} from "@app/commands"
import {pushModal} from "@app/modal"
export let event: TrustedEvent
export let replyTo: any = undefined
export let pubkeys: string[]
export let showPubkey = false
interface Props {
event: TrustedEvent
replyTo?: any
pubkeys: string[]
showPubkey?: boolean
}
const {event, replyTo = undefined, pubkeys, showPubkey = false}: Props = $props()
const thunk = $thunks[event.id]
const isOwn = event.pubkey === $pubkey
@@ -49,14 +53,14 @@
const togglePopover = () => {
if (popoverIsVisible) {
popover.hide()
popover?.hide()
} else {
popover.show()
popover?.show()
}
}
let popover: Instance
let popoverIsVisible = false
let popover: Instance | undefined = $state()
let popoverIsVisible = $state(false)
</script>
{#if thunk}
@@ -86,7 +90,7 @@
type="button"
class="opacity-0 transition-all"
class:group-hover:opacity-100={!isMobile}
on:click={togglePopover}>
onclick={togglePopover}>
<Icon icon="menu-dots" size={4} />
</button>
</Tippy>
@@ -97,16 +101,13 @@
{#if showPubkey}
<div class="flex items-center gap-2">
{#if !isOwn}
<Button on:click={openProfile} class="flex items-center gap-1">
<Button onclick={openProfile} class="flex items-center gap-1">
<Avatar
src={$profile?.picture}
class="border border-solid border-base-content"
size={4} />
<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}
</Button>
</div>
@@ -5,8 +5,12 @@
import EmojiButton from "@lib/components/EmojiButton.svelte"
import {makeReaction, sendWrapped} from "@app/commands"
export let event: TrustedEvent
export let pubkeys: string[]
interface Props {
event: TrustedEvent
pubkeys: string[]
}
const {event, pubkeys}: Props = $props()
const onEmoji = (emoji: NativeEmoji) =>
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
+3 -6
View File
@@ -5,10 +5,7 @@
import EventInfo from "@app/components/EventInfo.svelte"
import {pushModal} from "@app/modal"
export let event
export let pubkeys
export let popover
export let replyTo
const {event, pubkeys, popover, replyTo} = $props()
const reply = () => replyTo(event)
@@ -21,11 +18,11 @@
<div class="join border border-solid border-neutral text-xs">
<ChatMessageEmojiButton {event} {pubkeys} />
{#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" />
</Button>
{/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" />
</Button>
</div>
@@ -8,8 +8,7 @@
import {pushModal} from "@app/modal"
import {clip} from "@app/toast"
export let event
export let pubkeys
const {event, pubkeys} = $props()
const onEmoji = (emoji: NativeEmoji) => {
history.back()
@@ -27,15 +26,15 @@
</script>
<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" />
Send Reaction
</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" />
Copy Text
</Button>
<Button class="btn btn-neutral" on:click={showInfo}>
<Button class="btn btn-neutral" onclick={showInfo}>
<Icon size={4} icon="code-2" />
Message Details
</Button>
+12 -7
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import {goto} from "$app/navigation"
import {pubkey} from "@welshman/app"
import {preventDefault} from "@lib/html"
import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
@@ -13,21 +14,25 @@
const onSubmit = () => goto(makeChatPath([...pubkeys, $pubkey!]))
let pubkeys: string[] = []
let pubkeys: string[] = $state([])
</script>
<form class="column gap-4" on:submit|preventDefault={onSubmit}>
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
<ModalHeader>
<div slot="title">Start a Chat</div>
<div slot="info">Create an encrypted chat room for private conversations.</div>
{#snippet title()}
<div>Start a Chat</div>
{/snippet}
{#snippet info()}
<div>Create an encrypted chat room for private conversations.</div>
{/snippet}
</ModalHeader>
<Field>
<div slot="input">
{#snippet input()}
<ProfileMultiSelect autofocus bind:value={pubkeys} />
</div>
{/snippet}
</Field>
<ModalFooter>
<Button class="btn btn-link" on:click={back}>
<Button class="btn btn-link" onclick={back}>
<Icon icon="alt-arrow-left" />
Go back
</Button>
+4 -3
View File
@@ -3,11 +3,12 @@
import {publishDelete} from "@app/commands"
import {clearModals} from "@app/modal"
export let url
export let event
const {url, event} = $props()
const confirm = async () => {
await publishDelete({event, relays: [url]})
const snapshot = $state.snapshot(event)
await publishDelete({event: snapshot, relays: [url]})
clearModals()
}
+44 -31
View File
@@ -17,6 +17,7 @@
isAddress,
isNewline,
} from "@welshman/content"
import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
@@ -30,14 +31,27 @@
import ContentMention from "@app/components/ContentMention.svelte"
import {entityLink, userSettingValues} from "@app/state"
export let event
export let minLength = 500
export let maxLength = 700
export let showEntire = false
export let hideMedia = false
export let expandMode = "block"
export let quoteProps: Record<string, any> = {}
export let depth = 0
interface Props {
event: any
minLength?: number
maxLength?: number
showEntire?: boolean
hideMediaAtDepth?: number
expandMode?: string
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)
@@ -48,13 +62,13 @@
const isBlock = (i: number) => {
const parsed = fullContent[i]
if (!parsed || hideMedia) return false
if (!parsed || hideMediaAtDepth <= depth) return false
if (isLink(parsed) && $userSettingValues.show_media && isStartOrEnd(i)) {
return true
}
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i) && depth < 1) {
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i)) {
return true
}
@@ -82,20 +96,23 @@
warning = null
}
let warning =
$userSettingValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1]
let warning = $state(
$userSettingValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
)
$: shortContent = showEntire
? fullContent
: truncate(fullContent, {
minLength,
maxLength,
mediaLength: hideMedia ? 20 : 200,
})
const shortContent = $derived(
showEntire
? fullContent
: truncate(fullContent, {
minLength,
maxLength,
mediaLength: hideMediaAtDepth <= depth ? 20 : 200,
}),
)
$: hasEllipsis = shortContent.some(isEllipsis)
$: expandInline = hasEllipsis && expandMode === "inline"
$: expandBlock = hasEllipsis && expandMode === "block"
const hasEllipsis = $derived(shortContent.some(isEllipsis))
const expandInline = $derived(hasEllipsis && expandMode === "inline")
const expandBlock = $derived(hasEllipsis && expandMode === "block")
</script>
<div class="relative">
@@ -104,7 +121,7 @@
<Icon icon="danger" />
<p>
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>
</div>
{:else}
@@ -112,8 +129,8 @@
class="overflow-hidden text-ellipsis break-words"
style={expandBlock ? "mask-image: linear-gradient(0deg, transparent 0px, black 100px)" : ""}>
{#each shortContent as parsed, i}
{#if isNewline(parsed)}
<ContentNewline value={parsed.value.slice(isBlock(i - 1) ? 1 : 0)} />
{#if isNewline(parsed) && !isBlock(i - 1)}
<ContentNewline value={parsed.value} />
{:else if isTopic(parsed)}
<ContentTopic value={parsed.value} />
{:else if isCode(parsed)}
@@ -132,11 +149,7 @@
<ContentMention value={parsed.value} />
{:else if isEvent(parsed) || isAddress(parsed)}
{#if isBlock(i)}
<ContentQuote {...quoteProps} value={parsed.value} {depth} {event}>
<div slot="note-content" let:event>
<svelte:self {quoteProps} {hideMedia} {event} depth={depth + 1} />
</div>
</ContentQuote>
<ContentQuote {depth} {relays} {hideMediaAtDepth} value={parsed.value} {event} />
{:else}
<Link
external
@@ -158,7 +171,7 @@
<button
type="button"
class="btn btn-neutral"
on:click|stopPropagation|preventDefault={expand}>
onclick={stopPropagation(preventDefault(expand))}>
See more
</button>
</div>
+1 -2
View File
@@ -1,6 +1,5 @@
<script lang="ts">
export let value
export let isBlock
const {value, isBlock} = $props()
</script>
<code
+12 -4
View File
@@ -1,11 +1,14 @@
<script lang="ts">
import {ellipsize, postJson} from "@welshman/lib"
import {dufflepud, imgproxy} from "@app/state"
import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import {pushModal} from "@app/modal"
export let value
const {value} = $props()
let hideImage = $state(false)
const url = value.url.toString()
@@ -19,6 +22,10 @@
return json
}
const onError = () => {
hideImage = true
}
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
</script>
@@ -29,19 +36,20 @@
<track kind="captions" />
</video>
{: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" />
</button>
{:else}
{#await loadPreview()}
<div class="center my-12 w-full">
<span class="loading loading-spinner" />
<span class="loading loading-spinner"></span>
</div>
{:then preview}
<div class="bg-alt flex max-w-xl flex-col leading-normal">
{#if preview.image}
{#if preview.image && !hideImage}
<img
alt="Link preview"
onerror={onError}
src={imgproxy(preview.image)}
class="bg-alt max-h-72 object-contain object-center" />
{/if}
+2 -2
View File
@@ -2,11 +2,11 @@
import Button from "@lib/components/Button.svelte"
import {imgproxy} from "@app/state"
export let url
const {url} = $props()
const back = () => history.back()
</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" />
</Button>
+3 -2
View File
@@ -1,11 +1,12 @@
<script lang="ts">
import {displayUrl} from "@welshman/lib"
import {preventDefault} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import {pushModal} from "@app/modal"
export let value
const {value} = $props()
const url = value.url.toString()
@@ -14,7 +15,7 @@
{#if url.match(/\.(jpe?g|png|gif|webp)$/)}
<!-- 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" />
{displayUrl(url)}
</a>
+2 -2
View File
@@ -5,13 +5,13 @@
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/modal"
export let value
const {value} = $props()
const profile = deriveProfile(value.pubkey)
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey})
</script>
<Button on:click={openProfile} class="link-content">
<Button onclick={openProfile} class="link-content">
@{displayProfile($profile)}
</Button>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
export let value
const {value} = $props()
</script>
{#each value as _}
+16 -11
View File
@@ -3,18 +3,15 @@
import {goto} from "$app/navigation"
import {ctx, nthEq} from "@welshman/lib"
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 Spinner from "@lib/components/Spinner.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import {deriveEvent, entityLink, ROOM} from "@app/state"
import {makeThreadPath, makeRoomPath} from "@app/routes"
import {makeThreadPath, makeCalendarPath, makeRoomPath} from "@app/routes"
export let value
export let event
export let depth = 0
export let relays: string[] = []
export let minimal = false
const {value, event, depth, hideMediaAtDepth, relays = []} = $props()
const {id, identifier, kind, pubkey, relays: relayHints = []} = value
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
@@ -60,7 +57,7 @@
return Boolean(event)
}
const onClick = (e: Event) => {
const onclick = () => {
if ($quote) {
if ($quote.kind === DIRECT_MESSAGE) {
return scrollToEvent($quote.id)
@@ -74,6 +71,10 @@
return goto(makeThreadPath(url, $quote.id))
}
if ($quote.kind === EVENT_TIME) {
return goto(makeCalendarPath(url, $quote.id))
}
if ($quote.kind === MESSAGE) {
return scrollToEvent($quote.id) || openMessage(url, room, $quote.id)
}
@@ -86,6 +87,10 @@
return goto(makeThreadPath(url, id))
}
if (parseInt(kind) === EVENT_TIME) {
return goto(makeCalendarPath(url, id))
}
if (parseInt(kind) === MESSAGE) {
return scrollToEvent(id) || openMessage(url, room, id)
}
@@ -97,10 +102,10 @@
}
</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}
<NoteCard {minimal} event={$quote} class="bg-alt rounded-box p-4">
<slot name="note-content" event={$quote} {depth} />
<NoteCard event={$quote} class="bg-alt rounded-box p-4">
<NoteContent {hideMediaAtDepth} {relays} event={$quote} depth={depth + 1} />
</NoteCard>
{:else}
<div class="rounded-box p-4">
+2 -2
View File
@@ -3,12 +3,12 @@
import Button from "@lib/components/Button.svelte"
import {clip} from "@app/toast"
export let value
const {value} = $props()
const copy = () => clip(value)
</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" />
{value.slice(0, 16)}...
</Button>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
export let value
const {value} = $props()
</script>
<span class="link-content">
+4 -5
View File
@@ -7,15 +7,14 @@
import {pushModal} from "@app/modal"
import {BURROW_URL} from "@app/state"
export let email
export let confirm_token
const {email, confirm_token} = $props()
const login = () => {
pushModal(LogInPassword, {email}, {path: "/"})
}
let error: string
let loading = true
let error = $state("")
let loading = $state(true)
onMount(async () => {
const [res] = await Promise.all([
@@ -49,5 +48,5 @@
{/if}
</Spinner>
</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>
+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 {clip} from "@app/toast"
export let event
const {event} = $props()
const relays = ctx.app.router.Event(event).getUrls()
const nevent1 = nip19.neventEncode({...event, relays})
@@ -20,36 +20,48 @@
<div class="column gap-4">
<ModalHeader>
<div slot="title">Event Details</div>
<div slot="info">The full details of this event are shown below.</div>
{#snippet title()}
<div>Event Details</div>
{/snippet}
{#snippet info()}
<div>The full details of this event are shown below.</div>
{/snippet}
</ModalHeader>
<FieldInline>
<p slot="label">Event Link</p>
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
<Icon icon="file" />
<input type="text" class="ellipsize min-w-0 grow" value={nevent1} />
<Button on:click={copyLink} class="flex items-center">
<Icon icon="copy" />
</Button>
</label>
{#snippet label()}
<p>Event Link</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="file" />
<input type="text" class="ellipsize min-w-0 grow" value={nevent1} />
<Button onclick={copyLink} class="flex items-center">
<Icon icon="copy" />
</Button>
</label>
{/snippet}
</FieldInline>
<FieldInline>
<p slot="label">Author Pubkey</p>
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
<Icon icon="user-circle" />
<input type="text" class="ellipsize min-w-0 grow" value={npub1} />
<Button on:click={copyPubkey} class="flex items-center">
<Icon icon="copy" />
</Button>
</label>
{#snippet label()}
<p>Author Pubkey</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="user-circle" />
<input type="text" class="ellipsize min-w-0 grow" value={npub1} />
<Button onclick={copyPubkey} class="flex items-center">
<Icon icon="copy" />
</Button>
</label>
{/snippet}
</FieldInline>
<div class="relative">
<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">
<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
</Button>
</p>
</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>
-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">
import type {TrustedEvent} from "@welshman/util"
import {COMMENT} from "@welshman/util"
import {pubkey} from "@welshman/app"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import EventInfo from "@app/components/EventInfo.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 {pushModal} from "@app/modal"
export let url
export let event
export let onClick
const {
url,
noun,
event,
onClick,
}: {
url: string
noun: string
event: TrustedEvent
onClick: () => void
} = $props()
const isRoot = event.kind !== COMMENT
@@ -27,7 +36,7 @@
const share = () => {
onClick()
pushModal(ThreadShare, {url, event})
pushModal(EventShare, {url, event})
}
const showDelete = () => {
@@ -39,28 +48,28 @@
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl">
{#if isRoot}
<li>
<Button on:click={share}>
<Button onclick={share}>
<Icon size={4} icon="share-circle" />
Share to Chat
</Button>
</li>
{/if}
<li>
<Button on:click={showInfo}>
<Button onclick={showInfo}>
<Icon size={4} icon="code-2" />
Message Details
{noun} Details
</Button>
</li>
{#if event.pubkey === $pubkey}
<li>
<Button on:click={showDelete} class="text-error">
<Button onclick={showDelete} class="text-error">
<Icon size={4} icon="trash-bin-2" />
Delete Message
Delete {noun}
</Button>
</li>
{:else}
<li>
<Button class="text-error" on:click={report}>
<Button class="text-error" onclick={report}>
<Icon size={4} icon="danger" />
Report Content
</Button>
@@ -1,28 +1,25 @@
<script lang="ts">
import {writable} from "svelte/store"
import {EditorContent} from "svelte-tiptap"
import {isMobile} from "@lib/html"
import {isMobile, preventDefault} from "@lib/html"
import {fly, slideAndFade} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {publishComment} from "@app/commands"
import {tagRoom, GENERAL, PROTECTED} from "@app/state"
import {makeEditor} from "@app/editor"
import {pushToast} from "@app/toast"
export let url
export let event
export let onClose
export let onSubmit
const {url, event, onClose, onSubmit} = $props()
const uploading = writable(false)
const submit = () => {
if ($uploading) return
const content = $editor.getText({blockSeparator: "\n"}).trim()
const tags = [...$editor.storage.nostr.getEditorTags(), tagRoom(GENERAL, url), PROTECTED]
const content = editor.getText({blockSeparator: "\n"}).trim()
const tags = [...editor.storage.nostr.getEditorTags(), tagRoom(GENERAL, url), PROTECTED]
if (!content) {
return pushToast({
@@ -40,16 +37,16 @@
<form
in:fly
out:slideAndFade
on:submit|preventDefault={submit}
onsubmit={preventDefault(submit)}
class="card2 sticky bottom-2 z-feature mx-2 mt-4 bg-neutral">
<div class="relative">
<div class="note-editor flex-grow overflow-hidden">
<EditorContent editor={$editor} />
<EditorContent {editor} />
</div>
<Button
data-tip="Add an image"
class="tooltip tooltip-left absolute bottom-1 right-2"
on:click={$editor.commands.selectFiles}>
onclick={editor.commands.selectFiles}>
{#if $uploading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
@@ -58,7 +55,7 @@
</Button>
</div>
<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>
</ModalFooter>
</form>
+40 -24
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import Field from "@lib/components/Field.svelte"
@@ -8,8 +9,7 @@
import {pushToast} from "@app/toast"
import {publishReport} from "@app/commands"
export let url
export let event
const {url, event} = $props()
const back = () => history.back()
@@ -31,37 +31,53 @@
return pushToast({message: "Your report has been sent!"})
}
let reason = ""
let content = ""
let loading = false
let reason = $state("")
let content = $state("")
let loading = $state(false)
</script>
<form class="column gap-4" on:submit|preventDefault={confirm}>
<form class="column gap-4" onsubmit={preventDefault(confirm)}>
<ModalHeader>
<div slot="title">Report Content</div>
<div slot="info">Flag inappropriate content.</div>
{#snippet title()}
<div>Report Content</div>
{/snippet}
{#snippet info()}
<div>Flag inappropriate content.</div>
{/snippet}
</ModalHeader>
<Field>
<p slot="label">Reason*</p>
<select slot="input" class="select select-bordered" bind:value={reason}>
<option disabled selected>Choose a reason</option>
<option>Nudity</option>
<option>Malware</option>
<option>Profanity</option>
<option>Illegal</option>
<option>Spam</option>
<option>Impersonation</option>
<option>Other</option>
</select>
<p slot="info">Please select a reason for your report.</p>
{#snippet label()}
<p>Reason*</p>
{/snippet}
{#snippet input()}
<select class="select select-bordered" bind:value={reason}>
<option disabled selected>Choose a reason</option>
<option>Nudity</option>
<option>Malware</option>
<option>Profanity</option>
<option>Illegal</option>
<option>Spam</option>
<option>Impersonation</option>
<option>Other</option>
</select>
{/snippet}
{#snippet info()}
<p>Please select a reason for your report.</p>
{/snippet}
</Field>
<Field>
<p slot="label">Details</p>
<textarea slot="input" class="textarea textarea-bordered" bind:value={content} />
<p slot="info">Please provide any additional details relevant to your report.</p>
{#snippet label()}
<p>Details</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>
<ModalFooter>
<Button class="btn btn-link" on:click={back}>
<Button class="btn btn-link" onclick={back}>
<Icon icon="alt-arrow-left" />
Go back
</Button>
+9 -6
View File
@@ -8,8 +8,7 @@
import Profile from "@app/components/Profile.svelte"
import {publishDelete} from "@app/commands"
export let url
export let event
const {url, event} = $props()
const reports = deriveEvents(repository, {
filters: [{kinds: [REPORT], "#e": [event.id]}],
@@ -30,8 +29,12 @@
<div class="column gap-4">
<ModalHeader>
<div slot="title">Report Details</div>
<div slot="info">All reports for this event are shown below.</div>
{#snippet title()}
<div>Report Details</div>
{/snippet}
{#snippet info()}
<div>All reports for this event are shown below.</div>
{/snippet}
</ModalHeader>
{#each $reports as report (report.id)}
{@const reason = getReason(report.tags)}
@@ -43,7 +46,7 @@
<span>Reported this event as "{reason}"</span>
</div>
{#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}
</div>
{#if report.content}
@@ -51,5 +54,5 @@
{/if}
</div>
{/each}
<Button class="btn btn-primary" on:click={back}>Got it</Button>
<Button class="btn btn-primary" onclick={back}>Got it</Button>
</div>
@@ -1,8 +1,7 @@
<script lang="ts">
import {nip19} from "nostr-tools"
import {goto} from "$app/navigation"
import {ctx} from "@welshman/lib"
import {toNostrURI} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {preventDefault} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
@@ -12,16 +11,12 @@
import {makeRoomPath} from "@app/routes"
import {setKey} from "@app/implicit"
export let url
export let event
const relays = ctx.app.router.Event(event).getUrls()
const nevent = nip19.neventEncode({id: event.id, relays})
const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props()
const back = () => history.back()
const onSubmit = () => {
setKey("content", toNostrURI(nevent))
setKey("share", event)
goto(makeRoomPath(url, selection), {replaceState: true})
}
@@ -29,13 +24,17 @@
selection = room === selection ? "" : room
}
let selection = ""
let selection = $state("")
</script>
<form class="column gap-4" on:submit|preventDefault={onSubmit}>
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
<ModalHeader>
<div slot="title">Share Thread</div>
<div slot="info">Which room would you like to share this thread to?</div>
{#snippet title()}
<div>Share {noun}</div>
{/snippet}
{#snippet info()}
<div>Which room would you like to share this event to?</div>
{/snippet}
</ModalHeader>
<div class="grid grid-cols-3 gap-2">
{#each $channelsByUrl.get(url) || [] as channel (channel.room)}
@@ -44,18 +43,18 @@
class="btn"
class:btn-neutral={selection !== channel.room}
class:btn-primary={selection === channel.room}
on:click={() => toggleRoom(channel.room)}>
onclick={() => toggleRoom(channel.room)}>
#<ChannelName {...channel} />
</button>
{/each}
</div>
<ModalFooter>
<Button class="btn btn-link" on:click={back}>
<Button class="btn btn-link" onclick={back}>
<Icon icon="alt-arrow-left" />
Go back
</Button>
<Button type="submit" class="btn btn-primary" disabled={!selection}>
Share Thread
Share {noun}
<Icon icon="alt-arrow-right" />
</Button>
</ModalFooter>
+4 -2
View File
@@ -7,7 +7,9 @@
<div class="column gap-4">
<ModalHeader>
<div slot="title">What is a bunker link?</div>
{#snippet title()}
<div>What is a bunker link?</div>
{/snippet}
</ModalHeader>
<p>
<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
>.
</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>
+4 -2
View File
@@ -7,7 +7,9 @@
<div class="column gap-4">
<ModalHeader>
<div slot="title">What is a nostr address?</div>
{#snippet title()}
<div>What is a nostr address?</div>
{/snippet}
</ModalHeader>
<p>
{PLATFORM_NAME} hosts spaces on the <Link external href="https://nostr.com/" class="underline"
@@ -23,5 +25,5 @@
class="underline">nostr.how</Link
>.
</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>
+6 -4
View File
@@ -16,7 +16,9 @@
<div class="column gap-4">
<ModalHeader>
<div slot="title">What is a private key?</div>
{#snippet title()}
<div>What is a private key?</div>
{/snippet}
</ModalHeader>
<p>
Most online services keep track of users by giving them a username and password. This gives the
@@ -36,16 +38,16 @@
</p>
<p>If you'd like to switch to self-custody, please click below to get started.</p>
<ModalFooter>
<Button class="btn btn-link" on:click={back}>
<Button class="btn btn-link" onclick={back}>
<Icon icon="alt-arrow-left" />
Go back
</Button>
<Button class="btn btn-primary" on:click={startEject}>
<Button class="btn btn-primary" onclick={startEject}>
<Icon icon="check-circle" />
I want to hold my own keys
</Button>
</ModalFooter>
{:else}
<Button class="btn btn-primary" on:click={back}>Got it</Button>
<Button class="btn btn-primary" onclick={back}>Got it</Button>
{/if}
</div>
+4 -2
View File
@@ -6,7 +6,9 @@
<div class="column gap-4">
<ModalHeader>
<div slot="title">What is nostr?</div>
{#snippet title()}
<div>What is nostr?</div>
{/snippet}
</ModalHeader>
<p>
<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
<Link external class="link" href="https://nosta.me/">nosta.me</Link>.
</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>
+4 -2
View File
@@ -7,7 +7,9 @@
<div class="column gap-4">
<ModalHeader>
<div slot="title">What is a relay?</div>
{#snippet title()}
<div>What is a relay?</div>
{/snippet}
</ModalHeader>
<p>
{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
double check that you have access to the relays you try to use by visiting their website.
</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>
+20 -8
View File
@@ -20,18 +20,30 @@
<h1 class="heading">Welcome to {PLATFORM_NAME}!</h1>
<p class="text-center">The chat app built for self-hosted communities.</p>
</div>
<Button on:click={logIn}>
<Button onclick={logIn}>
<CardButton class="!btn-primary">
<div slot="icon"><Icon icon="login-2" size={7} /></div>
<div slot="title">Log in</div>
<div slot="info">If you've been here before, you know the drill.</div>
{#snippet icon()}
<div><Icon icon="login-2" size={7} /></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>
</Button>
<Button on:click={signUp}>
<Button onclick={signUp}>
<CardButton>
<div slot="icon"><Icon icon="add-circle" size={7} /></div>
<div slot="title">Create an account</div>
<div slot="info">Just a few questions and you'll be on your way.</div>
{#snippet icon()}
<div><Icon icon="add-circle" size={7} /></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>
</Button>
<p class="text-center text-xs opacity-75">
+52 -50
View File
@@ -16,20 +16,13 @@
import {loadUserData} from "@app/commands"
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 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[] = []) => {
await loadUserData(session.pubkey, {relays})
@@ -39,41 +32,50 @@
clearModals()
}
const loginWithNip07 = withLoading("nip07", async () => {
const pubkey = await getNip07()?.getPublicKey()
const loginWithNip07 = async () => {
loading = "nip07"
if (pubkey) {
await onSuccess({method: "nip07", pubkey})
} else {
pushToast({
theme: "error",
message: "Something went wrong! Please try again.",
})
try {
const pubkey = await getNip07()?.getPublicKey()
if (pubkey) {
await onSuccess({method: "nip07", pubkey})
} else {
pushToast({
theme: "error",
message: "Something went wrong! Please try again.",
})
}
} finally {
loading = undefined
}
})
}
const loginWithNip55 = withLoading("nip55", async (app: any) => {
const signer = new Nip55Signer(app.packageName)
const pubkey = await signer.getPubkey()
const loginWithNip55 = async (app: any) => {
loading = "nip55"
if (pubkey) {
await onSuccess({method: "nip55", pubkey, signer: app.packageName})
} else {
pushToast({
theme: "error",
message: "Something went wrong! Please try again.",
})
try {
const signer = new Nip55Signer(app.packageName)
const pubkey = await signer.getPubkey()
if (pubkey) {
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 loginWithBunker = () => pushModal(LogInBunker)
let signers: any[] = []
let loading: string | undefined
$: hasSigner = getNip07() || signers.length > 0
const hasSigner = $derived(getNip07() || signers.length > 0)
onMount(async () => {
if (Capacitor.isNativePlatform()) {
@@ -86,13 +88,13 @@
<h1 class="heading">Log in with Nostr</h1>
<p class="m-auto max-w-sm text-center">
{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.
</p>
{#if getNip07()}
<Button disabled={loading} on:click={loginWithNip07} class="btn btn-primary">
<Button {disabled} onclick={loginWithNip07} class="btn btn-primary">
{#if loading === "nip07"}
<span class="loading loading-spinner mr-3" />
<span class="loading loading-spinner mr-3"></span>
{:else}
<Icon icon="widget" />
{/if}
@@ -100,9 +102,9 @@
</Button>
{/if}
{#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"}
<span class="loading loading-spinner mr-3" />
<span class="loading loading-spinner mr-3"></span>
{:else}
<img src={app.iconUrl} alt={app.name} width="20" height="20" />
{/if}
@@ -110,9 +112,9 @@
</Button>
{/each}
{#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"}
<span class="loading loading-spinner mr-3" />
<span class="loading loading-spinner mr-3"></span>
{:else}
<Icon icon="key" />
{/if}
@@ -120,16 +122,16 @@
</Button>
{/if}
<Button
disabled={loading}
on:click={loginWithBunker}
onclick={loginWithBunker}
{disabled}
class="btn {hasSigner || BURROW_URL ? 'btn-neutral' : 'btn-primary'}">
<Icon icon="cpu" />
Log in with Remote Signer
</Button>
{#if BURROW_URL && hasSigner}
<Button disabled={loading} on:click={loginWithPassword} class="btn">
<Button {disabled} onclick={loginWithPassword} class="btn">
{#if loading === "password"}
<span class="loading loading-spinner mr-3" />
<span class="loading loading-spinner mr-3"></span>
{:else}
<Icon icon="key" />
{/if}
@@ -139,7 +141,7 @@
{#if !hasSigner || !BURROW_URL}
<Link
external
disabled={loading}
{disabled}
href="https://nostrapps.com#signers"
class="btn {hasSigner || BURROW_URL ? '' : 'btn-neutral'}">
<Icon icon="compass" />
@@ -148,6 +150,6 @@
{/if}
<div class="text-sm">
Need an account?
<Button class="link" on:click={signUp}>Register instead</Button>
<Button class="link" onclick={signUp}>Register instead</Button>
</div>
</div>
+33 -24
View File
@@ -2,6 +2,7 @@
import {onMount, onDestroy} from "svelte"
import {Nip46Broker, getPubkey, makeSecret} from "@welshman/signer"
import {addSession} from "@welshman/app"
import {preventDefault} from "@lib/html"
import {slideAndFade} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
@@ -26,7 +27,7 @@
const back = () => history.back()
const onSubmit = async () => {
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(input)
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(bunker)
if (loading) {
return
@@ -59,18 +60,18 @@
clearModals()
}
let url = ""
let input = ""
let loading = false
let url = $state("")
let bunker = $state("")
let loading = $state(false)
$: {
$effect(() => {
// For testing and for play store reviewers
if (input === "reviewkey") {
if (bunker === "reviewkey") {
const secret = makeSecret()
addSession({method: "nip01", secret, pubkey: getPubkey(secret)})
}
}
})
onMount(async () => {
url = await broker.makeNostrconnectUrl({
@@ -121,35 +122,43 @@
})
</script>
<form class="column gap-4" on:submit|preventDefault={onSubmit}>
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
<ModalHeader>
<div slot="title">Log In</div>
<div slot="info">
Connect your signer by scanning the QR code below or pasting a bunker link.
</div>
{#snippet title()}
<div>Log In</div>
{/snippet}
{#snippet info()}
<div>Connect your signer by scanning the QR code below or pasting a bunker link.</div>
{/snippet}
</ModalHeader>
{#if !loading && url}
<div class="w-xs m-auto" out:slideAndFade>
<div class="flex justify-center" out:slideAndFade>
<QRCode code={url} />
</div>
{/if}
<Field>
<p slot="label">Bunker Link*</p>
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
<Icon icon="cpu" />
<input disabled={loading} bind:value={input} class="grow" placeholder="bunker://" />
</label>
<p slot="info">
A login link provided by a nostr signing app.
<Button class="link" on:click={() => pushModal(InfoBunker)}>What is a bunker link?</Button>
</p>
{#snippet label()}
<p>Bunker Link*</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="cpu" />
<input disabled={loading} bind:value={bunker} class="grow" placeholder="bunker://" />
</label>
{/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>
<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" />
Go back
</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>
<Icon icon="alt-arrow-right" />
</Button>
+35 -18
View File
@@ -4,6 +4,7 @@
import {Nip46Broker, makeSecret} from "@welshman/signer"
import {normalizeRelayUrl} from "@welshman/util"
import {addSession} from "@welshman/app"
import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import FieldInline from "@lib/components/FieldInline.svelte"
@@ -17,7 +18,11 @@
import {pushToast} from "@app/toast"
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()
@@ -50,8 +55,8 @@
}
let url = ""
let password = ""
let loading = false
let password = $state("")
let loading = $state(false)
onMount(async () => {
url = await broker.makeNostrconnectUrl({
@@ -100,32 +105,44 @@
})
</script>
<form class="column gap-4" on:submit|preventDefault={onSubmit}>
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
<ModalHeader>
<div slot="title">Log In</div>
<div slot="info">Log in using your email and password</div>
{#snippet title()}
<div>Log In</div>
{/snippet}
{#snippet info()}
<div>Log in using your email and password</div>
{/snippet}
</ModalHeader>
<FieldInline>
<p slot="label">Email</p>
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
<Icon icon="user-rounded" />
<input bind:value={email} />
</label>
{#snippet label()}
<p>Email</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="user-rounded" />
<input bind:value={email} />
</label>
{/snippet}
</FieldInline>
<FieldInline>
<p slot="label">Password</p>
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
<Icon icon="key" />
<input bind:value={password} type="password" />
</label>
{#snippet label()}
<p>Password</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="key" />
<input bind:value={password} type="password" />
</label>
{/snippet}
</FieldInline>
<p class="text-sm">
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>
</p>
<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" />
Go back
</Button>
+7 -4
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import {preventDefault} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
@@ -19,16 +20,18 @@
}
}
let loading = false
let loading = $state(false)
</script>
<form class="column gap-4" on:submit|preventDefault={doLogout}>
<form class="column gap-4" onsubmit={preventDefault(doLogout)}>
<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>
<p class="text-center">Your local database will be cleared.</p>
<ModalFooter>
<Button class="btn btn-link" on:click={back}>
<Button class="btn btn-link" onclick={back}>
<Icon icon="alt-arrow-left" />
Go back
</Button>
+37 -13
View File
@@ -13,33 +13,57 @@
<div class="column menu gap-2">
<Link replaceState href="/settings/profile">
<CardButton>
<div slot="icon"><Icon icon="user-rounded" size={7} /></div>
<div slot="title">Profile</div>
<div slot="info">Customize your user profile</div>
{#snippet icon()}
<div><Icon icon="user-rounded" size={7} /></div>
{/snippet}
{#snippet title()}
<div>Profile</div>
{/snippet}
{#snippet info()}
<div>Customize your user profile</div>
{/snippet}
</CardButton>
</Link>
<Link replaceState href="/settings/relays">
<CardButton>
<div slot="icon"><Icon icon="server" size={7} /></div>
<div slot="title">Relays</div>
<div slot="info">Control how {PLATFORM_NAME} talks to the network</div>
{#snippet icon()}
<div><Icon icon="server" size={7} /></div>
{/snippet}
{#snippet title()}
<div>Relays</div>
{/snippet}
{#snippet info()}
<div>Control how {PLATFORM_NAME} talks to the network</div>
{/snippet}
</CardButton>
</Link>
<Link replaceState href="/settings">
<CardButton>
<div slot="icon"><Icon icon="settings" size={7} /></div>
<div slot="title">Settings</div>
<div slot="info">Get into the details about how {PLATFORM_NAME} works</div>
{#snippet icon()}
<div><Icon icon="settings" size={7} /></div>
{/snippet}
{#snippet title()}
<div>Settings</div>
{/snippet}
{#snippet info()}
<div>Get into the details about how {PLATFORM_NAME} works</div>
{/snippet}
</CardButton>
</Link>
<Link replaceState href="/settings/about">
<CardButton>
<div slot="icon"><Icon icon="code-2" size={7} /></div>
<div slot="title">About</div>
<div slot="info">Learn about {PLATFORM_NAME} and support the developer</div>
{#snippet icon()}
<div><Icon icon="code-2" size={7} /></div>
{/snippet}
{#snippet title()}
<div>About</div>
{/snippet}
{#snippet info()}
<div>Learn about {PLATFORM_NAME} and support the developer</div>
{/snippet}
</CardButton>
</Link>
<Button on:click={logout} class="btn btn-neutral">
<Button onclick={logout} class="btn btn-neutral">
<Icon icon="exit" /> Log Out
</Button>
</div>
+31 -19
View File
@@ -26,9 +26,10 @@
import {pushModal} from "@app/modal"
import {makeSpacePath} from "@app/routes"
export let url
const {url} = $props()
const threadsPath = makeSpacePath(url, "threads")
const calendarPath = makeSpacePath(url, "calendar")
const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url)
@@ -55,14 +56,16 @@
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
let showMenu = false
let replaceState = false
let element: Element
let showMenu = $state(false)
let replaceState = $state(false)
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 () => {
replaceState = Boolean(element.closest(".drawer"))
onMount(() => {
replaceState = Boolean(element?.closest(".drawer"))
pullConservatively({relays: [url], filters: [{kinds: [GROUP_META]}]})
})
</script>
@@ -70,7 +73,7 @@
<div bind:this={element}>
<SecondaryNavSection class="max-h-screen">
<div>
<SecondaryNavItem class="w-full !justify-between" on:click={openMenu}>
<SecondaryNavItem class="w-full !justify-between" onclick={openMenu}>
<strong class="ellipsize">{displayRelayUrl(url)}</strong>
<Icon icon="alt-arrow-down" />
</SecondaryNavItem>
@@ -80,25 +83,25 @@
transition:fly
class="menu absolute z-popover mt-2 w-full rounded-box bg-base-100 p-2 shadow-xl">
<li>
<Button on:click={showMembers}>
<Button onclick={showMembers}>
<Icon icon="user-rounded" />
View Members ({members.length})
</Button>
</li>
<li>
<Button on:click={createInvite}>
<Button onclick={createInvite}>
<Icon icon="link-round" />
Create Invite
</Button>
</li>
<li>
{#if $userRoomsByUrl.has(url)}
<Button on:click={leaveSpace} class="text-error">
<Button onclick={leaveSpace} class="text-error">
<Icon icon="exit" />
Leave Space
</Button>
{:else}
<Button on:click={joinSpace} class="bg-primary text-primary-content">
<Button onclick={joinSpace} class="bg-primary text-primary-content">
<Icon icon="login-2" />
Join Space
</Button>
@@ -109,19 +112,28 @@
{/if}
</div>
<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
</SecondaryNavItem>
<SecondaryNavItem href={threadsPath} notification={$notifications.has(threadsPath)}>
<SecondaryNavItem
{replaceState}
href={threadsPath}
notification={$notifications.has(threadsPath)}>
<Icon icon="notes-minimalistic" /> Threads
</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>
{#each $userRooms as room, i (room)}
<MenuSpaceRoomItem notify {url} {room} />
<MenuSpaceRoomItem {replaceState} notify {url} {room} />
{/each}
{#if $otherRooms.length > 0}
<div class="h-2" />
<div class="h-2"></div>
<SecondaryNavHeader>
{#if $userRooms.length > 0}
Other Rooms
@@ -131,9 +143,9 @@
</SecondaryNavHeader>
{/if}
{#each $otherRooms as room, i (room)}
<MenuSpaceRoomItem {url} {room} />
<MenuSpaceRoomItem {replaceState} {url} {room} />
{/each}
<SecondaryNavItem on:click={addRoom}>
<SecondaryNavItem {replaceState} onclick={addRoom}>
<Icon icon="add-circle" />
Create room
</SecondaryNavItem>
+3 -3
View File
@@ -6,16 +6,16 @@
import {makeSpacePath} from "@app/routes"
import {pushDrawer} from "@app/modal"
export let url
const {url} = $props()
const path = makeSpacePath(url)
const openMenu = () => pushDrawer(MenuSpace, {url})
</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" />
{#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}
</Button>
+12 -4
View File
@@ -6,15 +6,23 @@
import {deriveChannel, channelIsLocked} from "@app/state"
import {notifications} from "@app/notifications"
export let url
export let room
export let notify = false
interface Props {
url: any
room: any
notify?: boolean
replaceState?: boolean
}
const {url, room, notify = false, replaceState = false}: Props = $props()
const path = makeRoomPath(url, room)
const channel = deriveChannel(url, room)
</script>
<SecondaryNavItem href={path} notification={notify ? $notifications.has(path) : false}>
<SecondaryNavItem
href={path}
{replaceState}
notification={notify ? $notifications.has(path) : false}>
{#if channelIsLocked($channel)}
<Icon icon="lock" size={4} />
{:else}

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