forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React, {useCallback} from 'react'
2import {type ListRenderItemInfo, View} from 'react-native'
3import {
4 type AppBskyActorDefs,
5 type AppBskyGraphGetList,
6 AtUri,
7 type ModerationOpts,
8} from '@atproto/api'
9import {
10 type InfiniteData,
11 type UseInfiniteQueryResult,
12} from '@tanstack/react-query'
13
14import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
15import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
16import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted'
17import {useAllListMembersQuery} from '#/state/queries/list-members'
18import {useSession} from '#/state/session'
19import {List, type ListRef} from '#/view/com/util/List'
20import {type SectionRef} from '#/screens/Profile/Sections/types'
21import {atoms as a, useTheme} from '#/alf'
22import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
23import {Default as ProfileCard} from '#/components/ProfileCard'
24import {IS_NATIVE, IS_WEB} from '#/env'
25
26function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic, index: number) {
27 return `${item.did}-${index}`
28}
29
30interface ProfilesListProps {
31 listUri: string
32 listMembersQuery: UseInfiniteQueryResult<
33 InfiniteData<AppBskyGraphGetList.OutputSchema>
34 >
35 moderationOpts: ModerationOpts
36 headerHeight: number
37 scrollElRef: ListRef
38}
39
40export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
41 function ProfilesListImpl(
42 {listUri, moderationOpts, headerHeight, scrollElRef},
43 ref,
44 ) {
45 const t = useTheme()
46 const bottomBarOffset = useBottomBarOffset(headerHeight)
47 const initialNumToRender = useInitialNumToRender()
48 const {currentAccount} = useSession()
49 const {data, refetch, isError} = useAllListMembersQuery(listUri)
50
51 const [isPTRing, setIsPTRing] = React.useState(false)
52
53 // The server returns these sorted by descending creation date, so we want to invert
54
55 const profiles = data
56 ?.filter(
57 p => !isBlockedOrBlocking(p.subject) && !p.subject.associated?.labeler,
58 )
59 .map(p => p.subject)
60 .reverse()
61 const isOwn = new AtUri(listUri).host === currentAccount?.did
62
63 const getSortedProfiles = () => {
64 if (!profiles) return
65 if (!isOwn) return profiles
66
67 const myIndex = profiles.findIndex(p => p.did === currentAccount?.did)
68 return myIndex !== -1
69 ? [
70 profiles[myIndex],
71 ...profiles.slice(0, myIndex),
72 ...profiles.slice(myIndex + 1),
73 ]
74 : profiles
75 }
76 const onScrollToTop = useCallback(() => {
77 scrollElRef.current?.scrollToOffset({
78 animated: IS_NATIVE,
79 offset: -headerHeight,
80 })
81 }, [scrollElRef, headerHeight])
82
83 React.useImperativeHandle(ref, () => ({
84 scrollToTop: onScrollToTop,
85 }))
86
87 const renderItem = ({
88 item,
89 index,
90 }: ListRenderItemInfo<AppBskyActorDefs.ProfileViewBasic>) => {
91 return (
92 <View
93 style={[
94 a.p_lg,
95 t.atoms.border_contrast_low,
96 (IS_WEB || index !== 0) && a.border_t,
97 ]}>
98 <ProfileCard
99 profile={item}
100 moderationOpts={moderationOpts}
101 logContext="StarterPackProfilesList"
102 />
103 </View>
104 )
105 }
106
107 if (!data) {
108 return (
109 <View
110 style={[
111 a.h_full_vh,
112 {marginTop: headerHeight, marginBottom: bottomBarOffset},
113 ]}>
114 <ListMaybePlaceholder
115 isLoading={true}
116 isError={isError}
117 onRetry={refetch}
118 />
119 </View>
120 )
121 }
122
123 if (data)
124 return (
125 <List
126 data={getSortedProfiles()}
127 renderItem={renderItem}
128 keyExtractor={keyExtractor}
129 ref={scrollElRef}
130 headerOffset={headerHeight}
131 ListFooterComponent={
132 <ListFooter
133 style={{paddingBottom: bottomBarOffset, borderTopWidth: 0}}
134 />
135 }
136 showsVerticalScrollIndicator={false}
137 desktopFixedHeight
138 initialNumToRender={initialNumToRender}
139 refreshing={isPTRing}
140 onRefresh={async () => {
141 setIsPTRing(true)
142 await refetch()
143 setIsPTRing(false)
144 }}
145 />
146 )
147 },
148)