forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useEffect, useImperativeHandle, useMemo} from 'react'
2import {findNodeHandle, type ListRenderItemInfo, View} from 'react-native'
3import {
4 type AppBskyLabelerDefs,
5 type InterpretedLabelValueDefinition,
6 interpretLabelValueDefinitions,
7 type ModerationOpts,
8} from '@atproto/api'
9import {msg} from '@lingui/core/macro'
10import {useLingui} from '@lingui/react'
11import {Trans} from '@lingui/react/macro'
12
13import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
14import {List, type ListRef} from '#/view/com/util/List'
15import {atoms as a, ios, tokens, useTheme} from '#/alf'
16import {Divider} from '#/components/Divider'
17import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
18import {ListFooter} from '#/components/Lists'
19import {Loader} from '#/components/Loader'
20import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
21import {Text} from '#/components/Typography'
22import {IS_IOS, IS_NATIVE} from '#/env'
23import {ErrorState} from '../ErrorState'
24import {type SectionRef} from './types'
25
26interface LabelsSectionProps {
27 ref: React.Ref<SectionRef>
28 isLabelerLoading: boolean
29 labelerInfo: AppBskyLabelerDefs.LabelerViewDetailed | undefined
30 labelerError: Error | null
31 moderationOpts: ModerationOpts
32 scrollElRef: ListRef
33 headerHeight: number
34 isFocused: boolean
35 setScrollViewTag: (tag: number | null) => void
36}
37
38export function ProfileLabelsSection({
39 ref,
40 isLabelerLoading,
41 labelerInfo,
42 labelerError,
43 moderationOpts,
44 scrollElRef,
45 headerHeight,
46 isFocused,
47 setScrollViewTag,
48}: LabelsSectionProps) {
49 const t = useTheme()
50
51 const onScrollToTop = useCallback(() => {
52 scrollElRef.current?.scrollToOffset({
53 animated: IS_NATIVE,
54 offset: -headerHeight,
55 })
56 }, [scrollElRef, headerHeight])
57
58 useImperativeHandle(ref, () => ({
59 scrollToTop: onScrollToTop,
60 }))
61
62 useEffect(() => {
63 if (IS_IOS && isFocused && scrollElRef.current) {
64 const nativeTag = findNodeHandle(scrollElRef.current)
65 setScrollViewTag(nativeTag)
66 }
67 }, [isFocused, scrollElRef, setScrollViewTag])
68
69 const isSubscribed = labelerInfo
70 ? !!isLabelerSubscribed(labelerInfo, moderationOpts)
71 : false
72
73 const labelValues = useMemo(() => {
74 if (isLabelerLoading || !labelerInfo || labelerError) return []
75 const customDefs = interpretLabelValueDefinitions(labelerInfo)
76 return labelerInfo.policies.labelValues
77 .filter((val, i, arr) => arr.indexOf(val) === i) // dedupe
78 .map(val => lookupLabelValueDefinition(val, customDefs))
79 .filter(
80 def => def && def?.configurable,
81 ) as InterpretedLabelValueDefinition[]
82 }, [labelerInfo, labelerError, isLabelerLoading])
83
84 const numItems = labelValues.length
85
86 const renderItem = useCallback(
87 ({item, index}: ListRenderItemInfo<InterpretedLabelValueDefinition>) => {
88 if (!labelerInfo) return null
89 return (
90 <View
91 style={[
92 t.atoms.bg_contrast_25,
93 index === 0 && [
94 a.overflow_hidden,
95 {
96 borderTopLeftRadius: tokens.borderRadius.md,
97 borderTopRightRadius: tokens.borderRadius.md,
98 },
99 ],
100 index === numItems - 1 && [
101 a.overflow_hidden,
102 {
103 borderBottomLeftRadius: tokens.borderRadius.md,
104 borderBottomRightRadius: tokens.borderRadius.md,
105 },
106 ],
107 ]}>
108 {index !== 0 && <Divider />}
109 <LabelerLabelPreference
110 disabled={isSubscribed ? undefined : true}
111 labelDefinition={item}
112 labelerDid={labelerInfo.creator.did}
113 />
114 </View>
115 )
116 },
117 [labelerInfo, isSubscribed, numItems, t],
118 )
119
120 return (
121 <View>
122 <List
123 ref={scrollElRef}
124 data={labelValues}
125 renderItem={renderItem}
126 keyExtractor={keyExtractor}
127 contentContainerStyle={a.px_xl}
128 headerOffset={headerHeight}
129 progressViewOffset={ios(0)}
130 ListHeaderComponent={
131 <LabelerListHeader
132 isLabelerLoading={isLabelerLoading}
133 labelerInfo={labelerInfo}
134 labelerError={labelerError}
135 hasValues={labelValues.length !== 0}
136 isSubscribed={isSubscribed}
137 />
138 }
139 ListFooterComponent={
140 <ListFooter
141 height={headerHeight + 180}
142 style={a.border_transparent}
143 />
144 }
145 />
146 </View>
147 )
148}
149
150function keyExtractor(item: InterpretedLabelValueDefinition) {
151 return item.identifier
152}
153
154export function LabelerListHeader({
155 isLabelerLoading,
156 labelerError,
157 labelerInfo,
158 hasValues,
159 isSubscribed,
160}: {
161 isLabelerLoading: boolean
162 labelerError?: Error | null
163 labelerInfo?: AppBskyLabelerDefs.LabelerViewDetailed
164 hasValues: boolean
165 isSubscribed: boolean
166}) {
167 const t = useTheme()
168 const {_} = useLingui()
169
170 if (isLabelerLoading) {
171 return (
172 <View style={[a.w_full, a.align_center, a.py_4xl]}>
173 <Loader size="xl" />
174 </View>
175 )
176 }
177
178 if (labelerError || !labelerInfo) {
179 return (
180 <View style={[a.w_full, a.align_center, a.py_4xl]}>
181 <ErrorState
182 error={
183 labelerError?.toString() ||
184 _(msg`Something went wrong, please try again.`)
185 }
186 />
187 </View>
188 )
189 }
190
191 return (
192 <View style={[a.py_xl]}>
193 <Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}>
194 <Trans>
195 Labels are annotations on users and content. They can be used to hide,
196 warn, and categorize the network.
197 </Trans>
198 </Text>
199 {labelerInfo?.creator.viewer?.blocking ? (
200 <View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}>
201 <CircleInfo size="sm" fill={t.atoms.text_contrast_medium.color} />
202 <Text style={[t.atoms.text_contrast_high, a.leading_snug, a.text_sm]}>
203 <Trans>
204 Blocking does not prevent this labeler from placing labels on your
205 account.
206 </Trans>
207 </Text>
208 </View>
209 ) : null}
210 {!hasValues ? (
211 <Text
212 style={[
213 a.pt_xl,
214 t.atoms.text_contrast_high,
215 a.leading_snug,
216 a.text_sm,
217 ]}>
218 <Trans>
219 This labeler hasn't declared what labels it publishes, and may not
220 be active.
221 </Trans>
222 </Text>
223 ) : !isSubscribed ? (
224 <Text
225 style={[
226 a.pt_xl,
227 t.atoms.text_contrast_high,
228 a.leading_snug,
229 a.text_sm,
230 ]}>
231 <Trans>
232 Subscribe to @{labelerInfo.creator.handle} to use these labels:
233 </Trans>
234 </Text>
235 ) : null}
236 </View>
237 )
238}