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