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
# 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
+1 -1
View File
@@ -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
View File
@@ -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
// },
};
+8 -4
View File
@@ -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 -1
View File
@@ -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>
+44 -30
View File
@@ -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
View File
@@ -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>
+5 -21
View File
@@ -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" />
+1 -1
View File
@@ -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}
+8 -14
View File
@@ -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 -1
View File
@@ -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}
+4 -3
View File
@@ -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">
+11 -9
View File
@@ -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" />
+14 -8
View File
@@ -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>
+6 -2
View File
@@ -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}
+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 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">
+1 -1
View File
@@ -8,5 +8,5 @@
</script>
{#if $profile}
<Content event={{content: $profile.about, tags: []}} hideMedia />
<Content event={{content: $profile.about, tags: []}} />
{/if}
+1 -1
View File
@@ -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
View File
@@ -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()})),
],
}),
)
+3 -2
View File
@@ -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}>
+1 -1
View File
@@ -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}
+25 -10
View File
@@ -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}
+54 -63
View File
@@ -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}