forked from coracle/flotilla
threads in rooms.
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {writable} from "svelte/store"
|
||||
import {deriveRelay, publishThunk, waitForThunkError} from "@welshman/app"
|
||||
import {makeEvent, THREAD} from "@welshman/util"
|
||||
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||
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"
|
||||
@@ -16,8 +17,12 @@
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import RoomImage from "@app/components/RoomImage.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {PROTECTED} from "@app/core/state"
|
||||
import {hasNip29, roomsByUrl, PROTECTED} from "@app/core/state"
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {DraftKey} from "@app/util/drafts"
|
||||
import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
|
||||
@@ -33,8 +38,11 @@
|
||||
shareToChat?: boolean
|
||||
}
|
||||
|
||||
const {url, h, shareToChat = false}: Props = $props()
|
||||
const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`)
|
||||
const {url, h: initialH, shareToChat = false}: Props = $props()
|
||||
const relay = deriveRelay(url)
|
||||
const rooms = $derived(($roomsByUrl.get(url) || []).map(room => room.h))
|
||||
const isNip29 = $derived(hasNip29($relay))
|
||||
const draftKey = new DraftKey<Values>(`thread:${url}:${initialH ?? ""}`)
|
||||
const initialValues = draftKey.get()
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -44,6 +52,20 @@
|
||||
|
||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
||||
|
||||
const toggleRoomPicker = () => {
|
||||
showRoomPicker = !showRoomPicker
|
||||
}
|
||||
|
||||
const pickSpace = () => {
|
||||
selectedH = undefined
|
||||
showRoomPicker = false
|
||||
}
|
||||
|
||||
const pickRoom = (h: string) => {
|
||||
selectedH = h
|
||||
showRoomPicker = false
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if ($uploading || loading) return
|
||||
|
||||
@@ -75,8 +97,8 @@
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
if (selectedH) {
|
||||
tags.push(["h", selectedH])
|
||||
}
|
||||
|
||||
const threadThunk = publishThunk({
|
||||
@@ -94,7 +116,7 @@
|
||||
history.back()
|
||||
|
||||
if (shareToChat) {
|
||||
publishRoomQuote({url, h, parent: threadThunk.event, protect})
|
||||
publishRoomQuote({url, h: selectedH, parent: threadThunk.event, protect})
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
@@ -102,6 +124,8 @@
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
let showRoomPicker = $state(false)
|
||||
let selectedH: string | undefined = $state(initialH)
|
||||
|
||||
let title = $state(initialValues?.title ?? "")
|
||||
let content = $state(initialValues?.content ?? "")
|
||||
@@ -131,6 +155,53 @@
|
||||
<ModalSubtitle>Share a link, or start a discussion.</ModalSubtitle>
|
||||
</ModalHeader>
|
||||
<div class="col-8 relative">
|
||||
{#if isNip29}
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Room</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="relative">
|
||||
<Button
|
||||
type="button"
|
||||
class="select select-bordered flex w-full items-center gap-2"
|
||||
onclick={toggleRoomPicker}>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
{#if selectedH}
|
||||
<RoomImage {url} h={selectedH} size={4} />
|
||||
<RoomName {url} h={selectedH} class="truncate text-left" />
|
||||
{:else}
|
||||
<RelayIcon {url} size={4} />
|
||||
<RelayName {url} class="truncate text-left" />
|
||||
{/if}
|
||||
</div>
|
||||
<Icon icon={AltArrowDown} size={4} />
|
||||
</Button>
|
||||
{#if showRoomPicker}
|
||||
<div
|
||||
class="absolute z-popover mt-1 w-full rounded-lg border border-base-300 bg-base-100 shadow-xl">
|
||||
<Button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 rounded-none border-0 bg-transparent px-3 py-2 hover:bg-base-200"
|
||||
onclick={pickSpace}>
|
||||
<RelayIcon {url} size={4} />
|
||||
<RelayName {url} />
|
||||
</Button>
|
||||
{#each rooms as h (h)}
|
||||
<Button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 rounded-none border-0 bg-transparent px-3 py-2 hover:bg-base-200"
|
||||
onclick={() => pickRoom(h)}>
|
||||
<RoomImage {url} {h} size={4} />
|
||||
<RoomName {url} {h} />
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Field>
|
||||
{/if}
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Title*</p>
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
removeFromListByPredicate,
|
||||
updateList,
|
||||
getTag,
|
||||
getTagValue,
|
||||
getListTags,
|
||||
getRelayTagValues,
|
||||
toNostrURI,
|
||||
@@ -391,8 +392,16 @@ export type CommentParams = {
|
||||
url?: string
|
||||
}
|
||||
|
||||
export const makeComment = ({url, event, content, tags = []}: CommentParams) =>
|
||||
makeEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event, url)]})
|
||||
export const makeComment = ({url, event, content, tags = []}: CommentParams) => {
|
||||
const allTags = [...tags, ...tagEventForComment(event, url)]
|
||||
const h = getTagValue("h", event.tags)
|
||||
|
||||
if (h) {
|
||||
allTags.push(["h", h])
|
||||
}
|
||||
|
||||
return makeEvent(COMMENT, {content, tags: allTags})
|
||||
}
|
||||
|
||||
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
||||
publishThunk({event: makeComment({url: relays[0], ...params}), relays})
|
||||
|
||||
@@ -14,6 +14,7 @@ const staticTitles = new Map<string, string>([
|
||||
["/spaces/[relay]/chat", "Space Chat"],
|
||||
["/spaces/[relay]/recent", "Recent Activity"],
|
||||
["/spaces/[relay]/threads", "Threads"],
|
||||
["/spaces/[relay]/[h]/threads", "Threads"],
|
||||
["/spaces/[relay]/classifieds", "Classifieds"],
|
||||
["/spaces/[relay]/calendar", "Calendar"],
|
||||
["/spaces/[relay]/goals", "Goals"],
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||
import InfoCircle from "@assets/icons/info-circle.svg?dataurl"
|
||||
import Login2 from "@assets/icons/login-3.svg?dataurl"
|
||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||
import cx from "classnames"
|
||||
import {fade, fly} from "@lib/transition"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
@@ -430,6 +432,9 @@
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<SpaceSearch {url} {h} />
|
||||
<Link href={roomPath + "/threads"} class="btn btn-neutral btn-sm btn-square">
|
||||
<Icon size={4} icon={NotesMinimalistic} />
|
||||
</Link>
|
||||
<Button class="btn btn-neutral btn-sm btn-square" onclick={showRoomDetail}>
|
||||
<Icon size={4} icon={InfoCircle} />
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {readable} from "svelte/store"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {page} from "$app/stores"
|
||||
import {sortBy, partition, spec, max, pushToMapKey} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {THREAD, getTagValue} from "@welshman/util"
|
||||
import {fly} from "@lib/transition"
|
||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||
import Add from "@assets/icons/add.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import RoomImage from "@app/components/RoomImage.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import ThreadItem from "@app/components/ThreadItem.svelte"
|
||||
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
||||
import {decodeRelay, makeCommentFilter} from "@app/core/state"
|
||||
import {makeFeed} from "@app/core/requests"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
const {relay, h} = $page.params as {relay: string; h: string}
|
||||
const url = decodeRelay(relay)
|
||||
|
||||
let loading = $state(true)
|
||||
let element: HTMLElement | undefined = $state()
|
||||
let events: Readable<TrustedEvent[]> = $state(readable([]))
|
||||
|
||||
const createThread = () => pushModal(ThreadCreate, {url, h})
|
||||
|
||||
const items = $derived.by(() => {
|
||||
const scores = new Map<string, number[]>()
|
||||
const [threads, comments] = partition(spec({kind: THREAD}), $events)
|
||||
|
||||
for (const comment of comments) {
|
||||
const id = getTagValue("E", comment.tags)
|
||||
|
||||
if (id) {
|
||||
pushToMapKey(scores, id, comment.created_at)
|
||||
}
|
||||
}
|
||||
|
||||
return sortBy(event => -max([...(scores.get(event.id) || []), event.created_at]), threads)
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const feed = makeFeed({
|
||||
url,
|
||||
element: element!,
|
||||
filters: [{kinds: [THREAD], "#h": [h]}, makeCommentFilter([THREAD], {"#h": [h]})],
|
||||
onBackwardExhausted: () => {
|
||||
loading = false
|
||||
},
|
||||
})
|
||||
|
||||
events = feed.events
|
||||
|
||||
return () => {
|
||||
feed.cleanup()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar>
|
||||
{#snippet leading()}
|
||||
<RoomImage {url} {h} />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<RoomName {url} {h} />
|
||||
<Icon icon={NotesMinimalistic} size={4} class="opacity-60" />
|
||||
<strong>Threads</strong>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Button class="btn btn-sm btn-primary" onclick={createThread}>
|
||||
<Icon icon={Add} />
|
||||
Create
|
||||
</Button>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
|
||||
<PageContent bind:element class="flex flex-col gap-2 p-2">
|
||||
{#each items as event (event.id)}
|
||||
<div in:fly>
|
||||
<ThreadItem {url} event={$state.snapshot(event)} />
|
||||
</div>
|
||||
{/each}
|
||||
<p class="flex h-10 items-center justify-center py-20">
|
||||
<Spinner {loading}>
|
||||
{#if loading}
|
||||
Looking for threads...
|
||||
{:else if items.length === 0}
|
||||
No threads found.
|
||||
{:else}
|
||||
That's all!
|
||||
{/if}
|
||||
</Spinner>
|
||||
</p>
|
||||
</PageContent>
|
||||
Reference in New Issue
Block a user