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