Compare commits

..

38 Commits

Author SHA1 Message Date
Jon Staab cee6c3c164 Bump versions 2025-01-28 19:22:57 -08:00
Jon Staab 06d0ae2798 Trim tiptap css 2025-01-28 16:23:19 -08:00
Jon Staab b129ef4242 Add build hash 2025-01-28 14:51:33 -08:00
Jon Staab 48a45f3a3a Add media server settings 2025-01-28 14:44:43 -08:00
Jon Staab ce1fb396e3 Add button to scroll to new messages in channel 2025-01-28 14:19:46 -08:00
Jon Staab e95c57bcb7 Replace nsec.app signup with njump.me 2025-01-28 13:04:37 -08:00
Jon Staab 414f5a5ace Update changelog 2025-01-28 12:33:35 -08:00
Jon Staab a331d24bb1 Bump welshman 2025-01-28 12:28:26 -08:00
Jon Staab fb53e53411 Add reply to long press menu 2025-01-28 09:47:11 -08:00
Jon Staab 1e7e439e3f Bump version 2025-01-28 09:30:17 -08:00
Jon Staab 3368cba1be Bump welshman/app 2025-01-28 09:26:11 -08:00
Jon Staab 4f0579bb7f Fix missing compose input, handle parents differently 2025-01-28 09:23:51 -08:00
Jon Staab 08e80262a4 Bump welshman, rework channel loading 2025-01-28 08:13:20 -08:00
Jon Staab e10b83bed8 Improve data loading a bit 2025-01-24 13:35:09 -08:00
Jon Staab fa17c398ca Make deploy documentation more clear 2025-01-24 09:49:14 -08:00
Jon Staab e0840f24dd Drop support for legacy messages 2025-01-24 09:36:24 -08:00
Jon Staab 8e38271534 Attempt to fix broken android 2025-01-20 08:40:37 -08:00
Jon Staab 86928fc12c Bump welshman 2025-01-17 15:01:05 -08:00
Jon Staab e15fb3ce9c Update app icon 2025-01-17 09:08:55 -08:00
Jon Staab bf1ab5f0ee Bump version 2025-01-17 05:30:08 -08:00
Jon Staab 59568f95f1 Update logo 2025-01-16 15:26:54 -08:00
Jon Staab 75d52e7e17 Outsource terms/privacy 2025-01-16 14:54:43 -08:00
Jon Staab bdb5d3dfaa Update changelog 2025-01-16 08:46:51 -08:00
Jon Staab c387b65460 Bump version to 0.2.3 2025-01-16 08:41:20 -08:00
Jon Staab 01c4219922 Add reviewkey auth bypass, remove note to self 2025-01-15 15:48:39 -08:00
Jon Staab 9ca4440038 Add terms/privacy notice to landing 2025-01-15 14:05:23 -08:00
Jon Staab d6cc414f41 Add terms and privacy 2025-01-15 13:56:04 -08:00
Jon Staab 7ccb2949a9 Update gitignore 2025-01-15 13:41:15 -08:00
Jon Staab 8d4e657af5 Tweak avatar again 2025-01-15 11:42:54 -08:00
Jon Staab 4886650dfa Add mark all read 2025-01-15 11:07:21 -08:00
Jon Staab e36e6093e9 Add avatar fallback 2025-01-15 10:56:56 -08:00
Jon Staab edd6e5c8fc Add send button 2025-01-15 09:10:48 -08:00
Jon Staab be7a42d951 Add reports to channel messages 2025-01-15 07:40:28 -08:00
Jon Staab af91fe129b Accommodate onion urls 2025-01-14 15:58:38 -08:00
Jon Staab 6fcf0e7f12 Fix migration 2025-01-14 15:03:36 -08:00
Jon Staab b6defe59a8 Improve loading and notifications 2025-01-02 16:58:04 -08:00
Jon Staab f618e4e1f3 Fix tiptap styling 2025-01-02 16:12:46 -08:00
Jon Staab 5253980cdc Update changelog 2025-01-02 15:59:51 -08:00
119 changed files with 4535 additions and 3415 deletions
+2
View File
@@ -1,6 +1,8 @@
VITE_DEFAULT_PUBKEYS=fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
VITE_BURROW_URL=
VITE_PLATFORM_URL=https://flotilla.social
VITE_PLATFORM_TERMS=https://flotilla.social/terms
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
VITE_PLATFORM_NAME=Flotilla
VITE_PLATFORM_LOGO=static/flotilla.png
VITE_PLATFORM_RELAY=
+11 -1
View File
@@ -1,3 +1,13 @@
src/assets
android
build
.idea
.gradle
*.png
*.ttf
gradlew*
_app
release
android/capacitor-cordova-android-plugins
android/app/src/androidTest
android/app/src/test
+3
View File
@@ -17,6 +17,9 @@ Thumbs.db
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Android
.idea
# Generated assets
static/favicon.ico
static/pwa-64x64.png
+37
View File
@@ -1,5 +1,42 @@
# Changelog
# 0.2.6
* Add reply to long-press menu
* Fix @-mentions
* Replace nsec.app signup with njump.me
* Add new messages button in rooms
* Add media server settings
* Add build hash to about page
# 0.2.5
* Improve room and data loading
* Use @welshman/editor
* Drop support for legacy event kinds
* Add support for back button navigation on android
* Remove note to self page (still available via chat)
* Improve chat conversation search
* Change how reply UI works
# 0.2.4
* Update icons
# 0.2.3
* Add NIP 56 reports for messages and threads
* Add ToS and privacy policy
* Add avatar fallback icons
* Add mark as read to chats
* Add send button to chat compose
* Accommodate onion URLs
* Improve loading and notifications
# 0.2.2
* Fix bug with sending messages
# 0.2.1
* Improve performance, as well as scrolling and loading
+5 -1
View File
@@ -6,7 +6,11 @@ If you would like to be interoperable with Flotilla, please check out this draft
# Deploy
To run your own Flotilla, it's as simple as `npm run build`, then serve the `build` directory.
To run your own Flotilla, it's as simple as:
- `npm install`
- `npm run build`
- `npx serve build`
## Environment
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "social.flotilla"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "0.2.2"
versionCode 6
versionName "0.2.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+1
View File
@@ -9,6 +9,7 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
implementation project(':nostr-signer-capacitor-plugin')
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 531 B

