forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {View} from 'react-native'
2import {type AppBskyLabelerDefs} from '@atproto/api'
3import {msg, Plural, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5import type React from 'react'
6
7import {getLabelingServiceTitle} from '#/lib/moderation'
8import {sanitizeHandle} from '#/lib/strings/handles'
9import {useLabelerInfoQuery} from '#/state/queries/labeler'
10import {UserAvatar} from '#/view/com/util/UserAvatar'
11import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
12import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
13import {Link as InternalLink, type LinkProps} from '#/components/Link'
14import {RichText} from '#/components/RichText'
15import {Text} from '#/components/Typography'
16import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '../icons/Chevron'
17
18type LabelingServiceProps = {
19 labeler: AppBskyLabelerDefs.LabelerViewDetailed
20}
21
22export function Outer({
23 children,
24 style,
25}: React.PropsWithChildren<ViewStyleProp>) {
26 return (
27 <View
28 style={[
29 a.flex_row,
30 a.gap_md,
31 a.w_full,
32 a.p_lg,
33 a.pr_md,
34 a.overflow_hidden,
35 style,
36 ]}>
37 {children}
38 </View>
39 )
40}
41
42export function Avatar({avatar}: {avatar?: string}) {
43 return <UserAvatar type="labeler" size={40} avatar={avatar} />
44}
45
46export function Title({value}: {value: string}) {
47 return (
48 <Text emoji style={[a.text_md, a.font_semi_bold, a.leading_tight]}>
49 {value}
50 </Text>
51 )
52}
53
54export function Description({value, handle}: {value?: string; handle: string}) {
55 const {_} = useLingui()
56 return value ? (
57 <Text numberOfLines={2}>
58 <RichText value={value} />
59 </Text>
60 ) : (
61 <Text emoji style={[a.leading_snug]}>
62 {_(msg`By ${sanitizeHandle(handle, '@')}`)}
63 </Text>
64 )
65}
66
67export function RegionalNotice() {
68 const t = useTheme()
69 return (
70 <View
71 style={[
72 a.flex_row,
73 a.align_center,
74 a.gap_xs,
75 a.pt_2xs,
76 {marginLeft: -2},
77 ]}>
78 <Flag fill={t.atoms.text_contrast_low.color} size="sm" />
79 <Text style={[a.italic, a.leading_snug]}>
80 <Trans>Required in your region</Trans>
81 </Text>
82 </View>
83 )
84}
85
86export function LikeCount({likeCount}: {likeCount: number}) {
87 const t = useTheme()
88 return (
89 <Text
90 style={[
91 a.mt_sm,
92 a.text_sm,
93 t.atoms.text_contrast_medium,
94 {fontWeight: '600'},
95 ]}>
96 <Trans>
97 Liked by <Plural value={likeCount} one="# user" other="# users" />
98 </Trans>
99 </Text>
100 )
101}
102
103export function Content({children}: React.PropsWithChildren<{}>) {
104 const t = useTheme()
105
106 return (
107 <View
108 style={[
109 a.flex_1,
110 a.flex_row,
111 a.gap_md,
112 a.align_center,
113 a.justify_between,
114 ]}>
115 <View style={[a.gap_2xs, a.flex_1]}>{children}</View>
116
117 <ChevronRight size="md" style={[a.z_10, t.atoms.text_contrast_low]} />
118 </View>
119 )
120}
121
122/**
123 * The canonical view for a labeling service. Use this or compose your own.
124 */
125export function Default({
126 labeler,
127 style,
128}: LabelingServiceProps & ViewStyleProp) {
129 return (
130 <Outer style={style}>
131 <Avatar avatar={labeler.creator.avatar} />
132 <Content>
133 <Title
134 value={getLabelingServiceTitle({
135 displayName: labeler.creator.displayName,
136 handle: labeler.creator.handle,
137 })}
138 />
139 <Description
140 value={labeler.creator.description}
141 handle={labeler.creator.handle}
142 />
143 {labeler.likeCount ? <LikeCount likeCount={labeler.likeCount} /> : null}
144 </Content>
145 </Outer>
146 )
147}
148
149export function Link({
150 children,
151 labeler,
152}: LabelingServiceProps & Pick<LinkProps, 'children'>) {
153 const {_} = useLingui()
154
155 return (
156 <InternalLink
157 to={{
158 screen: 'Profile',
159 params: {
160 name: labeler.creator.handle,
161 },
162 }}
163 label={_(
164 msg`View the labeling service provided by @${labeler.creator.handle}`,
165 )}>
166 {children}
167 </InternalLink>
168 )
169}
170
171// TODO not finished yet
172export function DefaultSkeleton() {
173 return (
174 <View>
175 <Text>Loading</Text>
176 </View>
177 )
178}
179
180export function Loader({
181 did,
182 loading: LoadingComponent = DefaultSkeleton,
183 error: ErrorComponent,
184 component: Component,
185}: {
186 did: string
187 loading?: React.ComponentType<{}>
188 error?: React.ComponentType<{error: string}>
189 component: React.ComponentType<{
190 labeler: AppBskyLabelerDefs.LabelerViewDetailed
191 }>
192}) {
193 const {isLoading, data, error} = useLabelerInfoQuery({did})
194
195 return isLoading ? (
196 LoadingComponent ? (
197 <LoadingComponent />
198 ) : null
199 ) : error || !data ? (
200 ErrorComponent ? (
201 <ErrorComponent error={error?.message || 'Unknown error'} />
202 ) : null
203 ) : (
204 <Component labeler={data} />
205 )
206}