Re-work space navigation #223
This commit is contained in:
@@ -1,31 +1,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
|
import {getTagValue} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.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 ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||||
import EventActivity from "@app/components/EventActivity.svelte"
|
import EventActivity from "@app/components/EventActivity.svelte"
|
||||||
import EventActions from "@app/components/EventActions.svelte"
|
import EventActions from "@app/components/EventActions.svelte"
|
||||||
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
|
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
|
||||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
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 {pushModal} from "@app/util/modal"
|
||||||
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||||
|
|
||||||
const {
|
type Props = {
|
||||||
url,
|
|
||||||
event,
|
|
||||||
showActivity = false,
|
|
||||||
}: {
|
|
||||||
url: string
|
url: string
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
|
showRoom?: boolean
|
||||||
showActivity?: 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 path = makeCalendarPath(url, event.id)
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
|
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
|
||||||
|
|
||||||
@@ -36,24 +38,27 @@
|
|||||||
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
{#if room && showRoom}
|
||||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full">
|
||||||
<ThunkStatusOrDeleted {event} />
|
Posted in #<ChannelName {room} {url} />
|
||||||
{#if showActivity}
|
</Link>
|
||||||
<EventActivity {url} {path} {event} />
|
{/if}
|
||||||
{/if}
|
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||||
<EventActions {url} {event} noun="Event">
|
<ThunkStatusOrDeleted {event} />
|
||||||
{#snippet customActions()}
|
{#if showActivity}
|
||||||
{#if event.pubkey === $pubkey}
|
<EventActivity {url} {path} {event} />
|
||||||
<li>
|
{/if}
|
||||||
<Button onclick={editEvent}>
|
<EventActions {url} {event} noun="Event">
|
||||||
<Icon size={4} icon={Pen2} />
|
{#snippet customActions()}
|
||||||
Edit Event
|
{#if event.pubkey === $pubkey}
|
||||||
</Button>
|
<li>
|
||||||
</li>
|
<Button onclick={editEvent}>
|
||||||
{/if}
|
<Icon size={4} icon={Pen2} />
|
||||||
{/snippet}
|
Edit Event
|
||||||
</EventActions>
|
</Button>
|
||||||
</div>
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</EventActions>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
room?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url}: Props = $props()
|
const {url, room}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CalendarEventForm {url}>
|
<CalendarEventForm {url} {room}>
|
||||||
{#snippet header()}
|
{#snippet header()}
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
|
|||||||
@@ -8,13 +8,16 @@
|
|||||||
|
|
||||||
const {event}: Props = $props()
|
const {event}: Props = $props()
|
||||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||||
const startDate = $derived(secondsToDate(parseInt(meta.start)))
|
const start = $derived(parseInt(meta.start))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
{#if !isNaN(start)}
|
||||||
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">
|
{@const startDate = secondsToDate(start)}
|
||||||
<strong>{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</strong>
|
<div
|
||||||
<span class="text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
|
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">
|
||||||
<span class="text-xs opacity-75"
|
<strong>{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</strong>
|
||||||
>{Intl.DateTimeFormat(LOCALE, {weekday: "long"}).format(startDate)}</span>
|
<span class="text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
|
||||||
</div>
|
<span class="text-xs opacity-75"
|
||||||
|
>{Intl.DateTimeFormat(LOCALE, {weekday: "long"}).format(startDate)}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
room?: string
|
||||||
header: Snippet
|
header: Snippet
|
||||||
initialValues?: {
|
initialValues?: {
|
||||||
d: string
|
d: string
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, header, initialValues}: Props = $props()
|
const {url, room, header, initialValues}: Props = $props()
|
||||||
|
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
@@ -84,6 +85,10 @@
|
|||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
tags.push(["h", room])
|
||||||
|
}
|
||||||
|
|
||||||
const event = makeEvent(EVENT_TIME, {content, tags})
|
const event = makeEvent(EVENT_TIME, {content, tags})
|
||||||
|
|
||||||
pushToast({message: "Your event has been saved!"})
|
pushToast({message: "Your event has been saved!"})
|
||||||
|
|||||||
@@ -17,18 +17,20 @@
|
|||||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||||
const start = $derived(parseInt(meta.start))
|
const start = $derived(parseInt(meta.start))
|
||||||
const end = $derived(parseInt(meta.end))
|
const end = $derived(parseInt(meta.end))
|
||||||
const startDateDisplay = $derived(formatTimestampAsDate(start))
|
|
||||||
const endDateDisplay = $derived(formatTimestampAsDate(end))
|
|
||||||
const isSingleDay = $derived(startDateDisplay === endDateDisplay)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-grow flex-wrap justify-between gap-2">
|
<div class="flex flex-grow flex-wrap justify-between gap-2">
|
||||||
<p class="text-xl">{meta.title || meta.name}</p>
|
<p class="text-xl">{meta.title || meta.name}</p>
|
||||||
<div class="flex items-center gap-2 text-sm">
|
{#if !isNaN(start) && !isNaN(end)}
|
||||||
<Icon icon={ClockCircle} size={4} />
|
{@const startDateDisplay = formatTimestampAsDate(start)}
|
||||||
<span class="sm:hidden">{formatTimestampAsDate(start)}</span>
|
{@const endDateDisplay = formatTimestampAsDate(end)}
|
||||||
{formatTimestampAsTime(start)} — {isSingleDay
|
{@const isSingleDay = startDateDisplay === endDateDisplay}
|
||||||
? formatTimestampAsTime(end)
|
<div class="flex items-center gap-2 text-sm">
|
||||||
: formatTimestamp(end)}
|
<Icon icon={ClockCircle} size={4} />
|
||||||
</div>
|
<span class="hidden sm:block">{formatTimestampAsDate(start)}</span>
|
||||||
|
{formatTimestampAsTime(start)} — {isSingleDay
|
||||||
|
? formatTimestampAsTime(end)
|
||||||
|
: formatTimestamp(end)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {getTagValue} from "@welshman/util"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
|
import ChannelLink from "@app/components/ChannelLink.svelte"
|
||||||
import {makeCalendarPath} from "@app/util/routes"
|
import {makeCalendarPath} from "@app/util/routes"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -12,6 +14,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {url, event}: Props = $props()
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
|
const room = getTagValue("h", event.tags)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link class="col-3 card2 bg-alt w-full cursor-pointer" href={makeCalendarPath(url, event.id)}>
|
<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">
|
<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">
|
<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>
|
</span>
|
||||||
<CalendarEventActions showActivity {url} {event} />
|
<CalendarEventActions showActivity {url} {event} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type {Instance} from "tippy.js"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import type {EventContent} from "@welshman/util"
|
import type {EventContent} from "@welshman/util"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
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 Plane from "@assets/icons/plane-2.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.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 EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {onDestroy, onMount} from "svelte"
|
import {onDestroy, onMount} from "svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url?: string
|
url?: string
|
||||||
|
room?: string
|
||||||
content?: string
|
content?: string
|
||||||
onEditPrevious?: () => void
|
onEditPrevious?: () => void
|
||||||
onSubmit: (event: EventContent) => void
|
onSubmit: (event: EventContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {content, onEditPrevious, onSubmit, url}: Props = $props()
|
const {url, room, content, onEditPrevious, onSubmit}: Props = $props()
|
||||||
|
|
||||||
const autofocus = !isMobile
|
const autofocus = !isMobile
|
||||||
|
|
||||||
@@ -36,6 +41,10 @@
|
|||||||
|
|
||||||
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||||
|
|
||||||
|
const showPopover = () => popover?.show()
|
||||||
|
|
||||||
|
const hidePopover = () => popover?.hide()
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading) return
|
if ($uploading) return
|
||||||
|
|
||||||
@@ -50,7 +59,9 @@
|
|||||||
ed.chain().clearContent().run()
|
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 () => {
|
onMount(async () => {
|
||||||
const ed = await editor
|
const ed = await editor
|
||||||
@@ -64,17 +75,32 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||||
<Button
|
<div class="join">
|
||||||
data-tip="Add an image"
|
<Button
|
||||||
class="center tooltip tooltip-right h-10 w-10 min-w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
|
data-tip="Add an image"
|
||||||
disabled={$uploading}
|
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"
|
||||||
onclick={uploadFiles}>
|
disabled={$uploading}
|
||||||
{#if $uploading}
|
onclick={uploadFiles}>
|
||||||
<span class="loading loading-spinner loading-xs"></span>
|
{#if $uploading}
|
||||||
{:else}
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
<Icon icon={GallerySend} />
|
{:else}
|
||||||
{/if}
|
<Icon icon={GallerySend} />
|
||||||
</Button>
|
{/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">
|
<div class="chat-editor flex-grow overflow-hidden">
|
||||||
<EditorContent {editor} />
|
<EditorContent {editor} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import NoteContent from "@app/components/NoteContent.svelte"
|
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
verb,
|
verb,
|
||||||
@@ -19,16 +19,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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>
|
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}
|
{#key event.id}
|
||||||
<NoteContent
|
<NoteContentMinimal trimParent {event} />
|
||||||
{event}
|
|
||||||
hideMediaAtDepth={0}
|
|
||||||
minLength={100}
|
|
||||||
maxLength={300}
|
|
||||||
expandMode="disabled" />
|
|
||||||
{/key}
|
{/key}
|
||||||
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||||
<Icon icon={CloseCircle} />
|
<Icon icon={CloseCircle} />
|
||||||
|
|||||||
@@ -1,24 +1,35 @@
|
|||||||
<script lang="ts">
|
<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 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 {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 TapTarget from "@lib/components/TapTarget.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.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 Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
|
||||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import ChannelMessageZapButton from "@app/components/ChannelMessageZapButton.svelte"
|
import ChannelItemZapButton from "@app/components/ChannelItemZapButton.svelte"
|
||||||
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
|
import ChannelItemEmojiButton from "@app/components/ChannelItemEmojiButton.svelte"
|
||||||
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
|
import ChannelItemMenuButton from "@app/components/ChannelItemMenuButton.svelte"
|
||||||
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte"
|
import ChannelItemMenuMobile from "@app/components/ChannelItemMenuMobile.svelte"
|
||||||
import {colors, ENABLE_ZAPS} from "@app/core/state"
|
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 {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
|
import {getChannelItemPath} from "@app/util/routes"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -42,16 +53,18 @@
|
|||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
|
|
||||||
const thunk = $thunks[event.id]
|
const thunk = $thunks[event.id]
|
||||||
|
const path = getChannelItemPath(url, event)
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
const today = formatTimestampAsDate(now())
|
const today = formatTimestampAsDate(now())
|
||||||
const profile = deriveProfile(event.pubkey, [url])
|
const profile = deriveProfile(event.pubkey, [url])
|
||||||
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
||||||
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
||||||
|
const comments = deriveEventsForUrl(url, [{kinds: [COMMENT], "#e": [event.id]}])
|
||||||
|
|
||||||
const reply = () => replyTo!(event)
|
const reply = () => replyTo!(event)
|
||||||
const edit = canEdit(event) ? () => onEdit(event) : undefined
|
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})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
||||||
|
|
||||||
@@ -65,7 +78,7 @@
|
|||||||
<TapTarget
|
<TapTarget
|
||||||
data-event={event.id}
|
data-event={event.id}
|
||||||
onTap={inert ? null : onTap}
|
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">
|
<div class="flex w-full gap-3 overflow-auto">
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<Button onclick={openProfile} class="flex items-start">
|
<Button onclick={openProfile} class="flex items-start">
|
||||||
@@ -90,10 +103,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text-sm">
|
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
|
||||||
<Content minimalQuote {event} {url} />
|
<ChannelItemContent {url} {event} />
|
||||||
{#if thunk}
|
{#if thunk}
|
||||||
<ThunkFailure showToastOnRetry {thunk} class="mt-2" />
|
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,15 +118,32 @@
|
|||||||
{deleteReaction}
|
{deleteReaction}
|
||||||
{createReaction}
|
{createReaction}
|
||||||
reactionClass="tooltip-right" />
|
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>
|
</div>
|
||||||
{#if !isMobile}
|
{#if !isMobile}
|
||||||
<button
|
<button
|
||||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||||
class:group-hover:opacity-100={!isMobile}>
|
class:group-hover:opacity-100={!isMobile}>
|
||||||
{#if ENABLE_ZAPS}
|
{#if ENABLE_ZAPS}
|
||||||
<ChannelMessageZapButton {url} {event} />
|
<ChannelItemZapButton {url} {event} />
|
||||||
{/if}
|
{/if}
|
||||||
<ChannelMessageEmojiButton {url} {event} />
|
<ChannelItemEmojiButton {url} {event} />
|
||||||
{#if replyTo}
|
{#if replyTo}
|
||||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||||
<Icon icon={Reply} size={4} />
|
<Icon icon={Reply} size={4} />
|
||||||
@@ -124,7 +154,7 @@
|
|||||||
<Icon icon={Pen} size={4} />
|
<Icon icon={Pen} size={4} />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<ChannelMessageMenuButton {url} {event} />
|
<ChannelItemMenuButton {url} {event} />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</TapTarget>
|
</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">
|
<script lang="ts">
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
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 Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import EventReport from "@app/components/EventReport.svelte"
|
import EventReport from "@app/components/EventReport.svelte"
|
||||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
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 = () => {
|
const report = () => {
|
||||||
onClick()
|
onClick()
|
||||||
@@ -32,7 +39,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<Button onclick={showInfo}>
|
<Button onclick={showInfo}>
|
||||||
<Icon size={4} icon={Code2} />
|
<Icon size={4} icon={Code2} />
|
||||||
Message Details
|
Show JSON
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{#if event.pubkey === $pubkey}
|
{#if event.pubkey === $pubkey}
|
||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Tippy from "@lib/components/Tippy.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()
|
const {url, event} = $props()
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<Tippy
|
<Tippy
|
||||||
bind:popover
|
bind:popover
|
||||||
component={ChannelMessageMenu}
|
component={ChannelItemMenu}
|
||||||
props={{url, event, onClick}}
|
props={{url, event, onClick}}
|
||||||
params={{trigger: "manual", interactive: true}} />
|
params={{trigger: "manual", interactive: true}} />
|
||||||
</div>
|
</div>
|
||||||
+36
-25
@@ -2,7 +2,14 @@
|
|||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
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 Button from "@lib/components/Button.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||||
import ZapButton from "@app/components/ZapButton.svelte"
|
import ZapButton from "@app/components/ZapButton.svelte"
|
||||||
@@ -10,12 +17,8 @@
|
|||||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {ENABLE_ZAPS} from "@app/core/state"
|
import {ENABLE_ZAPS} from "@app/core/state"
|
||||||
import {publishReaction, canEnforceNip70} from "@app/core/commands"
|
import {publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
|
import {getChannelItemPath} from "@app/util/routes"
|
||||||
import {pushModal} from "@app/util/modal"
|
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 = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -25,6 +28,8 @@
|
|||||||
|
|
||||||
const {url, event, reply}: Props = $props()
|
const {url, event, reply}: Props = $props()
|
||||||
|
|
||||||
|
const path = getChannelItemPath(url, event)
|
||||||
|
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const onEmoji = (async (event: TrustedEvent, url: string, emoji: NativeEmoji) => {
|
const onEmoji = (async (event: TrustedEvent, url: string, emoji: NativeEmoji) => {
|
||||||
@@ -49,29 +54,35 @@
|
|||||||
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="col-2">
|
<div class="flex flex-col gap-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>
|
|
||||||
{#if event.pubkey === $pubkey}
|
{#if event.pubkey === $pubkey}
|
||||||
<Button class="btn btn-neutral text-error" onclick={showDelete}>
|
<Button class="btn btn-neutral text-error" onclick={showDelete}>
|
||||||
<Icon size={4} icon={TrashBin2} />
|
<Icon size={4} icon={TrashBin2} />
|
||||||
Delete Message
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/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>
|
</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 CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import NoteContent from "@app/components/NoteContent.svelte"
|
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
verb,
|
verb,
|
||||||
@@ -19,16 +19,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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>
|
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}
|
{#key event.id}
|
||||||
<NoteContent
|
<NoteContentMinimal trimParent {event} />
|
||||||
{event}
|
|
||||||
hideMediaAtDepth={0}
|
|
||||||
minLength={100}
|
|
||||||
maxLength={300}
|
|
||||||
expandMode="disabled" />
|
|
||||||
{/key}
|
{/key}
|
||||||
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||||
<Icon icon={CloseCircle} />
|
<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,
|
isAddress,
|
||||||
isNewline,
|
isNewline,
|
||||||
} from "@welshman/content"
|
} from "@welshman/content"
|
||||||
|
import type {Parsed} from "@welshman/content"
|
||||||
import {preventDefault, stopPropagation} from "@lib/html"
|
import {preventDefault, stopPropagation} from "@lib/html"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
@@ -39,10 +40,8 @@
|
|||||||
minLength?: number
|
minLength?: number
|
||||||
maxLength?: number
|
maxLength?: number
|
||||||
showEntire?: boolean
|
showEntire?: boolean
|
||||||
hideMediaAtDepth?: number
|
|
||||||
expandMode?: string
|
expandMode?: string
|
||||||
minimalQuote?: boolean
|
trimParent?: boolean
|
||||||
depth?: number
|
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,10 +50,8 @@
|
|||||||
minLength = 500,
|
minLength = 500,
|
||||||
maxLength = 700,
|
maxLength = 700,
|
||||||
showEntire = $bindable(false),
|
showEntire = $bindable(false),
|
||||||
hideMediaAtDepth = 1,
|
|
||||||
expandMode = "block",
|
expandMode = "block",
|
||||||
minimalQuote = false,
|
trimParent = false,
|
||||||
depth = 0,
|
|
||||||
url,
|
url,
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
|
|
||||||
@@ -67,13 +64,13 @@
|
|||||||
const isBlock = (i: number) => {
|
const isBlock = (i: number) => {
|
||||||
const parsed = fullContent[i]
|
const parsed = fullContent[i]
|
||||||
|
|
||||||
if (!parsed || hideMediaAtDepth <= depth) return false
|
if (!parsed) return false
|
||||||
|
|
||||||
if (isLink(parsed) && $userSettingsValues.show_media && isStartAndEnd(i)) {
|
if (isLink(parsed) && $userSettingsValues.show_media && isStartAndEnd(i)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((isEvent(parsed) || isAddress(parsed)) && isStartAndEnd(i)) {
|
if (isQuote(parsed) && isStartAndEnd(i)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +92,8 @@
|
|||||||
|
|
||||||
const isStartAndEnd = (i: number) => isStart(i) && isEnd(i)
|
const isStartAndEnd = (i: number) => isStart(i) && isEnd(i)
|
||||||
|
|
||||||
|
const isQuote = (p: Parsed) => isEvent(p) || isAddress(p)
|
||||||
|
|
||||||
const ignoreWarning = () => {
|
const ignoreWarning = () => {
|
||||||
warning = null
|
warning = null
|
||||||
}
|
}
|
||||||
@@ -103,15 +102,37 @@
|
|||||||
$userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
|
$userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
|
||||||
)
|
)
|
||||||
|
|
||||||
const shortContent = $derived(
|
const dropWhile = <T,>(f: (x: T) => boolean, xs: Iterable<T>) => {
|
||||||
showEntire
|
const result: T[] = []
|
||||||
? fullContent
|
|
||||||
: truncate(fullContent, {
|
for (const x of xs) {
|
||||||
minLength,
|
if (result.length === 0 && f(x)) {
|
||||||
maxLength,
|
continue
|
||||||
mediaLength: hideMediaAtDepth <= depth ? 20 : 200,
|
}
|
||||||
}),
|
|
||||||
)
|
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 hasEllipsis = $derived(shortContent.some(isEllipsis))
|
||||||
const expandInline = $derived(hasEllipsis && expandMode === "inline")
|
const expandInline = $derived(hasEllipsis && expandMode === "inline")
|
||||||
@@ -152,15 +173,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{:else if isProfile(parsed)}
|
{:else if isProfile(parsed)}
|
||||||
<ContentMention value={parsed.value} {url} />
|
<ContentMention value={parsed.value} {url} />
|
||||||
{:else if isEvent(parsed) || isAddress(parsed)}
|
{:else if isQuote(parsed)}
|
||||||
{#if isBlock(i)}
|
{#if isBlock(i)}
|
||||||
<ContentQuote
|
<ContentQuote {url} value={parsed.value} {event} />
|
||||||
{depth}
|
|
||||||
{url}
|
|
||||||
{hideMediaAtDepth}
|
|
||||||
value={parsed.value}
|
|
||||||
{event}
|
|
||||||
minimal={minimalQuote} />
|
|
||||||
{:else}
|
{:else}
|
||||||
<Link
|
<Link
|
||||||
external
|
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 Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import NoteCard from "@app/components/NoteCard.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 {deriveEvent, entityLink} from "@app/core/state"
|
||||||
import {goToEvent} from "@app/util/routes"
|
import {goToEvent} from "@app/util/routes"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: any
|
value: any
|
||||||
hideMediaAtDepth: number
|
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
depth: number
|
|
||||||
url?: string
|
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 {id, identifier, kind, pubkey, relays = []} = value
|
||||||
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
|
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
|
||||||
@@ -43,17 +40,17 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</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 $quote}
|
||||||
{#if minimal && $quote.kind === MESSAGE}
|
{#if $quote.kind === MESSAGE}
|
||||||
<div
|
<div
|
||||||
class="border-l-2 border-solid border-l-primary py-1 pl-2 opacity-90"
|
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%);">
|
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>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<NoteCard event={$quote} {url} class="bg-alt rounded-box p-4">
|
<NoteCard event={$quote} {url} class="bg-alt rounded-box p-4">
|
||||||
<NoteContent {hideMediaAtDepth} {url} event={$quote} depth={depth + 1} />
|
<NoteContentMinimal {url} event={$quote} />
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.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 ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import {goToEvent} from "@app/util/routes"
|
import {goToEvent} from "@app/util/routes"
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<span class="text-nowrap">{formatTimestamp(earliest.created_at)}</span>
|
<span class="text-nowrap">{formatTimestamp(earliest.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
<Content minimalQuote minLength={100} maxLength={400} event={earliest} />
|
<NoteContentMinimal event={earliest} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-13 flex items-center justify-between">
|
<div class="ml-13 flex items-center justify-between">
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
{formatTimestamp(latest.created_at)}
|
{formatTimestamp(latest.created_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Content minimalQuote minLength={100} maxLength={400} event={latest} />
|
<NoteContentMinimal event={latest} />
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {COMMENT} 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 Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import EventReport from "@app/components/EventReport.svelte"
|
import EventReport from "@app/components/EventReport.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 {hasNip29} from "@app/core/state"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
import {makeSpaceChatPath} from "@app/util/routes"
|
||||||
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"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -32,7 +36,14 @@
|
|||||||
|
|
||||||
const showInfo = () => pushModal(EventInfo, {url, event})
|
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})
|
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.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 Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
@@ -11,7 +12,6 @@
|
|||||||
import ChannelName from "@app/components/ChannelName.svelte"
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
import {channelsByUrl} from "@app/core/state"
|
import {channelsByUrl} from "@app/core/state"
|
||||||
import {makeRoomPath} from "@app/util/routes"
|
import {makeRoomPath} from "@app/util/routes"
|
||||||
import {setKey} from "@lib/implicit"
|
|
||||||
|
|
||||||
const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props()
|
const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
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 ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||||
import EventActivity from "@app/components/EventActivity.svelte"
|
import EventActivity from "@app/components/EventActivity.svelte"
|
||||||
import EventActions from "@app/components/EventActions.svelte"
|
import EventActions from "@app/components/EventActions.svelte"
|
||||||
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
import {makeGoalPath} from "@app/util/routes"
|
import {makeGoalPath, makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: any
|
url: string
|
||||||
event: any
|
event: TrustedEvent
|
||||||
|
showRoom?: boolean
|
||||||
showActivity?: boolean
|
showActivity?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, event, showActivity = false}: Props = $props()
|
const {url, event, showRoom, showActivity}: Props = $props()
|
||||||
|
|
||||||
const shouldProtect = canEnforceNip70(url)
|
|
||||||
|
|
||||||
const path = makeGoalPath(url, event.id)
|
const path = makeGoalPath(url, event.id)
|
||||||
|
const room = getTagValue("h", event.tags)
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const deleteReaction = async (event: TrustedEvent) =>
|
const deleteReaction = async (event: TrustedEvent) =>
|
||||||
publishDelete({relays: [url], event, protect: await shouldProtect})
|
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||||
@@ -26,13 +30,16 @@
|
|||||||
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
{#if room && showRoom}
|
||||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full">
|
||||||
<ThunkStatusOrDeleted {event} />
|
Posted in #<ChannelName {room} {url} />
|
||||||
{#if showActivity}
|
</Link>
|
||||||
<EventActivity {url} {path} {event} />
|
{/if}
|
||||||
{/if}
|
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||||
<EventActions {url} {event} hideZap noun="Goal" />
|
<ThunkStatusOrDeleted {event} />
|
||||||
</div>
|
{#if showActivity}
|
||||||
|
<EventActivity {url} {path} {event} />
|
||||||
|
{/if}
|
||||||
|
<EventActions {url} {event} hideZap noun="Goal" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,12 @@
|
|||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {canEnforceNip70} from "@app/core/commands"
|
import {canEnforceNip70} from "@app/core/commands"
|
||||||
|
|
||||||
const {url} = $props()
|
type Props = {
|
||||||
|
url: string
|
||||||
|
room?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, room}: Props = $props()
|
||||||
|
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
@@ -59,6 +64,10 @@
|
|||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
tags.push(["h", room])
|
||||||
|
}
|
||||||
|
|
||||||
publishThunk({
|
publishThunk({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
event: makeEvent(ZAP_GOAL, {content, tags}),
|
event: makeEvent(ZAP_GOAL, {content, tags}),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
import GoalActions from "@app/components/GoalActions.svelte"
|
import GoalActions from "@app/components/GoalActions.svelte"
|
||||||
import GoalSummary from "@app/components/GoalSummary.svelte"
|
import GoalSummary from "@app/components/GoalSummary.svelte"
|
||||||
|
import ChannelLink from "@app/components/ChannelLink.svelte"
|
||||||
import {makeGoalPath} from "@app/util/routes"
|
import {makeGoalPath} from "@app/util/routes"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
const {url, event}: Props = $props()
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
const summary = getTagValue("summary", event.tags)
|
const summary = getTagValue("summary", event.tags)
|
||||||
|
const room = getTagValue("h", event.tags)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeGoalPath(url, event.id)}>
|
<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">
|
<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">
|
<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>
|
</span>
|
||||||
<GoalActions showActivity {url} {event} />
|
<GoalActions showActivity {url} {event} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,11 +9,12 @@
|
|||||||
import ZapButton from "@app/components/ZapButton.svelte"
|
import ZapButton from "@app/components/ZapButton.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url?: string
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, event}: Props = $props()
|
const {url, event, ...props}: Props = $props()
|
||||||
|
|
||||||
const zaps = deriveEventsMapped<Zap>(repository, {
|
const zaps = deriveEventsMapped<Zap>(repository, {
|
||||||
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
const daysOld = Math.ceil((now() - event.created_at) / DAY)
|
const daysOld = Math.ceil((now() - event.created_at) / DAY)
|
||||||
</script>
|
</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 class="flex gap-8">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xl text-primary">{zapAmount} sats</p>
|
<p class="text-xl text-primary">{zapAmount} sats</p>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
import {pushModal, clearModals} from "@app/util/modal"
|
import {pushModal, clearModals} from "@app/util/modal"
|
||||||
import {PLATFORM_NAME, BURROW_URL} from "@app/core/state"
|
import {PLATFORM_NAME, BURROW_URL} from "@app/core/state"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {loadUserData} from "@app/core/requests"
|
|
||||||
import {setChecked} from "@app/util/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
|
|
||||||
let signers: any[] = $state([])
|
let signers: any[] = $state([])
|
||||||
@@ -27,9 +26,7 @@
|
|||||||
|
|
||||||
const signUp = () => pushModal(SignUp)
|
const signUp = () => pushModal(SignUp)
|
||||||
|
|
||||||
const onSuccess = async (session: Session, relays: string[] = []) => {
|
const onSuccess = async (session: Session) => {
|
||||||
await loadUserData(session.pubkey, relays)
|
|
||||||
|
|
||||||
addSession(session)
|
addSession(session)
|
||||||
pushToast({message: "Successfully logged in!"})
|
pushToast({message: "Successfully logged in!"})
|
||||||
setChecked("*")
|
setChecked("*")
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
import BunkerConnect from "@app/components/BunkerConnect.svelte"
|
import BunkerConnect from "@app/components/BunkerConnect.svelte"
|
||||||
import BunkerUrl from "@app/components/BunkerUrl.svelte"
|
import BunkerUrl from "@app/components/BunkerUrl.svelte"
|
||||||
import {Nip46Controller} from "@app/util/nip46"
|
import {Nip46Controller} from "@app/util/nip46"
|
||||||
import {loadUserData} from "@app/core/requests"
|
|
||||||
import {clearModals} from "@app/util/modal"
|
import {clearModals} from "@app/util/modal"
|
||||||
import {setChecked} from "@app/util/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
@@ -33,9 +32,6 @@
|
|||||||
const pubkey = await controller.broker.getPublicKey()
|
const pubkey = await controller.broker.getPublicKey()
|
||||||
|
|
||||||
loginWithNip46(pubkey, controller.clientSecret, response.event.pubkey, SIGNER_RELAYS)
|
loginWithNip46(pubkey, controller.clientSecret, response.event.pubkey, SIGNER_RELAYS)
|
||||||
|
|
||||||
await loadUserData(pubkey)
|
|
||||||
|
|
||||||
setChecked("*")
|
setChecked("*")
|
||||||
clearModals()
|
clearModals()
|
||||||
},
|
},
|
||||||
@@ -75,8 +71,6 @@
|
|||||||
broker.cleanup()
|
broker.cleanup()
|
||||||
controller.stop()
|
controller.stop()
|
||||||
|
|
||||||
await loadUserData(pubkey)
|
|
||||||
|
|
||||||
loginWithNip46(pubkey, clientSecret, signerPubkey, relays)
|
loginWithNip46(pubkey, clientSecret, signerPubkey, relays)
|
||||||
} else {
|
} else {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
||||||
import {loadUserData} from "@app/core/requests"
|
|
||||||
import {clearModals, pushModal} from "@app/util/modal"
|
import {clearModals, pushModal} from "@app/util/modal"
|
||||||
import {setChecked} from "@app/util/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
@@ -96,8 +95,6 @@
|
|||||||
const pubkey = await broker.getPublicKey()
|
const pubkey = await broker.getPublicKey()
|
||||||
const session = makeNip46Session(pubkey, clientSecret, response.event.pubkey, relays)
|
const session = makeNip46Session(pubkey, clientSecret, response.event.pubkey, relays)
|
||||||
|
|
||||||
await loadUserData(pubkey)
|
|
||||||
|
|
||||||
addSession({...session, email})
|
addSession({...session, email})
|
||||||
broker.cleanup()
|
broker.cleanup()
|
||||||
setChecked("*")
|
setChecked("*")
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {displayRelayUrl, getTagValue} from "@welshman/util"
|
import {displayRelayUrl, getTagValue, EVENT_TIME, ZAP_GOAL, THREAD} from "@welshman/util"
|
||||||
import {deriveRelay} from "@welshman/app"
|
import {deriveRelay, repository} from "@welshman/app"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
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 UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
import Exit from "@assets/icons/logout-3.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 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 StarFallMinimalistic from "@assets/icons/star-fall-minimalistic-2.svg?dataurl"
|
||||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||||
import CalendarMinimalistic from "@assets/icons/calendar-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 ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||||
import Bell from "@assets/icons/bell.svg?dataurl"
|
import Bell from "@assets/icons/bell.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Popover from "@lib/components/Popover.svelte"
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
|
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
|
||||||
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
||||||
|
import SpaceDetail from "@app/components/SpaceDetail.svelte"
|
||||||
import SpaceInvite from "@app/components/SpaceInvite.svelte"
|
import SpaceInvite from "@app/components/SpaceInvite.svelte"
|
||||||
import SpaceExit from "@app/components/SpaceExit.svelte"
|
import SpaceExit from "@app/components/SpaceExit.svelte"
|
||||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||||
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import ProfileList from "@app/components/ProfileList.svelte"
|
import ProfileList from "@app/components/ProfileList.svelte"
|
||||||
import AlertAdd from "@app/components/AlertAdd.svelte"
|
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||||
import Alerts from "@app/components/Alerts.svelte"
|
import Alerts from "@app/components/Alerts.svelte"
|
||||||
@@ -36,13 +41,14 @@
|
|||||||
memberships,
|
memberships,
|
||||||
deriveUserRooms,
|
deriveUserRooms,
|
||||||
deriveOtherRooms,
|
deriveOtherRooms,
|
||||||
|
trackerStore,
|
||||||
hasNip29,
|
hasNip29,
|
||||||
alerts,
|
alerts,
|
||||||
deriveUserCanCreateRoom,
|
deriveUserCanCreateRoom,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {notifications} from "@app/util/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath, makeChatPath} from "@app/util/routes"
|
||||||
|
|
||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
@@ -53,8 +59,21 @@
|
|||||||
const calendarPath = makeSpacePath(url, "calendar")
|
const calendarPath = makeSpacePath(url, "calendar")
|
||||||
const userRooms = deriveUserRooms(url)
|
const userRooms = deriveUserRooms(url)
|
||||||
const otherRooms = deriveOtherRooms(url)
|
const otherRooms = deriveOtherRooms(url)
|
||||||
|
const owner = $derived($relay?.profile?.pubkey)
|
||||||
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
|
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 = () => {
|
const openMenu = () => {
|
||||||
showMenu = true
|
showMenu = true
|
||||||
}
|
}
|
||||||
@@ -63,6 +82,8 @@
|
|||||||
showMenu = !showMenu
|
showMenu = !showMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showDetail = () => pushModal(SpaceDetail, {url}, {replaceState})
|
||||||
|
|
||||||
const showMembers = () =>
|
const showMembers = () =>
|
||||||
pushModal(
|
pushModal(
|
||||||
ProfileList,
|
ProfileList,
|
||||||
@@ -103,17 +124,28 @@
|
|||||||
<div bind:this={element} class="flex h-full flex-col justify-between">
|
<div bind:this={element} class="flex h-full flex-col justify-between">
|
||||||
<SecondaryNavSection>
|
<SecondaryNavSection>
|
||||||
<div>
|
<div>
|
||||||
<SecondaryNavItem class="w-full !justify-between" onclick={openMenu}>
|
<Button
|
||||||
<strong class="ellipsize flex items-center gap-3">
|
class="flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
||||||
{displayRelayUrl(url)}
|
onclick={openMenu}>
|
||||||
</strong>
|
<div class="flex items-center justify-between">
|
||||||
<Icon icon={AltArrowDown} />
|
<strong class="ellipsize flex items-center gap-3">
|
||||||
</SecondaryNavItem>
|
<RelayName {url} />
|
||||||
|
</strong>
|
||||||
|
<Icon icon={AltArrowDown} />
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-primary">{displayRelayUrl(url)}</span>
|
||||||
|
</Button>
|
||||||
{#if showMenu}
|
{#if showMenu}
|
||||||
<Popover hideOnClick onClose={toggleMenu}>
|
<Popover hideOnClick onClose={toggleMenu}>
|
||||||
<ul
|
<ul
|
||||||
transition:fly
|
transition:fly
|
||||||
class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-xl">
|
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>
|
<li>
|
||||||
<Button onclick={showMembers}>
|
<Button onclick={showMembers}>
|
||||||
<Icon icon={UserRounded} />
|
<Icon icon={UserRounded} />
|
||||||
@@ -126,6 +158,14 @@
|
|||||||
Create Invite
|
Create Invite
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
|
{#if owner}
|
||||||
|
<li>
|
||||||
|
<Link href={makeChatPath([owner])}>
|
||||||
|
<Icon icon={Letter} />
|
||||||
|
Contact Owner
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
<li>
|
<li>
|
||||||
{#if $userRoomsByUrl.has(url)}
|
{#if $userRoomsByUrl.has(url)}
|
||||||
<Button onclick={leaveSpace} class="text-error">
|
<Button onclick={leaveSpace} class="text-error">
|
||||||
@@ -144,10 +184,19 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto">
|
<div class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto">
|
||||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url)}>
|
{#if hasNip29($relay)}
|
||||||
<Icon icon={HomeSmile} /> Home
|
<SecondaryNavItem {replaceState} href={makeSpacePath(url, "recent")}>
|
||||||
</SecondaryNavItem>
|
<Icon icon={History} /> Recent Activity
|
||||||
{#if ENABLE_ZAPS}
|
</SecondaryNavItem>
|
||||||
|
{:else}
|
||||||
|
<SecondaryNavItem
|
||||||
|
{replaceState}
|
||||||
|
href={chatPath}
|
||||||
|
notification={$notifications.has(chatPath)}>
|
||||||
|
<Icon icon={ChatRound} /> Chat
|
||||||
|
</SecondaryNavItem>
|
||||||
|
{/if}
|
||||||
|
{#if ENABLE_ZAPS && spaceKinds.has(ZAP_GOAL)}
|
||||||
<SecondaryNavItem
|
<SecondaryNavItem
|
||||||
{replaceState}
|
{replaceState}
|
||||||
href={goalsPath}
|
href={goalsPath}
|
||||||
@@ -155,18 +204,22 @@
|
|||||||
<Icon icon={StarFallMinimalistic} /> Goals
|
<Icon icon={StarFallMinimalistic} /> Goals
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
<SecondaryNavItem
|
{#if spaceKinds.has(THREAD)}
|
||||||
{replaceState}
|
<SecondaryNavItem
|
||||||
href={threadsPath}
|
{replaceState}
|
||||||
notification={$notifications.has(threadsPath)}>
|
href={threadsPath}
|
||||||
<Icon icon={NotesMinimalistic} /> Threads
|
notification={$notifications.has(threadsPath)}>
|
||||||
</SecondaryNavItem>
|
<Icon icon={NotesMinimalistic} /> Threads
|
||||||
<SecondaryNavItem
|
</SecondaryNavItem>
|
||||||
{replaceState}
|
{/if}
|
||||||
href={calendarPath}
|
{#if spaceKinds.has(EVENT_TIME)}
|
||||||
notification={$notifications.has(calendarPath)}>
|
<SecondaryNavItem
|
||||||
<Icon icon={CalendarMinimalistic} /> Calendar
|
{replaceState}
|
||||||
</SecondaryNavItem>
|
href={calendarPath}
|
||||||
|
notification={$notifications.has(calendarPath)}>
|
||||||
|
<Icon icon={CalendarMinimalistic} /> Calendar
|
||||||
|
</SecondaryNavItem>
|
||||||
|
{/if}
|
||||||
{#if hasNip29($relay)}
|
{#if hasNip29($relay)}
|
||||||
{#if $userRooms.length > 0}
|
{#if $userRooms.length > 0}
|
||||||
<div class="h-2"></div>
|
<div class="h-2"></div>
|
||||||
@@ -194,13 +247,6 @@
|
|||||||
Create room
|
Create room
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
|
||||||
<SecondaryNavItem
|
|
||||||
{replaceState}
|
|
||||||
href={chatPath}
|
|
||||||
notification={$notifications.has(chatPath)}>
|
|
||||||
<Icon icon={ChatRound} /> Chat
|
|
||||||
</SecondaryNavItem>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</SecondaryNavSection>
|
</SecondaryNavSection>
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {ComponentProps} from "svelte"
|
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 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()
|
const props: ComponentProps<typeof Content> = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if props.event.kind === EVENT_TIME}
|
{#if props.event.kind === EVENT_TIME}
|
||||||
<div class="flex items-start gap-4">
|
<NoteContentEventTime {...props} />
|
||||||
<CalendarEventDate event={props.event} />
|
{:else if props.event.kind === THREAD}
|
||||||
<div class="flex flex-grow flex-col">
|
<NoteContentThread {...props} />
|
||||||
<CalendarEventHeader event={props.event} />
|
{:else if props.event.kind === ZAP_GOAL}
|
||||||
<div class="flex py-2 opacity-50">
|
<NoteContentGoal {...props} />
|
||||||
<div class="h-px flex-grow bg-base-content opacity-25"></div>
|
|
||||||
</div>
|
|
||||||
<Content {...props} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Content {...props} />
|
<Content {...props} />
|
||||||
{/if}
|
{/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">
|
<script lang="ts">
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
||||||
|
import {encodeRelay} from "@app/core/state"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
import {lastPageBySpaceUrl} from "@app/util/history"
|
||||||
import {notifications} from "@app/util/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
|
|
||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
const path = makeSpacePath(url)
|
const path = makeSpacePath(url)
|
||||||
|
|
||||||
|
const onClick = () => goto(lastPageBySpaceUrl.get(encodeRelay(url)) || path)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
|
onclick={onClick}
|
||||||
title={displayRelayUrl(url)}
|
title={displayRelayUrl(url)}
|
||||||
href={path}
|
|
||||||
class="tooltip-right"
|
class="tooltip-right"
|
||||||
notification={$notifications.has(path)}>
|
notification={$notifications.has(path)}>
|
||||||
<SpaceAvatar {url} />
|
<SpaceAvatar {url} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {removeNil} from "@welshman/lib"
|
import {removeNil} from "@welshman/lib"
|
||||||
import {deriveProfile} from "@welshman/app"
|
import {deriveProfile} from "@welshman/app"
|
||||||
import Content from "@app/components/Content.svelte"
|
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
@@ -14,5 +14,5 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $profile}
|
{#if $profile}
|
||||||
<Content event={{content: $profile.about || "", tags: []}} hideMediaAtDepth={0} />
|
<ContentMinimal event={{content: $profile.about || "", tags: []}} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
||||||
</script>
|
</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} />
|
@<ProfileName {pubkey} {url} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-tip={`This content has been reported as "${displayList(reportReasons)}".`}
|
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}
|
class:tooltip={!noTooltip && !isMobile}
|
||||||
onclick={stopPropagation(preventDefault(onReportClick))}>
|
onclick={stopPropagation(preventDefault(onReportClick))}>
|
||||||
<Icon icon={Danger} />
|
<Icon icon={Danger} />
|
||||||
@@ -134,11 +134,10 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-tip={tooltip}
|
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:tooltip={!noTooltip && !isMobile}
|
||||||
class:border={isOwn}
|
class:btn-neutral={!isOwn}
|
||||||
class:border-solid={isOwn}
|
class:btn-primary={isOwn}>
|
||||||
class:border-primary={isOwn}>
|
|
||||||
<Reaction event={zaps[0].request} />
|
<Reaction event={zaps[0].request} />
|
||||||
<span>{amount}</span>
|
<span>{amount}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -152,11 +151,10 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-tip={tooltip}
|
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:tooltip={!noTooltip && !isMobile}
|
||||||
class:border={isOwn}
|
class:btn-neutral={!isOwn}
|
||||||
class:border-solid={isOwn}
|
class:btn-primary={isOwn}
|
||||||
class:border-primary={isOwn}
|
|
||||||
onclick={stopPropagation(preventDefault(onClick))}>
|
onclick={stopPropagation(preventDefault(onClick))}>
|
||||||
<Reaction event={events[0]} />
|
<Reaction event={events[0]} />
|
||||||
{#if events.length > 1}
|
{#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">
|
<script lang="ts">
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
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 ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||||
import EventActivity from "@app/components/EventActivity.svelte"
|
import EventActivity from "@app/components/EventActivity.svelte"
|
||||||
import EventActions from "@app/components/EventActions.svelte"
|
import EventActions from "@app/components/EventActions.svelte"
|
||||||
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
import {makeThreadPath} from "@app/util/routes"
|
import {makeThreadPath, makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: any
|
url: string
|
||||||
event: any
|
event: TrustedEvent
|
||||||
|
showRoom?: boolean
|
||||||
showActivity?: boolean
|
showActivity?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, event, showActivity = false}: Props = $props()
|
const {url, event, showRoom, showActivity}: Props = $props()
|
||||||
|
|
||||||
const shouldProtect = canEnforceNip70(url)
|
|
||||||
|
|
||||||
const path = makeThreadPath(url, event.id)
|
const path = makeThreadPath(url, event.id)
|
||||||
|
const room = getTagValue("h", event.tags)
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const deleteReaction = async (event: TrustedEvent) =>
|
const deleteReaction = async (event: TrustedEvent) =>
|
||||||
publishDelete({relays: [url], event, protect: await shouldProtect})
|
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||||
@@ -26,13 +30,16 @@
|
|||||||
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
{#if room && showRoom}
|
||||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
<Link href={makeSpacePath(url, room)} class="btn btn-neutral btn-xs rounded-full">
|
||||||
<ThunkStatusOrDeleted {event} />
|
Posted in #<ChannelName {room} {url} />
|
||||||
{#if showActivity}
|
</Link>
|
||||||
<EventActivity {url} {path} {event} />
|
{/if}
|
||||||
{/if}
|
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||||
<EventActions {url} {event} noun="Thread" />
|
<ThunkStatusOrDeleted {event} />
|
||||||
</div>
|
{#if showActivity}
|
||||||
|
<EventActivity {url} {path} {event} />
|
||||||
|
{/if}
|
||||||
|
<EventActions {url} {event} noun="Thread" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,12 @@
|
|||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {canEnforceNip70} from "@app/core/commands"
|
import {canEnforceNip70} from "@app/core/commands"
|
||||||
|
|
||||||
const {url} = $props()
|
type Props = {
|
||||||
|
url: string
|
||||||
|
room?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, room}: Props = $props()
|
||||||
|
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
@@ -52,6 +57,10 @@
|
|||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
tags.push(["h", room])
|
||||||
|
}
|
||||||
|
|
||||||
publishThunk({
|
publishThunk({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
event: makeEvent(THREAD, {content, tags}),
|
event: makeEvent(THREAD, {content, tags}),
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {nthEq, formatTimestamp} from "@welshman/lib"
|
import {formatTimestamp} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {getTagValue} from "@welshman/util"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
import ThreadActions from "@app/components/ThreadActions.svelte"
|
import ThreadActions from "@app/components/ThreadActions.svelte"
|
||||||
|
import ChannelLink from "@app/components/ChannelLink.svelte"
|
||||||
import {makeThreadPath} from "@app/util/routes"
|
import {makeThreadPath} from "@app/util/routes"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -14,7 +16,8 @@
|
|||||||
|
|
||||||
const {url, event}: Props = $props()
|
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>
|
</script>
|
||||||
|
|
||||||
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeThreadPath(url, event.id)}>
|
<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" />
|
<Content {event} {url} expandMode="inline" />
|
||||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
<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">
|
<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>
|
</span>
|
||||||
<ThreadActions showActivity {url} {event} />
|
<ThreadActions showActivity {url} {event} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url?: string
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
children: Snippet
|
children: Snippet
|
||||||
replaceState?: boolean
|
replaceState?: boolean
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import {get, writable} from "svelte/store"
|
import {get, writable} from "svelte/store"
|
||||||
import {
|
import {
|
||||||
partition,
|
partition,
|
||||||
chunk,
|
|
||||||
sample,
|
|
||||||
sleep,
|
|
||||||
shuffle,
|
|
||||||
uniq,
|
uniq,
|
||||||
int,
|
int,
|
||||||
YEAR,
|
YEAR,
|
||||||
@@ -18,12 +14,9 @@ import {
|
|||||||
fromPairs,
|
fromPairs,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
MESSAGE,
|
|
||||||
DELETE,
|
DELETE,
|
||||||
THREAD,
|
|
||||||
EVENT_TIME,
|
EVENT_TIME,
|
||||||
AUTH_INVITE,
|
AUTH_INVITE,
|
||||||
COMMENT,
|
|
||||||
ALERT_EMAIL,
|
ALERT_EMAIL,
|
||||||
ALERT_WEB,
|
ALERT_WEB,
|
||||||
ALERT_IOS,
|
ALERT_IOS,
|
||||||
@@ -47,24 +40,10 @@ import {
|
|||||||
thunkQueue,
|
thunkQueue,
|
||||||
makeFeedController,
|
makeFeedController,
|
||||||
loadRelay,
|
loadRelay,
|
||||||
loadMutes,
|
|
||||||
loadFollows,
|
|
||||||
loadProfile,
|
|
||||||
loadBlossomServers,
|
|
||||||
loadRelaySelections,
|
|
||||||
loadInboxRelaySelections,
|
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {createScroller} from "@lib/html"
|
import {createScroller} from "@lib/html"
|
||||||
import {daysBetween} from "@lib/util"
|
import {daysBetween} from "@lib/util"
|
||||||
import {
|
import {NOTIFIER_RELAY, getUrlsForEvent} from "@app/core/state"
|
||||||
NOTIFIER_RELAY,
|
|
||||||
INDEXER_RELAYS,
|
|
||||||
defaultPubkeys,
|
|
||||||
userRoomsByUrl,
|
|
||||||
getUrlsForEvent,
|
|
||||||
loadMembership,
|
|
||||||
loadSettings,
|
|
||||||
} from "@app/core/state"
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
@@ -359,80 +338,6 @@ export const loadAlertStatuses = (pubkey: string) =>
|
|||||||
filters: [{kinds: [ALERT_STATUS], "#p": [pubkey]}],
|
filters: [{kinds: [ALERT_STATUS], "#p": [pubkey]}],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Application requests
|
|
||||||
|
|
||||||
export const listenForNotifications = () => {
|
|
||||||
const controller = new AbortController()
|
|
||||||
|
|
||||||
for (const [url, allRooms] of userRoomsByUrl.get()) {
|
|
||||||
// Limit how many rooms we load at a time, since we have to send a separate filter
|
|
||||||
// for each one due to relay29 being picky
|
|
||||||
const rooms = shuffle(Array.from(allRooms)).slice(0, 30)
|
|
||||||
|
|
||||||
load({
|
|
||||||
signal: controller.signal,
|
|
||||||
relays: [url],
|
|
||||||
filters: [
|
|
||||||
{kinds: [THREAD], limit: 1},
|
|
||||||
{kinds: [MESSAGE], limit: 1},
|
|
||||||
{kinds: [COMMENT], "#K": [String(THREAD)], limit: 1},
|
|
||||||
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
request({
|
|
||||||
signal: controller.signal,
|
|
||||||
relays: [url],
|
|
||||||
filters: [
|
|
||||||
{kinds: [THREAD], since: now()},
|
|
||||||
{kinds: [MESSAGE], since: now()},
|
|
||||||
{kinds: [COMMENT], "#K": [String(THREAD)], since: now()},
|
|
||||||
...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => controller.abort()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadUserData = async (pubkey: string, relays: string[] = []) => {
|
|
||||||
await Promise.race([sleep(3000), loadRelaySelections(pubkey, relays)])
|
|
||||||
|
|
||||||
const promise = Promise.race([
|
|
||||||
sleep(3000),
|
|
||||||
Promise.all([
|
|
||||||
loadInboxRelaySelections(pubkey, relays),
|
|
||||||
loadBlossomServers(pubkey, relays),
|
|
||||||
loadMembership(pubkey, relays),
|
|
||||||
loadSettings(pubkey, relays),
|
|
||||||
loadProfile(pubkey, relays),
|
|
||||||
loadFollows(pubkey, relays),
|
|
||||||
loadMutes(pubkey, relays),
|
|
||||||
loadAlertStatuses(pubkey),
|
|
||||||
loadAlerts(pubkey),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
|
|
||||||
// Load followed profiles slowly in the background without clogging other stuff up. Only use a single
|
|
||||||
// indexer relay to avoid too many redundant validations, which slow things down and eat bandwidth
|
|
||||||
promise.then(async () => {
|
|
||||||
for (const pubkeys of chunk(50, get(defaultPubkeys))) {
|
|
||||||
const relays = sample(1, INDEXER_RELAYS)
|
|
||||||
|
|
||||||
await sleep(1000)
|
|
||||||
|
|
||||||
for (const pubkey of pubkeys) {
|
|
||||||
loadMembership(pubkey, relays)
|
|
||||||
loadProfile(pubkey, relays)
|
|
||||||
loadFollows(pubkey, relays)
|
|
||||||
loadMutes(pubkey, relays)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
export const discoverRelays = (lists: List[]) =>
|
export const discoverRelays = (lists: List[]) =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
uniq(lists.flatMap($l => getRelaysFromList($l)))
|
uniq(lists.flatMap($l => getRelaysFromList($l)))
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
import {page} from "$app/stores"
|
||||||
|
import type {Unsubscriber} from "svelte/store"
|
||||||
|
import {derived, get} from "svelte/store"
|
||||||
|
import {call, chunk, sleep, now, identity, WEEK, ago} from "@welshman/lib"
|
||||||
|
import {
|
||||||
|
getListTags,
|
||||||
|
getRelayTagValues,
|
||||||
|
WRAP,
|
||||||
|
MESSAGE,
|
||||||
|
ZAP_GOAL,
|
||||||
|
THREAD,
|
||||||
|
EVENT_TIME,
|
||||||
|
COMMENT,
|
||||||
|
isSignedEvent,
|
||||||
|
} from "@welshman/util"
|
||||||
|
import {request, pull} from "@welshman/net"
|
||||||
|
import {
|
||||||
|
pubkey,
|
||||||
|
loadRelay,
|
||||||
|
userFollows,
|
||||||
|
userRelaySelections,
|
||||||
|
userInboxRelaySelections,
|
||||||
|
loadRelaySelections,
|
||||||
|
loadInboxRelaySelections,
|
||||||
|
loadBlossomServers,
|
||||||
|
loadFollows,
|
||||||
|
loadMutes,
|
||||||
|
loadProfile,
|
||||||
|
repository,
|
||||||
|
} from "@welshman/app"
|
||||||
|
import {
|
||||||
|
INDEXER_RELAYS,
|
||||||
|
canDecrypt,
|
||||||
|
loadSettings,
|
||||||
|
userMembership,
|
||||||
|
defaultPubkeys,
|
||||||
|
decodeRelay,
|
||||||
|
loadMembership,
|
||||||
|
} from "@app/core/state"
|
||||||
|
import {loadAlerts, loadAlertStatuses} from "@app/core/requests"
|
||||||
|
|
||||||
|
const syncRelays = () => {
|
||||||
|
for (const url of INDEXER_RELAYS) {
|
||||||
|
loadRelay(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribePage = page.subscribe($page => {
|
||||||
|
if ($page.params.relay) {
|
||||||
|
loadRelay(decodeRelay($page.params.relay))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const unsubscribeMembership = userMembership.subscribe($l => {
|
||||||
|
for (const url of getRelayTagValues(getListTags($l))) {
|
||||||
|
loadRelay(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribePage()
|
||||||
|
unsubscribeMembership()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncUserData = () => {
|
||||||
|
const unsubscribePubkey = pubkey.subscribe($pubkey => {
|
||||||
|
if ($pubkey) {
|
||||||
|
loadRelaySelections($pubkey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const unsubscribeSelections = userRelaySelections.subscribe($l => {
|
||||||
|
const $pubkey = pubkey.get()
|
||||||
|
|
||||||
|
if ($pubkey) {
|
||||||
|
loadAlerts($pubkey)
|
||||||
|
loadAlertStatuses($pubkey)
|
||||||
|
loadBlossomServers($pubkey)
|
||||||
|
loadFollows($pubkey)
|
||||||
|
loadMembership($pubkey)
|
||||||
|
loadMutes($pubkey)
|
||||||
|
loadProfile($pubkey)
|
||||||
|
loadSettings($pubkey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const unsubscribeFollows = userFollows.subscribe(async $l => {
|
||||||
|
for (const pubkeys of chunk(10, get(defaultPubkeys))) {
|
||||||
|
// This isn't urgent, avoid clogging other stuff up
|
||||||
|
await sleep(1000)
|
||||||
|
|
||||||
|
for (const pk of pubkeys) {
|
||||||
|
loadRelaySelections(pk).then(() => {
|
||||||
|
loadMembership(pk)
|
||||||
|
loadProfile(pk)
|
||||||
|
loadFollows(pk)
|
||||||
|
loadMutes(pk)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribePubkey()
|
||||||
|
unsubscribeSelections()
|
||||||
|
unsubscribeFollows()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncSpace = (url: string) => {
|
||||||
|
const controller = new AbortController()
|
||||||
|
|
||||||
|
// Load historical data
|
||||||
|
pull({
|
||||||
|
relays: [url],
|
||||||
|
signal: controller.signal,
|
||||||
|
filters: [{kinds: [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE, COMMENT]}],
|
||||||
|
events: repository
|
||||||
|
.query([{kinds: [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE, COMMENT]}])
|
||||||
|
.filter(isSignedEvent),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load new events
|
||||||
|
request({
|
||||||
|
relays: [url],
|
||||||
|
signal: controller.signal,
|
||||||
|
filters: [{kinds: [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE, COMMENT], since: now()}],
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => controller.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncSpaces = () => {
|
||||||
|
const unsubscribersByUrl = new Map<string, Unsubscriber>()
|
||||||
|
const unsubscribeMembership = userMembership.subscribe($l => {
|
||||||
|
const urls = getRelayTagValues(getListTags($l))
|
||||||
|
|
||||||
|
// Start syncing newly added spaces
|
||||||
|
for (const url of urls) {
|
||||||
|
if (!unsubscribersByUrl.has(url)) {
|
||||||
|
unsubscribersByUrl.set(url, syncSpace(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop syncing removed spaces
|
||||||
|
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
|
||||||
|
if (!urls.includes(url)) {
|
||||||
|
unsubscribersByUrl.delete(url)
|
||||||
|
unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Array.from(unsubscribersByUrl.values()).forEach(call)
|
||||||
|
unsubscribeMembership()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncDMRelay = (url: string, pubkey: string) => {
|
||||||
|
const controller = new AbortController()
|
||||||
|
|
||||||
|
// Load historical data
|
||||||
|
pull({
|
||||||
|
relays: [url],
|
||||||
|
signal: controller.signal,
|
||||||
|
filters: [{kinds: [WRAP], "#p": [pubkey], until: ago(WEEK, 2)}],
|
||||||
|
events: repository
|
||||||
|
.query([{kinds: [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE, COMMENT]}])
|
||||||
|
.filter(isSignedEvent),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load new events
|
||||||
|
request({
|
||||||
|
relays: [url],
|
||||||
|
signal: controller.signal,
|
||||||
|
filters: [{kinds: [WRAP], "#p": [pubkey], since: ago(WEEK, 2)}],
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => controller.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncDMs = () => {
|
||||||
|
const unsubscribersByUrl = new Map<string, Unsubscriber>()
|
||||||
|
|
||||||
|
let currentPubkey: string | undefined
|
||||||
|
|
||||||
|
const unsubscribeAll = () => {
|
||||||
|
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
|
||||||
|
unsubscribersByUrl.delete(url)
|
||||||
|
unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscribeAll = (pubkey: string, urls: string[]) => {
|
||||||
|
// Start syncing newly added relays
|
||||||
|
for (const url of urls) {
|
||||||
|
if (!unsubscribersByUrl.has(url)) {
|
||||||
|
unsubscribersByUrl.set(url, syncDMRelay(url, pubkey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop syncing removed spaces
|
||||||
|
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
|
||||||
|
if (!urls.includes(url)) {
|
||||||
|
unsubscribersByUrl.delete(url)
|
||||||
|
unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When pubkey changes, re-sync
|
||||||
|
const unsubscribePubkey = derived([pubkey, canDecrypt], identity).subscribe(
|
||||||
|
([$pubkey, $canDecrypt]) => {
|
||||||
|
if ($pubkey !== currentPubkey) {
|
||||||
|
unsubscribeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a pubkey, refresh our user's relay selections then sync our subscriptions
|
||||||
|
if ($pubkey && $canDecrypt) {
|
||||||
|
loadRelaySelections($pubkey)
|
||||||
|
.then(() => loadInboxRelaySelections($pubkey))
|
||||||
|
.then($l => subscribeAll($pubkey, getRelayTagValues(getListTags($l))))
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPubkey = $pubkey
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// When user inbox relays change, update synchronization
|
||||||
|
const unsubscribeSelections = userInboxRelaySelections.subscribe($l => {
|
||||||
|
const $pubkey = pubkey.get()
|
||||||
|
|
||||||
|
if ($pubkey && $l) {
|
||||||
|
subscribeAll($pubkey, getRelayTagValues(getListTags($l)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribeAll()
|
||||||
|
unsubscribePubkey()
|
||||||
|
unsubscribeSelections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const syncApplicationData = () => {
|
||||||
|
const unsubscribers = [syncRelays(), syncUserData(), syncSpaces(), syncDMs()]
|
||||||
|
|
||||||
|
return () => unsubscribers.forEach(call)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import {page} from "$app/stores"
|
||||||
|
|
||||||
|
export const lastPageBySpaceUrl = new Map<string, string>()
|
||||||
|
|
||||||
|
export const setupHistory = () => {
|
||||||
|
page.subscribe($page => {
|
||||||
|
if ($page.params.relay) {
|
||||||
|
lastPageBySpaceUrl.set($page.params.relay, $page.url.pathname)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,10 +3,11 @@ import {synced, throttled} from "@welshman/store"
|
|||||||
import {pubkey, relaysByUrl} from "@welshman/app"
|
import {pubkey, relaysByUrl} from "@welshman/app"
|
||||||
import {prop, spec, identity, now, groupBy} from "@welshman/lib"
|
import {prop, spec, identity, now, groupBy} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {EVENT_TIME, MESSAGE, THREAD, COMMENT, getTagValue} from "@welshman/util"
|
import {ZAP_GOAL, EVENT_TIME, MESSAGE, THREAD, COMMENT, getTagValue} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
makeSpacePath,
|
makeSpacePath,
|
||||||
makeChatPath,
|
makeChatPath,
|
||||||
|
makeGoalPath,
|
||||||
makeThreadPath,
|
makeThreadPath,
|
||||||
makeCalendarPath,
|
makeCalendarPath,
|
||||||
makeSpaceChatPath,
|
makeSpaceChatPath,
|
||||||
@@ -76,45 +77,52 @@ export const notifications = derived(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allThreadEvents = $repository.query([
|
const allGoalComments = $repository.query([{kinds: [COMMENT], "#K": [String(ZAP_GOAL)]}])
|
||||||
{kinds: [THREAD]},
|
|
||||||
{kinds: [COMMENT], "#K": [String(THREAD)]},
|
|
||||||
])
|
|
||||||
|
|
||||||
const allCalendarEvents = $repository.query([
|
const allThreadComments = $repository.query([{kinds: [COMMENT], "#K": [String(THREAD)]}])
|
||||||
{kinds: [EVENT_TIME]},
|
|
||||||
{kinds: [COMMENT], "#K": [String(EVENT_TIME)]},
|
|
||||||
])
|
|
||||||
|
|
||||||
const allMessageEvents = $repository.query([{kinds: [MESSAGE]}])
|
const allCalendarComments = $repository.query([{kinds: [COMMENT], "#K": [String(EVENT_TIME)]}])
|
||||||
|
|
||||||
|
const allMessages = $repository.query([{kinds: [MESSAGE, THREAD, ZAP_GOAL, EVENT_TIME]}])
|
||||||
|
|
||||||
for (const [url, rooms] of $userRoomsByUrl.entries()) {
|
for (const [url, rooms] of $userRoomsByUrl.entries()) {
|
||||||
const spacePath = makeSpacePath(url)
|
const spacePath = makeSpacePath(url)
|
||||||
|
const goalPath = makeGoalPath(url)
|
||||||
const threadPath = makeThreadPath(url)
|
const threadPath = makeThreadPath(url)
|
||||||
const calendarPath = makeCalendarPath(url)
|
const calendarPath = makeCalendarPath(url)
|
||||||
const messagesPath = makeSpaceChatPath(url)
|
const messagesPath = makeSpaceChatPath(url)
|
||||||
const threadEvents = allThreadEvents.filter(e => $getUrlsForEvent(e.id).includes(url))
|
const goalComments = allGoalComments.filter(e => $getUrlsForEvent(e.id).includes(url))
|
||||||
const calendarEvents = allCalendarEvents.filter(e => $getUrlsForEvent(e.id).includes(url))
|
const threadComments = allThreadComments.filter(e => $getUrlsForEvent(e.id).includes(url))
|
||||||
const messagesEvents = allMessageEvents.filter(e => $getUrlsForEvent(e.id).includes(url))
|
const calendarComments = allCalendarComments.filter(e => $getUrlsForEvent(e.id).includes(url))
|
||||||
|
const messages = allMessages.filter(e => $getUrlsForEvent(e.id).includes(url))
|
||||||
|
|
||||||
if (hasNotification(threadPath, threadEvents[0])) {
|
const commentsByGoalId = groupBy(
|
||||||
paths.add(spacePath)
|
e => getTagValue("E", e.tags),
|
||||||
paths.add(threadPath)
|
goalComments.filter(spec({kind: COMMENT})),
|
||||||
}
|
)
|
||||||
|
|
||||||
if (hasNotification(calendarPath, calendarEvents[0])) {
|
for (const [goalId, [comment]] of commentsByGoalId.entries()) {
|
||||||
paths.add(spacePath)
|
const goalItemPath = makeGoalPath(url, goalId)
|
||||||
paths.add(calendarPath)
|
|
||||||
|
if (hasNotification(goalPath, comment)) {
|
||||||
|
paths.add(goalPath)
|
||||||
|
}
|
||||||
|
if (hasNotification(goalItemPath, comment)) {
|
||||||
|
paths.add(goalItemPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const commentsByThreadId = groupBy(
|
const commentsByThreadId = groupBy(
|
||||||
e => getTagValue("E", e.tags),
|
e => getTagValue("E", e.tags),
|
||||||
threadEvents.filter(spec({kind: COMMENT})),
|
threadComments.filter(spec({kind: COMMENT})),
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const [threadId, [comment]] of commentsByThreadId.entries()) {
|
for (const [threadId, [comment]] of commentsByThreadId.entries()) {
|
||||||
const threadItemPath = makeThreadPath(url, threadId)
|
const threadItemPath = makeThreadPath(url, threadId)
|
||||||
|
|
||||||
|
if (hasNotification(threadPath, comment)) {
|
||||||
|
paths.add(threadPath)
|
||||||
|
}
|
||||||
if (hasNotification(threadItemPath, comment)) {
|
if (hasNotification(threadItemPath, comment)) {
|
||||||
paths.add(threadItemPath)
|
paths.add(threadItemPath)
|
||||||
}
|
}
|
||||||
@@ -122,24 +130,26 @@ export const notifications = derived(
|
|||||||
|
|
||||||
const commentsByEventId = groupBy(
|
const commentsByEventId = groupBy(
|
||||||
e => getTagValue("E", e.tags),
|
e => getTagValue("E", e.tags),
|
||||||
calendarEvents.filter(spec({kind: COMMENT})),
|
calendarComments.filter(spec({kind: COMMENT})),
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const [eventId, [comment]] of commentsByEventId.entries()) {
|
for (const [eventId, [comment]] of commentsByEventId.entries()) {
|
||||||
const calendarEventPath = makeCalendarPath(url, eventId)
|
const calendarItemPath = makeCalendarPath(url, eventId)
|
||||||
|
|
||||||
if (hasNotification(calendarEventPath, comment)) {
|
if (hasNotification(calendarPath, comment)) {
|
||||||
paths.add(calendarEventPath)
|
paths.add(calendarPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNotification(calendarItemPath, comment)) {
|
||||||
|
paths.add(calendarItemPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasNip29($relaysByUrl.get(url))) {
|
if (hasNip29($relaysByUrl.get(url))) {
|
||||||
for (const room of rooms) {
|
for (const room of rooms) {
|
||||||
const roomPath = makeRoomPath(url, room)
|
const roomPath = makeRoomPath(url, room)
|
||||||
const latestEvent = allMessageEvents.find(
|
const latestEvent = allMessages.find(
|
||||||
e =>
|
e => $getUrlsForEvent(e.id).includes(url) && e.tags.find(spec(["h", room])),
|
||||||
$getUrlsForEvent(e.id).includes(url) &&
|
|
||||||
e.tags.find(t => t[0] === "h" && t[1] === room),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (hasNotification(roomPath, latestEvent)) {
|
if (hasNotification(roomPath, latestEvent)) {
|
||||||
@@ -148,7 +158,7 @@ export const notifications = derived(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (hasNotification(messagesPath, messagesEvents[0])) {
|
if (hasNotification(messagesPath, messages[0])) {
|
||||||
paths.add(spacePath)
|
paths.add(spacePath)
|
||||||
paths.add(messagesPath)
|
paths.add(messagesPath)
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-1
@@ -3,7 +3,7 @@ import * as nip19 from "nostr-tools/nip19"
|
|||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {nthEq, sleep} from "@welshman/lib"
|
import {nthEq, sleep} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {tracker} from "@welshman/app"
|
import {tracker, relaysByUrl} from "@welshman/app"
|
||||||
import {scrollToEvent} from "@lib/html"
|
import {scrollToEvent} from "@lib/html"
|
||||||
import {identity} from "@welshman/lib"
|
import {identity} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
decodeRelay,
|
decodeRelay,
|
||||||
encodeRelay,
|
encodeRelay,
|
||||||
userRoomsByUrl,
|
userRoomsByUrl,
|
||||||
|
hasNip29,
|
||||||
ROOM,
|
ROOM,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
|
|
||||||
@@ -36,6 +37,14 @@ export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) =>
|
|||||||
.filter(identity)
|
.filter(identity)
|
||||||
.map(s => encodeURIComponent(s as string))
|
.map(s => encodeURIComponent(s as string))
|
||||||
.join("/")
|
.join("/")
|
||||||
|
} else {
|
||||||
|
const relay = relaysByUrl.get().get(url)
|
||||||
|
|
||||||
|
if (hasNip29(relay)) {
|
||||||
|
path += "/recent"
|
||||||
|
} else {
|
||||||
|
path += "/chat"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return path
|
return path
|
||||||
@@ -143,3 +152,14 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => {
|
|||||||
|
|
||||||
return entityLink(nip19.neventEncode({id: event.id, relays: urls}))
|
return entityLink(nip19.neventEncode({id: event.id, relays: urls}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getChannelItemPath = (url: string, event: TrustedEvent) => {
|
||||||
|
switch (event.kind) {
|
||||||
|
case THREAD:
|
||||||
|
return makeThreadPath(url, event.id)
|
||||||
|
case ZAP_GOAL:
|
||||||
|
return makeGoalPath(url, event.id)
|
||||||
|
case EVENT_TIME:
|
||||||
|
return makeCalendarPath(url, event.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+4
-10
@@ -167,20 +167,14 @@ const syncTracker = async () => {
|
|||||||
|
|
||||||
tracker.load(relaysById)
|
tracker.load(relaysById)
|
||||||
|
|
||||||
let p = Promise.resolve()
|
|
||||||
|
|
||||||
const updateOne = batch(3000, (ids: string[]) => {
|
const updateOne = batch(3000, (ids: string[]) => {
|
||||||
p = p.then(() => {
|
collection.add(ids.map(id => [id, Array.from(tracker.getRelays(id))]))
|
||||||
collection.add(ids.map(id => [id, Array.from(tracker.getRelays(id))]))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateAll = throttle(3000, () => {
|
const updateAll = throttle(3000, () => {
|
||||||
p = p.then(() => {
|
collection.set(
|
||||||
collection.set(
|
Array.from(tracker.relaysById.entries()).map(([id, relays]) => [id, Array.from(relays)]),
|
||||||
Array.from(tracker.relaysById.entries()).map(([id, relays]) => [id, Array.from(relays)]),
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
tracker.on("add", updateOne)
|
tracker.on("add", updateOne)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,31 +4,17 @@
|
|||||||
import {throttle} from "throttle-debounce"
|
import {throttle} from "throttle-debounce"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {get, derived} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import {App, type URLOpenListenerEvent} from "@capacitor/app"
|
import {App, type URLOpenListenerEvent} from "@capacitor/app"
|
||||||
import {dev} from "$app/environment"
|
import {dev} from "$app/environment"
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {sync, localStorageProvider} from "@welshman/store"
|
import {sync, localStorageProvider} from "@welshman/store"
|
||||||
import {
|
import {assoc, call, defer, dissoc, on, sleep, spec, TaskQueue} from "@welshman/lib"
|
||||||
ago,
|
|
||||||
assoc,
|
|
||||||
call,
|
|
||||||
defer,
|
|
||||||
dissoc,
|
|
||||||
identity,
|
|
||||||
memoize,
|
|
||||||
on,
|
|
||||||
sleep,
|
|
||||||
spec,
|
|
||||||
TaskQueue,
|
|
||||||
WEEK,
|
|
||||||
} from "@welshman/lib"
|
|
||||||
import type {TrustedEvent, StampedEvent} from "@welshman/util"
|
import type {TrustedEvent, StampedEvent} from "@welshman/util"
|
||||||
import {WRAP} from "@welshman/util"
|
import {WRAP} from "@welshman/util"
|
||||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
||||||
import type {Socket, RelayMessage, ClientMessage} from "@welshman/net"
|
import type {Socket, RelayMessage, ClientMessage} from "@welshman/net"
|
||||||
import {
|
import {
|
||||||
request,
|
|
||||||
defaultSocketPolicies,
|
defaultSocketPolicies,
|
||||||
makeSocketPolicyAuth,
|
makeSocketPolicyAuth,
|
||||||
SocketEvent,
|
SocketEvent,
|
||||||
@@ -40,7 +26,6 @@
|
|||||||
isClientClose,
|
isClientClose,
|
||||||
} from "@welshman/net"
|
} from "@welshman/net"
|
||||||
import {
|
import {
|
||||||
loadRelay,
|
|
||||||
repository,
|
repository,
|
||||||
pubkey,
|
pubkey,
|
||||||
session,
|
session,
|
||||||
@@ -64,20 +49,18 @@
|
|||||||
import {preferencesStorageProvider} from "@lib/storage"
|
import {preferencesStorageProvider} from "@lib/storage"
|
||||||
import AppContainer from "@app/components/AppContainer.svelte"
|
import AppContainer from "@app/components/AppContainer.svelte"
|
||||||
import ModalContainer from "@app/components/ModalContainer.svelte"
|
import ModalContainer from "@app/components/ModalContainer.svelte"
|
||||||
|
import {setupHistory} from "@app/util/history"
|
||||||
import {setupTracking} from "@app/util/tracking"
|
import {setupTracking} from "@app/util/tracking"
|
||||||
import {setupAnalytics} from "@app/util/analytics"
|
import {setupAnalytics} from "@app/util/analytics"
|
||||||
import {
|
import {
|
||||||
INDEXER_RELAYS,
|
|
||||||
userMembership,
|
|
||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
relaysPendingTrust,
|
relaysPendingTrust,
|
||||||
ensureUnwrapped,
|
ensureUnwrapped,
|
||||||
canDecrypt,
|
canDecrypt,
|
||||||
getSetting,
|
getSetting,
|
||||||
relaysMostlyRestricted,
|
relaysMostlyRestricted,
|
||||||
userInboxRelays,
|
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {loadUserData, listenForNotifications} from "@app/core/requests"
|
import {syncApplicationData} from "@app/core/sync"
|
||||||
import {theme} from "@app/util/theme"
|
import {theme} from "@app/util/theme"
|
||||||
import {toast, pushToast} from "@app/util/toast"
|
import {toast, pushToast} from "@app/util/toast"
|
||||||
import {initializePushNotifications} from "@app/util/push"
|
import {initializePushNotifications} from "@app/util/push"
|
||||||
@@ -192,8 +175,6 @@
|
|||||||
|
|
||||||
// TODO: remove ack result
|
// TODO: remove ack result
|
||||||
if (pubkey && ["ack", connectSecret].includes(result)) {
|
if (pubkey && ["ack", connectSecret].includes(result)) {
|
||||||
await loadUserData(pubkey)
|
|
||||||
|
|
||||||
loginWithNip46(pubkey, clientSecret, signerPubkey, relays)
|
loginWithNip46(pubkey, clientSecret, signerPubkey, relays)
|
||||||
broker.cleanup()
|
broker.cleanup()
|
||||||
success = true
|
success = true
|
||||||
@@ -224,6 +205,7 @@
|
|||||||
|
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
initialized = true
|
initialized = true
|
||||||
|
setupHistory()
|
||||||
setupTracking()
|
setupTracking()
|
||||||
setupAnalytics()
|
setupAnalytics()
|
||||||
|
|
||||||
@@ -374,46 +356,8 @@
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Load relay info
|
// Load user data, listen for messages, etc
|
||||||
for (const url of INDEXER_RELAYS) {
|
syncApplicationData()
|
||||||
loadRelay(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load user data
|
|
||||||
if ($pubkey) {
|
|
||||||
await loadUserData($pubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for space data, populate space-based notifications
|
|
||||||
let unsubSpaces: any
|
|
||||||
|
|
||||||
userMembership.subscribe(
|
|
||||||
memoize($membership => {
|
|
||||||
unsubSpaces?.()
|
|
||||||
unsubSpaces = listenForNotifications()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Listen for chats, populate chat-based notifications
|
|
||||||
let controller: AbortController
|
|
||||||
|
|
||||||
derived([pubkey, canDecrypt, userInboxRelays], identity).subscribe(
|
|
||||||
([$pubkey, $canDecrypt, $userInboxRelays]) => {
|
|
||||||
controller?.abort()
|
|
||||||
controller = new AbortController()
|
|
||||||
|
|
||||||
if ($pubkey && $canDecrypt) {
|
|
||||||
request({
|
|
||||||
signal: controller.signal,
|
|
||||||
relays: $userInboxRelays,
|
|
||||||
filters: [
|
|
||||||
{kinds: [WRAP], "#p": [$pubkey], since: ago(WEEK, 2)},
|
|
||||||
{kinds: [WRAP], "#p": [$pubkey], limit: 100},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// subscribe to badge count for changes
|
// subscribe to badge count for changes
|
||||||
notifications.badgeCount.subscribe(notifications.handleBadgeCountChanges)
|
notifications.badgeCount.subscribe(notifications.handleBadgeCountChanges)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||||
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
||||||
import ProfileDelete from "@app/components/ProfileDelete.svelte"
|
import ProfileDelete from "@app/components/ProfileDelete.svelte"
|
||||||
import SignerStatus from "@app/components/SignerStatus.svelte"
|
import SignerStatus from "@app/components/SignerStatus.svelte"
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{#key $profile?.about}
|
{#key $profile?.about}
|
||||||
<Content event={{content: $profile?.about || "", tags: []}} hideMediaAtDepth={0} />
|
<ContentMinimal event={{content: $profile?.about || "", tags: []}} />
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
{#if $session?.email}
|
{#if $session?.email}
|
||||||
|
|||||||
@@ -1,125 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {goto} from "$app/navigation"
|
||||||
import {deriveRelay} from "@welshman/app"
|
import {decodeRelay} from "@app/core/state"
|
||||||
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
import Login2 from "@assets/icons/login-3.svg?dataurl"
|
|
||||||
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
|
||||||
import Ghost from "@assets/icons/ghost-smile.svg?dataurl"
|
|
||||||
import BillList from "@assets/icons/bill-list.svg?dataurl"
|
|
||||||
import ShieldUser from "@assets/icons/shield-user.svg?dataurl"
|
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import PageBar from "@lib/components/PageBar.svelte"
|
|
||||||
import PageContent from "@lib/components/PageContent.svelte"
|
|
||||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
|
||||||
import ProfileLatest from "@app/components/ProfileLatest.svelte"
|
|
||||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
|
||||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
|
||||||
import SpaceQuickLinks from "@app/components/SpaceQuickLinks.svelte"
|
|
||||||
import SpaceRecentActivity from "@app/components/SpaceRecentActivity.svelte"
|
|
||||||
import SpaceRelayStatus from "@app/components/SpaceRelayStatus.svelte"
|
|
||||||
import {decodeRelay, userRoomsByUrl} from "@app/core/state"
|
|
||||||
import {makeChatPath} from "@app/util/routes"
|
|
||||||
import {pushModal} from "@app/util/modal"
|
|
||||||
|
|
||||||
const url = decodeRelay($page.params.relay!)
|
const url = decodeRelay($page.params.relay!)
|
||||||
const relay = deriveRelay(url)
|
|
||||||
const joinSpace = () => pushModal(SpaceJoin, {url})
|
|
||||||
|
|
||||||
const owner = $derived($relay?.profile?.pubkey)
|
goto(makeSpacePath(url, "recent"))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PageBar>
|
|
||||||
{#snippet icon()}
|
|
||||||
<div class="center">
|
|
||||||
<Icon icon={HomeSmile} />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet title()}
|
|
||||||
<strong>Home</strong>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet action()}
|
|
||||||
<div class="row-2">
|
|
||||||
{#if !$userRoomsByUrl.has(url)}
|
|
||||||
<Button class="btn btn-primary btn-sm" onclick={joinSpace}>
|
|
||||||
<Icon icon={Login2} />
|
|
||||||
Join Space
|
|
||||||
</Button>
|
|
||||||
{:else if owner}
|
|
||||||
<Link class="btn btn-primary btn-sm" href={makeChatPath([owner])}>
|
|
||||||
<Icon icon={Letter} />
|
|
||||||
Contact Owner
|
|
||||||
</Link>
|
|
||||||
{/if}
|
|
||||||
<MenuSpaceButton {url} />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</PageBar>
|
|
||||||
|
|
||||||
<PageContent class="flex flex-col gap-2 p-2 pt-4">
|
|
||||||
<div class="card2 bg-alt flex flex-col gap-4 text-left">
|
|
||||||
<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}
|
|
||||||
</div>
|
|
||||||
<SpaceQuickLinks {url} />
|
|
||||||
<div class="grid grid-cols-1 gap-2 lg:grid-cols-2">
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<SpaceRecentActivity {url} />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<SpaceRelayStatus {url} />
|
|
||||||
{#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>
|
|
||||||
</div>
|
|
||||||
</PageContent>
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
makeRoomMeta,
|
makeRoomMeta,
|
||||||
MESSAGE,
|
MESSAGE,
|
||||||
DELETE,
|
DELETE,
|
||||||
|
THREAD,
|
||||||
|
EVENT_TIME,
|
||||||
|
ZAP_GOAL,
|
||||||
ROOM_ADD_USER,
|
ROOM_ADD_USER,
|
||||||
ROOM_REMOVE_USER,
|
ROOM_REMOVE_USER,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
@@ -33,7 +36,7 @@
|
|||||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||||
import ChannelName from "@app/components/ChannelName.svelte"
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
import ChannelMessage from "@app/components/ChannelMessage.svelte"
|
import ChannelItem from "@app/components/ChannelItem.svelte"
|
||||||
import ChannelCompose from "@app/components/ChannelCompose.svelte"
|
import ChannelCompose from "@app/components/ChannelCompose.svelte"
|
||||||
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
|
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
|
||||||
import {
|
import {
|
||||||
@@ -65,7 +68,7 @@
|
|||||||
const lastChecked = $checked[$page.url.pathname]
|
const lastChecked = $checked[$page.url.pathname]
|
||||||
const url = decodeRelay(relay)
|
const url = decodeRelay(relay)
|
||||||
const channel = deriveChannel(url, room)
|
const channel = deriveChannel(url, room)
|
||||||
const filter = {kinds: [MESSAGE], "#h": [room]}
|
const filter = {kinds: [MESSAGE, THREAD, EVENT_TIME, ZAP_GOAL], "#h": [room]}
|
||||||
const isFavorite = $derived($userRoomsByUrl.get(url)?.has(room))
|
const isFavorite = $derived($userRoomsByUrl.get(url)?.has(room))
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
const membershipStatus = deriveUserMembershipStatus(url, room)
|
const membershipStatus = deriveUserMembershipStatus(url, room)
|
||||||
@@ -429,7 +432,7 @@
|
|||||||
<Divider>{value}</Divider>
|
<Divider>{value}</Divider>
|
||||||
{:else}
|
{:else}
|
||||||
<div in:slide class:-mt-1={!showPubkey}>
|
<div in:slide class:-mt-1={!showPubkey}>
|
||||||
<ChannelMessage
|
<ChannelItem
|
||||||
{url}
|
{url}
|
||||||
{replyTo}
|
{replyTo}
|
||||||
event={$state.snapshot(value as TrustedEvent)}
|
event={$state.snapshot(value as TrustedEvent)}
|
||||||
@@ -485,11 +488,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{#key eventToEdit}
|
{#key eventToEdit}
|
||||||
<ChannelCompose
|
<ChannelCompose
|
||||||
bind:this={compose}
|
|
||||||
content={eventToEdit?.content}
|
|
||||||
{onSubmit}
|
|
||||||
{url}
|
{url}
|
||||||
{onEditPrevious} />
|
{room}
|
||||||
|
{onSubmit}
|
||||||
|
{onEditPrevious}
|
||||||
|
content={eventToEdit?.content}
|
||||||
|
bind:this={compose} />
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,17 +46,19 @@
|
|||||||
let haveISeenTheFuture = false
|
let haveISeenTheFuture = false
|
||||||
let prevDateDisplay: string
|
let prevDateDisplay: string
|
||||||
|
|
||||||
return $events.map<Item>(event => {
|
return $events
|
||||||
const newDateDisplay = formatTimestampAsDate(getStart(event))
|
.filter(event => !isNaN(getStart(event)))
|
||||||
const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay
|
.map<Item>(event => {
|
||||||
const isFuture = todayDateDisplay === newDateDisplay || event.created_at > now()
|
const newDateDisplay = formatTimestampAsDate(getStart(event))
|
||||||
const isFirstFutureEvent = !haveISeenTheFuture && isFuture
|
const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay
|
||||||
|
const isFuture = todayDateDisplay === newDateDisplay || event.created_at > now()
|
||||||
|
const isFirstFutureEvent = !haveISeenTheFuture && isFuture
|
||||||
|
|
||||||
prevDateDisplay = newDateDisplay
|
prevDateDisplay = newDateDisplay
|
||||||
haveISeenTheFuture = isFuture
|
haveISeenTheFuture = isFuture
|
||||||
|
|
||||||
return {event, dateDisplay, isFirstFutureEvent}
|
return {event, dateDisplay, isFirstFutureEvent}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let previousScrollHeight = 0
|
let previousScrollHeight = 0
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-col justify-end sm:flex-row">
|
<div class="flex w-full flex-col justify-end sm:flex-row">
|
||||||
<CalendarEventActions {url} event={$event} />
|
<CalendarEventActions showRoom {url} event={$event} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if !showAll && $replies.length > 4}
|
{#if !showAll && $replies.length > 4}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import {readable} from "svelte/store"
|
import {readable} from "svelte/store"
|
||||||
import {now, formatTimestampAsDate, MINUTE, ago} from "@welshman/lib"
|
import {now, formatTimestampAsDate, MINUTE, ago} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {makeEvent, MESSAGE, DELETE} from "@welshman/util"
|
import {makeEvent, MESSAGE, DELETE, THREAD, EVENT_TIME, ZAP_GOAL} from "@welshman/util"
|
||||||
import {pubkey, publishThunk} from "@welshman/app"
|
import {pubkey, publishThunk} from "@welshman/app"
|
||||||
import {slide, fade, fly} from "@lib/transition"
|
import {slide, fade, fly} from "@lib/transition"
|
||||||
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||||
import ChannelMessage from "@app/components/ChannelMessage.svelte"
|
import ChannelItem from "@app/components/ChannelItem.svelte"
|
||||||
import ChannelCompose from "@app/components/ChannelCompose.svelte"
|
import ChannelCompose from "@app/components/ChannelCompose.svelte"
|
||||||
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
|
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
|
||||||
import {
|
import {
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
const mounted = now()
|
const mounted = now()
|
||||||
const lastChecked = $checked[$page.url.pathname]
|
const lastChecked = $checked[$page.url.pathname]
|
||||||
const url = decodeRelay($page.params.relay!)
|
const url = decodeRelay($page.params.relay!)
|
||||||
const filter = {kinds: [MESSAGE]}
|
const filter = {kinds: [MESSAGE, THREAD, EVENT_TIME, ZAP_GOAL]}
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const replyTo = (event: TrustedEvent) => {
|
const replyTo = (event: TrustedEvent) => {
|
||||||
@@ -282,11 +282,12 @@
|
|||||||
{:else if type === "date"}
|
{:else if type === "date"}
|
||||||
<Divider>{value}</Divider>
|
<Divider>{value}</Divider>
|
||||||
{:else}
|
{:else}
|
||||||
|
{@const event = $state.snapshot(value as TrustedEvent)}
|
||||||
<div in:slide class:-mt-1={!showPubkey}>
|
<div in:slide class:-mt-1={!showPubkey}>
|
||||||
<ChannelMessage
|
<ChannelItem
|
||||||
{url}
|
{url}
|
||||||
|
{event}
|
||||||
{replyTo}
|
{replyTo}
|
||||||
event={$state.snapshot(value as TrustedEvent)}
|
|
||||||
{showPubkey}
|
{showPubkey}
|
||||||
canEdit={canEditEvent}
|
canEdit={canEditEvent}
|
||||||
onEdit={onEditEvent} />
|
onEdit={onEditEvent} />
|
||||||
@@ -316,11 +317,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{#key eventToEdit}
|
{#key eventToEdit}
|
||||||
<ChannelCompose
|
<ChannelCompose
|
||||||
bind:this={compose}
|
|
||||||
content={eventToEdit?.content}
|
|
||||||
{onSubmit}
|
|
||||||
{url}
|
{url}
|
||||||
{onEditPrevious} />
|
{onSubmit}
|
||||||
|
{onEditPrevious}
|
||||||
|
content={eventToEdit?.content}
|
||||||
|
bind:this={compose} />
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
<div class="col-3 ml-12">
|
<div class="col-3 ml-12">
|
||||||
<Content showEntire event={{...$event, content: summary}} {url} />
|
<Content showEntire event={{...$event, content: summary}} {url} />
|
||||||
<GoalSummary event={$event} {url} />
|
<GoalSummary event={$event} {url} />
|
||||||
<GoalActions event={$event} {url} />
|
<GoalActions showRoom event={$event} {url} />
|
||||||
</div>
|
</div>
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
{#if !showAll && $replies.length > 4}
|
{#if !showAll && $replies.length > 4}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {derived} from "svelte/store"
|
||||||
|
import {page} from "$app/stores"
|
||||||
|
import {groupBy, ago, MONTH, first, last, uniq, avg, overlappingPairs} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {MESSAGE, getTagValue} from "@welshman/util"
|
||||||
|
import History from "@assets/icons/history.svg?dataurl"
|
||||||
|
import {createScroller} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import PageBar from "@lib/components/PageBar.svelte"
|
||||||
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
|
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||||
|
import ConversationCard from "@app/components/ConversationCard.svelte"
|
||||||
|
import {decodeRelay, deriveEventsForUrl} from "@app/core/state"
|
||||||
|
|
||||||
|
const url = decodeRelay($page.params.relay!)
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
let limit = $state(3)
|
||||||
|
let element: Element | undefined = $state()
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const scroller = createScroller({
|
||||||
|
element: element!,
|
||||||
|
onScroll: () => {
|
||||||
|
limit += 3
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => scroller.stop()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PageBar>
|
||||||
|
{#snippet icon()}
|
||||||
|
<div class="center">
|
||||||
|
<Icon icon={History} />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
|
<strong>Recent Activity</strong>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet action()}
|
||||||
|
<div class="row-2">
|
||||||
|
<MenuSpaceButton {url} />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</PageBar>
|
||||||
|
|
||||||
|
<div bind:this={element}>
|
||||||
|
<PageContent class="flex flex-col gap-2 p-2 pt-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}
|
||||||
|
</PageContent>
|
||||||
|
</div>
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
|
||||||
<div class="col-3 ml-12">
|
<div class="col-3 ml-12">
|
||||||
<Content showEntire event={$event} {url} />
|
<Content showEntire event={$event} {url} />
|
||||||
<ThreadActions event={$event} {url} />
|
<ThreadActions showRoom event={$event} {url} />
|
||||||
</div>
|
</div>
|
||||||
</NoteCard>
|
</NoteCard>
|
||||||
{#if !showAll && $replies.length > 4}
|
{#if !showAll && $replies.length > 4}
|
||||||
|
|||||||
Reference in New Issue
Block a user