After

Width:  |  Height:  |  Size: 899 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 B

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 B

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 B

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 697 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

+1 -3
View File
@@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
@@ -19,4 +17,4 @@
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
</style>
</resources>
</resources>
+1 -1
View File
@@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.1'
classpath 'com.android.tools.build:gradle:8.8.0'
classpath 'com.google.gms:google-services:4.4.0'
// NOTE: Do not place your application dependencies here; they belong
+3
View File
@@ -2,5 +2,8 @@
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
include ':nostr-signer-capacitor-plugin'
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/nostr-signer-capacitor-plugin/android')
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+11 -1
View File
@@ -14,9 +14,12 @@ fi
# https://stackoverflow.com/a/69127685/1467342
eval "$temp_env"
if [[ -z $VITE_BUILD_HASH ]]; then
export VITE_BUILD_HASH=$(git rev-parse --short HEAD)
fi
if [[ $VITE_PLATFORM_LOGO =~ ^https://* ]]; then
curl $VITE_PLATFORM_LOGO > static/logo.png
cp static/logo.png assets/logo.png
export VITE_PLATFORM_LOGO=static/logo.png
fi
@@ -28,3 +31,10 @@ perl -i -pe"s|{DESCRIPTION}|$VITE_PLATFORM_DESCRIPTION|g" build/index.html
perl -i -pe"s|{ACCENT}|$VITE_PLATFORM_ACCENT|g" build/index.html
perl -i -pe"s|{NAME}|$VITE_PLATFORM_NAME|g" build/index.html
perl -i -pe"s|{URL}|$VITE_PLATFORM_URL|g" build/index.html
npx cap sync
npx @capacitor/assets generate \
--iconBackgroundColor '#eeeeee' \
--iconBackgroundColorDark '#222222' \
--splashBackgroundColor '#ffffff' \
--splashBackgroundColorDark '#191E24'
+3
View File
@@ -4,6 +4,9 @@ const config: CapacitorConfig = {
appId: 'social.flotilla',
appName: 'Flotilla',
webDir: 'build'
server: {
androidScheme: "https"
},
plugins: {
SplashScreen: {
androidSplashResourceName: "splash"
+3516 -2878
View File
File diff suppressed because it is too large Load Diff
+18 -27
View File
@@ -1,18 +1,21 @@
{
"name": "flotilla",
"version": "0.2.2",
"version": "0.2.6",
"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 sync && cap build android --androidreleasetype APK --signing-type apksigner",
"release:android": "cap build android --androidreleasetype APK --signing-type apksigner",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check: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",
@@ -37,38 +40,28 @@
},
"type": "module",
"dependencies": {
"@capacitor/android": "^6.1.2",
"@capacitor/cli": "^6.1.2",
"@capacitor/core": "^6.1.2",
"@capacitor/android": "^7.0.1",
"@capacitor/app": "^7.0.0",
"@capacitor/cli": "^6.2.0",
"@capacitor/core": "^7.0.1",
"@noble/curves": "^1.5.0",
"@noble/hashes": "^1.4.0",
"@poppanator/sveltekit-svg": "^4.2.1",
"@sentry/browser": "^8.35.0",
"@sveltejs/adapter-static": "^3.0.4",
"@tiptap/extension-code": "^2.6.6",
"@tiptap/extension-code-block": "^2.6.6",
"@tiptap/extension-document": "^2.6.6",
"@tiptap/extension-dropcursor": "^2.6.6",
"@tiptap/extension-gapcursor": "^2.6.6",
"@tiptap/extension-hard-break": "^2.6.6",
"@tiptap/extension-history": "^2.6.6",
"@tiptap/extension-paragraph": "^2.6.6",
"@tiptap/extension-placeholder": "^2.9.1",
"@tiptap/extension-text": "^2.6.6",
"@tiptap/suggestion": "^2.6.4",
"@types/qrcode": "^1.5.5",
"@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.6",
"@welshman/app": "~0.0.36",
"@welshman/content": "~0.0.15",
"@welshman/dvm": "~0.0.13",
"@welshman/editor": "~0.0.4",
"@welshman/app": "~0.0.41",
"@welshman/content": "~0.0.16",
"@welshman/dvm": "~0.0.14",
"@welshman/editor": "~0.0.10",
"@welshman/feeds": "~0.0.30",
"@welshman/lib": "~0.0.37",
"@welshman/net": "~0.0.45",
"@welshman/signer": "~0.0.19",
"@welshman/lib": "~0.0.38",
"@welshman/net": "~0.0.46",
"@welshman/signer": "~0.0.20",
"@welshman/store": "~0.0.15",
"@welshman/util": "~0.0.55",
"@welshman/util": "~0.0.59",
"daisyui": "^4.12.10",
"date-picker-svelte": "^2.13.0",
"dotenv": "^16.4.5",
@@ -76,11 +69,9 @@
"fuse.js": "^7.0.0",
"husky": "^9.1.6",
"idb": "^8.0.0",
"nostr-editor": "^0.0.3",
"nostr-signer-capacitor-plugin": "^0.0.3",
"nostr-tools": "^2.7.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"qrcode": "^1.5.4",
"svelte-tiptap": "^1.1.3"
"qrcode": "^1.5.4"
}
}
+46 -29
View File
@@ -40,15 +40,14 @@
:root {
font-family: Lato;
}
[data-theme] {
--base-100: oklch(var(--b1));
--base-200: oklch(var(--b2));
--base-300: oklch(var(--b3));
--base-content: oklch(var(--bc));
--primary: oklch(var(--p));
--primary-content: oklch(var(--pc));
--secondary: oklch(var(--s));
--secondary-content: oklch(var(--sc));
}
.bg-alt,
@@ -120,6 +119,16 @@
@apply overflow-hidden text-ellipsis;
}
[data-tip]::before {
@apply ellipsize;
}
@media (max-width: 639px) {
[data-tip]::before {
display: none;
}
}
.content-padding-x {
@apply px-4 sm:px-8 md:px-12;
}
@@ -176,45 +185,53 @@
@apply -m-1 min-h-12 p-1;
}
.tiptap[contenteditable="true"] {
.tiptap {
--tiptap-object-bg: var(--base-100);
--tiptap-object-fg: var(--base-content);
--tiptap-active-bg: var(--primary);
--tiptap-active-fg: var(--primary-content);
}
.tiptap-suggestions {
--tiptap-object-bg: var(--base-100);
--tiptap-object-fg: var(--base-content);
--tiptap-active-bg: var(--base-300);
--tiptap-active-fg: var(--base-content);
}
.tiptap {
@apply max-h-[350px] overflow-y-auto p-2 px-4;
}
.chat-editor .tiptap[contenteditable="true"] {
@apply rounded-box bg-base-300;
.tiptap p.is-editor-empty:first-child::before {
opacity: 40%;
}
.input-editor .tiptap[contenteditable="true"] {
@apply input input-bordered h-auto p-[.65rem];
.chat-editor .tiptap {
@apply rounded-box bg-base-300 pr-12;
}
.note-editor .tiptap[contenteditable="true"] {
.note-editor .tiptap {
--tiptap-object-bg: var(--base-200);
@apply input input-bordered h-auto min-h-32 rounded-box p-[.65rem] pb-6;
}
.tiptap pre code {
@apply link-content block w-full;
.input-editor .tiptap {
--tiptap-object-bg: var(--base-200);
@apply input input-bordered h-auto p-[.65rem];
}
.tiptap p code {
@apply link-content;
}
/* link-content, based on tiptap */
.link-content,
.tiptap [tag] {
@apply max-w-full overflow-hidden text-ellipsis whitespace-nowrap rounded bg-neutral px-1 text-neutral-content no-underline;
}
.link-content.link-content-selected {
@apply bg-primary text-primary-content;
}
.tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
opacity: 50%;
.link-content {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-radius: 3px;
padding: 0 0.25rem;
background-color: var(--base-100);
color: var(--base-content);
}
/* date input */
+51 -31
View File
@@ -1,7 +1,9 @@
import * as nip19 from "nostr-tools/nip19"
import {get} from "svelte/store"
import {ctx, sample, uniq, sleep, chunk, equals} from "@welshman/lib"
import {
DELETE,
REPORT,
PROFILE,
INBOX_RELAYS,
RELAYS,
@@ -26,8 +28,9 @@ import {
getRelayTags,
isShareableRelayUrl,
getRelayTagValues,
toNostrURI,
} from "@welshman/util"
import type {TrustedEvent, EventTemplate, List} from "@welshman/util"
import type {TrustedEvent, EventContent, EventTemplate, List} from "@welshman/util"
import type {SubscribeRequestWithHandlers} from "@welshman/net"
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
@@ -45,7 +48,7 @@ import {
loadFollows,
loadMutes,
tagEvent,
tagReactionTo,
tagEventForReaction,
getRelayUrls,
userRelaySelections,
userInboxRelaySelections,
@@ -54,6 +57,8 @@ import {
addSession,
clearStorage,
dropSession,
tagEventForComment,
tagEventForQuote,
} from "@welshman/app"
import type {Thunk} from "@welshman/app"
import {
@@ -94,6 +99,22 @@ export const getThunkError = async (thunk: Thunk) => {
}
}
export const prependParent = (parent: TrustedEvent | undefined, {content, tags}: EventContent) => {
if (parent) {
const nevent = nip19.neventEncode({
id: parent.id,
kind: parent.kind,
author: parent.pubkey,
relays: ctx.app.router.Event(parent).limit(3).getUrls(),
})
tags = [...tags, tagEventForQuote(parent)]
content = toNostrURI(nevent) + "\n\n" + content
}
return {content, tags}
}
// Log in
export const loginWithNip46 = async ({
@@ -376,7 +397,6 @@ export const checkRelayAuth = async (url: string, timeout = 3000) => {
export const attemptRelayAccess = async (url: string, claim = "") => {
const checks = [
() => checkRelayProfile(url),
() => checkRelayConnection(url),
() => checkRelayAccess(url, claim),
() => checkRelayAuth(url),
@@ -430,13 +450,36 @@ export const makeDelete = ({event}: {event: TrustedEvent}) => {
export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) =>
publishThunk({event: makeDelete({event}), relays})
export type ReportParams = {
event: TrustedEvent
content: string
reason: string
}
export const makeReport = ({event, reason, content}: ReportParams) => {
const tags = [
["p", event.pubkey],
["e", event.id, reason],
]
return createEvent(REPORT, {content, tags})
}
export const publishReport = ({
relays,
event,
reason,
content,
}: ReportParams & {relays: string[]}) =>
publishThunk({event: makeReport({event, reason, content}), relays})
export type ReactionParams = {
event: TrustedEvent
content: string
}
export const makeReaction = ({event, content}: ReactionParams) => {
const tags = [["k", String(event.kind)], ...tagReactionTo(event)]
const tags = tagEventForReaction(event)
const groupTag = getTag("h", event.tags)
if (groupTag) {
@@ -450,37 +493,14 @@ export const makeReaction = ({event, content}: ReactionParams) => {
export const publishReaction = ({relays, ...params}: ReactionParams & {relays: string[]}) =>
publishThunk({event: makeReaction(params), relays})
export type ReplyParams = {
export type CommentParams = {
event: TrustedEvent
content: string
tags?: string[][]
}
export const makeComment = ({event, content, tags = []}: ReplyParams) => {
const seenRoots = new Set<string>()
export const makeComment = ({event, content, tags = []}: CommentParams) =>
createEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event)]})
for (const [raw, ...tag] of event.tags.filter(t => t[0].match(/^(k|e|a|i)$/i))) {
const T = raw.toUpperCase()
const t = raw.toLowerCase()
if (seenRoots.has(T)) {
tags.push([t, ...tag])
} else {
tags.push([T, ...tag])
seenRoots.add(T)
}
}
if (seenRoots.size === 0) {
tags.push(["K", String(event.kind)])
tags.push(["E", event.id])
}
tags.push(["k", String(event.kind)])
tags.push(["e", event.id])
return createEvent(COMMENT, {content, tags})
}
export const publishComment = ({relays, ...params}: ReplyParams & {relays: string[]}) =>
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
publishThunk({event: makeComment(params), relays})
+20 -11
View File
@@ -1,35 +1,37 @@
<script lang="ts">
import {onMount} from "svelte"
import {writable} from "svelte/store"
import {EditorContent} from "svelte-tiptap"
import {isMobile} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import {getEditor} from "@app/editor"
import {makeEditor} from "@app/editor"
export let onSubmit: any
export let content = ""
export let editor: ReturnType<typeof getEditor> | undefined = undefined
export const focus = () => $editor.chain().focus().run()
const uploading = writable(false)
let element: HTMLElement
const uploadFiles = () => $editor!.chain().selectFiles().run()
const submit = () => {
if ($uploading) return
onSubmit({
content: $editor!.getText({blockSeparator: "\n"}).trim(),
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()
}
onMount(() => {
editor = getEditor({autofocus: !isMobile, aggressive: true, element, submit, uploading})
const editor = makeEditor({autofocus: !isMobile, submit, uploading, aggressive: true})
onMount(() => {
$editor!.chain().setContent(content).run()
})
</script>
@@ -49,6 +51,13 @@
{/if}
</Button>
<div class="chat-editor flex-grow overflow-hidden">
<div bind:this={element} />
<EditorContent editor={$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}>
<Icon icon="plain" />
</Button>
</form>
@@ -0,0 +1,23 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {displayProfileByPubkey} from "@welshman/app"
import {slide} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Content from "@app/components/Content.svelte"
export let event: TrustedEvent
export let clear: () => void
</script>
<div
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
transition:slide>
<p class="text-primary">Replying to @{displayProfileByPubkey(event.pubkey)}</p>
{#key event.id}
<Content {event} hideMedia minLength={100} maxLength={300} expandMode="disabled" />
{/key}
<Button class="absolute right-2 top-2 cursor-pointer" on:click={clear}>
<Icon icon="close-circle" />
</Button>
</div>
+14 -4
View File
@@ -1,12 +1,14 @@
<script lang="ts">
import {hash} from "@welshman/lib"
import {now} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {
thunks,
pubkey,
deriveProfile,
deriveProfileDisplay,
formatTimestampAsDate,
formatTimestampAsTime,
pubkey,
} from "@welshman/app"
import {isMobile} from "@lib/html"
import LongPress from "@lib/components/LongPress.svelte"
@@ -31,13 +33,14 @@
export let inert = false
const thunk = $thunks[event.id]
const today = formatTimestampAsDate(now())
const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey)
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
const reply = () => replyTo(event)
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event})
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event, reply})
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
@@ -70,7 +73,14 @@
<Button on:click={openProfile} class="text-sm font-bold" style="color: {colorValue}">
{$profileDisplay}
</Button>
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
<span class="text-xs opacity-50">
{#if formatTimestampAsDate(event.created_at) === today}
Today
{:else}
{formatTimestampAsDate(event.created_at)}
{/if}
at {formatTimestampAsTime(event.created_at)}
</span>
</div>
{/if}
<div class="text-sm">
@@ -82,7 +92,7 @@
</div>
</div>
<div class="row-2 ml-10 mt-1">
<ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-right" />
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
</div>
<button
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
@@ -3,6 +3,7 @@
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import EventReport from "@app/components/EventReport.svelte"
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
import {pushModal} from "@app/modal"
@@ -10,6 +11,11 @@
export let event
export let onClick
const report = () => {
onClick()
pushModal(EventReport, {url, event})
}
const showInfo = () => {
onClick()
pushModal(EventInfo, {event})
@@ -35,5 +41,12 @@
Delete Message
</Button>
</li>
{:else}
<li>
<Button class="text-error" on:click={report}>
<Icon size={4} icon="danger" />
Report Content
</Button>
</li>
{/if}
</ul>
@@ -11,6 +11,7 @@
export let url
export let event
export let reply
const onEmoji = (emoji: NativeEmoji) => {
history.back()
@@ -19,6 +20,11 @@
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
const sendReply = () => {
history.back()
reply()
}
const showInfo = () => pushModal(EventInfo, {event}, {replaceState: true})
const showDelete = () => pushModal(ConfirmDelete, {url, event})
@@ -29,6 +35,10 @@
<Icon size={4} icon="smile-circle" />
Send Reaction
</Button>
<Button class="btn btn-neutral w-full" on:click={sendReply}>
<Icon size={4} icon="reply" />
Send Reply
</Button>
<Button class="btn btn-neutral" on:click={showInfo}>
<Icon size={4} icon="code-2" />
Message Details
+22 -24
View File
@@ -10,19 +10,10 @@
<script lang="ts">
import {onMount} from "svelte"
import {derived} from "svelte/store"
import type {Readable} from "svelte/store"
import type {Editor} from "svelte-tiptap"
import {nip19} from "nostr-tools"
import {int, nthNe, MINUTE, sortBy, remove, ctx} from "@welshman/lib"
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,
tagPubkey,
} from "@welshman/app"
import {pubkey, 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"
@@ -36,9 +27,10 @@
import ProfileList from "@app/components/ProfileList.svelte"
import ChatMessage from "@app/components/ChatMessage.svelte"
import ChatCompose from "@app/components/ChannelCompose.svelte"
import ChatComposeParent from "@app/components/ChannelComposeParent.svelte"
import {userSettingValues, deriveChat, splitChatId, PLATFORM_NAME} from "@app/state"
import {pushModal} from "@app/modal"
import {sendWrapped} from "@app/commands"
import {sendWrapped, prependParent} from "@app/commands"
export let id
@@ -57,28 +49,31 @@
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
const replyTo = (event: TrustedEvent) => {
const relays = ctx.app.router.Event(event).getUrls()
const nevent = nip19.neventEncode({...event, relays})
$editor.commands.insertNEvent({nevent})
$editor.commands.insertContent("\n")
$editor.commands.focus()
parent = event
compose.focus()
}
const onSubmit = async ({content, ...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)]
const clearParent = () => {
parent = undefined
}
const onSubmit = async ({content, tags}: EventContent) => {
await sendWrapped({
pubkeys,
template: createEvent(DIRECT_MESSAGE, {content, tags}),
template: createEvent(
DIRECT_MESSAGE,
prependParent(parent, {content, tags: tags.filter(nthNe(0, "p"))}),
),
delay: $userSettingValues.send_delay,
})
clearParent()
}
let loading = true
let editor: Readable<Editor>
let parent: TrustedEvent | undefined
let elements: Element[] = []
let compose: ChatCompose
$: {
elements = []
@@ -200,5 +195,8 @@
<slot name="info" />
</p>
</div>
<ChatCompose bind:editor {onSubmit} />
{#if parent}
<ChatComposeParent event={parent} clear={clearParent} />
{/if}
<ChatCompose bind:this={compose} {onSubmit} />
</div>
+2 -2
View File
@@ -41,10 +41,10 @@
<form class="column gap-4" on:submit|preventDefault={submit}>
<ModalHeader>
<div slot="title">Enable Messages</div>
<div slot="info">Do you want to enable notes and direct messages?</div>
<div slot="info">Do you want to enable direct messages?</div>
</ModalHeader>
<p>
By default, notes and direct messages are disabled, since loading them requires
By default, direct messages are disabled, since loading them requires
{PLATFORM_NAME} to download and decrypt a lot of data.
</p>
<p>
+4 -1
View File
@@ -34,7 +34,10 @@
<div class="flex flex-col justify-start gap-1">
<div class="flex items-center justify-between gap-2">
<div class="flex min-w-0 items-center gap-2">
{#if others.length === 1}
{#if others.length === 0}
<ProfileCircle pubkey={$pubkey} size={5} />
Note to self
{:else if others.length === 1}
<ProfileCircle pubkey={others[0]} size={5} />
<ProfileName pubkey={others[0]} />
{:else}
+25
View File
@@ -0,0 +1,25 @@
<script lang="ts">
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ChatStart from "@app/components/ChatStart.svelte"
import {setChecked} from "@app/notifications"
import {pushModal} from "@app/modal"
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
const markAsRead = () => {
setChecked("/chat/*")
history.back()
}
</script>
<div class="col-2">
<Button class="btn btn-primary" on:click={startChat}>
<Icon size={4} icon="add-circle" />
Start chat
</Button>
<Button class="btn btn-neutral" on:click={markAsRead}>
<Icon size={4} icon="check-circle" />
Mark all read
</Button>
</div>
+2 -1
View File
@@ -112,7 +112,8 @@
</div>
</Button>
{/if}
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
<span class="whitespace-nowrap text-xs opacity-50"
>{formatTimestampAsTime(event.created_at)}</span>
</div>
{/if}
<div class="text-sm">
+1 -1
View File
@@ -93,7 +93,7 @@
mediaLength: hideMedia ? 20 : 200,
})
$: hasEllipsis = shortContent.find(isEllipsis)
$: hasEllipsis = shortContent.some(isEllipsis)
$: expandInline = hasEllipsis && expandMode === "inline"
$: expandBlock = hasEllipsis && expandMode === "block"
</script>
+2 -2
View File
@@ -22,7 +22,7 @@
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
</script>
<Link external href={url} class="my-2 inline-block">
<Link external href={url} class="my-2 block">
<div class="overflow-hidden rounded-box leading-[0]">
{#if url.match(/\.(mov|webm|mp4)$/)}
<video controls src={url} class="max-h-96 object-contain object-center">
@@ -30,7 +30,7 @@
</video>
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
<button type="button" on:click|stopPropagation|preventDefault={expand}>
<img alt="Link preview" src={imgproxy(url)} class="m-auto max-h-96" />
<img alt="Link preview" src={imgproxy(url)} class="m-auto max-h-96 rounded-box" />
</button>
{:else}
{#await loadPreview()}
+5 -9
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {EditorContent} from "svelte-tiptap"
import {writable} from "svelte/store"
import {randomId} from "@welshman/lib"
import {createEvent, EVENT_TIME} from "@welshman/util"
@@ -11,7 +11,7 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
import {PROTECTED} from "@app/state"
import {getEditor} from "@app/editor"
import {makeEditor} from "@app/editor"
import {pushToast} from "@app/toast"
export let url
@@ -54,16 +54,12 @@
history.back()
}
let element: HTMLElement
let editor: ReturnType<typeof getEditor>
const editor = makeEditor({submit, uploading})
let title = ""
let location = ""
let start: Date
let end: Date
onMount(() => {
editor = getEditor({submit, element, uploading})
})
</script>
<form class="column gap-4" on:submit|preventDefault={submit}>
@@ -83,7 +79,7 @@
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">
<div bind:this={element} />
<EditorContent editor={$editor} />
</div>
<Button
data-tip="Add an image"
+73
View File
@@ -0,0 +1,73 @@
<script lang="ts">
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import Field from "@lib/components/Field.svelte"
import Icon from "@lib/components/Icon.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {pushToast} from "@app/toast"
import {publishReport} from "@app/commands"
export let url
export let event
const back = () => history.back()
const confirm = async () => {
if (!reason) {
return pushToast({
theme: "error",
message: "Please select a reason for your report.",
})
}
loading = true
await publishReport({event, reason: reason.toLowerCase(), content, relays: [url]})
loading = false
history.back()
return pushToast({message: "Your report has been sent!"})
}
let reason = ""
let content = ""
let loading = false
</script>
<form class="column gap-4" on:submit|preventDefault={confirm}>
<ModalHeader>
<div slot="title">Report Content</div>
<div slot="info">Flag inappropriate content.</div>
</ModalHeader>
<Field>
<p slot="label">Reason*</p>
<select slot="input" class="select select-bordered" bind:value={reason}>
<option disabled selected>Choose a reason</option>
<option>Nudity</option>
<option>Malware</option>
<option>Profanity</option>
<option>Illegal</option>
<option>Spam</option>
<option>Impersonation</option>
<option>Other</option>
</select>
<p slot="info">Please select a reason for your report.</p>
</Field>
<Field>
<p slot="label">Details</p>
<textarea slot="input" class="textarea textarea-bordered" bind:value={content} />
<p slot="info">Please provide any additional details relevant to your report.</p>
</Field>
<ModalFooter>
<Button class="btn btn-link" on:click={back}>
<Icon icon="alt-arrow-left" />
Go back
</Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Send Report</Spinner>
<Icon icon="alt-arrow-right" />
</Button>
</ModalFooter>
</form>
@@ -0,0 +1,55 @@
<script lang="ts">
import {getTag, REPORT} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {pubkey, repository} from "@welshman/app"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import Button from "@lib/components/Button.svelte"
import Profile from "@app/components/Profile.svelte"
import {publishDelete} from "@app/commands"
export let url
export let event
const reports = deriveEvents(repository, {
filters: [{kinds: [REPORT], "#e": [event.id]}],
})
const back = () => history.back()
const deleteReport = (report: TrustedEvent) => {
publishDelete({event: report, relays: [url]})
if ($reports.length === 0) {
history.back()
}
}
const getReason = (tags: string[][]) => getTag("e", tags)?.[2] || "other"
</script>
<div class="column gap-4">
<ModalHeader>
<div slot="title">Report Details</div>
<div slot="info">All reports for this event are shown below.</div>
</ModalHeader>
{#each $reports as report (report.id)}
{@const reason = getReason(report.tags)}
{@const remove = () => deleteReport(report)}
<div class="column gap-2">
<div class="flex justify-between">
<div>
<Profile pubkey={report.pubkey} />
<span>Reported this event as "{reason}"</span>
</div>
{#if report.pubkey === $pubkey}
<Button class="btn-default btn" on:click={remove}>Delete Report</Button>
{/if}
</div>
{#if report.content}
<p>"{report.content}"</p>
{/if}
</div>
{/each}
<Button class="btn btn-primary" on:click={back}>Got it</Button>
</div>
+7 -1
View File
@@ -1,11 +1,12 @@
<script lang="ts">
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Link from "@lib/components/Link.svelte"
import Dialog from "@lib/components/Dialog.svelte"
import CardButton from "@lib/components/CardButton.svelte"
import LogIn from "@app/components/LogIn.svelte"
import SignUp from "@app/components/SignUp.svelte"
import {PLATFORM_NAME} from "@app/state"
import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME} from "@app/state"
import {pushModal} from "@app/modal"
const logIn = () => pushModal(LogIn)
@@ -33,5 +34,10 @@
<div slot="info">Just a few questions and you'll be on your way.</div>
</CardButton>
</Button>
<p class="text-center text-xs opacity-75">
By using {PLATFORM_NAME}, you consent to our
<Link external class="link" href={PLATFORM_TERMS}>Terms of Service</Link> and
<Link external class="link" href={PLATFORM_PRIVACY}>Privacy Policy</Link>.
</p>
</div>
</Dialog>
+11 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {onMount, onDestroy} from "svelte"
import {Nip46Broker, makeSecret} from "@welshman/signer"
import {Nip46Broker, getPubkey, makeSecret} from "@welshman/signer"
import {addSession} from "@welshman/app"
import {slideAndFade} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte"
@@ -26,7 +26,7 @@
const back = () => history.back()
const onSubmit = async () => {
const {signerPubkey, connectSecret, relays} = broker.parseBunkerUrl(input)
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(input)
if (loading) {
return
@@ -63,6 +63,15 @@
let input = ""
let loading = false
$: {
// For testing and for play store reviewers
if (input === "reviewkey") {
const secret = makeSecret()
addSession({method: "nip01", secret, pubkey: getPubkey(secret)})
}
}
onMount(async () => {
url = await broker.makeNostrconnectUrl({
perms: NIP46_PERMS,
+1 -1
View File
@@ -71,7 +71,7 @@
<SecondaryNavSection class="max-h-screen">
<div>
<SecondaryNavItem class="w-full !justify-between" on:click={openMenu}>
<strong>{displayRelayUrl(url)}</strong>
<strong class="ellipsize">{displayRelayUrl(url)}</strong>
<Icon icon="alt-arrow-down" />
</SecondaryNavItem>
{#if showMenu}
+1 -1
View File
@@ -29,7 +29,7 @@
<NoteCard {event} class="card2 bg-alt">
<Content {event} expandMode="inline" />
<div class="flex w-full justify-between gap-2">
<ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-right">
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right">
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
<Icon icon="smile-circle" size={4} />
</EmojiButton>
+4 -8
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import {first, sortBy, ctx} from "@welshman/lib"
import {getAncestorTags} from "@welshman/util"
import {ctx} from "@welshman/lib"
import type {Filter} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {repository, load, loadRelaySelections, formatTimestampRelative} from "@welshman/app"
@@ -13,11 +12,9 @@
export let pubkey
const filters: Filter[] = [{authors: [pubkey]}]
const filters: Filter[] = [{authors: [pubkey], limit: 1}]
const events = deriveEvents(repository, {filters})
$: roots = $events.filter(e => getAncestorTags(e.tags).replies.length === 0)
onMount(async () => {
// Make sure we have their relay selections before we load their posts
await loadRelaySelections(pubkey)
@@ -39,10 +36,9 @@
</Link>
</div>
<ProfileInfo {pubkey} />
{#if roots.length > 0}
{@const event = first(sortBy(e => -e.created_at, roots))}
{#if $events.length > 0}
<div class="bg-alt badge badge-neutral border-none">
Last active {formatTimestampRelative(event.created_at)}
Last active {formatTimestampRelative($events[0].created_at)}
</div>
{/if}
<Link class="btn btn-primary sm:hidden" href={makeChatPath([pubkey])}>
+3 -11
View File
@@ -21,8 +21,6 @@
const showSettingsMenu = () => pushModal(MenuSettings)
const openNotes = () => ($canDecrypt ? goto("/notes") : pushModal(ChatEnable, {next: "/notes"}))
const openChat = () => ($canDecrypt ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
$: spaceUrls = Array.from($userRoomsByUrl.keys())
@@ -58,9 +56,6 @@
class="tooltip-right">
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
</PrimaryNavItem>
<PrimaryNavItem title="Notes" on:click={openNotes} class="tooltip-right">
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
</PrimaryNavItem>
<PrimaryNavItem
title="Messages"
on:click={openChat}
@@ -81,11 +76,8 @@
class="border-top fixed bottom-0 left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
<div class="content-padding-x content-sizing flex justify-between px-2">
<div class="flex gap-2 sm:gap-8">
<PrimaryNavItem title="Search" href="/people">
<Avatar icon="magnifer" class="!h-10 !w-10" />
</PrimaryNavItem>
<PrimaryNavItem title="Notes" on:click={openNotes}>
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
<PrimaryNavItem title="Home" href="/home">
<Avatar icon="home-smile" class="!h-10 !w-10" />
</PrimaryNavItem>
<PrimaryNavItem
title="Messages"
@@ -98,7 +90,7 @@
</PrimaryNavItem>
</div>
<PrimaryNavItem title="Settings" on:click={showSettingsMenu}>
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
<Avatar icon="settings" src={$userProfile?.picture} class="!h-10 !w-10" />
</PrimaryNavItem>
</div>
</div>
+2 -2
View File
@@ -2,7 +2,7 @@
import {onMount} from "svelte"
import {sortBy, uniqBy} from "@welshman/lib"
import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
import {NOTE, getAncestorTags} from "@welshman/util"
import {NOTE, getReplyTags} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {createFeedController} from "@welshman/app"
import {createScroller} from "@lib/html"
@@ -22,7 +22,7 @@
feedFromFilter({kinds: [NOTE], authors: [pubkey]}),
),
onEvent: (event: TrustedEvent) => {
if (getAncestorTags(event.tags).replies.length === 0) {
if (getReplyTags(event.tags).replies.length === 0) {
buffer.push(event)
}
},
+4 -1
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import type {SvelteComponent} from "svelte"
import {derived} from "svelte/store"
import {type Instance} from "tippy.js"
import {append, remove, uniq} from "@welshman/lib"
import {profileSearch} from "@welshman/app"
@@ -20,6 +21,8 @@
let popover: Instance
let instance: SvelteComponent
const search = derived(profileSearch, $profileSearch => $profileSearch.searchValues)
const selectPubkey = (pubkey: string) => {
term = ""
popover.hide()
@@ -76,8 +79,8 @@
component={Suggestions}
props={{
term,
search,
select: selectPubkey,
search: profileSearch,
component: ProfileSuggestion,
class: "rounded-box",
style: `left: 4px; width: ${input?.clientWidth + 12}px`,
+38 -14
View File
@@ -1,45 +1,69 @@
<script lang="ts">
import {onMount} from "svelte"
import {groupBy, uniqBy, batch} from "@welshman/lib"
import {REACTION, DELETE} from "@welshman/util"
import {groupBy, uniq, uniqBy, batch} from "@welshman/lib"
import {REACTION, getTag, REPORT, DELETE} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {pubkey, repository, load, displayProfileByPubkey} from "@welshman/app"
import {displayList} from "@lib/util"
import {isMobile} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import EventReportDetails from "@app/components/EventReportDetails.svelte"
import {displayReaction} from "@app/state"
import {pushModal} from "@app/modal"
export let event
export let onReactionClick
export let relays: string[] = []
export let url = ""
export let reactionClass = ""
export let noTooltip = false
const reports = deriveEvents(repository, {
filters: [{kinds: [REPORT], "#e": [event.id]}],
})
const reactions = deriveEvents(repository, {
filters: [{kinds: [REACTION], "#e": [event.id]}],
})
const onReportClick = () => pushModal(EventReportDetails, {url, event})
$: reportReasons = uniq($reports.map(e => getTag("e", e.tags)?.[2]))
$: groupedReactions = groupBy(
e => e.content,
uniqBy(e => e.pubkey + e.content, $reactions),
)
onMount(() => {
load({
relays,
filters: [{kinds: [REACTION, DELETE], "#e": [event.id]}],
onEvent: batch(300, (events: TrustedEvent[]) => {
load({
relays,
filters: [{kinds: [DELETE], "#e": events.map(e => e.id)}],
})
}),
})
if (url) {
load({
relays: [url],
filters: [{kinds: [REACTION, REPORT, DELETE], "#e": [event.id]}],
onEvent: batch(300, (events: TrustedEvent[]) => {
load({
relays: [url],
filters: [{kinds: [DELETE], "#e": events.map(e => e.id)}],
})
}),
})
}
})
</script>
{#if $reactions.length > 0}
{#if $reactions.length > 0 || $reports.length > 0}
<div class="flex min-w-0 flex-wrap gap-2">
{#if url && $reports.length > 0}
<button
type="button"
data-tip="{`This content has been reported as "${displayList(reportReasons)}".`}}"
class="btn btn-error btn-xs tooltip-right flex items-center gap-1 rounded-full"
class:tooltip={!noTooltip && !isMobile}
on:click|preventDefault|stopPropagation={onReportClick}>
<Icon icon="danger" />
<span>{$reports.length}</span>
</button>
{/if}
{#each groupedReactions.entries() as [content, events]}
{@const pubkeys = events.map(e => e.pubkey)}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
+23 -97
View File
@@ -1,107 +1,50 @@
<script lang="ts">
import {postJson, assoc} from "@welshman/lib"
import {makeSecret, Nip46Broker} from "@welshman/signer"
import {pubkey, loadHandle, updateSession} from "@welshman/app"
import {postJson} from "@welshman/lib"
import {isMobile} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte"
import FieldInline from "@lib/components/FieldInline.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import Divider from "@lib/components/Divider.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import LogIn from "@app/components/LogIn.svelte"
import InfoNostr from "@app/components/InfoNostr.svelte"
import SignUpSuccess from "@app/components/SignUpSuccess.svelte"
import {pushModal, clearModals} from "@app/modal"
import {setChecked} from "@app/notifications"
import {BURROW_URL, PLATFORM_NAME, NIP46_PERMS} from "@app/state"
import {pushModal} from "@app/modal"
import {BURROW_URL, PLATFORM_NAME} from "@app/state"
import {pushToast} from "@app/toast"
import {loginWithNip46} from "@app/commands"
const relays = ["wss://relay.nsec.app"]
const ac = window.location.origin
const signerDomain = "nsec.app"
const at = isMobile ? "android" : "web"
const signerPubkey = "e24a86943d37a91ab485d6f9a7c66097c25ddd67e8bd1b75ed252a3c266cf9bb"
const nstart = `https://start.njump.me/?an=Flotilla&at=${at}&ac=${ac}`
const login = () => pushModal(LogIn)
const withLoading =
(cb: (...args: any[]) => any) =>
async (...args: any[]) => {
loading = true
const signupPassword = async () => {
loading = true
try {
await cb(...args)
} finally {
loading = false
try {
const res = await postJson(BURROW_URL + "/user", {email, password})
if (res.error) {
pushToast({message: res.error, theme: "error"})
} else {
pushModal(SignUpSuccess, {email}, {replaceState: true})
}
} finally {
loading = false
}
const signupPassword = withLoading(async () => {
const res = await postJson(BURROW_URL + "/user", {email, password})
if (res.error) {
return pushToast({message: res.error, theme: "error"})
}
pushModal(SignUpSuccess, {email}, {replaceState: true})
})
const signupNsecApp = withLoading(async () => {
const handle = await loadHandle(`${username}@${signerDomain}`)
if (handle?.pubkey) {
return pushToast({
theme: "error",
message: "Sorry, it looks like that account already exists. Try logging in instead.",
})
}
const clientSecret = makeSecret()
const broker = Nip46Broker.get({
relays,
clientSecret,
signerPubkey,
algorithm: "nip04",
})
const userPubkey = await broker.createAccount(username, signerDomain, NIP46_PERMS)
if (!userPubkey) {
return pushToast({
theme: "error",
message: "Sorry, it looks like something went wrong. Please try again.",
})
}
// Now we can log in. Use the user's pubkey for the handler (legacy stuff)
const success = await loginWithNip46({relays, signerPubkey: userPubkey, clientSecret})
if (!success) {
return pushToast({
theme: "error",
message: "Sorry, it looks like something went wrong. Please try again.",
})
}
updateSession($pubkey!, assoc("email", email))
pushToast({message: "Successfully logged in!"})
setChecked("*")
clearModals()
})
}
const signup = () => {
if (BURROW_URL) {
signupPassword()
} else {
signupNsecApp()
}
}
let email = ""
let password = ""
let username = ""
let loading = false
</script>
@@ -136,29 +79,12 @@
on other nostr applications, you can create a nostr key yourself, or export your key from {PLATFORM_NAME}
later.
</p>
{:else}
<Field>
<div class="flex items-center gap-2" slot="input">
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="user-rounded" />
<input bind:value={username} class="grow" type="text" placeholder="username" />
</label>
@{signerDomain}
</div>
</Field>
<Button type="submit" class="btn btn-primary" disabled={loading || !username}>
<Spinner {loading}>Sign Up</Spinner>
<Icon icon="alt-arrow-right" />
</Button>
<Divider>Or</Divider>
{/if}
<Divider>Or</Divider>
<Link
external
href="https://nosta.me"
class="btn {username || email || password ? 'btn-neutral' : 'btn-primary'}">
<a href={nstart} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
<Icon icon="square-share-line" />
Get started on Nosta.me
</Link>
Get started on njump
</a>
<div class="text-sm">
Already have an account?
<Button class="link" on:click={login}>Log in instead</Button>
+1 -1
View File
@@ -56,7 +56,7 @@
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex flex-grow flex-wrap justify-end gap-2">
<ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-left" />
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-left" />
{#if $deleted}
<div class="btn btn-error btn-xs rounded-full">Deleted</div>
{:else if thunk}
+5 -9
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import {writable} from "svelte/store"
import {EditorContent} from "svelte-tiptap"
import {createEvent, THREAD} from "@welshman/util"
import {publishThunk} from "@welshman/app"
import {isMobile} from "@lib/html"
@@ -11,7 +11,7 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {pushToast} from "@app/toast"
import {GENERAL, tagRoom, PROTECTED} from "@app/state"
import {getEditor} from "@app/editor"
import {makeEditor} from "@app/editor"
export let url
@@ -53,13 +53,9 @@
history.back()
}
let title: string
let element: HTMLElement
let editor: ReturnType<typeof getEditor>
const editor = makeEditor({submit, uploading, placeholder: "What's on your mind?"})
onMount(() => {
editor = getEditor({submit, element, uploading, placeholder: "What's on your mind?"})
})
let title: string
</script>
<form class="column gap-4" on:submit|preventDefault={submit}>
@@ -83,7 +79,7 @@
<Field>
<p slot="label">Message*</p>
<div slot="input" class="note-editor flex-grow overflow-hidden">
<div bind:this={element} />
<EditorContent editor={$editor} />
</div>
</Field>
<Button
+13
View File
@@ -4,6 +4,7 @@
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 ConfirmDelete from "@app/components/ConfirmDelete.svelte"
import {pushModal} from "@app/modal"
@@ -14,6 +15,11 @@
const isRoot = event.kind !== COMMENT
const report = () => {
onClick()
pushModal(EventReport, {url, event})
}
const showInfo = () => {
onClick()
pushModal(EventInfo, {event})
@@ -52,5 +58,12 @@
Delete Message
</Button>
</li>
{:else}
<li>
<Button class="text-error" on:click={report}>
<Icon size={4} icon="danger" />
Report Content
</Button>
</li>
{/if}
</ul>
+4 -9
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import {writable} from "svelte/store"
import {EditorContent} from "svelte-tiptap"
import {isMobile} from "@lib/html"
import {fly, slideAndFade} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
@@ -8,7 +8,7 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {publishComment} from "@app/commands"
import {tagRoom, GENERAL, PROTECTED} from "@app/state"
import {getEditor} from "@app/editor"
import {makeEditor} from "@app/editor"
import {pushToast} from "@app/toast"
export let url
@@ -34,12 +34,7 @@
onSubmit(publishComment({event, content, tags, relays: [url]}))
}
let editor: ReturnType<typeof getEditor>
let element: HTMLElement
onMount(() => {
editor = getEditor({element, submit, uploading, autofocus: !isMobile})
})
const editor = makeEditor({submit, uploading, autofocus: !isMobile})
</script>
<form
@@ -49,7 +44,7 @@
class="card2 sticky bottom-2 z-feature mx-2 mt-4 bg-neutral">
<div class="relative">
<div class="note-editor flex-grow overflow-hidden">
<div bind:this={element} />
<EditorContent editor={$editor} />
</div>
<Button
data-tip="Add an image"
+8 -6
View File
@@ -42,9 +42,9 @@
}
</script>
<div class="flex justify-end px-1 text-xs {$$props.class}">
{#if isFailure && failure}
{@const [url, {message, status}] = failure}
{#if isFailure && failure}
{@const [url, {message, status}] = failure}
<div class="flex justify-end px-1 text-xs {$$props.class}">
<Tippy
class="flex items-center {$$props.class}"
component={ThunkStatusDetail}
@@ -55,7 +55,9 @@
<span>Failed to send!</span>
</span>
</Tippy>
{:else if canCancel || isPending}
</div>
{:else if canCancel || isPending}
<div class="flex justify-end px-1 text-xs {$$props.class}">
<span class="flex items-center gap-1 {$$props.class}">
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
<span class="opacity-50">Sending...</span>
@@ -63,5 +65,5 @@
<Button class="link" on:click={abort}>Cancel</Button>
{/if}
</span>
{/if}
</div>
</div>
{/if}

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