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