forked from coracle/flotilla
Work on feed page
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
onchange src -ik -- npx svelte-kit sync &
|
||||||
|
|
||||||
|
onchange src -ik -- bash -c 'unbuffer npx svelte-check --tsconfig ./tsconfig.json | less -R' &
|
||||||
|
|
||||||
|
wait
|
||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
"build": "./build.sh",
|
"build": "./build.sh",
|
||||||
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "./check.sh",
|
||||||
"lint": "prettier --check src && eslint src",
|
"lint": "prettier --check src && eslint src",
|
||||||
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write",
|
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write",
|
||||||
"format:all": "prettier --write src",
|
"format:all": "prettier --write src",
|
||||||
|
|||||||
+84
-1
@@ -27,6 +27,7 @@ import {
|
|||||||
randomId,
|
randomId,
|
||||||
tryCatch,
|
tryCatch,
|
||||||
fromPairs,
|
fromPairs,
|
||||||
|
groupBy,
|
||||||
remove,
|
remove,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import type {Override} from "@welshman/lib"
|
import type {Override} from "@welshman/lib"
|
||||||
@@ -48,6 +49,7 @@ import {
|
|||||||
makeDeriveEvent,
|
makeDeriveEvent,
|
||||||
makeLoadItem,
|
makeLoadItem,
|
||||||
makeDeriveItem,
|
makeDeriveItem,
|
||||||
|
deriveItems,
|
||||||
deriveItemsByKey,
|
deriveItemsByKey,
|
||||||
deriveDeduplicated,
|
deriveDeduplicated,
|
||||||
deriveEventsById,
|
deriveEventsById,
|
||||||
@@ -58,6 +60,8 @@ import {
|
|||||||
deriveEventsDesc,
|
deriveEventsDesc,
|
||||||
} from "@welshman/store"
|
} from "@welshman/store"
|
||||||
import {
|
import {
|
||||||
|
FEED,
|
||||||
|
FEEDS,
|
||||||
APP_DATA,
|
APP_DATA,
|
||||||
CLIENT_AUTH,
|
CLIENT_AUTH,
|
||||||
COMMENT,
|
COMMENT,
|
||||||
@@ -104,8 +108,18 @@ import {
|
|||||||
makeRoomMeta,
|
makeRoomMeta,
|
||||||
ManagementMethod,
|
ManagementMethod,
|
||||||
sortEventsDesc,
|
sortEventsDesc,
|
||||||
|
getAddress,
|
||||||
|
Address,
|
||||||
|
getIdFilters,
|
||||||
|
} from "@welshman/util"
|
||||||
|
import type {
|
||||||
|
TrustedEvent,
|
||||||
|
RelayProfile,
|
||||||
|
PublishedList,
|
||||||
|
PublishedRoomMeta,
|
||||||
|
List,
|
||||||
|
Filter,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, RelayProfile, PublishedRoomMeta, List, Filter} from "@welshman/util"
|
|
||||||
import {routerContext, Router} from "@welshman/router"
|
import {routerContext, Router} from "@welshman/router"
|
||||||
import {
|
import {
|
||||||
pubkey,
|
pubkey,
|
||||||
@@ -122,6 +136,7 @@ import {
|
|||||||
manageRelay,
|
manageRelay,
|
||||||
displayProfileByPubkey,
|
displayProfileByPubkey,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
|
import {readFeed} from "@lib/feeds"
|
||||||
|
|
||||||
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
||||||
|
|
||||||
@@ -915,6 +930,74 @@ export const deriveUserCanCreateRoom = (url: string) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Feeds
|
||||||
|
|
||||||
|
export const feedsByAddress = deriveItemsByKey({
|
||||||
|
repository,
|
||||||
|
getKey: feed => getAddress(feed.event),
|
||||||
|
filters: [{kinds: [FEED]}],
|
||||||
|
eventToItem: readFeed,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getFeedsByAddress = getter(feedsByAddress)
|
||||||
|
|
||||||
|
export const feeds = deriveItems(feedsByAddress)
|
||||||
|
|
||||||
|
export const getFeeds = getter(feeds)
|
||||||
|
|
||||||
|
export const getFeed = (address: string) => getFeedsByAddress().get(address)
|
||||||
|
|
||||||
|
export const fetchFeed = (address: string) => {
|
||||||
|
const {pubkey} = Address.from(address)
|
||||||
|
|
||||||
|
return load({
|
||||||
|
relays: Router.get().FromPubkey(pubkey).getUrls(),
|
||||||
|
filters: getIdFilters([address]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadFeed = makeLoadItem(fetchFeed, getFeed)
|
||||||
|
|
||||||
|
export const deriveFeed = makeDeriveItem(feedsByAddress, loadFeed)
|
||||||
|
|
||||||
|
// Feeds by pubkey
|
||||||
|
|
||||||
|
export const feedsByPubkey = derived(feeds, $feeds => groupBy(f => f.event.pubkey, $feeds))
|
||||||
|
|
||||||
|
export const getFeedsByPubkey = getter(feedsByPubkey)
|
||||||
|
|
||||||
|
export const getFeedsForPubkey = (pubkey: string) => getFeedsByPubkey().get(pubkey)
|
||||||
|
|
||||||
|
export const loadFeedsForPubkey = makeLoadItem(makeOutboxLoader(FEED), getFeedsForPubkey)
|
||||||
|
|
||||||
|
export const userFeeds = makeUserData(feedsByPubkey, loadFeedsForPubkey)
|
||||||
|
|
||||||
|
export const loadUserFeeds = makeUserLoader(loadFeedsForPubkey)
|
||||||
|
|
||||||
|
// Feed favorites
|
||||||
|
|
||||||
|
export const feedFavoritesByPubkey = deriveItemsByKey<PublishedList>({
|
||||||
|
repository,
|
||||||
|
getKey: list => list.event.pubkey,
|
||||||
|
filters: [{kinds: [FEEDS]}],
|
||||||
|
eventToItem: async event =>
|
||||||
|
readList(
|
||||||
|
asDecryptedEvent(event, {
|
||||||
|
content: await ensurePlaintext(event),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getFeedFavoritesByPubkey = getter(feedFavoritesByPubkey)
|
||||||
|
|
||||||
|
export const getFeedFavorites = (pubkey: string) => getFeedFavoritesByPubkey().get(pubkey)
|
||||||
|
|
||||||
|
export const loadFeedFavorites = makeLoadItem(makeOutboxLoader(FEEDS), getFeedFavorites)
|
||||||
|
|
||||||
|
export const userFeedFavorites = makeUserData(feedFavoritesByPubkey, loadFeedFavorites)
|
||||||
|
|
||||||
|
export const loadUserFeedFavorites = makeUserLoader(loadFeedFavorites)
|
||||||
|
|
||||||
// Other utils
|
// Other utils
|
||||||
|
|
||||||
export const encodeRelay = (url: string) =>
|
export const encodeRelay = (url: string) =>
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
getSpaceUrlsFromGroupList,
|
getSpaceUrlsFromGroupList,
|
||||||
getSpaceRoomsFromGroupList,
|
getSpaceRoomsFromGroupList,
|
||||||
makeCommentFilter,
|
makeCommentFilter,
|
||||||
|
loadFeedsForPubkey,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {hasBlossomSupport} from "@app/core/commands"
|
import {hasBlossomSupport} from "@app/core/commands"
|
||||||
|
|
||||||
@@ -200,6 +201,7 @@ const syncUserData = () => {
|
|||||||
loadMuteList($userRelayList.event.pubkey)
|
loadMuteList($userRelayList.event.pubkey)
|
||||||
loadProfile($userRelayList.event.pubkey)
|
loadProfile($userRelayList.event.pubkey)
|
||||||
loadSettings($userRelayList.event.pubkey)
|
loadSettings($userRelayList.event.pubkey)
|
||||||
|
loadFeedsForPubkey($userRelayList.event.pubkey)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {fromPairs, parseJson, randomId} from "@welshman/lib"
|
||||||
|
import {FEED, Address} from "@welshman/util"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {
|
||||||
|
makeIntersectionFeed,
|
||||||
|
hasSubFeeds,
|
||||||
|
isTagFeed,
|
||||||
|
isAuthorFeed,
|
||||||
|
isScopeFeed,
|
||||||
|
} from "@welshman/feeds"
|
||||||
|
import type {Feed as IFeed} from "@welshman/feeds"
|
||||||
|
|
||||||
|
export type Feed = {
|
||||||
|
title: string
|
||||||
|
identifier: string
|
||||||
|
description: string
|
||||||
|
definition: IFeed
|
||||||
|
event?: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PublishedFeed = Omit<Feed, "event"> & {
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeFeedDefinition = (feed: IFeed) =>
|
||||||
|
hasSubFeeds(feed) ? feed : makeIntersectionFeed(feed)
|
||||||
|
|
||||||
|
export const makeFeed = (feed: Partial<Feed> = {}): Feed => ({
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
identifier: randomId(),
|
||||||
|
definition: makeIntersectionFeed(),
|
||||||
|
...feed,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const readFeed = (event: TrustedEvent) => {
|
||||||
|
const {d: identifier, title = "", description = "", feed = ""} = fromPairs(event.tags)
|
||||||
|
const definition = parseJson(feed) || makeIntersectionFeed()
|
||||||
|
|
||||||
|
return {title, identifier, description, definition, event} as PublishedFeed
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createFeed = ({identifier, definition, title, description}: Feed) => ({
|
||||||
|
kind: FEED,
|
||||||
|
content: "",
|
||||||
|
tags: [
|
||||||
|
["d", identifier],
|
||||||
|
["alt", title],
|
||||||
|
["title", title],
|
||||||
|
["description", description],
|
||||||
|
["feed", JSON.stringify(definition)],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const editFeed = (feed: PublishedFeed) => ({
|
||||||
|
kind: FEED,
|
||||||
|
content: feed.event.content,
|
||||||
|
tags: Object.entries({
|
||||||
|
...fromPairs(feed.event.tags),
|
||||||
|
title: feed.title,
|
||||||
|
alt: feed.title,
|
||||||
|
description: feed.description,
|
||||||
|
feed: JSON.stringify(feed.definition),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const displayFeed = (feed?: Feed) => feed?.title || "[no name]"
|
||||||
|
|
||||||
|
export const isTopicFeed = (f: IFeed) => isTagFeed(f) && f[1] === "#t"
|
||||||
|
|
||||||
|
export const isMentionFeed = (f: IFeed) => isTagFeed(f) && f[1] === "#p"
|
||||||
|
|
||||||
|
export const isAddressFeed = (f: IFeed) => isTagFeed(f) && f[1] === "#a"
|
||||||
|
|
||||||
|
export const isContextFeed = (f: IFeed) =>
|
||||||
|
isTagFeed(f) && f[1] === "#a" && f.slice(2).every(Address.isAddress)
|
||||||
|
|
||||||
|
export const isPeopleFeed = (f: IFeed) => isAuthorFeed(f) || isScopeFeed(f)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {Snippet} from "svelte"
|
||||||
|
import {getAddress} from "@welshman/util"
|
||||||
|
import History from "@assets/icons/history.svg?dataurl"
|
||||||
|
import Minus from "@assets/icons/minus.svg?dataurl"
|
||||||
|
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
||||||
|
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
|
||||||
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
|
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
||||||
|
import Page from "@lib/components/Page.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import {userFeeds} from "@app/core/state"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children?: Snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
const {children}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SecondaryNav>
|
||||||
|
<SecondaryNavSection>
|
||||||
|
<SecondaryNavItem href="/home">
|
||||||
|
<Icon icon={History} /> Recent Activity
|
||||||
|
</SecondaryNavItem>
|
||||||
|
</SecondaryNavSection>
|
||||||
|
<SecondaryNavSection>
|
||||||
|
<SecondaryNavHeader>Your Feeds</SecondaryNavHeader>
|
||||||
|
{#each $userFeeds as feed (feed.event.id)}
|
||||||
|
<SecondaryNavItem href="/home/feed/{getAddress(feed.event)}">
|
||||||
|
<Icon icon={Minus} />
|
||||||
|
{feed.title}
|
||||||
|
</SecondaryNavItem>
|
||||||
|
{/each}
|
||||||
|
</SecondaryNavSection>
|
||||||
|
</SecondaryNav>
|
||||||
|
<Page>
|
||||||
|
{@render children?.()}
|
||||||
|
</Page>
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
getTagValue,
|
getTagValue,
|
||||||
getTagValues,
|
getTagValues,
|
||||||
getIdAndAddress,
|
getIdAndAddress,
|
||||||
|
getParentIdOrAddr,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
@@ -129,7 +130,7 @@
|
|||||||
makeIntersectionFeed(makeScopeFeed(Scope.Follows), makeKindFeed(NOTE)),
|
makeIntersectionFeed(makeScopeFeed(Scope.Follows), makeKindFeed(NOTE)),
|
||||||
),
|
),
|
||||||
onEvent: batch(100, (evts: TrustedEvent[]) => {
|
onEvent: batch(100, (evts: TrustedEvent[]) => {
|
||||||
events.update($events => [...$events, ...evts])
|
events.update($events => [...$events, ...evts.filter(e => !getParentIdOrAddr(e))])
|
||||||
}),
|
}),
|
||||||
onExhausted: () => {
|
onExhausted: () => {
|
||||||
loading = false
|
loading = false
|
||||||
@@ -141,7 +142,6 @@
|
|||||||
delay: 800,
|
delay: 800,
|
||||||
threshold: 3000,
|
threshold: 3000,
|
||||||
onScroll: async () => {
|
onScroll: async () => {
|
||||||
console.log("scroll")
|
|
||||||
limit.update($limit => {
|
limit.update($limit => {
|
||||||
if ($events.length - $limit < 50) {
|
if ($events.length - $limit < 50) {
|
||||||
ctrl.load(50)
|
ctrl.load(50)
|
||||||
@@ -175,7 +175,6 @@
|
|||||||
<div class="row-2"></div>
|
<div class="row-2"></div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</PageBar>
|
</PageBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col gap-2 p-2 pt-4" bind:element>
|
<PageContent class="flex flex-col gap-2 p-2 pt-4" bind:element>
|
||||||
{#each $recentActivity as { type, event, url, count } (event.id)}
|
{#each $recentActivity as { type, event, url, count } (event.id)}
|
||||||
{#if type === "message"}
|
{#if type === "message"}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {writable} from "svelte/store"
|
||||||
|
import {batch, call} from "@welshman/lib"
|
||||||
|
import type {MakeNonOptional} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {makeFeedController} from "@welshman/app"
|
||||||
|
import {page} from "$app/stores"
|
||||||
|
import {createScroller} from "@lib/html"
|
||||||
|
import PageBar from "@lib/components/PageBar.svelte"
|
||||||
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
|
import NoteItem from "@app/components/NoteItem.svelte"
|
||||||
|
import {deriveFeed} from "@app/core/state"
|
||||||
|
|
||||||
|
const {address} = $page.params as MakeNonOptional<typeof $page.params>
|
||||||
|
const events = writable<TrustedEvent[]>([])
|
||||||
|
const controller = new AbortController()
|
||||||
|
const feed = deriveFeed(address)
|
||||||
|
const limit = writable(0)
|
||||||
|
|
||||||
|
let loading = $state(true)
|
||||||
|
let element: Element | undefined = $state()
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if ($feed) {
|
||||||
|
const promise = call(async () => {
|
||||||
|
const ctrl = makeFeedController({
|
||||||
|
useWindowing: true,
|
||||||
|
signal: controller.signal,
|
||||||
|
feed: $feed.definition,
|
||||||
|
onEvent: batch(100, (evts: TrustedEvent[]) => {
|
||||||
|
events.update($events => [...$events, ...evts])
|
||||||
|
}),
|
||||||
|
onExhausted: () => {
|
||||||
|
loading = false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const scroller = createScroller({
|
||||||
|
element: element!,
|
||||||
|
delay: 800,
|
||||||
|
threshold: 3000,
|
||||||
|
onScroll: async () => {
|
||||||
|
limit.update($limit => {
|
||||||
|
if ($events.length - $limit < 50) {
|
||||||
|
ctrl.load(50)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $limit + 10
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
scroller.stop()
|
||||||
|
controller.abort()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => promise.then(call)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $feed}
|
||||||
|
<PageBar>
|
||||||
|
{#snippet title()}
|
||||||
|
<h1 class="text-xl">{$feed.title}</h1>
|
||||||
|
{/snippet}
|
||||||
|
</PageBar>
|
||||||
|
<PageContent class="flex flex-col gap-2 p-2 pt-4" bind:element>
|
||||||
|
{#each $events as event (event.id)}
|
||||||
|
<NoteItem {event} />
|
||||||
|
{:else}
|
||||||
|
{#if loading}
|
||||||
|
<div class="flex justify-center items-center py-20">
|
||||||
|
<span class="loading loading-spinner mr-3"></span>
|
||||||
|
Loading your feed...
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="flex flex-col items-center py-20 text-center">No content found!</p>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</PageContent>
|
||||||
|
{/if}
|
||||||
Reference in New Issue
Block a user