Browse Source

add a publication and article feed to profiles

imwald
Silberengel 1 month ago
parent
commit
9998dcbf7f
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 2
      src/components/KindFilter/index.tsx
  4. 14
      src/components/Profile/ProfileFeedWithPins.tsx
  5. 44
      src/components/Profile/ProfilePublicationsFeed.tsx
  6. 9
      src/components/Profile/index.tsx
  7. 18
      src/constants.ts
  8. 6
      src/i18n/locales/de.ts
  9. 6
      src/i18n/locales/en.ts

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "jumble-imwald", "name": "jumble-imwald",
"version": "21.0.2", "version": "21.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "jumble-imwald", "name": "jumble-imwald",
"version": "21.0.2", "version": "21.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "jumble-imwald", "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", "description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble",
"private": true, "private": true,
"type": "module", "type": "module",

2
src/components/KindFilter/index.tsx

@ -17,8 +17,6 @@ const KIND_1 = kinds.ShortTextNote
const KIND_1111 = ExtendedKind.COMMENT const KIND_1111 = ExtendedKind.COMMENT
const KIND_FILTER_OPTIONS = [ 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: [kinds.Highlights], label: 'Highlights' },
{ kindGroup: [ExtendedKind.POLL], label: 'Polls' }, { kindGroup: [ExtendedKind.POLL], label: 'Polls' },
{ kindGroup: [ExtendedKind.ZAP_POLL], label: 'Zap polls' }, { kindGroup: [ExtendedKind.ZAP_POLL], label: 'Zap polls' },

14
src/components/Profile/ProfileFeedWithPins.tsx

