diff --git a/AGENTS.md b/AGENTS.md index 9b37324f..74bbf0ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -167,6 +167,7 @@ src/ - Do not use `any`. If there are type errors related to `unknown`, they are likely because the upstream definition of the data is incorrect. - When dynamically building classes, use `cx` from `classnames` rather than embedded ternaries or svelte 4's old `class:` syntax. - When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss +- Do not define svelte event handlers inline, instead name them and put them in the script section of templates ## Common Tasks diff --git a/src/app/components/ConversationCard.svelte b/src/app/components/ConversationCard.svelte index fb29c76b..8dbc8927 100644 --- a/src/app/components/ConversationCard.svelte +++ b/src/app/components/ConversationCard.svelte @@ -1,75 +1,63 @@ - - {/if} + diff --git a/src/routes/spaces/[relay]/recent/+page.svelte b/src/routes/spaces/[relay]/recent/+page.svelte index 415e44f0..5dc14db7 100644 --- a/src/routes/spaces/[relay]/recent/+page.svelte +++ b/src/routes/spaces/[relay]/recent/+page.svelte @@ -2,71 +2,136 @@ 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 {groupBy, ago, WEEK, first, sortBy, now} from "@welshman/lib" + import {MESSAGE, THREAD, ZAP_GOAL, EVENT_TIME, COMMENT, getTagValue} from "@welshman/util" import History from "@assets/icons/history.svg?dataurl" + import Add from "@assets/icons/add.svg?dataurl" import {createScroller} from "@lib/html" import Icon from "@lib/components/Icon.svelte" + import Button from "@lib/components/Button.svelte" import PageBar from "@lib/components/PageBar.svelte" import PageContent from "@lib/components/PageContent.svelte" import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte" import ConversationCard from "@app/components/ConversationCard.svelte" + import ThreadCard from "@app/components/ThreadCard.svelte" + import GoalCard from "@app/components/GoalCard.svelte" + import CalendarEventCard from "@app/components/CalendarEventCard.svelte" + import CommentCard from "@app/components/CommentCard.svelte" import {decodeRelay, deriveEventsForUrl} from "@app/core/state" + import {goToSpace} from "@app/util/routes" const url = decodeRelay($page.params.relay!) - const since = ago(MONTH) + const since = ago(WEEK) + const currentTime = now() + const messages = deriveEventsForUrl(url, [{kinds: [MESSAGE], since}]) + const threads = deriveEventsForUrl(url, [{kinds: [THREAD], since}]) + const goals = deriveEventsForUrl(url, [{kinds: [ZAP_GOAL], since}]) + const events = deriveEventsForUrl(url, [{kinds: [EVENT_TIME]}]) + const comments = deriveEventsForUrl(url, [{kinds: [COMMENT], since}]) - const conversations = derived(messages, $messages => { - const convs = [] + const recentActivity = derived( + [messages, threads, goals, events, comments], + ([$messages, $threads, $goals, $events, $comments]) => { + const activity: Array<{ + type: "message" | "thread" | "goal" | "event" | "upcoming" | "comment" + event: any + timestamp: number + h?: string + latest?: any + count?: number + replyCount?: number + }> = [] - for (const [h, 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)) + const byRoom = groupBy(e => getTagValue("h", e.tags), $messages) + for (const [h, roomMessages] of byRoom.entries()) { + const latest = first(roomMessages) + if (latest) { + activity.push({ + type: "message", + event: latest, + timestamp: latest.created_at, + h, + latest, + count: roomMessages.length, + }) } - - prevCreatedAt = message.created_at } - if (group.length > 0) { - groups.push(group.splice(0)) + for (const thread of $threads) { + const replies = $comments.filter(c => getTagValue("E", c.tags) === thread.id) + activity.push({ + type: "thread", + event: thread, + timestamp: thread.created_at, + h: getTagValue("h", thread.tags), + replyCount: replies.length, + }) } - // 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({h, events, latest, earliest, participants}) + for (const goal of $goals) { + const replies = $comments.filter(c => getTagValue("E", c.tags) === goal.id) + activity.push({ + type: "goal", + event: goal, + timestamp: goal.created_at, + h: getTagValue("h", goal.tags), + replyCount: replies.length, + }) } - } - return convs - }) + const pastEvents = $events.filter(e => { + const start = getTagValue("start", e.tags) + return start && parseInt(start) < currentTime + }) + for (const event of pastEvents.slice(0, 5)) { + const replies = $comments.filter(c => getTagValue("E", c.tags) === event.id) + activity.push({ + type: "event", + event, + timestamp: event.created_at, + h: getTagValue("h", event.tags), + replyCount: replies.length, + }) + } - let limit = $state(3) + const upcomingEvents = $events.filter(e => { + const start = getTagValue("start", e.tags) + return start && parseInt(start) >= currentTime + }) + for (const event of upcomingEvents.slice(0, 3)) { + const replies = $comments.filter(c => getTagValue("E", c.tags) === event.id) + const start = getTagValue("start", event.tags) + activity.push({ + type: "upcoming", + event, + timestamp: start ? parseInt(start) : event.created_at, + h: getTagValue("h", event.tags), + replyCount: replies.length, + }) + } + + for (const comment of $comments) { + activity.push({ + type: "comment", + event: comment, + timestamp: comment.created_at, + h: getTagValue("h", comment.tags), + }) + } + + return sortBy(a => -a.timestamp, activity) + }, + ) + + let limit = $state(20) let element: Element | undefined = $state() onMount(() => { const scroller = createScroller({ element: element!, onScroll: () => { - limit += 3 + limit += 10 }, }) @@ -92,26 +157,37 @@
- {#if $conversations.length === 0} - {#if $messages.length > 0} - {@const events = $messages.slice(0, 1)} - {@const event = events[0]} - {@const h = getTagValue("h", event.tags)} - - {:else} -
-

No recent conversations

+ {#if $recentActivity.length === 0} +
+ +
+

No Recent Activity

+

There hasn't been any activity in the last week.

- {/if} +
+ +
+
{:else} - {#each $conversations.slice(0, limit) as { h, events, latest, earliest, participants } (latest.id)} - + {#each $recentActivity.slice(0, limit) as activity (activity.event.id)} + {#if activity.type === "message"} + + {:else if activity.type === "thread"} + + {:else if activity.type === "goal"} + + {:else if activity.type === "event" || activity.type === "upcoming"} + + {:else if activity.type === "comment"} + + {/if} {/each} {/if}