Compare commits

...

11 Commits

Author SHA1 Message Date
Jon Staab 27d9d4fff1 Update changelog 2025-02-13 16:32:34 -08:00
Jon Staab c089812363 Show spinner when joining room 2025-02-13 15:34:14 -08:00
Jon Staab 07dd1e97dc Fix long-running subscriptions clogging things up 2025-02-13 14:50:03 -08:00
Jon Staab 7f6a1bff34 Re-work threads page, fix some iphone bugs 2025-02-13 10:52:00 -08:00
Jon Staab 7d1310722a Factor out calendar event component, render calendar event notes better 2025-02-11 16:00:14 -08:00
Jon Staab cb57710654 Clean up quotes/depth 2025-02-11 15:37:55 -08:00
Jon Staab c74c116667 Fix page bar margin #112 2025-02-11 15:16:24 -08:00
Jon Staab 0ba55f2387 Attempt to fix new messages button #114 2025-02-11 11:49:17 -08:00
Jon Staab 622214713b replace state when navigating from space menu 2025-02-11 11:42:49 -08:00
Jon Staab d8cf48381b Build before other stuff 2025-02-06 12:52:42 -08:00
Jon Staab 7dc7b5abeb Bump android version 2025-02-06 12:43:18 -08:00
31 changed files with 364 additions and 249 deletions
+9
View File
@@ -1,5 +1,14 @@
# Changelog # Changelog
# 0.2.8
* Show spinner when joining a room
* Reduce self-rate limiting of REQs
* Fix disabled signers link
* Prepare for iOS release
* Improve threads and calendar pages
* Improve quote rendering and new messages button
# 0.2.7 # 0.2.7
* Add calendar events * Add calendar events
+1 -1
View File
@@ -7,7 +7,7 @@ android {
applicationId "social.flotilla" applicationId "social.flotilla"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 7 versionCode 9
versionName "0.2.7" versionName "0.2.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
+1 -1
View File
@@ -14,7 +14,7 @@ const config: CapacitorConfig = {
}, },
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload // Use this for live reload https://capacitorjs.com/docs/guides/live-reload
// server: { // server: {
// url: "http://192.168.1.251:1847", // url: "http://192.168.1.250:1847",
// cleartext: true // cleartext: true
// }, // },
}; };
+8 -4
View File
@@ -351,12 +351,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = S26U9DYW3A; DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.2.8;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -374,12 +376,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = S26U9DYW3A; DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0; MARKETING_VERSION = 0.2.8;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla; PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
+5 -1
View File
@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Flotilla</string> <string>Flotilla</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@@ -30,6 +30,8 @@
<array> <array>
<string>armv7</string> <string>armv7</string>
</array> </array>
<key>UIStatusBarStyle</key>
<string></string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
@@ -45,5 +47,7 @@
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<true/> <true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict> </dict>
</plist> </plist>
+44 -30
View File
@@ -1,12 +1,12 @@
{ {
"name": "flotilla", "name": "flotilla",
"version": "0.2.7", "version": "0.2.8",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "flotilla", "name": "flotilla",
"version": "0.2.7", "version": "0.2.8",
"dependencies": { "dependencies": {
"@capacitor/android": "^7.0.0", "@capacitor/android": "^7.0.0",
"@capacitor/app": "^7.0.0", "@capacitor/app": "^7.0.0",
@@ -22,15 +22,15 @@
"@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.6", "@vite-pwa/sveltekit": "^0.6.6",
"@welshman/app": "~0.0.42", "@welshman/app": "~0.0.42",
"@welshman/content": "~0.0.17", "@welshman/content": "~0.0.18",
"@welshman/dvm": "~0.0.14", "@welshman/dvm": "~0.0.14",
"@welshman/editor": "~0.0.13", "@welshman/editor": "~0.0.15",
"@welshman/feeds": "~0.0.30", "@welshman/feeds": "~0.0.30",
"@welshman/lib": "~0.0.40", "@welshman/lib": "~0.0.41",
"@welshman/net": "~0.0.46", "@welshman/net": "~0.0.47",
"@welshman/signer": "~0.0.20", "@welshman/signer": "~0.0.20",
"@welshman/store": "~0.0.16", "@welshman/store": "~0.0.16",
"@welshman/util": "~0.0.60", "@welshman/util": "~0.0.61",
"daisyui": "^4.12.10", "daisyui": "^4.12.10",
"date-picker-svelte": "^2.13.0", "date-picker-svelte": "^2.13.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
@@ -4766,13 +4766,13 @@
} }
}, },
"node_modules/@welshman/content": { "node_modules/@welshman/content": {
"version": "0.0.17", "version": "0.0.18",
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.17.tgz", "resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.18.tgz",
"integrity": "sha512-xiBSL8BSzHrwRmGqKXkR/S6EK7a1wT1rG1qdlQN30lBX5ZS+NSkoI0aNuF8p313mElHNZWgrqxFaat+FML4yOw==", "integrity": "sha512-7LHs9xKStrkaet9VY1PWSEUWrdIaIThIo+ByN6lF3nRZwPTExrBy4rPXnEa5roVAAwgmlhXw3zTkfGP15V6joQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^7.0.2", "@braintree/sanitize-url": "^7.0.2",
"@welshman/lib": "~0.0.37", "@welshman/lib": "~0.0.40",
"nostr-tools": "^2.7.2" "nostr-tools": "^2.7.2"
}, },
"engines": { "engines": {
@@ -4793,9 +4793,9 @@
} }
}, },
"node_modules/@welshman/editor": { "node_modules/@welshman/editor": {
"version": "0.0.13", "version": "0.0.15",
"resolved": "https://registry.npmjs.org/@welshman/editor/-/editor-0.0.13.tgz", "resolved": "https://registry.npmjs.org/@welshman/editor/-/editor-0.0.15.tgz",
"integrity": "sha512-860kn8iOXHKGBOnL3zalFQVw8eeILNU6YQ4V+xFtgqIxxCMk1c/9F5k0k0OyloUqRNjtSG6hvLdQLacBvhz2WQ==", "integrity": "sha512-Eg3alzv+cjCXtr6oEItRqoRSD4DTllt3c2JyJTxpF/KNiy8XHHMeUSpVFgph3+pAt5jwyl6b1feKPEwpShgqHw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tiptap/core": "^2.11.5", "@tiptap/core": "^2.11.5",
@@ -4813,11 +4813,25 @@
"@tiptap/suggestion": "^2.11.5", "@tiptap/suggestion": "^2.11.5",
"@welshman/lib": "~0.0.40", "@welshman/lib": "~0.0.40",
"@welshman/util": "^0.0.60", "@welshman/util": "^0.0.60",
"nostr-editor": "^0.0.4-pre.12", "nostr-editor": "^0.0.4-pre.13",
"nostr-tools": "^2.10.4", "nostr-tools": "^2.10.4",
"tippy.js": "^6.3.7" "tippy.js": "^6.3.7"
} }
}, },
"node_modules/@welshman/editor/node_modules/@welshman/util": {
"version": "0.0.60",
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.60.tgz",
"integrity": "sha512-kqZgYnrwxKx0JTDZnTSaQYc2ev7E9ZjNDy5MclX36d5T/qPUspmwksAOodFJY9kJoJd49bf1omAmBTgnFJfeHw==",
"license": "MIT",
"dependencies": {
"@types/ws": "^8.5.13",
"@welshman/lib": "~0.0.37",
"nostr-tools": "^2.7.2"
},
"engines": {
"node": ">=10.4.0"
}
},
"node_modules/@welshman/feeds": { "node_modules/@welshman/feeds": {
"version": "0.0.30", "version": "0.0.30",
"resolved": "https://registry.npmjs.org/@welshman/feeds/-/feeds-0.0.30.tgz", "resolved": "https://registry.npmjs.org/@welshman/feeds/-/feeds-0.0.30.tgz",
@@ -4829,9 +4843,9 @@
} }
}, },
"node_modules/@welshman/lib": { "node_modules/@welshman/lib": {
"version": "0.0.40", "version": "0.0.41",
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.40.tgz", "resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.41.tgz",
"integrity": "sha512-6Qk5fJABv+7HPqhNC5eLM4VZxCLpcu22nShmrNMbamkMwr4eLj2Bl4dRmuzFsvMcsL/Jc148zqpfuq37CY2NCw==", "integrity": "sha512-FMJVoPZw8Vi1fd2/ulwqlBS1tvjkFAm9lg+Dz5SXItXxrNC06YMRTjGjInCBEkArrvNGPUjchzSFDNmbH0fxHQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@scure/base": "^1.1.6", "@scure/base": "^1.1.6",
@@ -4840,13 +4854,13 @@
} }
}, },
"node_modules/@welshman/net": { "node_modules/@welshman/net": {
"version": "0.0.46", "version": "0.0.47",
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.46.tgz", "resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.47.tgz",
"integrity": "sha512-ehH4grz0VHjuofyVUE3r5GoynHTh+cIT/XFH6ov6nOGRU/LZXCLGk/9CUPlqNYHRfc/zBtaIyfVu0AelLqV6lw==", "integrity": "sha512-/mIr+QyLH+RlD16rsPDTIW250lOm5eNaLO6dhZw8dMKznMhVtSWe/X/lJZOXmexzbB2z7WYZVN5x5TggZROyxA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@welshman/lib": "~0.0.37", "@welshman/lib": "~0.0.40",
"@welshman/util": "~0.0.58", "@welshman/util": "~0.0.59",
"isomorphic-ws": "^5.0.0", "isomorphic-ws": "^5.0.0",
"ws": "^8.16.0" "ws": "^8.16.0"
} }
@@ -4917,13 +4931,13 @@
} }
}, },
"node_modules/@welshman/util": { "node_modules/@welshman/util": {
"version": "0.0.60", "version": "0.0.61",
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.60.tgz", "resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.61.tgz",
"integrity": "sha512-kqZgYnrwxKx0JTDZnTSaQYc2ev7E9ZjNDy5MclX36d5T/qPUspmwksAOodFJY9kJoJd49bf1omAmBTgnFJfeHw==", "integrity": "sha512-+l4YX01msAtnyylzpIFIAYubvnBLyr9hGx3iRO5LS3OPv/yUDOeyYJseWDqorkIiN5BRT7PCgnWJdlQP71ZtAw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/ws": "^8.5.13", "@types/ws": "^8.5.13",
"@welshman/lib": "~0.0.37", "@welshman/lib": "~0.0.40",
"nostr-tools": "^2.7.2" "nostr-tools": "^2.7.2"
}, },
"engines": { "engines": {
@@ -10152,9 +10166,9 @@
} }
}, },
"node_modules/nostr-editor": { "node_modules/nostr-editor": {
"version": "0.0.4-pre.12", "version": "0.0.4-pre.13",
"resolved": "https://registry.npmjs.org/nostr-editor/-/nostr-editor-0.0.4-pre.12.tgz", "resolved": "https://registry.npmjs.org/nostr-editor/-/nostr-editor-0.0.4-pre.13.tgz",
"integrity": "sha512-vztmbEKxt2jnO1JEoprwVf3s4TN4D3B0fcsrhckOITR1KaDX88QhIG+qTee92xp+n96vYj4GQt0W06rSv3NXHA==", "integrity": "sha512-izIidrrIjQp41MAY2dNoticQSc0E5XOFKEe04tmZdTdF9Ry8CKxIdv6yvO3qh4gdhrOq+QPLTRii6X3X5iC/5Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"light-bolt11-decoder": "^3.1.1" "light-bolt11-decoder": "^3.1.1"
+8 -8
View File
@@ -1,12 +1,12 @@
{ {
"name": "flotilla", "name": "flotilla",
"version": "0.2.7", "version": "0.2.8",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "./build.sh", "build": "./build.sh",
"sourcemaps": "./sourcemaps.sh", "sourcemaps": "./build.sh && ./sourcemaps.sh",
"release:android": "cap build android --androidreleasetype APK --signing-type apksigner", "release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check src && eslint src", "lint": "prettier --check src && eslint src",
@@ -51,15 +51,15 @@
"@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.6", "@vite-pwa/sveltekit": "^0.6.6",
"@welshman/app": "~0.0.42", "@welshman/app": "~0.0.42",
"@welshman/content": "~0.0.17", "@welshman/content": "~0.0.18",
"@welshman/dvm": "~0.0.14", "@welshman/dvm": "~0.0.14",
"@welshman/editor": "~0.0.13", "@welshman/editor": "~0.0.15",
"@welshman/feeds": "~0.0.30", "@welshman/feeds": "~0.0.30",
"@welshman/lib": "~0.0.40", "@welshman/lib": "~0.0.41",
"@welshman/net": "~0.0.46", "@welshman/net": "~0.0.47",
"@welshman/signer": "~0.0.20", "@welshman/signer": "~0.0.20",
"@welshman/store": "~0.0.16", "@welshman/store": "~0.0.16",
"@welshman/util": "~0.0.60", "@welshman/util": "~0.0.61",
"daisyui": "^4.12.10", "daisyui": "^4.12.10",
"date-picker-svelte": "^2.13.0", "date-picker-svelte": "^2.13.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
@@ -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>
+5 -21
View File
@@ -1,38 +1,22 @@
<script lang="ts"> <script lang="ts">
import {fromPairs} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {formatTimestamp, formatTimestampAsDate, formatTimestampAsTime} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import CalendarEventActions from "@app/components/CalendarEventActions.svelte" import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte" import ProfileLink from "@app/components/ProfileLink.svelte"
import {makeCalendarPath} from "@app/routes" import {makeCalendarPath} from "@app/routes"
const { type Props = {
url,
event,
}: {
url: string url: string
event: TrustedEvent event: TrustedEvent
} = $props() }
const meta = $derived(fromPairs(event.tags) as Record<string, string>) const {url, event}: Props = $props()
const end = $derived(parseInt(meta.end))
const start = $derived(parseInt(meta.start))
const startDateDisplay = $derived(formatTimestampAsDate(start))
const endDateDisplay = $derived(formatTimestampAsDate(end))
const isSingleDay = $derived(startDateDisplay === endDateDisplay)
</script> </script>
<Link class="col-3 card2 bg-alt w-full cursor-pointer" href={makeCalendarPath(url, event.id)}> <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"> <div class="flex items-center justify-between gap-2">
<p class="text-xl">{meta.title || meta.name}</p> <CalendarEventHeader {event} />
<div class="flex items-center gap-2 text-sm">
<Icon icon="clock-circle" size={4} />
{formatTimestampAsTime(start)}{isSingleDay
? formatTimestampAsTime(end)
: formatTimestamp(end)}
</div>
</div> </div>
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"> <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"> <span class="whitespace-nowrap py-1 text-sm opacity-75">
@@ -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}
@@ -4,7 +4,7 @@
import {slide} from "@lib/transition" import {slide} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Content from "@app/components/Content.svelte" import NoteContent from "@app/components/NoteContent.svelte"
const { const {
verb, verb,
@@ -22,7 +22,12 @@
transition:slide> transition:slide>
<p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p> <p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
{#key event.id} {#key event.id}
<Content {event} hideMedia minLength={100} maxLength={300} expandMode="disabled" /> <NoteContent
{event}
hideMediaAtDepth={0}
minLength={100}
maxLength={300}
expandMode="disabled" />
{/key} {/key}
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}> <Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
<Icon icon="close-circle" /> <Icon icon="close-circle" />
+1 -1
View File
@@ -89,7 +89,7 @@
</div> </div>
{/if} {/if}
<div class="text-sm"> <div class="text-sm">
<Content {event} quoteProps={{minimal: true, relays: [url]}} /> <Content {event} relays={[url]} />
{#if thunk} {#if thunk}
<ThunkStatus {thunk} class="mt-2" /> <ThunkStatus {thunk} class="mt-2" />
{/if} {/if}
+8 -14
View File
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {fromNostrURI} from "@welshman/util" import {fromNostrURI} from "@welshman/util"
import {nthEq} from "@welshman/lib" import {nthEq} from "@welshman/lib"
import { import {
@@ -22,7 +21,6 @@
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Content from "@app/components/Content.svelte"
import ContentToken from "@app/components/ContentToken.svelte" import ContentToken from "@app/components/ContentToken.svelte"
import ContentCode from "@app/components/ContentCode.svelte" import ContentCode from "@app/components/ContentCode.svelte"
import ContentLinkInline from "@app/components/ContentLinkInline.svelte" import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
@@ -38,9 +36,9 @@
minLength?: number minLength?: number
maxLength?: number maxLength?: number
showEntire?: boolean showEntire?: boolean
hideMedia?: boolean hideMediaAtDepth?: number
expandMode?: string expandMode?: string
quoteProps?: Record<string, any> relays?: string[]
depth?: number depth?: number
} }
@@ -49,9 +47,9 @@
minLength = 500, minLength = 500,
maxLength = 700, maxLength = 700,
showEntire = $bindable(false), showEntire = $bindable(false),
hideMedia = false, hideMediaAtDepth = 1,
expandMode = "block", expandMode = "block",
quoteProps = {}, relays = [],
depth = 0, depth = 0,
}: Props = $props() }: Props = $props()
@@ -64,13 +62,13 @@
const isBlock = (i: number) => { const isBlock = (i: number) => {
const parsed = fullContent[i] const parsed = fullContent[i]
if (!parsed || hideMedia) return false if (!parsed || hideMediaAtDepth <= depth) return false
if (isLink(parsed) && $userSettingValues.show_media && isStartOrEnd(i)) { if (isLink(parsed) && $userSettingValues.show_media && isStartOrEnd(i)) {
return true return true
} }
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i) && depth < 1) { if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i)) {
return true return true
} }
@@ -108,7 +106,7 @@
: truncate(fullContent, { : truncate(fullContent, {
minLength, minLength,
maxLength, maxLength,
mediaLength: hideMedia ? 20 : 200, mediaLength: hideMediaAtDepth <= depth ? 20 : 200,
}), }),
) )
@@ -151,11 +149,7 @@
<ContentMention value={parsed.value} /> <ContentMention value={parsed.value} />
{:else if isEvent(parsed) || isAddress(parsed)} {:else if isEvent(parsed) || isAddress(parsed)}
{#if isBlock(i)} {#if isBlock(i)}
<ContentQuote {...quoteProps} value={parsed.value} {event}> <ContentQuote {depth} {relays} {hideMediaAtDepth} value={parsed.value} {event} />
{#snippet noteContent({event, minimal}: {event: TrustedEvent; minimal: boolean})}
<Content {quoteProps} hideMedia={minimal || hideMedia} {event} depth={depth + 1} />
{/snippet}
</ContentQuote>
{:else} {:else}
<Link <Link
external external
+8 -1
View File
@@ -8,6 +8,8 @@
const {value} = $props() const {value} = $props()
let hideImage = $state(false)
const url = value.url.toString() const url = value.url.toString()
const loadPreview = async () => { const loadPreview = async () => {
@@ -20,6 +22,10 @@
return json return json
} }
const onError = () => {
hideImage = true
}
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true}) const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
</script> </script>
@@ -40,9 +46,10 @@
</div> </div>
{:then preview} {:then preview}
<div class="bg-alt flex max-w-xl flex-col leading-normal"> <div class="bg-alt flex max-w-xl flex-col leading-normal">
{#if preview.image} {#if preview.image && !hideImage}
<img <img
alt="Link preview" alt="Link preview"
onerror={onError}
src={imgproxy(preview.image)} src={imgproxy(preview.image)}
class="bg-alt max-h-72 object-contain object-center" /> class="bg-alt max-h-72 object-contain object-center" />
{/if} {/if}
+4 -3
View File
@@ -7,10 +7,11 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import NoteCard from "@app/components/NoteCard.svelte" import NoteCard from "@app/components/NoteCard.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import {deriveEvent, entityLink, ROOM} from "@app/state" import {deriveEvent, entityLink, ROOM} from "@app/state"
import {makeThreadPath, makeCalendarPath, makeRoomPath} from "@app/routes" import {makeThreadPath, makeCalendarPath, makeRoomPath} from "@app/routes"
const {value, event, noteContent, relays = [], minimal = false} = $props() const {value, event, depth, hideMediaAtDepth, relays = []} = $props()
const {id, identifier, kind, pubkey, relays: relayHints = []} = value const {id, identifier, kind, pubkey, relays: relayHints = []} = value
const idOrAddress = id || new Address(kind, pubkey, identifier).toString() const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
@@ -103,8 +104,8 @@
<Button class="my-2 block max-w-full text-left" {onclick}> <Button class="my-2 block max-w-full text-left" {onclick}>
{#if $quote} {#if $quote}
<NoteCard {minimal} event={$quote} class="bg-alt rounded-box p-4"> <NoteCard event={$quote} class="bg-alt rounded-box p-4">
{@render noteContent({event: $quote, minimal})} <NoteContent {hideMediaAtDepth} {relays} event={$quote} depth={depth + 1} />
</NoteCard> </NoteCard>
{:else} {:else}
<div class="rounded-box p-4"> <div class="rounded-box p-4">
+11 -9
View File
@@ -16,6 +16,11 @@
import {loadUserData} from "@app/commands" import {loadUserData} from "@app/commands"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
let signers: any[] = $state([])
let loading: string | undefined = $state()
const disabled = $derived(loading ? true : undefined)
const signUp = () => pushModal(SignUp) const signUp = () => pushModal(SignUp)
const onSuccess = async (session: Session, relays: string[] = []) => { const onSuccess = async (session: Session, relays: string[] = []) => {
@@ -70,9 +75,6 @@
const loginWithBunker = () => pushModal(LogInBunker) const loginWithBunker = () => pushModal(LogInBunker)
let signers: any[] = $state([])
let loading: string | undefined = $state()
const hasSigner = $derived(getNip07() || signers.length > 0) const hasSigner = $derived(getNip07() || signers.length > 0)
onMount(async () => { onMount(async () => {
@@ -90,7 +92,7 @@
you to own your social identity. you to own your social identity.
</p> </p>
{#if getNip07()} {#if getNip07()}
<Button disabled={Boolean(loading)} onclick={loginWithNip07} class="btn btn-primary"> <Button {disabled} onclick={loginWithNip07} class="btn btn-primary">
{#if loading === "nip07"} {#if loading === "nip07"}
<span class="loading loading-spinner mr-3"></span> <span class="loading loading-spinner mr-3"></span>
{:else} {:else}
@@ -100,7 +102,7 @@
</Button> </Button>
{/if} {/if}
{#each signers as app} {#each signers as app}
<Button disabled={Boolean(loading)} class="btn btn-primary" onclick={() => loginWithNip55(app)}> <Button {disabled} class="btn btn-primary" onclick={() => loginWithNip55(app)}>
{#if loading === "nip55"} {#if loading === "nip55"}
<span class="loading loading-spinner mr-3"></span> <span class="loading loading-spinner mr-3"></span>
{:else} {:else}
@@ -110,7 +112,7 @@
</Button> </Button>
{/each} {/each}
{#if BURROW_URL && !hasSigner} {#if BURROW_URL && !hasSigner}
<Button disabled={Boolean(loading)} onclick={loginWithPassword} class="btn btn-primary"> <Button {disabled} onclick={loginWithPassword} class="btn btn-primary">
{#if loading === "password"} {#if loading === "password"}
<span class="loading loading-spinner mr-3"></span> <span class="loading loading-spinner mr-3"></span>
{:else} {:else}
@@ -121,13 +123,13 @@
{/if} {/if}
<Button <Button
onclick={loginWithBunker} onclick={loginWithBunker}
disabled={Boolean(loading)} {disabled}
class="btn {hasSigner || BURROW_URL ? 'btn-neutral' : 'btn-primary'}"> class="btn {hasSigner || BURROW_URL ? 'btn-neutral' : 'btn-primary'}">
<Icon icon="cpu" /> <Icon icon="cpu" />
Log in with Remote Signer Log in with Remote Signer
</Button> </Button>
{#if BURROW_URL && hasSigner} {#if BURROW_URL && hasSigner}
<Button disabled={Boolean(loading)} onclick={loginWithPassword} class="btn"> <Button {disabled} onclick={loginWithPassword} class="btn">
{#if loading === "password"} {#if loading === "password"}
<span class="loading loading-spinner mr-3"></span> <span class="loading loading-spinner mr-3"></span>
{:else} {:else}
@@ -139,7 +141,7 @@
{#if !hasSigner || !BURROW_URL} {#if !hasSigner || !BURROW_URL}
<Link <Link
external external
disabled={Boolean(loading)} {disabled}
href="https://nostrapps.com#signers" href="https://nostrapps.com#signers"
class="btn {hasSigner || BURROW_URL ? '' : 'btn-neutral'}"> class="btn {hasSigner || BURROW_URL ? '' : 'btn-neutral'}">
<Icon icon="compass" /> <Icon icon="compass" />
+14 -8
View File
@@ -57,14 +57,14 @@
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState}) const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
let showMenu = $state(false) let showMenu = $state(false)
let replaceState = false let replaceState = $state(false)
let element: Element | undefined = $state() let element: Element | undefined = $state()
const members = $derived( const members = $derived(
$memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey), $memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey),
) )
onMount(async () => { onMount(() => {
replaceState = Boolean(element?.closest(".drawer")) replaceState = Boolean(element?.closest(".drawer"))
pullConservatively({relays: [url], filters: [{kinds: [GROUP_META]}]}) pullConservatively({relays: [url], filters: [{kinds: [GROUP_META]}]})
}) })
@@ -112,19 +112,25 @@
{/if} {/if}
</div> </div>
<div class="flex min-h-0 flex-col gap-1 overflow-auto"> <div class="flex min-h-0 flex-col gap-1 overflow-auto">
<SecondaryNavItem href={makeSpacePath(url)}> <SecondaryNavItem {replaceState} href={makeSpacePath(url)}>
<Icon icon="home-smile" /> Home <Icon icon="home-smile" /> Home
</SecondaryNavItem> </SecondaryNavItem>
<SecondaryNavItem href={threadsPath} notification={$notifications.has(threadsPath)}> <SecondaryNavItem
{replaceState}
href={threadsPath}
notification={$notifications.has(threadsPath)}>
<Icon icon="notes-minimalistic" /> Threads <Icon icon="notes-minimalistic" /> Threads
</SecondaryNavItem> </SecondaryNavItem>
<SecondaryNavItem href={calendarPath} notification={$notifications.has(calendarPath)}> <SecondaryNavItem
{replaceState}
href={calendarPath}
notification={$notifications.has(calendarPath)}>
<Icon icon="calendar-minimalistic" /> Calendar <Icon icon="calendar-minimalistic" /> Calendar
</SecondaryNavItem> </SecondaryNavItem>
<div class="h-2"></div> <div class="h-2"></div>
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader> <SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
{#each $userRooms as room, i (room)} {#each $userRooms as room, i (room)}
<MenuSpaceRoomItem notify {url} {room} /> <MenuSpaceRoomItem {replaceState} notify {url} {room} />
{/each} {/each}
{#if $otherRooms.length > 0} {#if $otherRooms.length > 0}
<div class="h-2"></div> <div class="h-2"></div>
@@ -137,9 +143,9 @@
</SecondaryNavHeader> </SecondaryNavHeader>
{/if} {/if}
{#each $otherRooms as room, i (room)} {#each $otherRooms as room, i (room)}
<MenuSpaceRoomItem {url} {room} /> <MenuSpaceRoomItem {replaceState} {url} {room} />
{/each} {/each}
<SecondaryNavItem onclick={addRoom}> <SecondaryNavItem {replaceState} onclick={addRoom}>
<Icon icon="add-circle" /> <Icon icon="add-circle" />
Create room Create room
</SecondaryNavItem> </SecondaryNavItem>
+6 -2
View File
@@ -10,15 +10,19 @@
url: any url: any
room: any room: any
notify?: boolean notify?: boolean
replaceState?: boolean
} }
const {url, room, notify = false}: Props = $props() const {url, room, notify = false, replaceState = false}: Props = $props()
const path = makeRoomPath(url, room) const path = makeRoomPath(url, room)
const channel = deriveChannel(url, room) const channel = deriveChannel(url, room)
</script> </script>
<SecondaryNavItem href={path} notification={notify ? $notifications.has(path) : false}> <SecondaryNavItem
href={path}
{replaceState}
notification={notify ? $notifications.has(path) : false}>
{#if channelIsLocked($channel)} {#if channelIsLocked($channel)}
<Icon icon="lock" size={4} /> <Icon icon="lock" size={4} />
{:else} {:else}
+26
View File
@@ -0,0 +1,26 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {EVENT_TIME} from "@welshman/util"
import Content from "@app/components/Content.svelte"
import CalendarEventDate from "@app/components/CalendarEventDate.svelte"
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
const props: ComponentProps<typeof Content> = $props()
</script>
{#if props.event.kind === EVENT_TIME}
<div class="flex items-start gap-4">
<CalendarEventDate event={props.event} />
<div class="flex flex-grow flex-col">
<div class="flex flex-grow flex-wrap justify-between gap-2">
<CalendarEventHeader event={props.event} />
</div>
<div class="flex py-2 opacity-50">
<div class="h-px flex-grow bg-base-content opacity-25"></div>
</div>
<Content {...props} />
</div>
</div>
{:else}
<Content {...props} />
{/if}
+2 -2
View File
@@ -4,7 +4,7 @@
import {pubkey} from "@welshman/app" import {pubkey} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import EmojiButton from "@lib/components/EmojiButton.svelte" import EmojiButton from "@lib/components/EmojiButton.svelte"
import Content from "@app/components/Content.svelte" import NoteContent from "@app/components/NoteContent.svelte"
import NoteCard from "@app/components/NoteCard.svelte" import NoteCard from "@app/components/NoteCard.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte" import ReactionSummary from "@app/components/ReactionSummary.svelte"
import {publishDelete, publishReaction} from "@app/commands" import {publishDelete, publishReaction} from "@app/commands"
@@ -26,7 +26,7 @@
</script> </script>
<NoteCard {event} class="card2 bg-alt"> <NoteCard {event} class="card2 bg-alt">
<Content {event} expandMode="inline" /> <NoteContent {event} expandMode="inline" />
<div class="flex w-full justify-between gap-2"> <div class="flex w-full justify-between gap-2">
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right"> <ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right">
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box"> <EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
+1 -1
View File
@@ -8,5 +8,5 @@
</script> </script>
{#if $profile} {#if $profile}
<Content event={{content: $profile.about, tags: []}} hideMedia /> <Content event={{content: $profile.about, tags: []}} />
{/if} {/if}
+1 -1
View File
@@ -32,7 +32,7 @@
{formatTimestamp(event.created_at)} {formatTimestamp(event.created_at)}
</p> </p>
{/if} {/if}
<Content {event} expandMode="inline" quoteProps={{relays: [url]}} /> <Content {event} expandMode="inline" relays={[url]} />
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"> <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"> <span class="whitespace-nowrap py-1 text-sm opacity-75">
Posted by <ProfileLink pubkey={event.pubkey} /> Posted by <ProfileLink pubkey={event.pubkey} />
+23 -4
View File
@@ -1,5 +1,5 @@
import {get, writable} from "svelte/store" import {get, writable} from "svelte/store"
import {partition, int, YEAR, MONTH, insert, sortBy, assoc, now} from "@welshman/lib" import {partition, shuffle, int, YEAR, MONTH, insert, sortBy, assoc, now} from "@welshman/lib"
import { import {
MESSAGE, MESSAGE,
DELETE, DELETE,
@@ -55,6 +55,7 @@ export const makeFeed = ({
feedFilters, feedFilters,
subscriptionFilters, subscriptionFilters,
element, element,
onEvent,
onExhausted, onExhausted,
initialEvents = [], initialEvents = [],
}: { }: {
@@ -62,12 +63,21 @@ export const makeFeed = ({
feedFilters: Filter[] feedFilters: Filter[]
subscriptionFilters: Filter[] subscriptionFilters: Filter[]
element: HTMLElement element: HTMLElement
onEvent?: (event: TrustedEvent) => void
onExhausted?: () => void onExhausted?: () => void
initialEvents?: TrustedEvent[] initialEvents?: TrustedEvent[]
}) => { }) => {
const seen = new Set<string>()
const buffer = writable<TrustedEvent[]>([]) const buffer = writable<TrustedEvent[]>([])
const events = writable(initialEvents) const events = writable(initialEvents)
for (const event of initialEvents) {
if (!seen.has(event.id)) {
seen.add(event.id)
onEvent?.(event)
}
}
const insertEvent = (event: TrustedEvent) => { const insertEvent = (event: TrustedEvent) => {
buffer.update($buffer => { buffer.update($buffer => {
for (let i = 0; i < $buffer.length; i++) { for (let i = 0; i < $buffer.length; i++) {
@@ -77,6 +87,11 @@ export const makeFeed = ({
return [...$buffer, event] return [...$buffer, event]
}) })
if (!seen.has(event.id)) {
seen.add(event.id)
onEvent?.(event)
}
} }
const removeEvents = (ids: string[]) => { const removeEvents = (ids: string[]) => {
@@ -270,13 +285,17 @@ export const makeCalendarFeed = ({
export const listenForNotifications = () => { export const listenForNotifications = () => {
const subs: Subscription[] = [] const subs: Subscription[] = []
for (const [url, rooms] of userRoomsByUrl.get()) { for (const [url, allRooms] of userRoomsByUrl.get()) {
// Limit how many rooms we load at a time, since we have to send a separate filter
// for each one due to nip 29 breaking postel's law
const rooms = shuffle(Array.from(allRooms)).slice(0, 30)
load({ load({
relays: [url], relays: [url],
filters: [ filters: [
{kinds: [THREAD], limit: 1}, {kinds: [THREAD], limit: 1},
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1}, {kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
...Array.from(rooms).map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})), ...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
], ],
}) })
@@ -286,7 +305,7 @@ export const listenForNotifications = () => {
filters: [ filters: [
{kinds: [THREAD], since: now()}, {kinds: [THREAD], since: now()},
{kinds: [COMMENT], "#K": [String(THREAD)], since: now()}, {kinds: [COMMENT], "#K": [String(THREAD)], since: now()},
...Array.from(rooms).map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})), ...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
], ],
}), }),
) )
+3 -2
View File
@@ -24,15 +24,16 @@
import {fade} from "@lib/transition" import {fade} from "@lib/transition"
import {page} from "$app/stores" import {page} from "$app/stores"
const {children, href = "", notification = false, ...restProps} = $props() const {children, href = "", notification = false, replaceState = false, ...restProps} = $props()
const active = $derived($page.url.pathname === href) const active = $derived($page.url.pathname === href)
</script> </script>
{#if href} {#if href}
<a <a
{...restProps}
{href} {href}
{...restProps}
data-sveltekit-replacestate={replaceState}
class="{restProps.class} relative flex items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content" class="{restProps.class} relative flex items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content"
class:text-base-content={active} class:text-base-content={active}
class:bg-base-100={active}> class:bg-base-100={active}>
+1 -1
View File
@@ -50,7 +50,7 @@
</Button> </Button>
</div> </div>
{#key $profile?.about} {#key $profile?.about}
<Content event={{content: $profile?.about || "", tags: []}} hideMedia /> <Content event={{content: $profile?.about || "", tags: []}} hideMediaAtDepth={0} />
{/key} {/key}
</div> </div>
{#if $session?.email} {#if $session?.email}
+25 -10
View File
@@ -50,8 +50,12 @@
const joinRoom = async () => { const joinRoom = async () => {
if (hasNip29($relay)) { if (hasNip29($relay)) {
joiningRoom = true
const message = await getThunkError(nip29.joinRoom(url, room)) const message = await getThunkError(nip29.joinRoom(url, room))
joiningRoom = false
if (message && !message.includes("already")) { if (message && !message.includes("already")) {
return pushToast({theme: "error", message}) return pushToast({theme: "error", message})
} }
@@ -126,7 +130,8 @@
const scrollToBottom = () => element?.scrollTo({top: 0, behavior: "smooth"}) const scrollToBottom = () => element?.scrollTo({top: 0, behavior: "smooth"})
let loading = $state(true) let joiningRoom = $state(false)
let loadingEvents = $state(true)
let share = $state(popKey<TrustedEvent | undefined>("share")) let share = $state(popKey<TrustedEvent | undefined>("share"))
let parent: TrustedEvent | undefined = $state() let parent: TrustedEvent | undefined = $state()
let element: HTMLElement | undefined = $state() let element: HTMLElement | undefined = $state()
@@ -147,6 +152,12 @@
let newMessagesSeen = false let newMessagesSeen = false
if (events) { if (events) {
const lastUserEvent = $events.find(e => e.pubkey === $pubkey)
// Adjust last checked to account for messages that came from a different device
const adjustedLastChecked =
lastChecked && lastUserEvent ? Math.max(lastUserEvent.created_at, lastChecked) : lastChecked
for (const event of $events.toReversed()) { for (const event of $events.toReversed()) {
if (seen.has(event.id)) { if (seen.has(event.id)) {
continue continue
@@ -156,9 +167,9 @@
if ( if (
!newMessagesSeen && !newMessagesSeen &&
adjustedLastChecked &&
event.pubkey !== $pubkey && event.pubkey !== $pubkey &&
lastChecked && event.created_at > adjustedLastChecked
event.created_at > lastChecked
) { ) {
elements.push({type: "new-messages", id: "new-messages"}) elements.push({type: "new-messages", id: "new-messages"})
newMessagesSeen = true newMessagesSeen = true
@@ -196,7 +207,7 @@
subscriptionFilters: [{kinds: [DELETE, REACTION, MESSAGE], "#h": [room], since: now()}], subscriptionFilters: [{kinds: [DELETE, REACTION, MESSAGE], "#h": [room], since: now()}],
initialEvents: getEventsForUrl(url, [{...filter, limit: 20}]), initialEvents: getEventsForUrl(url, [{...filter, limit: 20}]),
onExhausted: () => { onExhausted: () => {
loading = false loadingEvents = false
}, },
})) }))
}) })
@@ -211,7 +222,7 @@
}) })
</script> </script>
<div class="saib relative flex h-full flex-col"> <div class="relative flex h-full flex-col">
<PageBar> <PageBar>
{#snippet icon()} {#snippet icon()}
<div class="center"> <div class="center">
@@ -232,8 +243,12 @@
Leave Room Leave Room
</Button> </Button>
{:else} {:else}
<Button class="btn btn-neutral btn-sm" onclick={joinRoom}> <Button class="btn btn-neutral btn-sm" disabled={joiningRoom} onclick={joinRoom}>
<Icon icon="login-2" /> {#if joiningRoom}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon icon="login-2" />
{/if}
Join Room Join Room
</Button> </Button>
{/if} {/if}
@@ -265,8 +280,8 @@
{/if} {/if}
{/each} {/each}
<p class="flex h-10 items-center justify-center py-20"> <p class="flex h-10 items-center justify-center py-20">
{#if loading} {#if loadingEvents}
<Spinner loading>Looking for messages...</Spinner> <Spinner loading={loadingEvents}>Looking for messages...</Spinner>
{:else} {:else}
<Spinner>End of message history</Spinner> <Spinner>End of message history</Spinner>
{/if} {/if}
@@ -281,7 +296,7 @@
</div> </div>
</div> </div>
{/if} {/if}
<div class="saib"> <div>
{#if parent} {#if parent}
<ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" /> <ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" />
{/if} {/if}
@@ -126,7 +126,11 @@
<strong>Calendar</strong> <strong>Calendar</strong>
{/snippet} {/snippet}
{#snippet action()} {#snippet action()}
<div class="md:hidden"> <div class="row-2">
<Button class="btn btn-primary btn-sm" onclick={createEvent}>
<Icon icon="calendar-add" />
Create an Event
</Button>
<MenuSpaceButton {url} /> <MenuSpaceButton {url} />
</div> </div>
{/snippet} {/snippet}
@@ -157,12 +161,4 @@
<p class="flex h-10 items-center justify-center py-20" transition:fly>That's all!</p> <p class="flex h-10 items-center justify-center py-20" transition:fly>That's all!</p>
{/if} {/if}
</div> </div>
<Button
class="tooltip tooltip-left fixed bottom-16 right-2 z-feature p-1 md:bottom-4 md:right-4"
data-tip="Create an Event"
onclick={createEvent}>
<div class="btn btn-circle btn-primary flex h-12 w-12 items-center justify-center">
<Icon icon="calendar-add" />
</div>
</Button>
</div> </div>
@@ -1,17 +1,9 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {sortBy, fromPairs, sleep} from "@welshman/lib" import {sortBy, sleep} from "@welshman/lib"
import {COMMENT, getTagValue} from "@welshman/util" import {COMMENT, getTagValue} from "@welshman/util"
import { import {repository, subscribe} from "@welshman/app"
repository,
subscribe,
formatTimestamp,
LOCALE,
secondsToDate,
formatTimestampAsDate,
formatTimestampAsTime,
} from "@welshman/app"
import {deriveEvents} from "@welshman/store" import {deriveEvents} from "@welshman/store"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import PageBar from "@lib/components/PageBar.svelte" import PageBar from "@lib/components/PageBar.svelte"
@@ -20,8 +12,10 @@
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import NoteCard from "@app/components/NoteCard.svelte" import NoteCard from "@app/components/NoteCard.svelte"
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte" import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
import CalendarEventActions from "@app/components/CalendarEventActions.svelte" import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
import CalendarEventMeta from "@app/components/CalendarEventMeta.svelte"
import CalendarEventDate from "@app/components/CalendarEventDate.svelte"
import EventReply from "@app/components/EventReply.svelte" import EventReply from "@app/components/EventReply.svelte"
import {deriveEvent, decodeRelay} from "@app/state" import {deriveEvent, decodeRelay} from "@app/state"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
@@ -31,13 +25,6 @@
const event = deriveEvent(id) const event = deriveEvent(id)
const filters = [{kinds: [COMMENT], "#E": [id]}] const filters = [{kinds: [COMMENT], "#E": [id]}]
const replies = deriveEvents(repository, {filters}) const replies = deriveEvents(repository, {filters})
const meta = $derived(fromPairs($event.tags) as Record<string, string>)
const end = $derived(parseInt(meta.end))
const start = $derived(parseInt(meta.start))
const startDate = $derived(secondsToDate(start))
const startDateDisplay = $derived(formatTimestampAsDate(start))
const endDateDisplay = $derived(formatTimestampAsDate(end))
const isSingleDay = $derived(startDateDisplay === endDateDisplay)
const back = () => history.back() const back = () => history.back()
@@ -95,39 +82,18 @@
{/if} {/if}
<div class="card2 bg-alt col-3 z-feature"> <div class="card2 bg-alt col-3 z-feature">
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<div <CalendarEventDate event={$event} />
class="flex h-24 w-24 flex-col items-center justify-center gap-1 rounded-box border border-solid border-base-content p-2">
<span class="text-lg"
>{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</span>
<span class="text-4xl"
>{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
</div>
<div class="flex flex-grow flex-col"> <div class="flex flex-grow flex-col">
<div class="flex flex-grow justify-between gap-2"> <div class="flex flex-grow justify-between gap-2">
<p class="text-xl">{meta.title || meta.name}</p> <CalendarEventHeader event={$event} />
<div class="flex items-center gap-2 text-sm">
<Icon icon="clock-circle" size={4} />
{formatTimestampAsTime(start)}{isSingleDay
? formatTimestampAsTime(end)
: formatTimestamp(end)}
</div>
</div> </div>
<div class="flex items-center gap-2 text-sm opacity-75"> <div class="flex items-center gap-2 text-sm opacity-75">
<span> <CalendarEventMeta event={$event} />
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}
</div> </div>
<div class="flex py-2 opacity-50"> <div class="flex py-2 opacity-50">
<div class="h-px flex-grow bg-base-content opacity-25"></div> <div class="h-px flex-grow bg-base-content opacity-25"></div>
</div> </div>
<Content showEntire event={$event} quoteProps={{relays: [url]}} /> <Content showEntire event={$event} relays={[url]} />
</div> </div>
</div> </div>
<div class="flex w-full flex-col justify-end sm:flex-row"> <div class="flex w-full flex-col justify-end sm:flex-row">
@@ -141,12 +107,12 @@
<p>Failed to load comments.</p> <p>Failed to load comments.</p>
{/await} {/await}
{/if} {/if}
<PageBar class="mx-0"> <PageBar class="!mx-0">
{#snippet icon()} {#snippet icon()}
<div> <div>
<Button class="btn btn-neutral btn-sm" onclick={back}> <Button class="btn btn-neutral btn-sm flex-nowrap whitespace-nowrap" onclick={back}>
<Icon icon="alt-arrow-left" /> <Icon icon="alt-arrow-left" />
Go back <span class="hidden sm:inline">Go back</span>
</Button> </Button>
</div> </div>
{/snippet} {/snippet}
+54 -63
View File
@@ -1,13 +1,10 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {derived} from "svelte/store"
import {page} from "$app/stores" import {page} from "$app/stores"
import {sortBy, min, nthEq} from "@welshman/lib" import {sortBy, max, nthEq} from "@welshman/lib"
import {THREAD, COMMENT, getListTags, getPubkeyTagValues} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {throttled} from "@welshman/store" import {THREAD, REACTION, DELETE, COMMENT, getListTags, getPubkeyTagValues} from "@welshman/util"
import {feedFromFilters, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds" import {userMutes} from "@welshman/app"
import {createFeedController, userMutes} from "@welshman/app"
import {createScroller, type Scroller} from "@lib/html"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -16,69 +13,63 @@
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte" import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
import ThreadItem from "@app/components/ThreadItem.svelte" import ThreadItem from "@app/components/ThreadItem.svelte"
import ThreadCreate from "@app/components/ThreadCreate.svelte" import ThreadCreate from "@app/components/ThreadCreate.svelte"
import {decodeRelay, deriveEventsForUrl} from "@app/state" import {decodeRelay, getEventsForUrl} from "@app/state"
import {setChecked} from "@app/notifications" import {setChecked} from "@app/notifications"
import {makeFeed} from "@app/requests"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay)
const threadFilter = {kinds: [THREAD]}
const commentFilter = {kinds: [COMMENT], "#K": [String(THREAD)]}
const feed = feedFromFilters([threadFilter, commentFilter])
const threads = deriveEventsForUrl(url, [threadFilter])
const comments = deriveEventsForUrl(url, [commentFilter])
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes)) const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
const threads: TrustedEvent[] = $state([])
const comments: TrustedEvent[] = $state([])
const events = throttled( let loading = $state(true)
800, let element: HTMLElement | undefined = $state()
derived([threads, comments], ([$threads, $comments]) => {
const scores = new Map<string, number>()
for (const comment of $comments) {
const id = comment.tags.find(nthEq(0, "E"))?.[1]
if (id) {
scores.set(id, min([scores.get(id), -comment.created_at]))
}
}
return sortBy(
e => min([scores.get(e.id), -e.created_at]),
$threads.filter(e => !mutedPubkeys.includes(e.pubkey)),
)
}),
)
const createThread = () => pushModal(ThreadCreate, {url}) const createThread = () => pushModal(ThreadCreate, {url})
const ctrl = createFeedController({ const events = $derived.by(() => {
useWindowing: true, const scores = new Map<string, number>()
feed: makeIntersectionFeed(makeRelayFeed(url), feed),
onExhausted: () => { for (const comment of comments) {
loading = false const id = comment.tags.find(nthEq(0, "E"))?.[1]
},
if (id) {
scores.set(id, max([scores.get(id), comment.created_at]))
}
}
return sortBy(e => -max([scores.get(e.id), e.created_at]), threads)
}) })
let limit = 10 $inspect({threads, comments, events})
let loading = $state(true)
let element: Element | undefined = $state()
let scroller: Scroller
onMount(() => { onMount(() => {
scroller = createScroller({ const {cleanup} = makeFeed({
element: element!, element: element!,
delay: 300, relays: [url],
threshold: 3000, feedFilters: [{kinds: [THREAD, COMMENT]}],
onScroll: () => { subscriptionFilters: [
limit += 10 {kinds: [THREAD, REACTION, DELETE]},
{kinds: [COMMENT], "#K": [String(THREAD)]},
if ($events.length - limit < 10) { ],
ctrl.load(50) initialEvents: getEventsForUrl(url, [{kinds: [THREAD, COMMENT], limit: 10}]),
onEvent: event => {
if (event.kind === THREAD && !mutedPubkeys.includes(event.pubkey)) {
threads.push(event)
} }
if (event.kind === COMMENT) {
comments.push(event)
}
},
onExhausted: () => {
loading = false
}, },
}) })
return () => { return () => {
scroller?.stop() cleanup()
setChecked($page.url.pathname) setChecked($page.url.pathname)
} }
}) })
@@ -105,21 +96,21 @@
{/snippet} {/snippet}
</PageBar> </PageBar>
<div class="flex flex-grow flex-col gap-2 overflow-auto p-2"> <div class="flex flex-grow flex-col gap-2 overflow-auto p-2">
{#each $events as event (event.id)} {#each events as event (event.id)}
<div in:fly> <div in:fly>
<ThreadItem {url} {event} /> <ThreadItem {url} {event} />
</div> </div>
{/each} {/each}
{#if loading || $events.length === 0} <p class="flex h-10 items-center justify-center py-20">
<p class="flex h-10 items-center justify-center py-20" out:fly> <Spinner {loading}>
<Spinner {loading}> {#if loading}
{#if loading} Looking for threads...
Looking for threads... {:else if events.length === 0}
{:else if $events.length === 0} No threads found.
No threads found. {:else}
{/if} That's all!
</Spinner> {/if}
</p> </Spinner>
{/if} </p>
</div> </div>
</div> </div>
@@ -79,7 +79,7 @@
{/if} {/if}
<NoteCard event={$event} class="card2 bg-alt z-feature w-full"> <NoteCard event={$event} class="card2 bg-alt z-feature w-full">
<div class="col-3 ml-12"> <div class="col-3 ml-12">
<Content showEntire event={$event} quoteProps={{relays: [url]}} /> <Content showEntire event={$event} relays={[url]} />
<ThreadActions event={$event} {url} /> <ThreadActions event={$event} {url} />
</div> </div>
</NoteCard> </NoteCard>
@@ -90,12 +90,12 @@
<p>Failed to load thread.</p> <p>Failed to load thread.</p>
{/await} {/await}
{/if} {/if}
<PageBar class="mx-0"> <PageBar class="!mx-0">
{#snippet icon()} {#snippet icon()}
<div> <div>
<Button class="btn btn-neutral btn-sm" onclick={back}> <Button class="btn btn-neutral btn-sm flex-nowrap whitespace-nowrap" onclick={back}>
<Icon icon="alt-arrow-left" /> <Icon icon="alt-arrow-left" />
Go back <span class="hidden sm:inline">Go back</span>
</Button> </Button>
</div> </div>
{/snippet} {/snippet}