forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useMemo, useState} from 'react'
2import {
3 LayoutAnimation,
4 type StyleProp,
5 View,
6 type ViewStyle,
7} from 'react-native'
8import {type ModerationUI} from '@atproto/api'
9import {msg} from '@lingui/core/macro'
10import {useLingui} from '@lingui/react'
11import {Trans} from '@lingui/react/macro'
12
13import {
14 ADULT_CONTENT_LABELS,
15 type AdultSelfLabel,
16 isJustAMute,
17} from '#/lib/moderation'
18import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
19import {getDefinition, getLabelStrings} from '#/lib/moderation/useLabelInfo'
20import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
21import {sanitizeDisplayName} from '#/lib/strings/display-names'
22import {useLabelDefinitions} from '#/state/preferences'
23import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
24import {Button} from '#/components/Button'
25import {
26 ModerationDetailsDialog,
27 useModerationDetailsDialogControl,
28} from '#/components/moderation/ModerationDetailsDialog'
29import {Text} from '#/components/Typography'
30
31export function ContentHider({
32 testID,
33 modui,
34 ignoreMute,
35 style,
36 activeStyle,
37 childContainerStyle,
38 children,
39}: {
40 testID?: string
41 modui: ModerationUI | undefined
42 ignoreMute?: boolean
43 style?: StyleProp<ViewStyle>
44 activeStyle?: StyleProp<ViewStyle>
45 childContainerStyle?: StyleProp<ViewStyle>
46 children?: React.ReactNode | ((props: {active: boolean}) => React.ReactNode)
47}) {
48 const blur = modui?.blurs[0]
49 if (!blur || (ignoreMute && isJustAMute(modui))) {
50 return (
51 <View testID={testID} style={style}>
52 {typeof children === 'function' ? children({active: false}) : children}
53 </View>
54 )
55 }
56 return (
57 <ContentHiderActive
58 testID={testID}
59 modui={modui}
60 style={[style, activeStyle]}
61 childContainerStyle={childContainerStyle}>
62 {typeof children === 'function' ? children({active: true}) : children}
63 </ContentHiderActive>
64 )
65}
66
67function ContentHiderActive({
68 testID,
69 modui,
70 style,
71 childContainerStyle,
72 children,
73}: {
74 testID?: string
75 modui: ModerationUI
76 style?: StyleProp<ViewStyle>
77 childContainerStyle?: StyleProp<ViewStyle>
78 children?: React.ReactNode
79}) {
80 const t = useTheme()
81 const {_} = useLingui()
82 const {gtMobile} = useBreakpoints()
83 const [override, setOverride] = useState(false)
84 const control = useModerationDetailsDialogControl()
85 const {labelDefs} = useLabelDefinitions()
86 const globalLabelStrings = useGlobalLabelStrings()
87 const {i18n} = useLingui()
88 const blur = modui?.blurs[0]
89 const desc = useModerationCauseDescription(blur)
90
91 const labelName = useMemo(() => {
92 if (!modui?.blurs || !blur) {
93 return undefined
94 }
95 if (
96 blur.type !== 'label' ||
97 (blur.type === 'label' && blur.source.type !== 'user')
98 ) {
99 if (desc.isSubjectAccount) {
100 return _(msg`${desc.name} (Account)`)
101 } else {
102 return desc.name
103 }
104 }
105
106 let hasAdultContentLabel = false
107 const selfBlurNames = modui.blurs
108 .filter(cause => {
109 if (cause.type !== 'label') {
110 return false
111 }
112 if (cause.source.type !== 'user') {
113 return false
114 }
115 if (ADULT_CONTENT_LABELS.includes(cause.label.val as AdultSelfLabel)) {
116 if (hasAdultContentLabel) {
117 return false
118 }
119 hasAdultContentLabel = true
120 }
121 return true
122 })
123 .slice(0, 2)
124 .map(cause => {
125 if (cause.type !== 'label') {
126 return
127 }
128
129 const def = cause.labelDef || getDefinition(labelDefs, cause.label)
130 if (def.identifier === 'porn' || def.identifier === 'sexual') {
131 return _(msg`Adult Content`)
132 }
133 return getLabelStrings(i18n.locale, globalLabelStrings, def).name
134 })
135
136 if (selfBlurNames.length === 0) {
137 return desc.name
138 }
139 return [...new Set(selfBlurNames)].join(', ')
140 }, [
141 _,
142 modui?.blurs,
143 blur,
144 desc.name,
145 desc.isSubjectAccount,
146 labelDefs,
147 i18n.locale,
148 globalLabelStrings,
149 ])
150
151 return (
152 <View testID={testID} style={[a.overflow_hidden, style]}>
153 <ModerationDetailsDialog control={control} modcause={blur} />
154
155 <Button
156 onPress={e => {
157 e.preventDefault()
158 e.stopPropagation()
159 if (!modui.noOverride) {
160 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
161 setOverride(v => !v)
162 } else {
163 control.open()
164 }
165 }}
166 label={desc.name}
167 accessibilityHint={
168 modui.noOverride
169 ? _(msg`Learn more about the moderation applied to this content`)
170 : override
171 ? _(msg`Hides the content`)
172 : _(msg`Shows the content`)
173 }>
174 {state => (
175 <View
176 style={[
177 a.flex_row,
178 a.w_full,
179 a.justify_start,
180 a.align_center,
181 a.py_md,
182 a.px_lg,
183 a.gap_xs,
184 a.rounded_sm,
185 t.atoms.bg_contrast_25,
186 gtMobile && [a.gap_sm, a.py_lg, a.mt_xs, a.px_xl],
187 (state.hovered || state.pressed) && t.atoms.bg_contrast_50,
188 ]}>
189 <desc.icon
190 size="md"
191 fill={t.atoms.text_contrast_medium.color}
192 style={{marginLeft: -2}}
193 />
194 <Text
195 style={[
196 a.flex_1,
197 a.text_left,
198 a.font_semi_bold,
199 a.leading_snug,
200 gtMobile && [a.font_semi_bold],
201 t.atoms.text_contrast_medium,
202 web({
203 marginBottom: 1,
204 }),
205 ]}
206 numberOfLines={2}>
207 {labelName}
208 </Text>
209 {!modui.noOverride && (
210 <Text
211 style={[
212 a.font_semi_bold,
213 a.leading_snug,
214 gtMobile && [a.font_semi_bold],
215 t.atoms.text_contrast_high,
216 web({
217 marginBottom: 1,
218 }),
219 ]}>
220 {override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
221 </Text>
222 )}
223 </View>
224 )}
225 </Button>
226
227 {desc.source && blur.type === 'label' && !override && (
228 <Button
229 onPress={e => {
230 e.preventDefault()
231 e.stopPropagation()
232 control.open()
233 }}
234 label={_(
235 msg`Learn more about the moderation applied to this content`,
236 )}
237 style={[a.pt_sm]}>
238 {state => (
239 <Text
240 style={[
241 a.flex_1,
242 a.text_sm,
243 a.font_normal,
244 a.leading_snug,
245 t.atoms.text_contrast_medium,
246 a.text_left,
247 ]}>
248 {desc.sourceType === 'user' ? (
249 <Trans>Labeled by the author.</Trans>
250 ) : (
251 <Trans>Labeled by {sanitizeDisplayName(desc.source!)}.</Trans>
252 )}{' '}
253 <Text
254 style={[
255 {color: t.palette.primary_500},
256 a.text_sm,
257 state.hovered && [web({textDecoration: 'underline'})],
258 ]}>
259 <Trans>Learn more.</Trans>
260 </Text>
261 </Text>
262 )}
263 </Button>
264 )}
265
266 {override && <View style={childContainerStyle}>{children}</View>}
267 </View>
268 )
269}