Re-work space navigation #223

This commit is contained in:
Jon Staab
2025-10-06 11:23:19 -07:00
committed by hodlbod
parent b3533c285f
commit f9ac13ba11
68 changed files with 2807 additions and 884 deletions
+4 -119
View File
@@ -1,125 +1,10 @@
<script lang="ts">
import {page} from "$app/stores"
import {displayRelayUrl} from "@welshman/util"
import {deriveRelay} from "@welshman/app"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
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"
import {goto} from "$app/navigation"
import {decodeRelay} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
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>
<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>
+11 -7
View File
@@ -13,6 +13,9 @@
makeRoomMeta,
MESSAGE,
DELETE,
THREAD,
EVENT_TIME,
ZAP_GOAL,
ROOM_ADD_USER,
ROOM_REMOVE_USER,
} from "@welshman/util"
@@ -33,7 +36,7 @@
import ThunkToast from "@app/components/ThunkToast.svelte"
import MenuSpaceButton from "@app/components/MenuSpaceButton.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 ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
import {
@@ -65,7 +68,7 @@
const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay(relay)
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 shouldProtect = canEnforceNip70(url)
const membershipStatus = deriveUserMembershipStatus(url, room)
@@ -429,7 +432,7 @@
<Divider>{value}</Divider>
{:else}
<div in:slide class:-mt-1={!showPubkey}>
<ChannelMessage
<ChannelItem
{url}
{replyTo}
event={$state.snapshot(value as TrustedEvent)}
@@ -485,11 +488,12 @@
</div>
{#key eventToEdit}
<ChannelCompose
bind:this={compose}
content={eventToEdit?.content}
{onSubmit}
{url}
{onEditPrevious} />
{room}
{onSubmit}
{onEditPrevious}
content={eventToEdit?.content}
bind:this={compose} />
{/key}
{/if}
</div>
@@ -46,17 +46,19 @@
let haveISeenTheFuture = false
let prevDateDisplay: string
return $events.map<Item>(event => {
const newDateDisplay = formatTimestampAsDate(getStart(event))
const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay
const isFuture = todayDateDisplay === newDateDisplay || event.created_at > now()
const isFirstFutureEvent = !haveISeenTheFuture && isFuture
return $events
.filter(event => !isNaN(getStart(event)))
.map<Item>(event => {
const newDateDisplay = formatTimestampAsDate(getStart(event))
const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay
const isFuture = todayDateDisplay === newDateDisplay || event.created_at > now()
const isFirstFutureEvent = !haveISeenTheFuture && isFuture
prevDateDisplay = newDateDisplay
haveISeenTheFuture = isFuture
prevDateDisplay = newDateDisplay
haveISeenTheFuture = isFuture
return {event, dateDisplay, isFirstFutureEvent}
})
return {event, dateDisplay, isFirstFutureEvent}
})
})
let previousScrollHeight = 0
@@ -93,7 +93,7 @@
</div>
</div>
<div class="flex w-full flex-col justify-end sm:flex-row">
<CalendarEventActions {url} event={$event} />
<CalendarEventActions showRoom {url} event={$event} />
</div>
</div>
{#if !showAll && $replies.length > 4}
+10 -9
View File
@@ -5,7 +5,7 @@
import {readable} from "svelte/store"
import {now, formatTimestampAsDate, MINUTE, ago} from "@welshman/lib"
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 {slide, fade, fly} from "@lib/transition"
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
@@ -18,7 +18,7 @@
import Divider from "@lib/components/Divider.svelte"
import ThunkToast from "@app/components/ThunkToast.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 ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
import {
@@ -38,7 +38,7 @@
const mounted = now()
const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay($page.params.relay!)
const filter = {kinds: [MESSAGE]}
const filter = {kinds: [MESSAGE, THREAD, EVENT_TIME, ZAP_GOAL]}
const shouldProtect = canEnforceNip70(url)
const replyTo = (event: TrustedEvent) => {
@@ -282,11 +282,12 @@
{:else if type === "date"}
<Divider>{value}</Divider>
{:else}
{@const event = $state.snapshot(value as TrustedEvent)}
<div in:slide class:-mt-1={!showPubkey}>
<ChannelMessage
<ChannelItem
{url}
{event}
{replyTo}
event={$state.snapshot(value as TrustedEvent)}
{showPubkey}
canEdit={canEditEvent}
onEdit={onEditEvent} />
@@ -316,11 +317,11 @@
</div>
{#key eventToEdit}
<ChannelCompose
bind:this={compose}
content={eventToEdit?.content}
{onSubmit}
{url}
{onEditPrevious} />
{onSubmit}
{onEditPrevious}
content={eventToEdit?.content}
bind:this={compose} />
{/key}
</div>
@@ -87,7 +87,7 @@
<div class="col-3 ml-12">
<Content showEntire event={{...$event, content: summary}} {url} />
<GoalSummary event={$event} {url} />
<GoalActions event={$event} {url} />
<GoalActions showRoom event={$event} {url} />
</div>
</NoteCard>
{#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">
<div class="col-3 ml-12">
<Content showEntire event={$event} {url} />
<ThreadActions event={$event} {url} />
<ThreadActions showRoom event={$event} {url} />
</div>
</NoteCard>
{#if !showAll && $replies.length > 4}