Add the ability to save events to the library
This commit is contained in:
parent
61a8c44750
commit
ff5d7aeb87
25 changed files with 207 additions and 126 deletions
|
|
@ -52,14 +52,13 @@
|
||||||
<EmojiButton {onEmoji} class="button button-neutral button-xs join-item">
|
<EmojiButton {onEmoji} class="button button-neutral button-xs join-item">
|
||||||
<Icon icon={SmileCircle} size={4} />
|
<Icon icon={SmileCircle} size={4} />
|
||||||
</EmojiButton>
|
</EmojiButton>
|
||||||
<Tippy
|
<Button onclick={showPopover} class="flex join-item button button-neutral button-xs">
|
||||||
bind:popover
|
<Tippy
|
||||||
class="flex join-item button button-neutral button-xs"
|
bind:popover
|
||||||
component={EventMenu}
|
component={EventMenu}
|
||||||
props={{url, noun, event, customActions, onClick: hidePopover}}
|
props={{url, noun, event, customActions, onClick: hidePopover}}
|
||||||
params={{trigger: "manual", interactive: true}}>
|
params={{trigger: "manual", interactive: true}}>
|
||||||
<Button onclick={showPopover}>
|
|
||||||
<Icon icon={MenuDots} size={4} />
|
<Icon icon={MenuDots} size={4} />
|
||||||
</Button>
|
</Tippy>
|
||||||
</Tippy>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import {Router} from "@welshman/router"
|
import {Router} from "@welshman/router"
|
||||||
import {LOCALE, secondsToDate} from "@welshman/lib"
|
import {LOCALE, secondsToDate} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl, toNostrURI} from "@welshman/util"
|
||||||
import {tracker} from "@welshman/app"
|
import {tracker} from "@welshman/app"
|
||||||
import FileText from "@assets/icons/file-text.svg?dataurl"
|
import FileText from "@assets/icons/file-text.svg?dataurl"
|
||||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
const {url, event}: Props = $props()
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
const relays = url ? [url] : Router.get().Event(event).getUrls()
|
const relays = url ? [url] : Router.get().Event(event).getUrls()
|
||||||
const nevent1 = nip19.neventEncode({...event, relays})
|
const nevent1 = toNostrURI(nip19.neventEncode({...event, relays}))
|
||||||
const seenOn = tracker.getRelays(event.id)
|
const seenOn = tracker.getRelays(event.id)
|
||||||
const npub1 = nip19.npubEncode(event.pubkey)
|
const npub1 = nip19.npubEncode(event.pubkey)
|
||||||
const json = JSON.stringify(event, null, 2)
|
const json = JSON.stringify(event, null, 2)
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {COMMENT, ManagementMethod} from "@welshman/util"
|
import {COMMENT, getAddress, ManagementMethod} from "@welshman/util"
|
||||||
import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app"
|
import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app"
|
||||||
|
import GalleryWide from "@assets/icons/gallery-wide.svg?dataurl"
|
||||||
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
|
|
@ -17,6 +18,7 @@
|
||||||
import Report from "@app/components/Report.svelte"
|
import Report from "@app/components/Report.svelte"
|
||||||
import EventShare from "@app/components/EventShare.svelte"
|
import EventShare from "@app/components/EventShare.svelte"
|
||||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
|
import PinboardSelect from "@app/components/PinboardSelect.svelte"
|
||||||
import {hasNip29} from "@app/relays"
|
import {hasNip29} from "@app/relays"
|
||||||
import {deriveUserIsSpaceAdmin} from "@app/members"
|
import {deriveUserIsSpaceAdmin} from "@app/members"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
@ -40,6 +42,8 @@
|
||||||
|
|
||||||
const showInfo = () => pushModal(EventInfo, {url, event})
|
const showInfo = () => pushModal(EventInfo, {url, event})
|
||||||
|
|
||||||
|
const addToLibrary = () => pushModal(PinboardSelect, {url, address: getAddress(event)})
|
||||||
|
|
||||||
const share = async () => {
|
const share = async () => {
|
||||||
if (hasNip29($relaysByUrl.get(url))) {
|
if (hasNip29($relaysByUrl.get(url))) {
|
||||||
pushModal(EventShare, {url, event})
|
pushModal(EventShare, {url, event})
|
||||||
|
|
@ -79,6 +83,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
<ul class="menu whitespace-nowrap rounded-2xl bg-surface p-2" bind:this={ul}>
|
||||||
|
<li>
|
||||||
|
<Button onclick={showInfo}>
|
||||||
|
<Icon size={4} icon={Code2} />
|
||||||
|
{noun} Details
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
{#if isRoot}
|
{#if isRoot}
|
||||||
<li>
|
<li>
|
||||||
<Button onclick={share}>
|
<Button onclick={share}>
|
||||||
|
|
@ -88,9 +98,9 @@
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
<li>
|
<li>
|
||||||
<Button onclick={showInfo}>
|
<Button onclick={addToLibrary}>
|
||||||
<Icon size={4} icon={Code2} />
|
<Icon size={4} icon={GalleryWide} />
|
||||||
{noun} Details
|
Add to Library
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{@render customActions?.()}
|
{@render customActions?.()}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
|
|
@ -14,7 +13,7 @@
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
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 RoomName from "@app/components/RoomName.svelte"
|
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||||
import {roomsByUrl} from "@app/groups"
|
import {roomsByUrl} from "@app/groups"
|
||||||
import {makeRoomPath} from "@app/routes"
|
import {makeRoomPath} from "@app/routes"
|
||||||
|
|
||||||
|
|
@ -40,13 +39,15 @@
|
||||||
<ModalTitle>Share {noun}</ModalTitle>
|
<ModalTitle>Share {noun}</ModalTitle>
|
||||||
<ModalSubtitle>Which room would you like to share this event to?</ModalSubtitle>
|
<ModalSubtitle>Which room would you like to share this event to?</ModalSubtitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
{#each $roomsByUrl.get(url) || [] as room (room.h)}
|
{#each $roomsByUrl.get(url) || [] as room (room.h)}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
class={cx(`button button-${selection === room.h ? "primary" : "neutral"}`)}
|
class="flex justify-center button {selection === room.h
|
||||||
|
? 'button-primary'
|
||||||
|
: 'button-neutral'}"
|
||||||
onclick={() => toggleRoom(room.h)}>
|
onclick={() => toggleRoom(room.h)}>
|
||||||
#<RoomName {...room} />
|
<RoomNameWithImage {...room} />
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
address: string
|
address: string
|
||||||
|
reference?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, address}: Props = $props()
|
const {url, address, reference}: Props = $props()
|
||||||
|
|
||||||
const submit = async ({title, topics, value}: PinFormValues) => {
|
const submit = async ({title, topics, value}: PinFormValues) => {
|
||||||
const params = referenceToPin(value)
|
const params = referenceToPin(value)
|
||||||
|
|
@ -18,4 +19,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PinForm {url} heading="Add Link" action="Add link" successMessage="Link added!" {submit} />
|
<PinForm
|
||||||
|
{url}
|
||||||
|
heading="Add Link"
|
||||||
|
action="Add link"
|
||||||
|
successMessage="Link added!"
|
||||||
|
values={{value: reference}}
|
||||||
|
{submit} />
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
import StringMultiInput from "@lib/components/StringMultiInput.svelte"
|
import StringMultiInput from "@lib/components/StringMultiInput.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
import {clearModals} from "@app/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -50,7 +51,7 @@
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
} else {
|
} else {
|
||||||
pushToast({message: successMessage})
|
pushToast({message: successMessage})
|
||||||
back()
|
clearModals()
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
|
|
|
||||||
59
src/app/components/PinboardSelect.svelte
Normal file
59
src/app/components/PinboardSelect.svelte
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
|
import PinAdd from "@app/components/PinAdd.svelte"
|
||||||
|
import {deriveBoards, addressToReference, type Board} from "@app/pinboards"
|
||||||
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
address: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, address}: Props = $props()
|
||||||
|
|
||||||
|
const boards = deriveBoards(url)
|
||||||
|
const reference = addressToReference(address)
|
||||||
|
|
||||||
|
const selectBoard = (board: Board) => pushModal(PinAdd, {url, address: board.address, reference})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal class="flex flex-col gap-2">
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Add to Library</ModalTitle>
|
||||||
|
<ModalSubtitle>on <RelayName {url} class="text-primary" /></ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody class="flex flex-col gap-2">
|
||||||
|
{#if $boards.length === 0}
|
||||||
|
<div class="flex flex-col items-center gap-4 py-8 text-center">
|
||||||
|
<p class="opacity-70">This space doesn't have any shelves yet.</p>
|
||||||
|
<Link href={makeSpacePath(url, "library")} class="button button-primary">
|
||||||
|
Go to Library
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#each $boards as board (board.address)}
|
||||||
|
<Button
|
||||||
|
class="card card-sm card-interactive flex flex-row items-center justify-between gap-3"
|
||||||
|
onclick={() => selectBoard(board)}>
|
||||||
|
<span class="flex min-w-0 flex-col">
|
||||||
|
<strong class="ellipsize">{board.title || "Untitled shelf"}</strong>
|
||||||
|
{#if board.description}
|
||||||
|
<span class="ellipsize text-sm opacity-70">{board.description}</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<Icon size={4} icon={AltArrowRight} class="shrink-0" />
|
||||||
|
</Button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {ManagementMethod} from "@welshman/util"
|
import {ManagementMethod, getAddress} from "@welshman/util"
|
||||||
import {pubkey, manageRelay, repository} from "@welshman/app"
|
import {pubkey, manageRelay, repository} from "@welshman/app"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
|
import GalleryWide from "@assets/icons/gallery-wide.svg?dataurl"
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
import Danger from "@assets/icons/danger.svg?dataurl"
|
import Danger from "@assets/icons/danger.svg?dataurl"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
|
@ -10,6 +11,7 @@
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import Report from "@app/components/Report.svelte"
|
import Report from "@app/components/Report.svelte"
|
||||||
|
import PinboardSelect from "@app/components/PinboardSelect.svelte"
|
||||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
@ -25,6 +27,11 @@
|
||||||
|
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
|
||||||
|
const addToLibrary = () => {
|
||||||
|
onClick()
|
||||||
|
pushModal(PinboardSelect, {url, address: getAddress(event)})
|
||||||
|
}
|
||||||
|
|
||||||
const report = () => {
|
const report = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(Report, {url, event})
|
pushModal(Report, {url, event})
|
||||||
|
|
@ -65,7 +72,13 @@
|
||||||
<li>
|
<li>
|
||||||
<Button onclick={showInfo}>
|
<Button onclick={showInfo}>
|
||||||
<Icon size={4} icon={Code2} />
|
<Icon size={4} icon={Code2} />
|
||||||
Show JSON
|
Message Details
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Button onclick={addToLibrary}>
|
||||||
|
<Icon size={4} icon={GalleryWide} />
|
||||||
|
Add to Library
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{#if event.pubkey === $pubkey}
|
{#if event.pubkey === $pubkey}
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,12 @@
|
||||||
|
|
||||||
<svelte:document onmousemove={onMouseMove} />
|
<svelte:document onmousemove={onMouseMove} />
|
||||||
|
|
||||||
<Tippy
|
<Button onclick={open} class="button button-xs button-neutral join-item">
|
||||||
bind:popover
|
<Tippy
|
||||||
component={RoomItemMenu}
|
bind:popover
|
||||||
class="button button-xs button-neutral join-item"
|
component={RoomItemMenu}
|
||||||
props={{url, event, onClick}}
|
props={{url, event, onClick}}
|
||||||
params={{trigger: "manual", interactive: true}}>
|
params={{trigger: "manual", interactive: true}}>
|
||||||
<Button onclick={open}>
|
|
||||||
<Icon icon={MenuDots} size={4} />
|
<Icon icon={MenuDots} size={4} />
|
||||||
</Button>
|
</Tippy>
|
||||||
</Tippy>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if $members.length === 0}
|
{:else if $members.length === 0}
|
||||||
<div class="card bg-surface p-4">
|
<div class="card bg-surface p-4">
|
||||||
<span class="text-muted">No members yet</span>
|
<span class="text-content-muted">No members yet</span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each $members as pubkey (pubkey)}
|
{#each $members as pubkey (pubkey)}
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,9 @@
|
||||||
const {url, h, ...props}: Props = $props()
|
const {url, h, ...props}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex grow items-center justify-between gap-4 {props.class}">
|
<div class="flex min-w-0 items-center gap-2 {props.class}">
|
||||||
<div class="flex items-center gap-2">
|
<RoomImage {url} {h} />
|
||||||
<RoomImage {url} {h} />
|
<div class="min-w-0 overflow-hidden text-ellipsis">
|
||||||
<div class="min-w-0 overflow-hidden text-ellipsis">
|
<RoomName {url} {h} />
|
||||||
<RoomName {url} {h} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
<SecondaryNavItem href={path} title={roomName} {replaceState} {notification}>
|
<SecondaryNavItem href={path} title={roomName} {replaceState} {notification}>
|
||||||
<RoomNameWithImage {url} {h} />
|
<RoomNameWithImage {url} {h} />
|
||||||
{#if showDifferenceIcon}
|
{#if showDifferenceIcon}
|
||||||
<Icon icon={$shouldNotifyForRoom ? Bell : BellOff} size={4} class="opacity-50" />
|
<Icon icon={$shouldNotifyForRoom ? Bell : BellOff} size={4} class="ml-auto opacity-50" />
|
||||||
{/if}
|
{/if}
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||||
import ThreadBoardItem from "@app/components/ThreadBoardItem.svelte"
|
import ThreadBoardItem from "@app/components/ThreadBoardItem.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -13,27 +13,28 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="card card-flat overflow-hidden pb-4 p-0">
|
<section class="card card-flat overflow-hidden pb-4 p-0">
|
||||||
<header class="flex gap-2 justify-between bg-surface px-4 py-2.5">
|
<header
|
||||||
<h2 class="text-sm font-bold">
|
class="flex gap-2 items-center justify-between px-4 py-3 border-b border-line border-solid">
|
||||||
|
<h2 class="text-lg">
|
||||||
{#if h}
|
{#if h}
|
||||||
#<RoomName {url} {h} />
|
<RoomNameWithImage {url} {h} />
|
||||||
{:else}
|
{:else}
|
||||||
General
|
General
|
||||||
{/if}
|
{/if}
|
||||||
</h2>
|
</h2>
|
||||||
<span class="text-xs text-muted">
|
<span class="text-content-muted text-sm">
|
||||||
{threads.length}
|
{threads.length}
|
||||||
{threads.length === 1 ? "topic" : "topics"}
|
{threads.length === 1 ? "Topic" : "Topics"}
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
<table class="w-full border-collapse">
|
<table class="w-full border-collapse">
|
||||||
<thead
|
<thead
|
||||||
class="hidden text-xs font-bold uppercase tracking-wide text-muted sm:table-header-group">
|
class="hidden text-xs font-bold uppercase tracking-wide text-content-muted sm:table-header-group border-b border-line border-solid bg-surface-less">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-2 text-left">Topic</th>
|
<th class="px-4 py-3 text-left">Topic</th>
|
||||||
<th class="w-32 px-4 py-2 text-left">Author</th>
|
<th class="w-32 px-4 py-3 text-left">Author</th>
|
||||||
<th class="w-20 px-4 py-2 text-center">Replies</th>
|
<th class="w-20 px-4 py-3 text-center">Replies</th>
|
||||||
<th class="w-32 px-4 py-2 text-right">Last post</th>
|
<th class="w-32 px-4 py-3 text-right">Last post</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {COMMENT, getTagValue} from "@welshman/util"
|
import {COMMENT, getTagValue} from "@welshman/util"
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import {deriveEventsForUrl} from "@app/repository"
|
import {deriveEventsForUrl} from "@app/repository"
|
||||||
|
|
@ -25,21 +24,19 @@
|
||||||
const goToThread = () => goto(path)
|
const goToThread = () => goto(path)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<tr class="cursor-pointer transition-colors hover:bg-surface text-sm" onclick={goToThread}>
|
<tr
|
||||||
<td class="px-4 py-2 align-top">
|
class="cursor-pointer transition-colors hover:bg-surface-less text-sm border-b border-solid border-line"
|
||||||
<Link href={path} class="ellipsize font-semibold">
|
onclick={goToThread}>
|
||||||
{title || "Untitled thread"}
|
<td class="px-4 py-3 align-top ellipsize">
|
||||||
</Link>
|
{title || "Untitled thread"}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 align-middle">
|
<td class="px-4 py-3 align-middle flex items-center gap-2">
|
||||||
<div class="flex items-center gap-2">
|
<ProfileCircle pubkey={event.pubkey} {url} size={5} />
|
||||||
<ProfileCircle pubkey={event.pubkey} {url} size={5} />
|
<span class="ellipsize">
|
||||||
<span class="ellipsize">
|
<ProfileName pubkey={event.pubkey} {url} />
|
||||||
<ProfileName pubkey={event.pubkey} {url} />
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 align-middle text-right">
|
<td class="px-4 py-3 align-middle text-right">
|
||||||
{replyCount}
|
{replyCount}
|
||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap px-4 py-2 align-middle text-right">
|
<td class="whitespace-nowrap px-4 py-2 align-middle text-right">
|
||||||
|
|
|
||||||
|
|
@ -138,9 +138,9 @@
|
||||||
{:else if $callState === CallState.Connected}
|
{:else if $callState === CallState.Connected}
|
||||||
<span class="text-sm font-semibold text-success">Voice Connected</span>
|
<span class="text-sm font-semibold text-success">Voice Connected</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-sm font-semibold text-muted">Disconnected</span>
|
<span class="text-sm font-semibold text-content-muted">Disconnected</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="ellipsize text-xs text-muted">
|
<span class="ellipsize text-xs text-content-muted">
|
||||||
{roomName} / {spaceName}
|
{roomName} / {spaceName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import {
|
||||||
getTagValue,
|
getTagValue,
|
||||||
matchFilters,
|
matchFilters,
|
||||||
readList,
|
readList,
|
||||||
sortEventsDesc,
|
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {Filter, PublishedList, TrustedEvent} from "@welshman/util"
|
import type {Filter, PublishedList, TrustedEvent} from "@welshman/util"
|
||||||
import {load, mergeRepositoryUpdates, request} from "@welshman/net"
|
import {load, mergeRepositoryUpdates, request} from "@welshman/net"
|
||||||
|
|
@ -124,13 +123,8 @@ export const makeFeed = ({
|
||||||
const events = writable<TrustedEvent[]>([])
|
const events = writable<TrustedEvent[]>([])
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
|
|
||||||
let interval = int(MONTH)
|
let interval = int(6, MONTH)
|
||||||
let buffer = sortEventsDesc(
|
let buffer: TrustedEvent[] = []
|
||||||
uniqBy(
|
|
||||||
e => e.id,
|
|
||||||
relays.flatMap(url => Array.from(getEventsForUrl(url, filters))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
let backwardWindow = [at - interval, at]
|
let backwardWindow = [at - interval, at]
|
||||||
let forwardWindow = [at, at + interval]
|
let forwardWindow = [at, at + interval]
|
||||||
|
|
||||||
|
|
@ -145,7 +139,7 @@ export const makeFeed = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch-insert events into the visible store with a single update
|
// Batch-insert events into the visible store with a single update
|
||||||
const insertEvents = (newEvents: TrustedEvent[]) => {
|
const insertEvents = (newEvents: Iterable<TrustedEvent>) => {
|
||||||
const visible: TrustedEvent[] = []
|
const visible: TrustedEvent[] = []
|
||||||
|
|
||||||
for (const event of newEvents) {
|
for (const event of newEvents) {
|
||||||
|
|
@ -153,10 +147,9 @@ export const makeFeed = ({
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
seen.add(event.id)
|
|
||||||
|
|
||||||
if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) {
|
if (between([backwardWindow[0], forwardWindow[1]], event.created_at)) {
|
||||||
visible.push(event)
|
visible.push(event)
|
||||||
|
seen.add(event.id)
|
||||||
} else {
|
} else {
|
||||||
insertIntoBuffer(event)
|
insertIntoBuffer(event)
|
||||||
}
|
}
|
||||||
|
|
@ -274,6 +267,10 @@ export const makeFeed = ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (const url of relays) {
|
||||||
|
insertEvents(getEventsForUrl(url, filters))
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
events,
|
events,
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
|
|
|
||||||
|
|
@ -166,8 +166,8 @@ export const deleteBoard = (url: string, address: string) =>
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const pinToReference = (pin: Pin): string => {
|
const valueToReference = (value: string[]): string => {
|
||||||
const [type, data = "", relay] = pin.value
|
const [type, data = "", relay] = value
|
||||||
const relays = relay ? [relay] : []
|
const relays = relay ? [relay] : []
|
||||||
|
|
||||||
if (type === "e") return nip19.neventEncode({id: data, relays})
|
if (type === "e") return nip19.neventEncode({id: data, relays})
|
||||||
|
|
@ -181,6 +181,10 @@ export const pinToReference = (pin: Pin): string => {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const pinToReference = (pin: Pin): string => valueToReference(pin.value)
|
||||||
|
|
||||||
|
export const addressToReference = (address: string): string => valueToReference(["a", address])
|
||||||
|
|
||||||
export const referenceToPin = (reference: string): Partial<Pin> | undefined => {
|
export const referenceToPin = (reference: string): Partial<Pin> | undefined => {
|
||||||
const trimmed = reference.trim()
|
const trimmed = reference.trim()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
const {children, ...props}: Props = $props()
|
const {children, ...props}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 p-2 text-xs uppercase text-subtle">
|
<div class="flex items-center gap-2 p-2 text-xs uppercase text-content-subtle">
|
||||||
<div class="h-px grow" style="background-color: var(--line)"></div>
|
<div class="h-px grow" style="background-color: var(--line)"></div>
|
||||||
{#if children}
|
{#if children}
|
||||||
<p>{@render children?.()}</p>
|
<p>{@render children?.()}</p>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if props.info}
|
{#if props.info}
|
||||||
<p class="text-sm text-muted">
|
<p class="text-sm text-content-muted">
|
||||||
{@render props.info()}
|
{@render props.info()}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
ondrop={stopPropagation(preventDefault(onDrop))}>
|
ondrop={stopPropagation(preventDefault(onDrop))}>
|
||||||
<div class="flex flex-col items-center gap-2 text-center">
|
<div class="flex flex-col items-center gap-2 text-center">
|
||||||
<Icon icon={GallerySend} size={8} />
|
<Icon icon={GallerySend} size={8} />
|
||||||
<p class="text-muted text-sm">Drag and drop images or click to select</p>
|
<p class="text-content-muted text-sm">Drag and drop images or click to select</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,4 @@
|
||||||
const {children}: Props = $props()
|
const {children}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p class="text-muted text-center text-sm">{@render children()}</p>
|
<p class="text-content-muted text-center text-sm">{@render children()}</p>
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@
|
||||||
.input:focus-within,
|
.input:focus-within,
|
||||||
.tiptap:focus,
|
.tiptap:focus,
|
||||||
.tiptap:focus-within {
|
.tiptap:focus-within {
|
||||||
@apply outline-none;
|
@apply border-primary outline-none;
|
||||||
border: 1px solid var(--primary) !important;
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 4px color-mix(in srgb, var(--primary) 18%, transparent),
|
0 0 0 4px color-mix(in srgb, var(--primary) 18%, transparent),
|
||||||
inset 0 2px 4px rgba(0, 0, 0, 0.04);
|
inset 0 2px 4px rgba(0, 0, 0, 0.04);
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@
|
||||||
onclick={() => openSpace(url)}>
|
onclick={() => openSpace(url)}>
|
||||||
<div class="flex w-full items-start gap-2">
|
<div class="flex w-full items-start gap-2">
|
||||||
<div
|
<div
|
||||||
class="mt-4 flex cursor-grab p-1 text-subtle transition-colors group-hover:text-muted">
|
class="mt-4 flex cursor-grab p-1 text-content-subtle transition-colors group-hover:text-content-muted">
|
||||||
<Icon icon={DragHandle} />
|
<Icon icon={DragHandle} />
|
||||||
</div>
|
</div>
|
||||||
<RelaySummary hideFavorites {url} />
|
<RelaySummary hideFavorites {url} />
|
||||||
|
|
|
||||||
|
|
@ -128,23 +128,21 @@
|
||||||
</SpaceBar>
|
</SpaceBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col gap-4 p-4">
|
<PageContent class="flex flex-col gap-4 p-4">
|
||||||
<div class="card flex flex-col gap-2">
|
<label class="input input-group flex w-full items-center gap-2 card">
|
||||||
<label class="input input-sm input-group flex w-full items-center gap-2">
|
<Icon size={4} icon={Magnifier} />
|
||||||
<Icon size={4} icon={Magnifier} />
|
<input
|
||||||
<input
|
bind:value={term}
|
||||||
bind:value={term}
|
class="min-w-0 grow"
|
||||||
class="min-w-0 grow"
|
type="text"
|
||||||
type="text"
|
placeholder="Search people or roles..." />
|
||||||
placeholder="Search people or roles..." />
|
</label>
|
||||||
</label>
|
{#if visibleMembers.length === 0}
|
||||||
{#if visibleMembers.length === 0}
|
<p class="flex flex-col items-center py-20 text-center">No members found.</p>
|
||||||
<p class="flex flex-col items-center py-20 text-center">No members found.</p>
|
{:else}
|
||||||
{:else}
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
<div class="grid grid-cols-1 gap-2 lg:grid-cols-2 xl:grid-cols-3">
|
{#each visibleMembers as { pubkey, roleList } (pubkey)}
|
||||||
{#each visibleMembers as { pubkey, roleList } (pubkey)}
|
<SpaceMember {url} {pubkey} roles={roleList} />
|
||||||
<SpaceMember {url} {pubkey} roles={roleList} />
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|
|
||||||
|
|
@ -86,22 +86,20 @@
|
||||||
</SpaceBar>
|
</SpaceBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col gap-4 p-4">
|
<PageContent class="flex flex-col gap-4 p-4">
|
||||||
<div class="card flex flex-col gap-2">
|
<label class="card input input-group flex w-full items-center gap-2">
|
||||||
<label class="input input-sm input-group flex w-full items-center gap-2">
|
<Icon size={4} icon={Magnifier} />
|
||||||
<Icon size={4} icon={Magnifier} />
|
<input bind:value={term} class="min-w-0 grow" type="text" placeholder="Search library..." />
|
||||||
<input bind:value={term} class="min-w-0 grow" type="text" placeholder="Search library..." />
|
</label>
|
||||||
</label>
|
<Masonry items={filtered} getKey={board => board.address} columnWidth={80} gap={4}>
|
||||||
<Masonry items={filtered} getKey={board => board.address} columnWidth={80} gap={2}>
|
{#snippet child(board)}
|
||||||
{#snippet child(board)}
|
<PinboardItem {url} {board} />
|
||||||
<PinboardItem {url} {board} />
|
{/snippet}
|
||||||
{/snippet}
|
</Masonry>
|
||||||
</Masonry>
|
{#if loading}
|
||||||
{#if loading}
|
<p class="flex items-center justify-center py-20">
|
||||||
<p class="flex items-center justify-center py-20">
|
<Spinner loading>Loading library...</Spinner>
|
||||||
<Spinner loading>Loading library...</Spinner>
|
</p>
|
||||||
</p>
|
{:else if filtered.length === 0}
|
||||||
{:else if filtered.length === 0}
|
<p class="flex flex-col items-center py-20 text-center">No shelves found.</p>
|
||||||
<p class="flex flex-col items-center py-20 text-center">No shelves found.</p>
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue