Add classified listings

This commit is contained in:
Jon Staab
2026-02-03 11:00:13 -08:00
parent 1da6833c71
commit 119c09d730
25 changed files with 816 additions and 28 deletions
@@ -0,0 +1,45 @@
<script lang="ts">
import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue} from "@welshman/util"
import Link from "@lib/components/Link.svelte"
import RoomName from "@app/components/RoomName.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
import {makeClassifiedPath, makeSpacePath} from "@app/util/routes"
interface Props {
url: string
event: TrustedEvent
showRoom?: boolean
showActivity?: boolean
}
const {url, event, showRoom, showActivity}: Props = $props()
const h = getTagValue("h", event.tags)
const path = makeClassifiedPath(url, event.id)
const shouldProtect = canEnforceNip70(url)
const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await shouldProtect})
const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script>
<div class="flex flex-grow flex-wrap justify-end gap-2">
{#if h && showRoom}
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
Posted in #<RoomName {h} {url} />
</Link>
{/if}
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
<ThunkStatusOrDeleted {event} />
{#if showActivity}
<EventActivity {url} {path} {event} />
{/if}
<EventActions {url} {event} noun="Listing" />
</div>
+149
View File
@@ -0,0 +1,149 @@
<script lang="ts">
import {writable} from "svelte/store"
import {makeEvent, CLASSIFIED} from "@welshman/util"
import {publishThunk} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state"
import {makeEditor} from "@app/editor"
import {canEnforceNip70} from "@app/core/commands"
type Props = {
url: string
h?: string
}
const {url, h}: Props = $props()
const shouldProtect = canEnforceNip70(url)
const uploading = writable(false)
const back = () => history.back()
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
const submit = async () => {
if ($uploading) return
if (!title) {
return pushToast({
theme: "error",
message: "Please provide a title for your listing.",
})
}
const ed = await editor
const content = ed.getText({blockSeparator: "\n"}).trim()
if (!content.trim()) {
return pushToast({
theme: "error",
message: "Please provide a message for your listing.",
})
}
const tags = [...ed.storage.nostr.getEditorTags(), ["title", title]]
if (await shouldProtect) {
tags.push(PROTECTED)
}
if (h) {
tags.push(["h", h])
}
publishThunk({
relays: [url],
event: makeEvent(CLASSIFIED, {content, tags}),
})
history.back()
}
const editor = makeEditor({url, submit, uploading, placeholder: "What's on your mind?"})
let title: string = $state("")
</script>
<Modal tag="form" onsubmit={preventDefault(submit)}>
<ModalBody>
<ModalHeader>
<ModalTitle>Create a Classified Listing</ModalTitle>
<ModalSubtitle>Advertise a job, sale, or need.</ModalSubtitle>
</ModalHeader>
<div class="col-8 relative">
<Field>
{#snippet label()}
<p>Title*</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<!-- svelte-ignore a11y_autofocus -->
<input
autofocus={!isMobile}
bind:value={title}
class="grow"
type="text"
placeholder="What is this listing for?" />
</label>
{/snippet}
</Field>
<Field>
{#snippet label()}
<p>Description*</p>
{/snippet}
{#snippet input()}
<div class="note-editor flex-grow overflow-hidden">
<EditorContent {editor} />
</div>
{/snippet}
</Field>
<Field>
{#snippet label()}
<p>Price*</p>
{/snippet}
{#snippet input()}
todo: value and search select inline
{/snippet}
</Field>
<Field>
{#snippet label()}
<p>Images</p>
{/snippet}
{#snippet input()}
todo: attach multiple images
{/snippet}
</Field>
<Button
data-tip="Add an image"
class="tooltip tooltip-left absolute bottom-1 right-2"
onclick={selectFiles}>
{#if $uploading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Icon icon={Paperclip} size={3} />
{/if}
</Button>
</div>
</ModalBody>
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button type="submit" class="btn btn-primary">Create Listing</Button>
</ModalFooter>
</Modal>
+49
View File
@@ -0,0 +1,49 @@
<script lang="ts">
import {formatTimestamp} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {getTagValue} from "@welshman/util"
import Link from "@lib/components/Link.svelte"
import Content from "@app/components/Content.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
import ClassifiedActions from "@app/components/ClassifiedActions.svelte"
import RoomLink from "@app/components/RoomLink.svelte"
import {makeClassifiedPath} from "@app/util/routes"
type Props = {
url: string
event: TrustedEvent
}
const {url, event}: Props = $props()
const title = getTagValue("title", event.tags)
const h = getTagValue("h", event.tags)
</script>
<Link
class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
href={makeClassifiedPath(url, event.id)}>
{#if title}
<div class="flex w-full items-center justify-between gap-2">
<p class="text-xl">{title}</p>
<p class="text-sm opacity-75">
{formatTimestamp(event.created_at)}
</p>
</div>
{:else}
<p class="mb-3 h-0 text-xs opacity-75">
{formatTimestamp(event.created_at)}
</p>
{/if}
<Content {event} {url} expandMode="inline" />
<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} {url} />
{#if h}
in <RoomLink {url} {h} />
{/if}
</span>
<ClassifiedActions showActivity {url} {event} />
</div>
</Link>
+6 -5
View File
@@ -5,19 +5,20 @@
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
import {makeThreadPath} from "@app/util/routes"
import {makeSpacePath} from "@app/util/routes"
interface Props {
url: any
event: any
url: string
event: TrustedEvent
segment: string
showActivity?: boolean
}
const {url, event, showActivity = false}: Props = $props()
const {url, event, segment, showActivity = false}: Props = $props()
const shouldProtect = canEnforceNip70(url)
const path = makeThreadPath(url, event.id)
const path = makeSpacePath(url, segment, event.id)
const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await shouldProtect})
+12 -2
View File
@@ -3,11 +3,13 @@
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic.svg?dataurl"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import CaseMinimalistic from "@assets/icons/case-minimalistic.svg?dataurl"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import {pushModal} from "@app/util/modal"
import CalendarEventCreate from "@app/components/CalendarEventCreate.svelte"
import ThreadCreate from "@app/components/ThreadCreate.svelte"
import ClassifiedCreate from "@app/components/ClassifiedCreate.svelte"
import GoalCreate from "@app/components/GoalCreate.svelte"
type Props = {
@@ -24,6 +26,8 @@
const createThread = () => pushModal(ThreadCreate, {url, h})
const createClassified = () => pushModal(ClassifiedCreate, {url, h})
let ul: Element
onMount(() => {
@@ -35,13 +39,19 @@
<li>
<Button onclick={createGoal}>
<Icon size={4} icon={StarFallMinimalistic} />
Create Funding Goal
Funding Goal
</Button>
</li>
<li>
<Button onclick={createCalendarEvent}>
<Icon size={4} icon={CalendarMinimalistic} />
Create Calendar Event
Calendar Event
</Button>
</li>
<li>
<Button onclick={createClassified}>
<Icon size={4} icon={CaseMinimalistic} />
Classified Listing
</Button>
</li>
<li>
+4 -1
View File
@@ -14,6 +14,7 @@
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import {clip} from "@app/util/toast"
@@ -106,6 +107,8 @@
</Button>
</p>
</div>
<Button class="btn btn-primary" onclick={() => history.back()}>Got it</Button>
</ModalBody>
<ModalFooter>
<Button class="btn btn-primary flex-grow" onclick={() => history.back()}>Got it</Button>
</ModalFooter>
</Modal>
+4 -1
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {EVENT_TIME, ZAP_GOAL, THREAD} from "@welshman/util"
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED} from "@welshman/util"
import NoteContentEventTime from "@app/components/NoteContentEventTime.svelte"
import NoteContentThread from "@app/components/NoteContentThread.svelte"
import NoteContentClassified from "@app/components/NoteContentClassified.svelte"
import NoteContentGoal from "@app/components/NoteContentGoal.svelte"
import Content from "@app/components/Content.svelte"
@@ -13,6 +14,8 @@
<NoteContentEventTime {...props} />
{:else if props.event.kind === THREAD}
<NoteContentThread {...props} />
{:else if props.event.kind === CLASSIFIED}
<NoteContentClassified {...props} />
{:else if props.event.kind === ZAP_GOAL}
<NoteContentGoal {...props} />
{:else}
@@ -0,0 +1,18 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {getTagValue} from "@welshman/util"
import Content from "@app/components/Content.svelte"
const props: ComponentProps<typeof Content> = $props()
const title = getTagValue("title", props.event.tags)
</script>
<div class="flex flex-col gap-2">
{#if title}
<p class="text-xl">{title}</p>
{/if}
{#if props.event.content}
<Content {...props} />
{/if}
</div>
+4 -1
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {EVENT_TIME, ZAP_GOAL, THREAD} from "@welshman/util"
import {EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED} from "@welshman/util"
import NoteContentMinimalEventTime from "@app/components/NoteContentMinimalEventTime.svelte"
import NoteContentMinimalThread from "@app/components/NoteContentMinimalThread.svelte"
import NoteContentMinimalClassified from "@app/components/NoteContentMinimalClassified.svelte"
import NoteContentMinimalGoal from "@app/components/NoteContentMinimalGoal.svelte"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
@@ -14,6 +15,8 @@
<NoteContentMinimalEventTime {...props} />
{:else if props.event.kind === THREAD}
<NoteContentMinimalThread {...props} />
{:else if props.event.kind === CLASSIFIED}
<NoteContentMinimalClassified {...props} />
{:else if props.event.kind === ZAP_GOAL}
<NoteContentMinimalGoal {...props} />
{:else}
@@ -0,0 +1,16 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {getTagValue} from "@welshman/util"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
const props: ComponentProps<typeof ContentMinimal> = $props()
const title = getTagValue("title", props.event.tags)
</script>
{#if title}
<span class="text-sm">{title}</span>
{/if}
{#if props.event.content}
<ContentMinimal {...props} />
{/if}
+2 -4
View File
@@ -131,8 +131,7 @@
<form class="relative flex gap-2 p-2" onsubmit={preventDefault(submit)}>
<div class="join">
<Button
data-tip="Add an image"
class="center join-item tooltip tooltip-right h-10 w-10 min-w-10 rounded-full border border-solid border-base-200 bg-base-300"
class="center join-item h-10 w-10 min-w-10 rounded-full border border-solid border-base-200 bg-base-300"
disabled={$uploading}
onclick={uploadFiles}>
{#if $uploading}
@@ -147,8 +146,7 @@
props={{url, h, onClick: hidePopover}}
params={{trigger: "manual", interactive: true}}>
<Button
data-tip="More options"
class="center join-item tooltip tooltip-right h-10 w-10 min-w-10 rounded-full border border-solid border-base-200 bg-base-300"
class="center join-item h-10 w-10 min-w-10 rounded-full border border-solid border-base-200 bg-base-300"
disabled={$uploading}
onclick={showPopover}>
<Icon icon={WidgetAdd} />
+11 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import {onMount} from "svelte"
import {derived} from "svelte/store"
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, REPORT} from "@welshman/util"
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, REPORT} from "@welshman/util"
import {deriveRelay, pubkey} from "@welshman/app"
import {fly} from "@lib/transition"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
@@ -16,6 +16,7 @@
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic-2.svg?dataurl"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
import CaseMinimalistic from "@assets/icons/case-minimalistic.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
@@ -62,6 +63,7 @@
const chatPath = makeSpacePath(url, "chat")
const goalsPath = makeSpacePath(url, "goals")
const threadsPath = makeSpacePath(url, "threads")
const classifiedsPath = makeSpacePath(url, "classifieds")
const calendarPath = makeSpacePath(url, "calendar")
const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url)
@@ -228,6 +230,14 @@
<Icon icon={NotesMinimalistic} /> Threads
</SecondaryNavItem>
{/if}
{#if $spaceKinds.has(CLASSIFIED)}
<SecondaryNavItem
{replaceState}
href={classifiedsPath}
notification={$notifications.has(classifiedsPath)}>
<Icon icon={CaseMinimalistic} /> Classifieds
</SecondaryNavItem>
{/if}
{#if $spaceKinds.has(EVENT_TIME)}
<SecondaryNavItem
{replaceState}
+14 -2
View File
@@ -80,6 +80,7 @@ import {
ROOM_REMOVE_MEMBER,
ROOMS,
THREAD,
CLASSIFIED,
WRAP,
PROFILE,
ZAP_GOAL,
@@ -169,7 +170,18 @@ export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
export const NIP46_PERMS =
"nip44_encrypt,nip44_decrypt," +
[CLIENT_AUTH, RELAY_JOIN, MESSAGE, THREAD, COMMENT, ROOMS, WRAP, REACTION, ZAP_REQUEST]
[
CLIENT_AUTH,
RELAY_JOIN,
MESSAGE,
THREAD,
CLASSIFIED,
COMMENT,
ROOMS,
WRAP,
REACTION,
ZAP_REQUEST,
]
.map(k => `sign_event:${k}`)
.join(",")
@@ -256,7 +268,7 @@ if (ENABLE_ZAPS) {
REACTION_KINDS.push(ZAP_RESPONSE)
}
export const CONTENT_KINDS = [ZAP_GOAL, EVENT_TIME, THREAD]
export const CONTENT_KINDS = [ZAP_GOAL, EVENT_TIME, THREAD, CLASSIFIED]
export const MESSAGE_KINDS = [...CONTENT_KINDS, MESSAGE]
+26
View File
@@ -40,6 +40,7 @@ import {
ZAP_GOAL,
EVENT_TIME,
THREAD,
CLASSIFIED,
COMMENT,
DELETE,
getTagValue,
@@ -56,6 +57,7 @@ import {
makeChatPath,
makeGoalPath,
makeThreadPath,
makeClassifiedPath,
makeCalendarPath,
makeSpaceChatPath,
makeRoomPath,
@@ -106,12 +108,14 @@ export const setChecked = (key: string) => checked.update(state => ({...state, [
const goalCommentFilters = [{kinds: [COMMENT], "#K": [String(ZAP_GOAL)]}]
const threadCommentFilters = [{kinds: [COMMENT], "#K": [String(THREAD)]}]
const classifiedCommentFilters = [{kinds: [COMMENT], "#K": [String(CLASSIFIED)]}]
const calendarCommentFilters = [{kinds: [COMMENT], "#K": [String(EVENT_TIME)]}]
const messageFilters = [{kinds: MESSAGE_KINDS}]
const dmFilters = [{kinds: DM_KINDS}]
const allFilters = flatten([
goalCommentFilters,
threadCommentFilters,
classifiedCommentFilters,
calendarCommentFilters,
messageFilters,
dmFilters,
@@ -129,6 +133,7 @@ export const notifications = derived(
relaysByUrl,
deriveEventsByIdByUrl({tracker, repository, filters: goalCommentFilters}),
deriveEventsByIdByUrl({tracker, repository, filters: threadCommentFilters}),
deriveEventsByIdByUrl({tracker, repository, filters: classifiedCommentFilters}),
deriveEventsByIdByUrl({tracker, repository, filters: calendarCommentFilters}),
deriveEventsByIdByUrl({tracker, repository, filters: messageFilters}),
],
@@ -143,6 +148,7 @@ export const notifications = derived(
$relaysByUrl,
goalCommentsByUrl,
threadCommentsByUrl,
classifiedCommentsByUrl,
calendarCommentsByUrl,
messagesByUrl,
]) => {
@@ -181,10 +187,12 @@ export const notifications = derived(
const spacePathMobile = spacePath + ":mobile"
const goalPath = makeGoalPath(url)
const threadPath = makeThreadPath(url)
const classifiedPath = makeClassifiedPath(url)
const calendarPath = makeCalendarPath(url)
const messagesPath = makeSpaceChatPath(url)
const goalComments = sortEventsDesc(goalCommentsByUrl.get(url)?.values() || [])
const threadComments = sortEventsDesc(threadCommentsByUrl.get(url)?.values() || [])
const classifiedComments = sortEventsDesc(classifiedCommentsByUrl.get(url)?.values() || [])
const calendarComments = sortEventsDesc(calendarCommentsByUrl.get(url)?.values() || [])
const messages = sortEventsDesc(messagesByUrl.get(url)?.values() || [])
@@ -224,6 +232,24 @@ export const notifications = derived(
}
}
const commentsByClassifiedId = groupBy(
e => getTagValue("E", e.tags),
classifiedComments.filter(spec({kind: COMMENT})),
)
for (const [classifiedId, [comment]] of commentsByClassifiedId.entries()) {
const classifiedItemPath = makeClassifiedPath(url, classifiedId)
if (hasNotification(classifiedPath, comment)) {
paths.add(spacePathMobile)
paths.add(classifiedPath)
}
if (hasNotification(classifiedItemPath, comment)) {
paths.add(classifiedItemPath)
}
}
const commentsByEventId = groupBy(
e => getTagValue("E", e.tags),
calendarComments.filter(spec({kind: COMMENT})),
+14
View File
@@ -13,6 +13,7 @@ import {
DIRECT_MESSAGE_FILE,
MESSAGE,
THREAD,
CLASSIFIED,
ZAP_GOAL,
EVENT_TIME,
getPubkeyTagValues,
@@ -66,6 +67,9 @@ export const makeGoalPath = (url: string, eventId?: string) => makeSpacePath(url
export const makeThreadPath = (url: string, eventId?: string) =>
makeSpacePath(url, "threads", eventId)
export const makeClassifiedPath = (url: string, eventId?: string) =>
makeSpacePath(url, "classifieds", eventId)
export const makeCalendarPath = (url: string, eventId?: string) =>
makeSpacePath(url, "calendar", eventId)
@@ -121,6 +125,10 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
return makeThreadPath(url, event.id)
}
if (event.kind === CLASSIFIED) {
return makeClassifiedPath(url, event.id)
}
if (event.kind === EVENT_TIME) {
return makeCalendarPath(url, event.id)
}
@@ -141,6 +149,10 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
return makeThreadPath(url, id)
}
if (parseInt(kind) === CLASSIFIED) {
return makeClassifiedPath(url, id)
}
if (parseInt(kind) === EVENT_TIME) {
return makeCalendarPath(url, id)
}
@@ -158,6 +170,8 @@ export const getRoomItemPath = (url: string, event: TrustedEvent) => {
switch (event.kind) {
case THREAD:
return makeThreadPath(url, event.id)
case CLASSIFIED:
return makeClassifiedPath(url, event.id)
case ZAP_GOAL:
return makeGoalPath(url, event.id)
case EVENT_TIME:
+2 -7
View File
@@ -8,12 +8,8 @@ import {
ALERT_WEB,
APP_DATA,
BLOSSOM_SERVERS,
DIRECT_MESSAGE_FILE,
DIRECT_MESSAGE,
EVENT_TIME,
FOLLOWS,
MESSAGING_RELAYS,
MESSAGE,
MUTES,
PROFILE,
RELAY_ADD_MEMBER,
@@ -30,8 +26,6 @@ import {
ROOM_DELETE,
ROOM_REMOVE_MEMBER,
ROOMS,
THREAD,
ZAP_GOAL,
verifiedSymbol,
} from "@welshman/util"
import type {Zapper, TrustedEvent, RelayProfile} from "@welshman/util"
@@ -53,6 +47,7 @@ import {
} from "@welshman/app"
import {isMobile} from "@lib/html"
import type {IDBTable} from "@lib/indexeddb"
import {MESSAGE_KINDS, DM_KINDS} from "@app/core/state"
const kinds = {
meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS],
@@ -67,7 +62,7 @@ const kinds = {
ROOM_REMOVE_MEMBER,
ROOM_CREATE_PERMISSION,
],
content: [EVENT_TIME, THREAD, MESSAGE, ZAP_GOAL, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE],
content: [...MESSAGE_KINDS, ...DM_KINDS],
}
const rankEvent = (event: TrustedEvent) => {