Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0849f7ee4c |
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
|
import {deriveRelay, publishThunk, waitForThunkError} from "@welshman/app"
|
||||||
import {makeEvent, THREAD} from "@welshman/util"
|
import {makeEvent, THREAD} from "@welshman/util"
|
||||||
import {publishThunk, waitForThunkError} from "@welshman/app"
|
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
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 Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -16,8 +17,12 @@
|
|||||||
import Modal from "@lib/components/Modal.svelte"
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import EditorContent from "@app/editor/EditorContent.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 {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 {makeEditor} from "@app/editor"
|
||||||
import {DraftKey} from "@app/util/drafts"
|
import {DraftKey} from "@app/util/drafts"
|
||||||
import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
|
import {canEnforceNip70, publishRoomQuote} from "@app/core/commands"
|
||||||
@@ -33,8 +38,11 @@
|
|||||||
shareToChat?: boolean
|
shareToChat?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, h, shareToChat = false}: Props = $props()
|
const {url, h: initialH, shareToChat = false}: Props = $props()
|
||||||
const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`)
|
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 initialValues = draftKey.get()
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
@@ -44,6 +52,20 @@
|
|||||||
|
|
||||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
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 () => {
|
const submit = async () => {
|
||||||
if ($uploading || loading) return
|
if ($uploading || loading) return
|
||||||
|
|
||||||
@@ -75,8 +97,8 @@
|
|||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (h) {
|
if (selectedH) {
|
||||||
tags.push(["h", h])
|
tags.push(["h", selectedH])
|
||||||
}
|
}
|
||||||
|
|
||||||
const threadThunk = publishThunk({
|
const threadThunk = publishThunk({
|
||||||
@@ -94,7 +116,7 @@
|
|||||||
history.back()
|
history.back()
|
||||||
|
|
||||||
if (shareToChat) {
|
if (shareToChat) {
|
||||||
publishRoomQuote({url, h, parent: threadThunk.event, protect})
|
publishRoomQuote({url, h: selectedH, parent: threadThunk.event, protect})
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
@@ -102,6 +124,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
|
let showRoomPicker = $state(false)
|
||||||
|
let selectedH: string | undefined = $state(initialH)
|
||||||
|
|
||||||
let title = $state(initialValues?.title ?? "")
|
let title = $state(initialValues?.title ?? "")
|
||||||
let content = $state(initialValues?.content ?? "")
|
let content = $state(initialValues?.content ?? "")
|
||||||
@@ -131,6 +155,53 @@
|
|||||||
<ModalSubtitle>Share a link, or start a discussion.</ModalSubtitle>
|
<ModalSubtitle>Share a link, or start a discussion.</ModalSubtitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div class="col-8 relative">
|
<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>
|
<Field>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Title*</p>
|
<p>Title*</p>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
removeFromListByPredicate,
|
removeFromListByPredicate,
|
||||||
updateList,
|
updateList,
|
||||||
getTag,
|
getTag,
|
||||||
|
getTagValue,
|
||||||
getListTags,
|
getListTags,
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
toNostrURI,
|
toNostrURI,
|
||||||
@@ -391,8 +392,16 @@ export type CommentParams = {
|
|||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeComment = ({url, event, content, tags = []}: CommentParams) =>
|
export const makeComment = ({url, event, content, tags = []}: CommentParams) => {
|
||||||
makeEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event, url)]})
|
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[]}) =>
|
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
||||||
publishThunk({event: makeComment({url: relays[0], ...params}), relays})
|
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]/chat", "Space Chat"],
|
||||||
["/spaces/[relay]/recent", "Recent Activity"],
|
["/spaces/[relay]/recent", "Recent Activity"],
|
||||||
["/spaces/[relay]/threads", "Threads"],
|
["/spaces/[relay]/threads", "Threads"],
|
||||||
|
["/spaces/[relay]/[h]/threads", "Threads"],
|
||||||
["/spaces/[relay]/classifieds", "Classifieds"],
|
["/spaces/[relay]/classifieds", "Classifieds"],
|
||||||
["/spaces/[relay]/calendar", "Calendar"],
|
["/spaces/[relay]/calendar", "Calendar"],
|
||||||
["/spaces/[relay]/goals", "Goals"],
|
["/spaces/[relay]/goals", "Goals"],
|
||||||
|
|||||||
@@ -14,11 +14,13 @@
|
|||||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||||
import InfoCircle from "@assets/icons/info-circle.svg?dataurl"
|
import InfoCircle from "@assets/icons/info-circle.svg?dataurl"
|
||||||
import Login2 from "@assets/icons/login-3.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 cx from "classnames"
|
||||||
import {fade, fly} from "@lib/transition"
|
import {fade, fly} from "@lib/transition"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
import PageContent from "@lib/components/PageContent.svelte"
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||||
@@ -430,6 +432,9 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
<SpaceSearch {url} {h} />
|
<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}>
|
<Button class="btn btn-neutral btn-sm btn-square" onclick={showRoomDetail}>
|
||||||
<Icon size={4} icon={InfoCircle} />
|
<Icon size={4} icon={InfoCircle} />
|
||||||
</Button>
|
</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