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 @@ @@ -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",

2
package.json

@ -1,6 +1,6 @@ @@ -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",

2
src/components/KindFilter/index.tsx

@ -17,8 +17,6 @@ const KIND_1 = kinds.ShortTextNote @@ -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' },

14
src/components/Profile/ProfileFeedWithPins.tsx

@ -1,7 +1,7 @@ @@ -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 @@ -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 @@ -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 @@ -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(
() =>

44
src/components/Profile/ProfilePublicationsFeed.tsx

@ -0,0 +1,44 @@ @@ -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' @@ -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({ @@ -183,6 +184,7 @@ export default function Profile({
const profileFeedRef = feedRef ?? internalFeedRef
const postsFeedRef = useRef<{ refresh: () => void }>(null)
const mediaFeedRef = useRef<TNoteListRef>(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({ @@ -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({ @@ -652,10 +655,11 @@ export default function Profile({
</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">
<TabsTrigger value="posts">{t('Posts')}</TabsTrigger>
<TabsTrigger value="media">{t('Media')}</TabsTrigger>
<TabsTrigger value="publications">{t('Articles and Publications')}</TabsTrigger>
</TabsList>
<TabsContent value="posts" className="min-w-0 focus-visible:outline-none">
<ProfileFeedWithPins ref={postsFeedRef} pubkey={pubkey} />
@ -663,6 +667,9 @@ export default function Profile({ @@ -663,6 +667,9 @@ export default function Profile({
<TabsContent value="media" className="min-w-0 focus-visible:outline-none">
<ProfileMediaFeed ref={mediaFeedRef} pubkey={pubkey} />
</TabsContent>
<TabsContent value="publications" className="min-w-0 focus-visible:outline-none">
<ProfilePublicationsFeed ref={publicationsFeedRef} pubkey={pubkey} />
</TabsContent>
</Tabs>
{openPublicMessageTo && (
<PostEditor

18
src/constants.ts

@ -496,6 +496,24 @@ export const PROFILE_FEED_KINDS = SUPPORTED_KINDS.filter( @@ -496,6 +496,24 @@ export const PROFILE_FEED_KINDS = SUPPORTED_KINDS.filter(
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,
* 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 { @@ -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.':

6
src/i18n/locales/en.ts

@ -697,6 +697,12 @@ export default { @@ -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.':

Loading…
Cancel
Save