From 9998dcbf7f13686e3ff8d663310473270e445157 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 29 Mar 2026 12:45:28 +0200 Subject: [PATCH] add a publication and article feed to profiles --- package-lock.json | 4 +- package.json | 2 +- src/components/KindFilter/index.tsx | 2 - .../Profile/ProfileFeedWithPins.tsx | 14 ++++-- .../Profile/ProfilePublicationsFeed.tsx | 44 +++++++++++++++++++ src/components/Profile/index.tsx | 9 +++- src/constants.ts | 18 ++++++++ src/i18n/locales/de.ts | 6 +++ src/i18n/locales/en.ts | 6 +++ 9 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 src/components/Profile/ProfilePublicationsFeed.tsx diff --git a/package-lock.json b/package-lock.json index 6e7c1d4c..938c2238 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jumble-imwald", - "version": "21.0.2", + "version": "21.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jumble-imwald", - "version": "21.0.2", + "version": "21.1.0", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", diff --git a/package.json b/package.json index 2f3cead7..cafade31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jumble-imwald", - "version": "21.0.2", + "version": "21.1.0", "description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble", "private": true, "type": "module", diff --git a/src/components/KindFilter/index.tsx b/src/components/KindFilter/index.tsx index d8ca933e..d3ed999d 100644 --- a/src/components/KindFilter/index.tsx +++ b/src/components/KindFilter/index.tsx @@ -17,8 +17,6 @@ const KIND_1 = kinds.ShortTextNote const KIND_1111 = ExtendedKind.COMMENT const KIND_FILTER_OPTIONS = [ - { kindGroup: [kinds.LongFormArticle], label: 'Articles' }, - { kindGroup: [ExtendedKind.WIKI_ARTICLE, ExtendedKind.WIKI_ARTICLE_MARKDOWN], label: 'Wiki Articles' }, { kindGroup: [kinds.Highlights], label: 'Highlights' }, { kindGroup: [ExtendedKind.POLL], label: 'Polls' }, { kindGroup: [ExtendedKind.ZAP_POLL], label: 'Zap polls' }, diff --git a/src/components/Profile/ProfileFeedWithPins.tsx b/src/components/Profile/ProfileFeedWithPins.tsx index 1cdfa05a..faf31e74 100644 --- a/src/components/Profile/ProfileFeedWithPins.tsx +++ b/src/components/Profile/ProfileFeedWithPins.tsx @@ -1,7 +1,7 @@ import NoteCard from '@/components/NoteCard' import ProfileSearchBar from '@/components/ui/ProfileSearchBar' import { Skeleton } from '@/components/ui/skeleton' -import { ExtendedKind, PROFILE_FEED_KINDS } from '@/constants' +import { ExtendedKind, PROFILE_POSTS_TAB_KINDS, PROFILE_PUBLICATIONS_TAB_KINDS } from '@/constants' import { isReplyNoteEvent } from '@/lib/event' import { getZapInfoFromEvent } from '@/lib/event-metadata' import { useProfilePins } from '@/hooks/useProfilePins' @@ -54,6 +54,7 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string return next.sort((a, b) => a - b) }, [showKinds]) const hideReplies = useHideRepliesLikeMainFeed() + const publicationsKindSet = useMemo(() => new Set(PROFILE_PUBLICATIONS_TAB_KINDS), []) const [searchQuery, setSearchQuery] = useState('') const [isRefreshing, setIsRefreshing] = useState(false) const [showCount, setShowCount] = useState(INITIAL_SHOW_COUNT) @@ -76,10 +77,12 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string const cacheKey = useMemo(() => `${pubkey}-profile-unified-${zapReplyThreshold}`, [pubkey, zapReplyThreshold]) + const postsTabKinds = useMemo(() => [...PROFILE_POSTS_TAB_KINDS], []) + const { events: timelineEvents, isLoading: loadingTimeline, refresh: refreshTimeline } = useProfileTimeline({ pubkey, cacheKey, - kinds: PROFILE_FEED_KINDS, + kinds: postsTabKinds, limit: 200, filterPredicate }) @@ -160,8 +163,11 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string ) const filteredPins = useMemo( - () => applySearch(pinEvents).filter((e) => !isEventDeleted(e)), - [pinEvents, applySearch, isEventDeleted] + () => + applySearch(pinEvents) + .filter((e) => !isEventDeleted(e)) + .filter((e) => !publicationsKindSet.has(e.kind)), + [pinEvents, applySearch, isEventDeleted, publicationsKindSet] ) const filteredRest = useMemo( () => diff --git a/src/components/Profile/ProfilePublicationsFeed.tsx b/src/components/Profile/ProfilePublicationsFeed.tsx new file mode 100644 index 00000000..2641463d --- /dev/null +++ b/src/components/Profile/ProfilePublicationsFeed.tsx @@ -0,0 +1,44 @@ +import ProfileSearchBar from '@/components/ui/ProfileSearchBar' +import { PROFILE_PUBLICATIONS_TAB_KINDS } from '@/constants' +import { forwardRef, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import ProfileTimeline from './ProfileTimeline' + +const ProfilePublicationsFeed = forwardRef<{ refresh: () => void }, { pubkey: string }>(({ pubkey }, ref) => { + const { t } = useTranslation() + const [searchQuery, setSearchQuery] = useState('') + + const kindsList = useMemo(() => [...PROFILE_PUBLICATIONS_TAB_KINDS], []) + const cacheKey = useMemo(() => `${pubkey}-profile-publications`, [pubkey]) + + const getKindLabel = (_kindValue: string) => t('articles and publications') + + return ( +
+
+ +
+ +
+ ) +}) + +ProfilePublicationsFeed.displayName = 'ProfilePublicationsFeed' + +export default ProfilePublicationsFeed diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index dea26527..aacf9705 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -47,6 +47,7 @@ import NotFound from '../NotFound' import FollowedBy from './FollowedBy' import ProfileFeedWithPins from './ProfileFeedWithPins' import ProfileMediaFeed from './ProfileMediaFeed' +import ProfilePublicationsFeed from './ProfilePublicationsFeed' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import type { TNoteListRef } from '@/components/NoteList' import ProfileInteractionsAccordion from './ProfileInteractionsAccordion' @@ -183,6 +184,7 @@ export default function Profile({ const profileFeedRef = feedRef ?? internalFeedRef const postsFeedRef = useRef<{ refresh: () => void }>(null) const mediaFeedRef = useRef(null) + const publicationsFeedRef = useRef<{ refresh: () => void }>(null) const { profile, isFetching } = useFetchProfile(id) const { pubkey: accountPubkey, publish, checkLogin } = useNostr() @@ -353,6 +355,7 @@ export default function Profile({ profileInteractionsRefreshRef.current?.() postsFeedRef.current?.refresh() mediaFeedRef.current?.refresh() + publicationsFeedRef.current?.refresh() } } return () => { @@ -652,10 +655,11 @@ export default function Profile({ - + {t('Posts')} {t('Media')} + {t('Articles and Publications')} @@ -663,6 +667,9 @@ export default function Profile({ + + + {openPublicMessageTo && ( (PROFILE_PUBLICATIONS_TAB_KINDS) + +/** + * Kinds subscribed on the profile Posts tab only. Omits {@link PROFILE_PUBLICATIONS_TAB_KINDS} so those events + * appear on the dedicated tab; {@link PROFILE_FEED_KINDS} is unchanged for the home feed and kind-filter defaults. + */ +export const PROFILE_POSTS_TAB_KINDS: readonly number[] = PROFILE_FEED_KINDS.filter( + (k) => !PROFILE_PUBLICATIONS_TAB_KIND_SET.has(k) +) + /** * {@link PROFILE_FEED_KINDS} without reposts (kind 6 / 16). Default for the global kind filter, home feed, * and most faux spells. Reposts are still shown on profile timelines, Spells → Following, and Follows latest. diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 98c8e32b..8ee54f92 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -651,6 +651,12 @@ export default { '{{votes}} · {{pct}}%': '{{votes}} · {{pct}}%', Poll: 'Umfrage', Media: 'Medien', + 'Articles and Publications': 'Artikel und Veröffentlichungen', + 'Search articles...': 'Artikel suchen…', + 'Refreshing articles...': 'Artikel werden aktualisiert…', + 'No articles or publications found': 'Keine Artikel oder Veröffentlichungen gefunden', + 'No articles or publications match your search': 'Keine Artikel oder Veröffentlichungen entsprechen der Suche', + 'articles and publications': 'Artikel und Veröffentlichungen', Interests: 'Interessen', Calendar: 'Kalender', 'No subscribed interests yet.': diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index af984970..23604e97 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -697,6 +697,12 @@ export default { '{{n}} zaps': '{{n}} zaps', Poll: 'Poll', Media: 'Media', + 'Articles and Publications': 'Articles and Publications', + 'Search articles...': 'Search articles...', + 'Refreshing articles...': 'Refreshing articles...', + 'No articles or publications found': 'No articles or publications found', + 'No articles or publications match your search': 'No articles or publications match your search', + 'articles and publications': 'articles and publications', Interests: 'Interests', Calendar: 'Calendar', 'No subscribed interests yet.':