Re-work space navigation #223
This commit is contained in:
@@ -1,31 +1,33 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.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 CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
|
||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||
import {makeCalendarPath} from "@app/util/routes"
|
||||
import {makeCalendarPath, makeSpacePath} from "@app/util/routes"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||
|
||||
const {
|
||||
url,
|
||||
event,
|
||||
showActivity = false,
|
||||
}: {
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
showRoom?: boolean
|
||||
showActivity?: boolean
|
||||
} = $props()
|
||||
}
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
const {url, event, showRoom, showActivity}: Props = $props()
|
||||
|
||||
const room = getTagValue("h", event.tags)
|
||||
const path = makeCalendarPath(url, event.id)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
|
||||
|
||||
@@ -36,24 +38,27 @@
|
||||
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Event">
|
||||
{#snippet customActions()}
|
||||
{#if event.pubkey === $pubkey}
|
||||
<li>
|
||||
<Button onclick={editEvent}>
|
||||
<Icon size={4} icon={Pen2} />
|
||||
Edit Event
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EventActions>
|
||||
</div>
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
{#if room && showRoom}
|
||||
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<ChannelName {room} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Event">
|
||||
{#snippet customActions()}
|
||||
{#if event.pubkey === $pubkey}
|
||||
<li>
|
||||
<Button onclick={editEvent}>
|
||||
<Icon size={4} icon={Pen2} />
|
||||
Edit Event
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EventActions>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
}
|
||||
|
||||
const {url}: Props = $props()
|
||||
const {url, room}: Props = $props()
|
||||
</script>
|
||||
|
||||
<CalendarEventForm {url}>
|
||||
<CalendarEventForm {url} {room}>
|
||||
{#snippet header()}
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
|
||||
@@ -8,13 +8,16 @@
|
||||
|
||||
const {event}: Props = $props()
|
||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||
const startDate = $derived(secondsToDate(parseInt(meta.start)))
|
||||
const start = $derived(parseInt(meta.start))
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="hidden h-32 w-32 min-w-32 flex-col items-center justify-center gap-1 rounded-box bg-base-300 p-2 sm:flex">
|
||||
<strong>{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</strong>
|
||||
<span class="text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
|
||||
<span class="text-xs opacity-75"
|
||||
>{Intl.DateTimeFormat(LOCALE, {weekday: "long"}).format(startDate)}</span>
|
||||
</div>
|
||||
{#if !isNaN(start)}
|
||||
{@const startDate = secondsToDate(start)}
|
||||
<div
|
||||
class="hidden h-32 w-32 min-w-32 flex-col items-center justify-center gap-1 rounded-box bg-base-300 p-2 sm:flex">
|
||||
<strong>{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</strong>
|
||||
<span class="text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
|
||||
<span class="text-xs opacity-75"
|
||||
>{Intl.DateTimeFormat(LOCALE, {weekday: "long"}).format(startDate)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
header: Snippet
|
||||
initialValues?: {
|
||||
d: string
|
||||
@@ -34,7 +35,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const {url, header, initialValues}: Props = $props()
|
||||
const {url, room, header, initialValues}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -84,6 +85,10 @@
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (room) {
|
||||
tags.push(["h", room])
|
||||
}
|
||||
|
||||
const event = makeEvent(EVENT_TIME, {content, tags})
|
||||
|
||||
pushToast({message: "Your event has been saved!"})
|
||||
|
||||
@@ -17,18 +17,20 @@
|
||||
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>
|
||||
|
||||
<div class="flex flex-grow flex-wrap justify-between gap-2">
|
||||
<p class="text-xl">{meta.title || meta.name}</p>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Icon icon={ClockCircle} size={4} />
|
||||
<span class="sm:hidden">{formatTimestampAsDate(start)}</span>
|
||||
{formatTimestampAsTime(start)} — {isSingleDay
|
||||
? formatTimestampAsTime(end)
|
||||
: formatTimestamp(end)}
|
||||
</div>
|
||||
{#if !isNaN(start) && !isNaN(end)}
|
||||
{@const startDateDisplay = formatTimestampAsDate(start)}
|
||||
{@const endDateDisplay = formatTimestampAsDate(end)}
|
||||
{@const isSingleDay = startDateDisplay === endDateDisplay}
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Icon icon={ClockCircle} size={4} />
|
||||
<span class="hidden sm:block">{formatTimestampAsDate(start)}</span>
|
||||
{formatTimestampAsTime(start)} — {isSingleDay
|
||||
? formatTimestampAsTime(end)
|
||||
: formatTimestamp(end)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
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 ChannelLink from "@app/components/ChannelLink.svelte"
|
||||
import {makeCalendarPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -12,6 +14,8 @@
|
||||
}
|
||||
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const room = getTagValue("h", event.tags)
|
||||
</script>
|
||||
|
||||
<Link class="col-3 card2 bg-alt w-full cursor-pointer" href={makeCalendarPath(url, event.id)}>
|
||||
@@ -19,6 +23,9 @@
|
||||
<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 room}
|
||||
in <ChannelLink {url} {room} />
|
||||
{/if}
|
||||
</span>
|
||||
<CalendarEventActions showActivity {url} {event} />
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
<script lang="ts">
|
||||
import type {Instance} from "tippy.js"
|
||||
import {writable} from "svelte/store"
|
||||
import type {EventContent} from "@welshman/util"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||
import WidgetAdd from "@assets/icons/widget-add.svg?dataurl"
|
||||
import Plane from "@assets/icons/plane-2.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import ComposeMenu from "@app/components/ComposeMenu.svelte"
|
||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {onDestroy, onMount} from "svelte"
|
||||
|
||||
type Props = {
|
||||
url?: string
|
||||
room?: string
|
||||
content?: string
|
||||
onEditPrevious?: () => void
|
||||
onSubmit: (event: EventContent) => void
|
||||
}
|
||||
|
||||
const {content, onEditPrevious, onSubmit, url}: Props = $props()
|
||||
const {url, room, content, onEditPrevious, onSubmit}: Props = $props()
|
||||
|
||||
const autofocus = !isMobile
|
||||
|
||||
@@ -36,6 +41,10 @@
|
||||
|
||||
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||
|
||||
const showPopover = () => popover?.show()
|
||||
|
||||
const hidePopover = () => popover?.hide()
|
||||
|
||||
const submit = async () => {
|
||||
if ($uploading) return
|
||||
|
||||
@@ -50,7 +59,9 @@
|
||||
ed.chain().clearContent().run()
|
||||
}
|
||||
|
||||
const editor = makeEditor({url, autofocus, content, submit, uploading, aggressive: true})
|
||||
const editor = makeEditor({url, content, autofocus, submit, uploading, aggressive: true})
|
||||
|
||||
let popover: Instance | undefined = $state()
|
||||
|
||||
onMount(async () => {
|
||||
const ed = await editor
|
||||
@@ -64,17 +75,32 @@
|
||||
</script>
|
||||
|
||||
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||
<Button
|
||||
data-tip="Add an image"
|
||||
class="center tooltip tooltip-right h-10 w-10 min-w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
|
||||
disabled={$uploading}
|
||||
onclick={uploadFiles}>
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Icon icon={GallerySend} />
|
||||
{/if}
|
||||
</Button>
|
||||
<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"
|
||||
disabled={$uploading}
|
||||
onclick={uploadFiles}>
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Icon icon={GallerySend} />
|
||||
{/if}
|
||||
</Button>
|
||||
<Tippy
|
||||
bind:popover
|
||||
component={ComposeMenu}
|
||||
props={{url, room, 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"
|
||||
disabled={$uploading}
|
||||
onclick={showPopover}>
|
||||
<Icon icon={WidgetAdd} />
|
||||
</Button>
|
||||
</Tippy>
|
||||
</div>
|
||||
<div class="chat-editor flex-grow overflow-hidden">
|
||||
<EditorContent {editor} />
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
||||
|
||||
const {
|
||||
verb,
|
||||
@@ -19,16 +19,11 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
|
||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
|
||||
transition:slide>
|
||||
<p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||
<p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||
{#key event.id}
|
||||
<NoteContent
|
||||
{event}
|
||||
hideMediaAtDepth={0}
|
||||
minLength={100}
|
||||
maxLength={300}
|
||||
expandMode="disabled" />
|
||||
<NoteContentMinimal trimParent {event} />
|
||||
{/key}
|
||||
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||
<Icon icon={CloseCircle} />
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
<script lang="ts">
|
||||
import {hash, now, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
|
||||
import cx from "classnames"
|
||||
import {hash, now, displayList, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {thunks, deriveProfile, deriveProfileDisplay} from "@welshman/app"
|
||||
import {MESSAGE, COMMENT} from "@welshman/util"
|
||||
import {
|
||||
thunks,
|
||||
pubkey,
|
||||
deriveProfile,
|
||||
deriveProfileDisplay,
|
||||
displayProfileByPubkey,
|
||||
} from "@welshman/app"
|
||||
import {isMobile} from "@lib/html"
|
||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import ReplyAlt from "@assets/icons/reply.svg?dataurl"
|
||||
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ChannelMessageZapButton from "@app/components/ChannelMessageZapButton.svelte"
|
||||
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
|
||||
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
|
||||
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte"
|
||||
import {colors, ENABLE_ZAPS} from "@app/core/state"
|
||||
import ChannelItemZapButton from "@app/components/ChannelItemZapButton.svelte"
|
||||
import ChannelItemEmojiButton from "@app/components/ChannelItemEmojiButton.svelte"
|
||||
import ChannelItemMenuButton from "@app/components/ChannelItemMenuButton.svelte"
|
||||
import ChannelItemMenuMobile from "@app/components/ChannelItemMenuMobile.svelte"
|
||||
import ChannelItemContent from "@app/components/ChannelItemContent.svelte"
|
||||
import {colors, ENABLE_ZAPS, deriveEventsForUrl} from "@app/core/state"
|
||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||
import {getChannelItemPath} from "@app/util/routes"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
interface Props {
|
||||
@@ -42,16 +53,18 @@
|
||||
}: Props = $props()
|
||||
|
||||
const thunk = $thunks[event.id]
|
||||
const path = getChannelItemPath(url, event)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
const today = formatTimestampAsDate(now())
|
||||
const profile = deriveProfile(event.pubkey, [url])
|
||||
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
||||
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
||||
const comments = deriveEventsForUrl(url, [{kinds: [COMMENT], "#e": [event.id]}])
|
||||
|
||||
const reply = () => replyTo!(event)
|
||||
const edit = canEdit(event) ? () => onEdit(event) : undefined
|
||||
|
||||
const onTap = () => pushModal(ChannelMessageMenuMobile, {url, event, reply, edit})
|
||||
const onTap = () => pushModal(ChannelItemMenuMobile, {url, event, reply, edit})
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
||||
|
||||
@@ -65,7 +78,7 @@
|
||||
<TapTarget
|
||||
data-event={event.id}
|
||||
onTap={inert ? null : onTap}
|
||||
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left">
|
||||
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left hover:bg-base-100/50">
|
||||
<div class="flex w-full gap-3 overflow-auto">
|
||||
{#if showPubkey}
|
||||
<Button onclick={openProfile} class="flex items-start">
|
||||
@@ -90,10 +103,10 @@
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-sm">
|
||||
<Content minimalQuote {event} {url} />
|
||||
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
|
||||
<ChannelItemContent {url} {event} />
|
||||
{#if thunk}
|
||||
<ThunkFailure showToastOnRetry {thunk} class="mt-2" />
|
||||
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,15 +118,32 @@
|
||||
{deleteReaction}
|
||||
{createReaction}
|
||||
reactionClass="tooltip-right" />
|
||||
{#if path && $comments.length > 0}
|
||||
{@const pubkeys = $comments.map(e => e.pubkey)}
|
||||
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
|
||||
{@const tooltip = `${info} commented`}
|
||||
<div data-tip={tooltip} class="tooltip tooltip-right flex">
|
||||
<Link
|
||||
href={path}
|
||||
class={cx("btn btn-xs gap-1 rounded-full", {
|
||||
"btn-neutral": !isOwn,
|
||||
"btn-primary": isOwn,
|
||||
})}>
|
||||
<Icon icon={ReplyAlt} />
|
||||
<span>{$comments.length} comment{$comments.length === 1 ? "" : "s"}</span>
|
||||
</Link>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !isMobile}
|
||||
<button
|
||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||
class:group-hover:opacity-100={!isMobile}>
|
||||
{#if ENABLE_ZAPS}
|
||||
<ChannelMessageZapButton {url} {event} />
|
||||
<ChannelItemZapButton {url} {event} />
|
||||
{/if}
|
||||
<ChannelMessageEmojiButton {url} {event} />
|
||||
<ChannelItemEmojiButton {url} {event} />
|
||||
{#if replyTo}
|
||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||
<Icon icon={Reply} size={4} />
|
||||
@@ -124,7 +154,7 @@
|
||||
<Icon icon={Pen} size={4} />
|
||||
</Button>
|
||||
{/if}
|
||||
<ChannelMessageMenuButton {url} {event} />
|
||||
<ChannelItemMenuButton {url} {event} />
|
||||
</button>
|
||||
{/if}
|
||||
</TapTarget>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import type {ComponentProps} from "svelte"
|
||||
import {MESSAGE} from "@welshman/util"
|
||||
import {isMobile} from "@lib/html"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import {getChannelItemPath} from "@app/util/routes"
|
||||
|
||||
const props: ComponentProps<typeof NoteContent> = $props()
|
||||
|
||||
const path = getChannelItemPath(props.url!, props.event)
|
||||
</script>
|
||||
|
||||
<div class={cx("text-sm", {"card2 card2-sm bg-alt": props.event.kind !== MESSAGE})}>
|
||||
{#if path && !isMobile}
|
||||
<Link href={path}>
|
||||
<NoteContent {...props} />
|
||||
</Link>
|
||||
{:else}
|
||||
<NoteContent {...props} />
|
||||
{/if}
|
||||
</div>
|
||||
+12
-5
@@ -1,16 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import Danger from "@assets/icons/danger.svg?dataurl"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import EventReport from "@app/components/EventReport.svelte"
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import Danger from "@assets/icons/danger.svg?dataurl"
|
||||
|
||||
const {url, event, onClick} = $props()
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const {url, event, onClick}: Props = $props()
|
||||
|
||||
const report = () => {
|
||||
onClick()
|
||||
@@ -32,7 +39,7 @@
|
||||
<li>
|
||||
<Button onclick={showInfo}>
|
||||
<Icon size={4} icon={Code2} />
|
||||
Message Details
|
||||
Show JSON
|
||||
</Button>
|
||||
</li>
|
||||
{#if event.pubkey === $pubkey}
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import ChannelMessageMenu from "@app/components/ChannelMessageMenu.svelte"
|
||||
import ChannelItemMenu from "@app/components/ChannelItemMenu.svelte"
|
||||
|
||||
const {url, event} = $props()
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</Button>
|
||||
<Tippy
|
||||
bind:popover
|
||||
component={ChannelMessageMenu}
|
||||
component={ChannelItemMenu}
|
||||
props={{url, event, onClick}}
|
||||
params={{trigger: "manual", interactive: true}} />
|
||||
</div>
|
||||
+36
-25
@@ -2,7 +2,14 @@
|
||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||
import ZapButton from "@app/components/ZapButton.svelte"
|
||||
@@ -10,12 +17,8 @@
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import {ENABLE_ZAPS} from "@app/core/state"
|
||||
import {publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||
import {getChannelItemPath} from "@app/util/routes"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
@@ -25,6 +28,8 @@
|
||||
|
||||
const {url, event, reply}: Props = $props()
|
||||
|
||||
const path = getChannelItemPath(url, event)
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
const onEmoji = (async (event: TrustedEvent, url: string, emoji: NativeEmoji) => {
|
||||
@@ -49,29 +54,35 @@
|
||||
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||
</script>
|
||||
|
||||
<div class="col-2">
|
||||
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
||||
<Icon size={4} icon={SmileCircle} />
|
||||
Send Reaction
|
||||
</Button>
|
||||
{#if ENABLE_ZAPS}
|
||||
<ZapButton replaceState {url} {event} class="btn btn-secondary w-full">
|
||||
<Icon size={4} icon={Bolt} />
|
||||
Send Zap
|
||||
</ZapButton>
|
||||
{/if}
|
||||
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
||||
<Icon size={4} icon={Reply} />
|
||||
Send Reply
|
||||
</Button>
|
||||
<Button class="btn btn-neutral" onclick={showInfo}>
|
||||
<Icon size={4} icon={Code2} />
|
||||
Message Details
|
||||
</Button>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if event.pubkey === $pubkey}
|
||||
<Button class="btn btn-neutral text-error" onclick={showDelete}>
|
||||
<Icon size={4} icon={TrashBin2} />
|
||||
Delete Message
|
||||
Delete
|
||||
</Button>
|
||||
{/if}
|
||||
<Button class="btn btn-neutral" onclick={showInfo}>
|
||||
<Icon size={4} icon={Code2} />
|
||||
Show JSON
|
||||
</Button>
|
||||
{#if path}
|
||||
<Link class="btn btn-neutral" href={path}>
|
||||
<Icon size={4} icon={MenuDots} />
|
||||
View Details
|
||||
</Link>
|
||||
{/if}
|
||||
<Button class="btn btn-outline btn-neutral w-full" onclick={sendReply}>
|
||||
<Icon size={4} icon={Reply} />
|
||||
Reply
|
||||
</Button>
|
||||
<Button class="btn btn-secondary w-full" onclick={showEmojiPicker}>
|
||||
<Icon size={4} icon={SmileCircle} />
|
||||
React
|
||||
</Button>
|
||||
{#if ENABLE_ZAPS}
|
||||
<ZapButton replaceState {url} {event} class="btn btn-primary w-full">
|
||||
<Icon size={4} icon={Bolt} />
|
||||
Zap
|
||||
</ZapButton>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
room: string
|
||||
url: string
|
||||
class?: string
|
||||
unstyled?: boolean
|
||||
}
|
||||
|
||||
const {room, url, unstyled, ...props}: Props = $props()
|
||||
|
||||
const path = makeSpacePath(url, room)
|
||||
</script>
|
||||
|
||||
<Link href={path} class={cx(props.class, {"link-content bg-alt": !unstyled})}>
|
||||
#<ChannelName {room} {url} />
|
||||
</Link>
|
||||
@@ -5,7 +5,7 @@
|
||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
||||
|
||||
const {
|
||||
verb,
|
||||
@@ -19,16 +19,11 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
|
||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
|
||||
transition:slide>
|
||||
<p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||
<p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||
{#key event.id}
|
||||
<NoteContent
|
||||
{event}
|
||||
hideMediaAtDepth={0}
|
||||
minLength={100}
|
||||
maxLength={300}
|
||||
expandMode="disabled" />
|
||||
<NoteContentMinimal trimParent {event} />
|
||||
{/key}
|
||||
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||
<Icon icon={CloseCircle} />
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
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 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 GoalCreate from "@app/components/GoalCreate.svelte"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
onClick: () => void
|
||||
room?: string
|
||||
}
|
||||
|
||||
const {url, room, onClick}: Props = $props()
|
||||
|
||||
const createGoal = () => pushModal(GoalCreate, {url, room})
|
||||
|
||||
const createCalendarEvent = () => pushModal(CalendarEventCreate, {url, room})
|
||||
|
||||
const createThread = () => pushModal(ThreadCreate, {url, room})
|
||||
|
||||
let ul: Element
|
||||
|
||||
onMount(() => {
|
||||
ul.addEventListener("click", onClick)
|
||||
})
|
||||
</script>
|
||||
|
||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl" bind:this={ul}>
|
||||
<li>
|
||||
<Button onclick={createGoal}>
|
||||
<Icon size={4} icon={StarFallMinimalistic} />
|
||||
Create Funding Goal
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button onclick={createCalendarEvent}>
|
||||
<Icon size={4} icon={CalendarMinimalistic} />
|
||||
Create Calendar Event
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button onclick={createThread}>
|
||||
<Icon size={4} icon={NotesMinimalistic} />
|
||||
Create Thread
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -18,6 +18,7 @@
|
||||
isAddress,
|
||||
isNewline,
|
||||
} from "@welshman/content"
|
||||
import type {Parsed} from "@welshman/content"
|
||||
import {preventDefault, stopPropagation} from "@lib/html"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
@@ -39,10 +40,8 @@
|
||||
minLength?: number
|
||||
maxLength?: number
|
||||
showEntire?: boolean
|
||||
hideMediaAtDepth?: number
|
||||
expandMode?: string
|
||||
minimalQuote?: boolean
|
||||
depth?: number
|
||||
trimParent?: boolean
|
||||
url?: string
|
||||
}
|
||||
|
||||
@@ -51,10 +50,8 @@
|
||||
minLength = 500,
|
||||
maxLength = 700,
|
||||
showEntire = $bindable(false),
|
||||
hideMediaAtDepth = 1,
|
||||
expandMode = "block",
|
||||
minimalQuote = false,
|
||||
depth = 0,
|
||||
trimParent = false,
|
||||
url,
|
||||
}: Props = $props()
|
||||
|
||||
@@ -67,13 +64,13 @@
|
||||
const isBlock = (i: number) => {
|
||||
const parsed = fullContent[i]
|
||||
|
||||
if (!parsed || hideMediaAtDepth <= depth) return false
|
||||
if (!parsed) return false
|
||||
|
||||
if (isLink(parsed) && $userSettingsValues.show_media && isStartAndEnd(i)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if ((isEvent(parsed) || isAddress(parsed)) && isStartAndEnd(i)) {
|
||||
if (isQuote(parsed) && isStartAndEnd(i)) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -95,6 +92,8 @@
|
||||
|
||||
const isStartAndEnd = (i: number) => isStart(i) && isEnd(i)
|
||||
|
||||
const isQuote = (p: Parsed) => isEvent(p) || isAddress(p)
|
||||
|
||||
const ignoreWarning = () => {
|
||||
warning = null
|
||||
}
|
||||
@@ -103,15 +102,37 @@
|
||||
$userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
|
||||
)
|
||||
|
||||
const shortContent = $derived(
|
||||
showEntire
|
||||
? fullContent
|
||||
: truncate(fullContent, {
|
||||
minLength,
|
||||
maxLength,
|
||||
mediaLength: hideMediaAtDepth <= depth ? 20 : 200,
|
||||
}),
|
||||
)
|
||||
const dropWhile = <T,>(f: (x: T) => boolean, xs: Iterable<T>) => {
|
||||
const result: T[] = []
|
||||
|
||||
for (const x of xs) {
|
||||
if (result.length === 0 && f(x)) {
|
||||
continue
|
||||
}
|
||||
|
||||
result.push(x)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const shortContent = $derived.by(() => {
|
||||
let result = fullContent
|
||||
|
||||
if (trimParent && result.length > 0 && isQuote(result[0])) {
|
||||
result = dropWhile(p => isQuote(p) || isNewline(p), result)
|
||||
}
|
||||
|
||||
if (!showEntire) {
|
||||
result = truncate(result, {
|
||||
minLength,
|
||||
maxLength,
|
||||
mediaLength: 200,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const hasEllipsis = $derived(shortContent.some(isEllipsis))
|
||||
const expandInline = $derived(hasEllipsis && expandMode === "inline")
|
||||
@@ -152,15 +173,9 @@
|
||||
{/if}
|
||||
{:else if isProfile(parsed)}
|
||||
<ContentMention value={parsed.value} {url} />
|
||||
{:else if isEvent(parsed) || isAddress(parsed)}
|
||||
{:else if isQuote(parsed)}
|
||||
{#if isBlock(i)}
|
||||
<ContentQuote
|
||||
{depth}
|
||||
{url}
|
||||
{hideMediaAtDepth}
|
||||
value={parsed.value}
|
||||
{event}
|
||||
minimal={minimalQuote} />
|
||||
<ContentQuote {url} value={parsed.value} {event} />
|
||||
{:else}
|
||||
<Link
|
||||
external
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<script lang="ts">
|
||||
import {fromNostrURI} from "@welshman/util"
|
||||
import {nthEq} from "@welshman/lib"
|
||||
import {
|
||||
parse,
|
||||
truncate,
|
||||
renderAsHtml,
|
||||
isText,
|
||||
isEmoji,
|
||||
isTopic,
|
||||
isCode,
|
||||
isCashu,
|
||||
isInvoice,
|
||||
isLink,
|
||||
isProfile,
|
||||
isEvent,
|
||||
isAddress,
|
||||
isNewline,
|
||||
} from "@welshman/content"
|
||||
import type {Parsed} from "@welshman/content"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ContentToken from "@app/components/ContentToken.svelte"
|
||||
import ContentEmoji from "@app/components/ContentEmoji.svelte"
|
||||
import ContentCode from "@app/components/ContentCode.svelte"
|
||||
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
|
||||
import ContentNewline from "@app/components/ContentNewline.svelte"
|
||||
import ContentTopic from "@app/components/ContentTopic.svelte"
|
||||
import ContentMention from "@app/components/ContentMention.svelte"
|
||||
import {entityLink, userSettingsValues} from "@app/core/state"
|
||||
|
||||
interface Props {
|
||||
event: any
|
||||
trimParent?: boolean
|
||||
url?: string
|
||||
}
|
||||
|
||||
const {event, trimParent = false, url}: Props = $props()
|
||||
|
||||
const fullContent = parse(event)
|
||||
|
||||
const isBoundary = (i: number) => {
|
||||
const parsed = fullContent[i]
|
||||
|
||||
if (!parsed || isNewline(parsed)) return true
|
||||
if (isText(parsed)) return Boolean(parsed.value.match(/^\s+$/))
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const isStart = (i: number) => isBoundary(i - 1)
|
||||
|
||||
const isEnd = (i: number) => isBoundary(i + 1)
|
||||
|
||||
const isStartAndEnd = (i: number) => isStart(i) && isEnd(i)
|
||||
|
||||
const isQuote = (p: Parsed) => isEvent(p) || isAddress(p)
|
||||
|
||||
const ignoreWarning = () => {
|
||||
warning = null
|
||||
}
|
||||
|
||||
let warning = $state(
|
||||
$userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
|
||||
)
|
||||
|
||||
const dropWhile = <T,>(f: (x: T) => boolean, xs: Iterable<T>) => {
|
||||
const result: T[] = []
|
||||
|
||||
for (const x of xs) {
|
||||
if (result.length === 0 && f(x)) {
|
||||
continue
|
||||
}
|
||||
|
||||
result.push(x)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const shortContent = $derived.by(() => {
|
||||
let result = fullContent
|
||||
|
||||
if (trimParent && result.length > 0 && isQuote(result[0])) {
|
||||
result = dropWhile(p => isQuote(p) || isNewline(p), result)
|
||||
}
|
||||
|
||||
return truncate(result, {minLength: 200, maxLength: 300, mediaLength: 20})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
{#if warning}
|
||||
<div class="card2 card2-sm bg-alt row-2">
|
||||
<Icon icon={Danger} />
|
||||
<p>
|
||||
This note has been flagged by the author as "{warning}".<br />
|
||||
<Button class="link" onclick={ignoreWarning}>Show anyway</Button>
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-hidden text-ellipsis break-words">
|
||||
{#each shortContent as parsed, i}
|
||||
{#if isNewline(parsed)}
|
||||
<ContentNewline value={parsed.value} />
|
||||
{:else if isTopic(parsed)}
|
||||
<ContentTopic value={parsed.value} />
|
||||
{:else if isEmoji(parsed)}
|
||||
<ContentEmoji value={parsed.value} />
|
||||
{:else if isCode(parsed)}
|
||||
<ContentCode
|
||||
value={parsed.value}
|
||||
isBlock={isStartAndEnd(i) || parsed.value.includes("\n")} />
|
||||
{:else if isCashu(parsed) || isInvoice(parsed)}
|
||||
<ContentToken value={parsed.value} />
|
||||
{:else if isLink(parsed)}
|
||||
<ContentLinkInline value={parsed.value} />
|
||||
{:else if isProfile(parsed)}
|
||||
<ContentMention value={parsed.value} {url} />
|
||||
{:else if isQuote(parsed)}
|
||||
<Link
|
||||
external
|
||||
class="overflow-hidden text-ellipsis whitespace-nowrap underline"
|
||||
href={entityLink(parsed.raw)}>
|
||||
{fromNostrURI(parsed.raw).slice(0, 16) + "…"}
|
||||
</Link>
|
||||
{:else}
|
||||
{@html renderAsHtml(parsed)}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -6,20 +6,17 @@
|
||||
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 NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
||||
import {deriveEvent, entityLink} from "@app/core/state"
|
||||
import {goToEvent} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
value: any
|
||||
hideMediaAtDepth: number
|
||||
event: TrustedEvent
|
||||
depth: number
|
||||
url?: string
|
||||
minimal?: boolean
|
||||
}
|
||||
|
||||
const {value, event, depth, hideMediaAtDepth, url, minimal}: Props = $props()
|
||||
const {value, event, url}: Props = $props()
|
||||
|
||||
const {id, identifier, kind, pubkey, relays = []} = value
|
||||
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
|
||||
@@ -43,17 +40,17 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button class="my-2 block max-w-full text-left" {onclick}>
|
||||
<Button class="my-2 block w-full max-w-full text-left" {onclick}>
|
||||
{#if $quote}
|
||||
{#if minimal && $quote.kind === MESSAGE}
|
||||
{#if $quote.kind === MESSAGE}
|
||||
<div
|
||||
class="border-l-2 border-solid border-l-primary py-1 pl-2 opacity-90"
|
||||
style="background-color: color-mix(in srgb, var(--primary) 10%, var(--base-300) 90%);">
|
||||
<NoteContent {hideMediaAtDepth} {url} event={$quote} depth={depth + 1} />
|
||||
<NoteContentMinimal trimParent {url} event={$quote} />
|
||||
</div>
|
||||
{:else}
|
||||
<NoteCard event={$quote} {url} class="bg-alt rounded-box p-4">
|
||||
<NoteContent {hideMediaAtDepth} {url} event={$quote} depth={depth + 1} />
|
||||
<NoteContentMinimal {url} event={$quote} />
|
||||
</NoteCard>
|
||||
{/if}
|
||||
{:else}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||
import {goToEvent} from "@app/util/routes"
|
||||
@@ -36,7 +36,7 @@
|
||||
{/if}
|
||||
<span class="text-nowrap">{formatTimestamp(earliest.created_at)}</span>
|
||||
</div>
|
||||
<Content minimalQuote minLength={100} maxLength={400} event={earliest} />
|
||||
<NoteContentMinimal event={earliest} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-13 flex items-center justify-between">
|
||||
@@ -67,7 +67,7 @@
|
||||
{formatTimestamp(latest.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<Content minimalQuote minLength={100} maxLength={400} event={latest} />
|
||||
<NoteContentMinimal event={latest} />
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import type {Snippet} from "svelte"
|
||||
import {goto} from "$app/navigation"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {COMMENT} from "@welshman/util"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import {pubkey, relaysByUrl} from "@welshman/app"
|
||||
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import Danger from "@assets/icons/danger.svg?dataurl"
|
||||
import {setKey} from "@lib/implicit"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import EventReport from "@app/components/EventReport.svelte"
|
||||
import EventShare from "@app/components/EventShare.svelte"
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import {hasNip29} from "@app/core/state"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import Danger from "@assets/icons/danger.svg?dataurl"
|
||||
import {makeSpaceChatPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
@@ -32,7 +36,14 @@
|
||||
|
||||
const showInfo = () => pushModal(EventInfo, {url, event})
|
||||
|
||||
const share = () => pushModal(EventShare, {url, event})
|
||||
const share = async () => {
|
||||
if (hasNip29($relaysByUrl.get(url))) {
|
||||
pushModal(EventShare, {url, event})
|
||||
} else {
|
||||
setKey("share", event)
|
||||
goto(makeSpaceChatPath(url))
|
||||
}
|
||||
}
|
||||
|
||||
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import {preventDefault} from "@lib/html"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import {setKey} from "@lib/implicit"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
@@ -11,7 +12,6 @@
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import {channelsByUrl} from "@app/core/state"
|
||||
import {makeRoomPath} from "@app/util/routes"
|
||||
import {setKey} from "@lib/implicit"
|
||||
|
||||
const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props()
|
||||
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import Link from "@lib/components/Link.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 ChannelName from "@app/components/ChannelName.svelte"
|
||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||
import {makeGoalPath} from "@app/util/routes"
|
||||
import {makeGoalPath, makeSpacePath} from "@app/util/routes"
|
||||
|
||||
interface Props {
|
||||
url: any
|
||||
event: any
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
showRoom?: boolean
|
||||
showActivity?: boolean
|
||||
}
|
||||
|
||||
const {url, event, showActivity = false}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
const {url, event, showRoom, showActivity}: Props = $props()
|
||||
|
||||
const path = makeGoalPath(url, event.id)
|
||||
const room = getTagValue("h", event.tags)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
const deleteReaction = async (event: TrustedEvent) =>
|
||||
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||
@@ -26,13 +30,16 @@
|
||||
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} hideZap noun="Goal" />
|
||||
</div>
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
{#if room && showRoom}
|
||||
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<ChannelName {room} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} hideZap noun="Goal" />
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {canEnforceNip70} from "@app/core/commands"
|
||||
|
||||
const {url} = $props()
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
}
|
||||
|
||||
const {url, room}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -59,6 +64,10 @@
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (room) {
|
||||
tags.push(["h", room])
|
||||
}
|
||||
|
||||
publishThunk({
|
||||
relays: [url],
|
||||
event: makeEvent(ZAP_GOAL, {content, tags}),
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import GoalActions from "@app/components/GoalActions.svelte"
|
||||
import GoalSummary from "@app/components/GoalSummary.svelte"
|
||||
import ChannelLink from "@app/components/ChannelLink.svelte"
|
||||
import {makeGoalPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -16,6 +17,7 @@
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const summary = getTagValue("summary", event.tags)
|
||||
const room = getTagValue("h", event.tags)
|
||||
</script>
|
||||
|
||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeGoalPath(url, event.id)}>
|
||||
@@ -30,6 +32,9 @@
|
||||
<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 room}
|
||||
in <ChannelLink {url} {room} />
|
||||
{/if}
|
||||
</span>
|
||||
<GoalActions showActivity {url} {event} />
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
import ZapButton from "@app/components/ZapButton.svelte"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
url?: string
|
||||
event: TrustedEvent
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {url, event}: Props = $props()
|
||||
const {url, event, ...props}: Props = $props()
|
||||
|
||||
const zaps = deriveEventsMapped<Zap>(repository, {
|
||||
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
||||
@@ -27,7 +28,7 @@
|
||||
const daysOld = Math.ceil((now() - event.created_at) / DAY)
|
||||
</script>
|
||||
|
||||
<div class="card2 bg-alt flex flex-col gap-8">
|
||||
<div class="flex flex-col gap-8 {props.class}">
|
||||
<div class="flex gap-8">
|
||||
<div>
|
||||
<p class="text-xl text-primary">{zapAmount} sats</p>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import {pushModal, clearModals} from "@app/util/modal"
|
||||
import {PLATFORM_NAME, BURROW_URL} from "@app/core/state"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {loadUserData} from "@app/core/requests"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
|
||||
let signers: any[] = $state([])
|
||||
@@ -27,9 +26,7 @@
|
||||
|
||||
const signUp = () => pushModal(SignUp)
|
||||
|
||||
const onSuccess = async (session: Session, relays: string[] = []) => {
|
||||
await loadUserData(session.pubkey, relays)
|
||||
|
||||
const onSuccess = async (session: Session) => {
|
||||
addSession(session)
|
||||
pushToast({message: "Successfully logged in!"})
|
||||
setChecked("*")
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
import BunkerConnect from "@app/components/BunkerConnect.svelte"
|
||||
import BunkerUrl from "@app/components/BunkerUrl.svelte"
|
||||
import {Nip46Controller} from "@app/util/nip46"
|
||||
import {loadUserData} from "@app/core/requests"
|
||||
import {clearModals} from "@app/util/modal"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
@@ -33,9 +32,6 @@
|
||||
const pubkey = await controller.broker.getPublicKey()
|
||||
|
||||
loginWithNip46(pubkey, controller.clientSecret, response.event.pubkey, SIGNER_RELAYS)
|
||||
|
||||
await loadUserData(pubkey)
|
||||
|
||||
setChecked("*")
|
||||
clearModals()
|
||||
},
|
||||
@@ -75,8 +71,6 @@
|
||||
broker.cleanup()
|
||||
controller.stop()
|
||||
|
||||
await loadUserData(pubkey)
|
||||
|
||||
loginWithNip46(pubkey, clientSecret, signerPubkey, relays)
|
||||
} else {
|
||||
return pushToast({
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
||||
import {loadUserData} from "@app/core/requests"
|
||||
import {clearModals, pushModal} from "@app/util/modal"
|
||||
import {setChecked} from "@app/util/notifications"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
@@ -96,8 +95,6 @@
|
||||
const pubkey = await broker.getPublicKey()
|
||||
const session = makeNip46Session(pubkey, clientSecret, response.event.pubkey, relays)
|
||||
|
||||
await loadUserData(pubkey)
|
||||
|
||||
addSession({...session, email})
|
||||
broker.cleanup()
|
||||
setChecked("*")
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {displayRelayUrl, getTagValue} from "@welshman/util"
|
||||
import {deriveRelay} from "@welshman/app"
|
||||
import {displayRelayUrl, getTagValue, EVENT_TIME, ZAP_GOAL, THREAD} from "@welshman/util"
|
||||
import {deriveRelay, repository} from "@welshman/app"
|
||||
import {fly} from "@lib/transition"
|
||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||
import Exit from "@assets/icons/logout-3.svg?dataurl"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Login from "@assets/icons/login-3.svg?dataurl"
|
||||
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
|
||||
import History from "@assets/icons/history.svg?dataurl"
|
||||
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"
|
||||
@@ -16,14 +18,17 @@
|
||||
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Popover from "@lib/components/Popover.svelte"
|
||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
|
||||
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
||||
import SpaceDetail from "@app/components/SpaceDetail.svelte"
|
||||
import SpaceInvite from "@app/components/SpaceInvite.svelte"
|
||||
import SpaceExit from "@app/components/SpaceExit.svelte"
|
||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import ProfileList from "@app/components/ProfileList.svelte"
|
||||
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||
import Alerts from "@app/components/Alerts.svelte"
|
||||
@@ -36,13 +41,14 @@
|
||||
memberships,
|
||||
deriveUserRooms,
|
||||
deriveOtherRooms,
|
||||
trackerStore,
|
||||
hasNip29,
|
||||
alerts,
|
||||
deriveUserCanCreateRoom,
|
||||
} from "@app/core/state"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {makeSpacePath, makeChatPath} from "@app/util/routes"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
@@ -53,8 +59,21 @@
|
||||
const calendarPath = makeSpacePath(url, "calendar")
|
||||
const userRooms = deriveUserRooms(url)
|
||||
const otherRooms = deriveOtherRooms(url)
|
||||
const owner = $derived($relay?.profile?.pubkey)
|
||||
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
|
||||
|
||||
const spaceKinds = $derived(
|
||||
Array.from($trackerStore.getIds(url)).reduce((kinds, id) => {
|
||||
const event = repository.getEvent(id)
|
||||
|
||||
if (event) {
|
||||
kinds.add(event.kind)
|
||||
}
|
||||
|
||||
return kinds
|
||||
}, new Set()),
|
||||
)
|
||||
|
||||
const openMenu = () => {
|
||||
showMenu = true
|
||||
}
|
||||
@@ -63,6 +82,8 @@
|
||||
showMenu = !showMenu
|
||||
}
|
||||
|
||||
const showDetail = () => pushModal(SpaceDetail, {url}, {replaceState})
|
||||
|
||||
const showMembers = () =>
|
||||
pushModal(
|
||||
ProfileList,
|
||||
@@ -103,17 +124,28 @@
|
||||
<div bind:this={element} class="flex h-full flex-col justify-between">
|
||||
<SecondaryNavSection>
|
||||
<div>
|
||||
<SecondaryNavItem class="w-full !justify-between" onclick={openMenu}>
|
||||
<strong class="ellipsize flex items-center gap-3">
|
||||
{displayRelayUrl(url)}
|
||||
</strong>
|
||||
<Icon icon={AltArrowDown} />
|
||||
</SecondaryNavItem>
|
||||
<Button
|
||||
class="flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
||||
onclick={openMenu}>
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="ellipsize flex items-center gap-3">
|
||||
<RelayName {url} />
|
||||
</strong>
|
||||
<Icon icon={AltArrowDown} />
|
||||
</div>
|
||||
<span class="text-xs text-primary">{displayRelayUrl(url)}</span>
|
||||
</Button>
|
||||
{#if showMenu}
|
||||
<Popover hideOnClick onClose={toggleMenu}>
|
||||
<ul
|
||||
transition:fly
|
||||
class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-xl">
|
||||
<li>
|
||||
<Button onclick={showDetail}>
|
||||
<Icon icon={RemoteControllerMinimalistic} />
|
||||
Space Information
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button onclick={showMembers}>
|
||||
<Icon icon={UserRounded} />
|
||||
@@ -126,6 +158,14 @@
|
||||
Create Invite
|
||||
</Button>
|
||||
</li>
|
||||
{#if owner}
|
||||
<li>
|
||||
<Link href={makeChatPath([owner])}>
|
||||
<Icon icon={Letter} />
|
||||
Contact Owner
|
||||
</Link>
|
||||
</li>
|
||||
{/if}
|
||||
<li>
|
||||
{#if $userRoomsByUrl.has(url)}
|
||||
<Button onclick={leaveSpace} class="text-error">
|
||||
@@ -144,10 +184,19 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto">
|
||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url)}>
|
||||
<Icon icon={HomeSmile} /> Home
|
||||
</SecondaryNavItem>
|
||||
{#if ENABLE_ZAPS}
|
||||
{#if hasNip29($relay)}
|
||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}>
|
||||
<Icon icon={History} /> Recent Activity
|
||||
</SecondaryNavItem>
|
||||
{:else}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={chatPath}
|
||||
notification={$notifications.has(chatPath)}>
|
||||
<Icon icon={ChatRound} /> Chat
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if ENABLE_ZAPS && spaceKinds.has(ZAP_GOAL)}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={goalsPath}
|
||||
@@ -155,18 +204,22 @@
|
||||
<Icon icon={StarFallMinimalistic} /> Goals
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={threadsPath}
|
||||
notification={$notifications.has(threadsPath)}>
|
||||
<Icon icon={NotesMinimalistic} /> Threads
|
||||
</SecondaryNavItem>
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={calendarPath}
|
||||
notification={$notifications.has(calendarPath)}>
|
||||
<Icon icon={CalendarMinimalistic} /> Calendar
|
||||
</SecondaryNavItem>
|
||||
{#if spaceKinds.has(THREAD)}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={threadsPath}
|
||||
notification={$notifications.has(threadsPath)}>
|
||||
<Icon icon={NotesMinimalistic} /> Threads
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if spaceKinds.has(EVENT_TIME)}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={calendarPath}
|
||||
notification={$notifications.has(calendarPath)}>
|
||||
<Icon icon={CalendarMinimalistic} /> Calendar
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{#if hasNip29($relay)}
|
||||
{#if $userRooms.length > 0}
|
||||
<div class="h-2"></div>
|
||||
@@ -194,13 +247,6 @@
|
||||
Create room
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
{:else}
|
||||
<SecondaryNavItem
|
||||
{replaceState}
|
||||
href={chatPath}
|
||||
notification={$notifications.has(chatPath)}>
|
||||
<Icon icon={ChatRound} /> Chat
|
||||
</SecondaryNavItem>
|
||||
{/if}
|
||||
</div>
|
||||
</SecondaryNavSection>
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type {ComponentProps} from "svelte"
|
||||
import {EVENT_TIME} from "@welshman/util"
|
||||
import {EVENT_TIME, ZAP_GOAL, THREAD} from "@welshman/util"
|
||||
import NoteContentEventTime from "@app/components/NoteContentEventTime.svelte"
|
||||
import NoteContentThread from "@app/components/NoteContentThread.svelte"
|
||||
import NoteContentGoal from "@app/components/NoteContentGoal.svelte"
|
||||
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">
|
||||
<CalendarEventHeader event={props.event} />
|
||||
<div class="flex py-2 opacity-50">
|
||||
<div class="h-px flex-grow bg-base-content opacity-25"></div>
|
||||
</div>
|
||||
<Content {...props} />
|
||||
</div>
|
||||
</div>
|
||||
<NoteContentEventTime {...props} />
|
||||
{:else if props.event.kind === THREAD}
|
||||
<NoteContentThread {...props} />
|
||||
{:else if props.event.kind === ZAP_GOAL}
|
||||
<NoteContentGoal {...props} />
|
||||
{:else}
|
||||
<Content {...props} />
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type {ComponentProps} from "svelte"
|
||||
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>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<CalendarEventDate event={props.event} />
|
||||
<div class="flex flex-grow flex-col">
|
||||
<CalendarEventHeader event={props.event} />
|
||||
<div class="flex py-2 opacity-50">
|
||||
<div class="h-px flex-grow bg-base-content opacity-25"></div>
|
||||
</div>
|
||||
<Content {...props} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type {ComponentProps} from "svelte"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import GoalSummary from "@app/components/GoalSummary.svelte"
|
||||
|
||||
const props: ComponentProps<typeof Content> = $props()
|
||||
|
||||
const content = getTagValue("summary", props.event.tags)
|
||||
const fakeEvent = {content, tags: props.event.tags}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-2xl">{props.event.content}</p>
|
||||
<Content {...props} event={fakeEvent} expandMode="inline" minLength={50} maxLength={300} />
|
||||
<GoalSummary url={props.url} event={props.event} />
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import type {ComponentProps} from "svelte"
|
||||
import {EVENT_TIME, ZAP_GOAL, THREAD} from "@welshman/util"
|
||||
import NoteContentMinimalEventTime from "@app/components/NoteContentMinimalEventTime.svelte"
|
||||
import NoteContentMinimalThread from "@app/components/NoteContentMinimalThread.svelte"
|
||||
import NoteContentMinimalGoal from "@app/components/NoteContentMinimalGoal.svelte"
|
||||
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||
|
||||
const props: ComponentProps<typeof ContentMinimal> = $props()
|
||||
</script>
|
||||
|
||||
<div class="text-xs">
|
||||
{#if props.event.kind === EVENT_TIME}
|
||||
<NoteContentMinimalEventTime {...props} />
|
||||
{:else if props.event.kind === THREAD}
|
||||
<NoteContentMinimalThread {...props} />
|
||||
{:else if props.event.kind === ZAP_GOAL}
|
||||
<NoteContentMinimalGoal {...props} />
|
||||
{:else}
|
||||
<ContentMinimal {...props} />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import type {ComponentProps} from "svelte"
|
||||
import {
|
||||
fromPairs,
|
||||
formatTimestamp,
|
||||
formatTimestampAsDate,
|
||||
formatTimestampAsTime,
|
||||
} from "@welshman/lib"
|
||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||
|
||||
const props: ComponentProps<typeof ContentMinimal> = $props()
|
||||
const meta = $derived(fromPairs(props.event.tags) as Record<string, string>)
|
||||
const start = $derived(parseInt(meta.start))
|
||||
const end = $derived(parseInt(meta.end))
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-grow flex-wrap justify-between gap-2">
|
||||
<p class="text-sm">{meta.title || meta.name}</p>
|
||||
{#if !isNaN(start) && !isNaN(end)}
|
||||
{@const startDateDisplay = formatTimestampAsDate(start)}
|
||||
{@const endDateDisplay = formatTimestampAsDate(end)}
|
||||
{@const isSingleDay = startDateDisplay === endDateDisplay}
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={ClockCircle} size={4} />
|
||||
<span class="hidden sm:block">{formatTimestampAsDate(start)}</span>
|
||||
{formatTimestampAsTime(start)} — {isSingleDay
|
||||
? formatTimestampAsTime(end)
|
||||
: formatTimestamp(end)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<ContentMinimal {...props} />
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import type {ComponentProps} from "svelte"
|
||||
import {sum} from "@welshman/lib"
|
||||
import type {Zap, TrustedEvent} from "@welshman/util"
|
||||
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
|
||||
import {deriveEventsMapped} from "@welshman/store"
|
||||
import {repository, getValidZap} from "@welshman/app"
|
||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||
|
||||
const props: ComponentProps<typeof ContentMinimal> = $props()
|
||||
|
||||
const content = getTagValue("summary", props.event.tags)
|
||||
const fakeEvent = {content, tags: props.event.tags}
|
||||
|
||||
const zaps = deriveEventsMapped<Zap>(repository, {
|
||||
filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}],
|
||||
itemToEvent: item => item.response,
|
||||
eventToItem: (response: TrustedEvent) => getValidZap(response, props.event),
|
||||
})
|
||||
|
||||
const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0")
|
||||
const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
|
||||
</script>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm">{props.event.content}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<Icon icon={Bolt} size={4} />
|
||||
{zapAmount}/{goalAmount} sats funded
|
||||
</div>
|
||||
</div>
|
||||
<ContentMinimal {...props} event={fakeEvent} />
|
||||
@@ -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}
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import type {ComponentProps} from "svelte"
|
||||
import {formatTimestamp} from "@welshman/lib"
|
||||
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}
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<p class="text-xl">{title}</p>
|
||||
<p class="text-sm opacity-75">
|
||||
{formatTimestamp(props.event.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="mb-3 h-0 text-xs opacity-75">
|
||||
{formatTimestamp(props.event.created_at)}
|
||||
</p>
|
||||
{/if}
|
||||
{#if props.event.content}
|
||||
<Content {...props} />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,18 +1,23 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
||||
import {encodeRelay} from "@app/core/state"
|
||||
import {makeSpacePath} from "@app/util/routes"
|
||||
import {lastPageBySpaceUrl} from "@app/util/history"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
|
||||
const {url} = $props()
|
||||
|
||||
const path = makeSpacePath(url)
|
||||
|
||||
const onClick = () => goto(lastPageBySpaceUrl.get(encodeRelay(url)) || path)
|
||||
</script>
|
||||
|
||||
<PrimaryNavItem
|
||||
onclick={onClick}
|
||||
title={displayRelayUrl(url)}
|
||||
href={path}
|
||||
class="tooltip-right"
|
||||
notification={$notifications.has(path)}>
|
||||
<SpaceAvatar {url} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import {deriveProfile} from "@welshman/app"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||
|
||||
export type Props = {
|
||||
pubkey: string
|
||||
@@ -14,5 +14,5 @@
|
||||
</script>
|
||||
|
||||
{#if $profile}
|
||||
<Content event={{content: $profile.about || "", tags: []}} hideMediaAtDepth={0} />
|
||||
<ContentMinimal event={{content: $profile.about || "", tags: []}} />
|
||||
{/if}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
||||
</script>
|
||||
|
||||
<Button onclick={preventDefault(openProfile)} class={cx(props.class, {"link-content": !unstyled})}>
|
||||
<Button
|
||||
onclick={preventDefault(openProfile)}
|
||||
class={cx(props.class, {"link-content bg-alt": !unstyled})}>
|
||||
@<ProfileName {pubkey} {url} />
|
||||
</Button>
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<button
|
||||
type="button"
|
||||
data-tip={`This content has been reported as "${displayList(reportReasons)}".`}
|
||||
class="btn btn-error btn-xs tooltip-right flex items-center gap-1 rounded-full"
|
||||
class="btn btn-error btn-xs tooltip-right flex items-center gap-1 rounded-full font-normal"
|
||||
class:tooltip={!noTooltip && !isMobile}
|
||||
onclick={stopPropagation(preventDefault(onReportClick))}>
|
||||
<Icon icon={Danger} />
|
||||
@@ -134,11 +134,10 @@
|
||||
<button
|
||||
type="button"
|
||||
data-tip={tooltip}
|
||||
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full {reactionClass}"
|
||||
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full text-xs font-normal {reactionClass}"
|
||||
class:tooltip={!noTooltip && !isMobile}
|
||||
class:border={isOwn}
|
||||
class:border-solid={isOwn}
|
||||
class:border-primary={isOwn}>
|
||||
class:btn-neutral={!isOwn}
|
||||
class:btn-primary={isOwn}>
|
||||
<Reaction event={zaps[0].request} />
|
||||
<span>{amount}</span>
|
||||
</button>
|
||||
@@ -152,11 +151,10 @@
|
||||
<button
|
||||
type="button"
|
||||
data-tip={tooltip}
|
||||
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full {reactionClass}"
|
||||
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full font-normal {reactionClass}"
|
||||
class:tooltip={!noTooltip && !isMobile}
|
||||
class:border={isOwn}
|
||||
class:border-solid={isOwn}
|
||||
class:border-primary={isOwn}
|
||||
class:btn-neutral={!isOwn}
|
||||
class:btn-primary={isOwn}
|
||||
onclick={stopPropagation(preventDefault(onClick))}>
|
||||
<Reaction event={events[0]} />
|
||||
{#if events.length > 1}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {deriveRelay} from "@welshman/app"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import ShieldUser from "@assets/icons/shield-user.svg?dataurl"
|
||||
import BillList from "@assets/icons/bill-list.svg?dataurl"
|
||||
import Ghost from "@assets/icons/ghost-smile.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import SpaceRelayStatus from "@app/components/SpaceRelayStatus.svelte"
|
||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||
import ProfileLatest from "@app/components/ProfileLatest.svelte"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
}
|
||||
|
||||
const {url}: Props = $props()
|
||||
const relay = deriveRelay(url)
|
||||
const owner = $derived($relay?.profile?.pubkey)
|
||||
|
||||
const back = () => history.back()
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
<div class="relative flex gap-4">
|
||||
<div class="relative">
|
||||
<div class="avatar relative">
|
||||
<div
|
||||
class="center !flex h-16 w-16 min-w-16 rounded-full border-2 border-solid border-base-300 bg-base-300">
|
||||
{#if $relay?.profile?.icon}
|
||||
<img alt="" src={$relay.profile.icon} />
|
||||
{:else}
|
||||
<Icon icon={Ghost} size={6} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-col gap-1">
|
||||
<h1 class="ellipsize whitespace-nowrap text-2xl font-bold">
|
||||
<RelayName {url} />
|
||||
</h1>
|
||||
<p class="ellipsize text-sm opacity-75">{displayRelayUrl(url)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<RelayDescription {url} />
|
||||
{#if $relay?.profile?.terms_of_service || $relay?.profile?.privacy_policy}
|
||||
<div class="flex gap-3">
|
||||
{#if $relay.profile.terms_of_service}
|
||||
<Link href={$relay.profile.terms_of_service} class="badge badge-neutral flex gap-2">
|
||||
<Icon icon={BillList} size={4} />
|
||||
Terms of Service
|
||||
</Link>
|
||||
{/if}
|
||||
{#if $relay.profile.privacy_policy}
|
||||
<Link href={$relay?.profile?.privacy_policy} class="badge badge-neutral flex gap-2">
|
||||
<Icon icon={ShieldUser} size={4} />
|
||||
Privacy Policy
|
||||
</Link>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<SpaceRelayStatus {url} />
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if owner}
|
||||
<div class="card2 bg-alt">
|
||||
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
|
||||
<Icon icon={UserRounded} />
|
||||
Latest Updates
|
||||
</h3>
|
||||
<ProfileLatest {url} pubkey={owner}>
|
||||
{#snippet fallback()}
|
||||
<p class="text-sm opacity-60">No recent posts from the relay admin</p>
|
||||
{/snippet}
|
||||
</ProfileLatest>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Button class="btn btn-primary" onclick={back}>Got it</Button>
|
||||
</div>
|
||||
@@ -1,144 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {deriveRelay} from "@welshman/app"
|
||||
import {fade} from "@lib/transition"
|
||||
import CompassBig from "@assets/icons/compass-big.svg?dataurl"
|
||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic.svg?dataurl"
|
||||
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import Lock from "@assets/icons/lock-keyhole.svg?dataurl"
|
||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.svelte"
|
||||
import {makeRoomPath, makeSpacePath} from "@app/util/routes"
|
||||
import {
|
||||
hasNip29,
|
||||
deriveUserRooms,
|
||||
deriveOtherRooms,
|
||||
makeChannelId,
|
||||
channelsById,
|
||||
} from "@app/core/state"
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
}
|
||||
|
||||
const {url}: Props = $props()
|
||||
const relay = deriveRelay(url)
|
||||
const userRooms = deriveUserRooms(url)
|
||||
const otherRooms = deriveOtherRooms(url)
|
||||
const chatPath = makeSpacePath(url, "chat")
|
||||
const goalsPath = makeSpacePath(url, "goals")
|
||||
const threadsPath = makeSpacePath(url, "threads")
|
||||
const calendarPath = makeSpacePath(url, "calendar")
|
||||
|
||||
const addRoom = () => pushModal(RoomCreate, {url})
|
||||
|
||||
const filteredRooms = $derived(() => {
|
||||
if (!term) return [...$userRooms, ...$otherRooms]
|
||||
|
||||
const query = term.toLowerCase()
|
||||
const allRooms = [...$userRooms, ...$otherRooms]
|
||||
|
||||
return allRooms.filter(room => {
|
||||
const channel = $channelsById.get(makeChannelId(url, room))
|
||||
const roomName = channel?.name || room
|
||||
return roomName.toLowerCase().includes(query)
|
||||
})
|
||||
})
|
||||
|
||||
let term = $state("")
|
||||
</script>
|
||||
|
||||
<div class="card2 bg-alt md:hidden">
|
||||
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
|
||||
<Icon icon={CompassBig} />
|
||||
Quick Links
|
||||
</h3>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Link href={goalsPath} class="btn btn-neutral w-full justify-start">
|
||||
<div class="relative flex items-center gap-2">
|
||||
<Icon icon={StarFallMinimalistic} />
|
||||
Goals
|
||||
{#if $notifications.has(goalsPath)}
|
||||
<div
|
||||
class="absolute -right-3 -top-1 h-2 w-2 rounded-full bg-neutral-content"
|
||||
transition:fade>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={threadsPath} class="btn btn-neutral w-full justify-start">
|
||||
<div class="relative flex items-center gap-2">
|
||||
<Icon icon={NotesMinimalistic} />
|
||||
Threads
|
||||
{#if $notifications.has(threadsPath)}
|
||||
<div
|
||||
class="absolute -right-3 -top-1 h-2 w-2 rounded-full bg-neutral-content"
|
||||
transition:fade>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Link>
|
||||
<Link href={calendarPath} class="btn btn-neutral w-full justify-start">
|
||||
<div class="relative flex items-center gap-2">
|
||||
<Icon icon={CalendarMinimalistic} />
|
||||
Calendar
|
||||
{#if $notifications.has(calendarPath)}
|
||||
<div
|
||||
class="absolute -right-3 -top-1 h-2 w-2 rounded-full bg-neutral-content"
|
||||
transition:fade>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Link>
|
||||
{#if hasNip29($relay)}
|
||||
{#if $userRooms.length + $otherRooms.length > 10}
|
||||
<label class="input input-sm input-bordered flex flex-grow items-center gap-2">
|
||||
<Icon icon={Magnifier} size={4} />
|
||||
<input bind:value={term} class="grow" type="text" placeholder="Search rooms..." />
|
||||
</label>
|
||||
{/if}
|
||||
{#each filteredRooms() as room (room)}
|
||||
{@const roomPath = makeRoomPath(url, room)}
|
||||
{@const channel = $channelsById.get(makeChannelId(url, room))}
|
||||
<Link href={roomPath} class="btn btn-neutral btn-sm relative w-full justify-start">
|
||||
<div class="flex min-w-0 items-center gap-2 overflow-hidden text-nowrap">
|
||||
{#if channel?.closed || channel?.private}
|
||||
<Icon icon={Lock} size={4} />
|
||||
{:else}
|
||||
<Icon icon={Hashtag} />
|
||||
{/if}
|
||||
<ChannelName {url} {room} />
|
||||
</div>
|
||||
{#if $notifications.has(roomPath)}
|
||||
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary" transition:fade>
|
||||
</div>
|
||||
{/if}
|
||||
</Link>
|
||||
{/each}
|
||||
<Button onclick={addRoom} class="btn btn-neutral btn-sm w-full justify-start">
|
||||
<Icon icon={AddCircle} />
|
||||
Create Room
|
||||
</Button>
|
||||
{:else}
|
||||
<Link href={chatPath} class="btn btn-neutral w-full justify-start">
|
||||
<div class="relative flex items-center gap-2">
|
||||
<Icon icon={ChatRound} />
|
||||
Chat
|
||||
{#if $notifications.has(chatPath)}
|
||||
<div class="absolute -right-3 -top-1 h-2 w-2 rounded-full bg-primary" transition:fade>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Link>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,106 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {derived} from "svelte/store"
|
||||
import {groupBy, ago, MONTH, first, last, uniq, avg, overlappingPairs} from "@welshman/lib"
|
||||
import {MESSAGE, getTagValue} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ConversationCard from "@app/components/ConversationCard.svelte"
|
||||
import {deriveEventsForUrl} from "@app/core/state"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
}
|
||||
|
||||
const {url}: Props = $props()
|
||||
const since = ago(MONTH)
|
||||
const messages = deriveEventsForUrl(url, [{kinds: [MESSAGE], since}])
|
||||
|
||||
const conversations = derived(messages, $messages => {
|
||||
const convs = []
|
||||
|
||||
for (const [room, messages] of groupBy(e => getTagValue("h", e.tags), $messages).entries()) {
|
||||
const avgTime = avg(overlappingPairs(messages).map(([a, b]) => a.created_at - b.created_at))
|
||||
const groups: TrustedEvent[][] = []
|
||||
const group: TrustedEvent[] = []
|
||||
|
||||
// Group conversations by time between messages
|
||||
let prevCreatedAt = messages[0].created_at
|
||||
for (const message of messages) {
|
||||
if (prevCreatedAt - message.created_at < avgTime) {
|
||||
group.push(message)
|
||||
} else {
|
||||
groups.push(group.splice(0))
|
||||
}
|
||||
|
||||
prevCreatedAt = message.created_at
|
||||
}
|
||||
|
||||
if (group.length > 0) {
|
||||
groups.push(group.splice(0))
|
||||
}
|
||||
|
||||
// Convert each group into a conversation
|
||||
for (const events of groups) {
|
||||
if (events.length < 2) {
|
||||
continue
|
||||
}
|
||||
|
||||
const latest = first(events)!
|
||||
const earliest = last(events)!
|
||||
const participants = uniq(events.map(msg => msg.pubkey))
|
||||
|
||||
convs.push({room, events, latest, earliest, participants})
|
||||
}
|
||||
}
|
||||
|
||||
return convs
|
||||
})
|
||||
|
||||
const viewMore = () => {
|
||||
limit += 3
|
||||
}
|
||||
|
||||
let limit = $state(3)
|
||||
</script>
|
||||
|
||||
<div class="card2 bg-alt">
|
||||
<div class="flex flex-col gap-4">
|
||||
<h3 class="flex items-center gap-2 text-lg font-semibold">
|
||||
<Icon icon={ChatRound} />
|
||||
Recent Conversations
|
||||
</h3>
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if $conversations.length === 0}
|
||||
{#if $messages.length > 0}
|
||||
{@const events = $messages.slice(0, 1)}
|
||||
{@const event = events[0]}
|
||||
{@const room = getTagValue("h", event.tags)}
|
||||
<ConversationCard
|
||||
{url}
|
||||
{room}
|
||||
{events}
|
||||
latest={event}
|
||||
earliest={event}
|
||||
participants={[event.pubkey]} />
|
||||
{:else}
|
||||
<div class="py-8 text-center opacity-70">
|
||||
<p>No recent conversations</p>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each $conversations.slice(0, limit) as { room, events, latest, earliest, participants } (latest.id)}
|
||||
<ConversationCard {url} {room} {events} {latest} {earliest} {participants} />
|
||||
{/each}
|
||||
{#if $conversations.length > limit}
|
||||
<Button class="btn btn-primary" onclick={viewMore}>
|
||||
View more conversations
|
||||
<Icon icon={AltArrowDown} />
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,23 +1,27 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ChannelName from "@app/components/ChannelName.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 {makeThreadPath} from "@app/util/routes"
|
||||
import {makeThreadPath, makeSpacePath} from "@app/util/routes"
|
||||
|
||||
interface Props {
|
||||
url: any
|
||||
event: any
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
showRoom?: boolean
|
||||
showActivity?: boolean
|
||||
}
|
||||
|
||||
const {url, event, showActivity = false}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
const {url, event, showRoom, showActivity}: Props = $props()
|
||||
|
||||
const path = makeThreadPath(url, event.id)
|
||||
const room = getTagValue("h", event.tags)
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
const deleteReaction = async (event: TrustedEvent) =>
|
||||
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||
@@ -26,13 +30,16 @@
|
||||
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Thread" />
|
||||
</div>
|
||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||
{#if room && showRoom}
|
||||
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full">
|
||||
Posted in #<ChannelName {room} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Thread" />
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {canEnforceNip70} from "@app/core/commands"
|
||||
|
||||
const {url} = $props()
|
||||
type Props = {
|
||||
url: string
|
||||
room?: string
|
||||
}
|
||||
|
||||
const {url, room}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
@@ -52,6 +57,10 @@
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
if (room) {
|
||||
tags.push(["h", room])
|
||||
}
|
||||
|
||||
publishThunk({
|
||||
relays: [url],
|
||||
event: makeEvent(THREAD, {content, tags}),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import {nthEq, formatTimestamp} from "@welshman/lib"
|
||||
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 ThreadActions from "@app/components/ThreadActions.svelte"
|
||||
import ChannelLink from "@app/components/ChannelLink.svelte"
|
||||
import {makeThreadPath} from "@app/util/routes"
|
||||
|
||||
type Props = {
|
||||
@@ -14,7 +16,8 @@
|
||||
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const title = event.tags.find(nthEq(0, "title"))?.[1]
|
||||
const title = getTagValue("title", event.tags)
|
||||
const room = getTagValue("h", event.tags)
|
||||
</script>
|
||||
|
||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeThreadPath(url, event.id)}>
|
||||
@@ -33,7 +36,11 @@
|
||||
<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} />
|
||||
Posted by
|
||||
<ProfileLink pubkey={event.pubkey} {url} />
|
||||
{#if room}
|
||||
in <ChannelLink {url} {room} />
|
||||
{/if}
|
||||
</span>
|
||||
<ThreadActions showActivity {url} {event} />
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import {pushModal} from "@app/util/modal"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
url?: string
|
||||
event: TrustedEvent
|
||||
children: Snippet
|
||||
replaceState?: boolean
|
||||
|
||||
Reference in New Issue
Block a user