forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {type StyleProp, View, type ViewStyle} from 'react-native'
2import {
3 type $Typed,
4 AppBskyFeedDefs,
5 type AppBskyGraphDefs,
6 AtUri,
7} from '@atproto/api'
8import {msg, Plural, Trans} from '@lingui/macro'
9import {useLingui} from '@lingui/react'
10
11import {sanitizeHandle} from '#/lib/strings/handles'
12import {
13 type FeedSourceInfo,
14 hydrateFeedGenerator,
15 hydrateList,
16 useFeedSourceInfoQuery,
17} from '#/state/queries/feed'
18import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
19import {UserAvatar} from '#/view/com/util/UserAvatar'
20import {atoms as a, useTheme} from '#/alf'
21import {Link} from '#/components/Link'
22import {RichText} from '#/components/RichText'
23import {Text} from '#/components/Typography'
24import {MissingFeed} from './MissingFeed'
25
26type FeedSourceCardProps = {
27 feedUri: string
28 feedData?:
29 | $Typed<AppBskyFeedDefs.GeneratorView>
30 | $Typed<AppBskyGraphDefs.ListView>
31 style?: StyleProp<ViewStyle>
32 showSaveBtn?: boolean
33 showDescription?: boolean
34 showLikes?: boolean
35 pinOnSave?: boolean
36 showMinimalPlaceholder?: boolean
37 hideTopBorder?: boolean
38 link?: boolean
39}
40
41export function FeedSourceCard({
42 feedUri,
43 feedData,
44 ...props
45}: FeedSourceCardProps) {
46 if (feedData) {
47 let feed: FeedSourceInfo
48 if (AppBskyFeedDefs.isGeneratorView(feedData)) {
49 feed = hydrateFeedGenerator(feedData)
50 } else {
51 feed = hydrateList(feedData)
52 }
53 return <FeedSourceCardLoaded feedUri={feedUri} feed={feed} {...props} />
54 } else {
55 return <FeedSourceCardWithoutData feedUri={feedUri} {...props} />
56 }
57}
58
59export function FeedSourceCardWithoutData({
60 feedUri,
61 ...props
62}: Omit<FeedSourceCardProps, 'feedData'>) {
63 const {data: feed, error} = useFeedSourceInfoQuery({
64 uri: feedUri,
65 })
66
67 return (
68 <FeedSourceCardLoaded
69 feedUri={feedUri}
70 feed={feed}
71 error={error}
72 {...props}
73 />
74 )
75}
76
77export function FeedSourceCardLoaded({
78 feedUri,
79 feed,
80 style,
81 showDescription = false,
82 showLikes = false,
83 showMinimalPlaceholder,
84 hideTopBorder,
85 link = true,
86 error,
87}: {
88 feedUri: string
89 feed?: FeedSourceInfo
90 style?: StyleProp<ViewStyle>
91 showDescription?: boolean
92 showLikes?: boolean
93 showMinimalPlaceholder?: boolean
94 hideTopBorder?: boolean
95 link?: boolean
96 error?: unknown
97}) {
98 const t = useTheme()
99 const {_} = useLingui()
100
101 /*
102 * LOAD STATE
103 *
104 * This state also captures the scenario where a feed can't load for whatever
105 * reason.
106 */
107 if (!feed) {
108 if (error) {
109 return (
110 <MissingFeed
111 uri={feedUri}
112 style={style}
113 hideTopBorder={hideTopBorder}
114 error={error}
115 />
116 )
117 }
118
119 return (
120 <FeedLoadingPlaceholder
121 style={[
122 t.atoms.border_contrast_low,
123 !(showMinimalPlaceholder || hideTopBorder) && a.border_t,
124 a.flex_1,
125 style,
126 ]}
127 showTopBorder={false}
128 showLowerPlaceholder={!showMinimalPlaceholder}
129 />
130 )
131 }
132
133 const inner = (
134 <>
135 <View style={[a.flex_row, a.align_center]}>
136 <View style={[a.mr_md]}>
137 <UserAvatar type="algo" size={36} avatar={feed.avatar} />
138 </View>
139 <View style={[a.flex_1]}>
140 <Text
141 emoji
142 style={[a.text_sm, a.font_semi_bold, a.leading_snug]}
143 numberOfLines={1}>
144 {feed.displayName}
145 </Text>
146 <Text
147 style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]}
148 numberOfLines={1}>
149 {feed.type === 'feed' ? (
150 <Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
151 ) : (
152 <Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
153 )}
154 </Text>
155 </View>
156 </View>
157 {showDescription && feed.description ? (
158 <RichText
159 style={[t.atoms.text_contrast_high, a.flex_1, a.flex_wrap]}
160 value={feed.description}
161 numberOfLines={3}
162 />
163 ) : null}
164 {showLikes && feed.type === 'feed' ? (
165 <Text
166 style={[
167 a.text_sm,
168 a.font_semi_bold,
169 t.atoms.text_contrast_medium,
170 a.leading_snug,
171 ]}>
172 <Trans>
173 Liked by{' '}
174 <Plural value={feed.likeCount || 0} one="# user" other="# users" />
175 </Trans>
176 </Text>
177 ) : null}
178 </>
179 )
180
181 if (link) {
182 return (
183 <Link
184 testID={`feed-${feed.displayName}`}
185 label={_(
186 feed.type === 'feed'
187 ? msg`${feed.displayName}, a feed by ${sanitizeHandle(feed.creatorHandle, '@')}, liked by ${feed.likeCount || 0}`
188 : msg`${feed.displayName}, a list by ${sanitizeHandle(feed.creatorHandle, '@')}`,
189 )}
190 to={{
191 screen: feed.type === 'feed' ? 'ProfileFeed' : 'ProfileList',
192 params: {name: feed.creatorDid, rkey: new AtUri(feed.uri).rkey},
193 }}
194 style={[
195 a.flex_1,
196 a.p_lg,
197 a.gap_md,
198 !hideTopBorder && !a.border_t,
199 t.atoms.border_contrast_low,
200 style,
201 ]}>
202 {inner}
203 </Link>
204 )
205 } else {
206 return (
207 <View
208 style={[
209 a.flex_1,
210 a.p_lg,
211 a.gap_md,
212 !hideTopBorder && !a.border_t,
213 t.atoms.border_contrast_low,
214 style,
215 ]}>
216 {inner}
217 </View>
218 )
219 }
220}