@ -1,7 +1,7 @@
import NoteCard from '@/components/NoteCard' import NoteCard from '@/components/NoteCard'
import ProfileSearchBar from '@/components/ui/ProfileSearchBar' import ProfileSearchBar from '@/components/ui/ProfileSearchBar'
import { Skeleton } from '@/components/ui/skeleton' 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 { isReplyNoteEvent } from '@/lib/event'
import { getZapInfoFromEvent } from '@/lib/event-metadata' import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { useProfilePins } from '@/hooks/useProfilePins' import { useProfilePins } from '@/hooks/useProfilePins'
@ -54,6 +54,7 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string
return next.sort((a, b) => a - b) return next.sort((a, b) => a - b)
}, [showKinds]) }, [showKinds])
const hideReplies = useHideRepliesLikeMainFeed() const hideReplies = useHideRepliesLikeMainFeed()
const publicationsKindSet = useMemo(() => new Set(PROFILE_PUBLICATIONS_TAB_KINDS), [])
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [isRefreshing, setIsRefreshing] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false)
const [showCount, setShowCount] = useState(INITIAL_SHOW_COUNT) 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 cacheKey = useMemo(() => `${pubkey}-profile-unified-${zapReplyThreshold}`, [pubkey, zapReplyThreshold])
const postsTabKinds = useMemo(() => [...PROFILE_POSTS_TAB_KINDS], [])
const { events: timelineEvents, isLoading: loadingTimeline, refresh: refreshTimeline } = useProfileTimeline({ const { events: timelineEvents, isLoading: loadingTimeline, refresh: refreshTimeline } = useProfileTimeline({
pubkey, pubkey,
cacheKey, cacheKey,
kinds: PROFILE_FEED_KINDS, kinds: postsTabKinds,
limit: 200, limit: 200,
filterPredicate filterPredicate
}) })
@ -160,8 +163,11 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string
) )
const filteredPins = useMemo( 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( const filteredRest = useMemo(
() => () =>

44
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 (
<div className="mt-4">
<div className="mb-2 flex flex-wrap items-center gap-2 px-2">
<ProfileSearchBar
onSearch={setSearchQuery}
placeholder={t('Search articles...')}
className="w-64 max-w-full"
/>
</div>
<ProfileTimeline
ref={ref}
pubkey={pubkey}
topSpace={0}
searchQuery={searchQuery}
kindFilter="all"
kinds={kindsList}
cacheKey={cacheKey}
getKindLabel={getKindLabel}
refreshLabel={t('Refreshing articles...')}
emptyLabel={t('No articles or publications found')}
emptySearchLabel={t('No articles or publications match your search')}
/>
</div>
)
})
ProfilePublicationsFeed.displayName = 'ProfilePublicationsFeed'
export default ProfilePublicationsFeed

9
src/components/Profile/index.tsx

@ -47,6 +47,7 @@ import NotFound from '../NotFound'
import FollowedBy from './FollowedBy' import FollowedBy from './FollowedBy'
import ProfileFeedWithPins from './ProfileFeedWithPins' import ProfileFeedWithPins from './ProfileFeedWithPins'
import ProfileMediaFeed from './ProfileMediaFeed' import ProfileMediaFeed from './ProfileMediaFeed'
import ProfilePublicationsFeed from './ProfilePublicationsFeed'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import type { TNoteListRef } from '@/components/NoteList' import type { TNoteListRef } from '@/components/NoteList'
import ProfileInteractionsAccordion from './ProfileInteractionsAccordion' import ProfileInteractionsAccordion from './ProfileInteractionsAccordion'
@ -183,6 +184,7 @@ export default function Profile({
const profileFeedRef = feedRef ?? internalFeedRef const profileFeedRef = feedRef ?? internalFeedRef
const postsFeedRef = useRef<{ refresh: () => void }>(null) const postsFeedRef = useRef<{ refresh: () => void }>(null)
const mediaFeedRef = useRef<TNoteListRef>(null) const mediaFeedRef = useRef<TNoteListRef>(null)
const publicationsFeedRef = useRef<{ refresh: () => void }>(null)
const { profile, isFetching } = useFetchProfile(id) const { profile, isFetching } = useFetchProfile(id)
const { pubkey: accountPubkey, publish, checkLogin } = useNostr() const { pubkey: accountPubkey, publish, checkLogin } = useNostr()
@ -353,6 +355,7 @@ export default function Profile({
profileInteractionsRefreshRef.current?.() profileInteractionsRefreshRef.current?.()
postsFeedRef.current?.refresh() postsFeedRef.current?.refresh()
mediaFeedRef.current?.refresh() mediaFeedRef.current?.refresh()
publicationsFeedRef.current?.refresh()
} }
} }
return () => { return () => {
@ -652,10 +655,11 @@ export default function Profile({
</div> </div>
</div> </div>
</div> </div>
<Tabs defaultValue="posts" className="min-w-0"> <Tabs defaultValue="posts" className="min-w-0 pt-4">
<TabsList className="mb-2 ml-1 w-auto justify-start md:ml-4"> <TabsList className="mb-2 ml-1 w-auto justify-start md:ml-4">
<TabsTrigger value="posts">{t('Posts')}</TabsTrigger> <TabsTrigger value="posts">{t('Posts')}</TabsTrigger>
<TabsTrigger value="media">{t('Media')}</TabsTrigger> <TabsTrigger value="media">{t('Media')}</TabsTrigger>
<TabsTrigger value="publications">{t('Articles and Publications')}</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="posts" className="min-w-0 focus-visible:outline-none"> <TabsContent value="posts" className="min-w-0 focus-visible:outline-none">
<ProfileFeedWithPins ref={postsFeedRef} pubkey={pubkey} /> <ProfileFeedWithPins ref={postsFeedRef} pubkey={pubkey} />
@ -663,6 +667,9 @@ export default function Profile({
<TabsContent value="media" className="min-w-0 focus-visible:outline-none"> <TabsContent value="media" className="min-w-0 focus-visible:outline-none">
<ProfileMediaFeed ref={mediaFeedRef} pubkey={pubkey} /> <ProfileMediaFeed ref={mediaFeedRef} pubkey={pubkey} />
</TabsContent> </TabsContent>
<TabsContent value="publications" className="min-w-0 focus-visible:outline-none">
<ProfilePublicationsFeed ref={publicationsFeedRef} pubkey={pubkey} />
</TabsContent>
</Tabs> </Tabs>
{openPublicMessageTo && ( {openPublicMessageTo && (
<PostEditor <PostEditor

18
src/constants.ts

@ -496,6 +496,24 @@ export const PROFILE_FEED_KINDS = SUPPORTED_KINDS.filter(
k !== ExtendedKind.APPLICATION_HANDLER_INFO k !== ExtendedKind.APPLICATION_HANDLER_INFO
) )
/** Long-form, wiki, and publication index events for the profile "Articles and Publications" tab. */
export const PROFILE_PUBLICATIONS_TAB_KINDS: readonly number[] = [
kinds.LongFormArticle,
ExtendedKind.PUBLICATION,
ExtendedKind.WIKI_ARTICLE,
ExtendedKind.WIKI_ARTICLE_MARKDOWN
]
const PROFILE_PUBLICATIONS_TAB_KIND_SET = new Set<number>(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, * {@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. * and most faux spells. Reposts are still shown on profile timelines, Spells Following, and Follows latest.

6
src/i18n/locales/de.ts

@ -651,6 +651,12 @@ export default {
'{{votes}} · {{pct}}%': '{{votes}} · {{pct}}%', '{{votes}} · {{pct}}%': '{{votes}} · {{pct}}%',
Poll: 'Umfrage', Poll: 'Umfrage',
Media: 'Medien', 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', Interests: 'Interessen',
Calendar: 'Kalender', Calendar: 'Kalender',
'No subscribed interests yet.': 'No subscribed interests yet.':

6
src/i18n/locales/en.ts

@ -697,6 +697,12 @@ export default {
'{{n}} zaps': '{{n}} zaps', '{{n}} zaps': '{{n}} zaps',
Poll: 'Poll', Poll: 'Poll',
Media: 'Media', 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', Interests: 'Interests',
Calendar: 'Calendar', Calendar: 'Calendar',
'No subscribed interests yet.': 'No subscribed interests yet.':

Loading…
Cancel
Save