fork
Configure Feed
Select the types of activity you want to include in your feed.
mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
fork
Configure Feed
Select the types of activity you want to include in your feed.
1import React from 'react'
2import {
3 Linking,
4 Pressable,
5 StyleProp,
6 StyleSheet,
7 View,
8 ViewStyle,
9} from 'react-native'
10import {AtUri} from '@atproto/api'
11import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
12import {msg, Plural, Trans} from '@lingui/macro'
13import {useLingui} from '@lingui/react'
14
15import {logger} from '#/logger'
16import {shouldClickOpenNewTab} from '#/platform/urls'
17import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
18import {
19 useAddSavedFeedsMutation,
20 usePreferencesQuery,
21 UsePreferencesQueryResponse,
22 useRemoveFeedMutation,
23} from '#/state/queries/preferences'
24import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
25import {usePalette} from 'lib/hooks/usePalette'
26import {sanitizeHandle} from 'lib/strings/handles'
27import {s} from 'lib/styles'
28import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
29import * as Toast from 'view/com/util/Toast'
30import {useTheme} from '#/alf'
31import {atoms as a} from '#/alf'
32import * as Prompt from '#/components/Prompt'
33import {RichText} from '#/components/RichText'
34import {Text} from '../util/text/Text'
35import {UserAvatar} from '../util/UserAvatar'
36
37export function FeedSourceCard({
38 feedUri,
39 style,
40 showSaveBtn = false,
41 showDescription = false,
42 showLikes = false,
43 pinOnSave = false,
44 showMinimalPlaceholder,
45 hideTopBorder,
46}: {
47 feedUri: string
48 style?: StyleProp<ViewStyle>
49 showSaveBtn?: boolean
50 showDescription?: boolean
51 showLikes?: boolean
52 pinOnSave?: boolean
53 showMinimalPlaceholder?: boolean
54 hideTopBorder?: boolean
55}) {
56 const {data: preferences} = usePreferencesQuery()
57 const {data: feed} = useFeedSourceInfoQuery({uri: feedUri})
58
59 return (
60 <FeedSourceCardLoaded
61 feedUri={feedUri}
62 feed={feed}
63 preferences={preferences}
64 style={style}
65 showSaveBtn={showSaveBtn}
66 showDescription={showDescription}
67 showLikes={showLikes}
68 pinOnSave={pinOnSave}
69 showMinimalPlaceholder={showMinimalPlaceholder}
70 hideTopBorder={hideTopBorder}
71 />
72 )
73}
74
75export function FeedSourceCardLoaded({
76 feedUri,
77 feed,
78 preferences,
79 style,
80 showSaveBtn = false,
81 showDescription = false,
82 showLikes = false,
83 pinOnSave = false,
84 showMinimalPlaceholder,
85 hideTopBorder,
86}: {
87 feedUri: string
88 feed?: FeedSourceInfo
89 preferences?: UsePreferencesQueryResponse
90 style?: StyleProp<ViewStyle>
91 showSaveBtn?: boolean
92 showDescription?: boolean
93 showLikes?: boolean
94 pinOnSave?: boolean
95 showMinimalPlaceholder?: boolean
96 hideTopBorder?: boolean
97}) {
98 const t = useTheme()
99 const pal = usePalette('default')
100 const {_} = useLingui()
101 const removePromptControl = Prompt.usePromptControl()
102 const navigation = useNavigationDeduped()
103
104 const {isPending: isAddSavedFeedPending, mutateAsync: addSavedFeeds} =
105 useAddSavedFeedsMutation()
106 const {isPending: isRemovePending, mutateAsync: removeFeed} =
107 useRemoveFeedMutation()
108
109 const savedFeedConfig = preferences?.savedFeeds?.find(
110 f => f.value === feedUri,
111 )
112 const isSaved = Boolean(savedFeedConfig)
113
114 const onSave = React.useCallback(async () => {
115 if (!feed || isSaved) return
116
117 try {
118 await addSavedFeeds([
119 {
120 type: 'feed',
121 value: feed.uri,
122 pinned: pinOnSave,
123 },
124 ])
125 Toast.show(_(msg`Added to my feeds`))
126 } catch (e) {
127 Toast.show(_(msg`There was an issue contacting your server`), 'xmark')
128 logger.error('Failed to save feed', {message: e})
129 }
130 }, [_, feed, pinOnSave, addSavedFeeds, isSaved])
131
132 const onUnsave = React.useCallback(async () => {
133 if (!savedFeedConfig) return
134
135 try {
136 await removeFeed(savedFeedConfig)
137 // await item.unsave()
138 Toast.show(_(msg`Removed from my feeds`))
139 } catch (e) {
140 Toast.show(_(msg`There was an issue contacting your server`), 'xmark')
141 logger.error('Failed to unsave feed', {message: e})
142 }
143 }, [_, removeFeed, savedFeedConfig])
144
145 const onToggleSaved = React.useCallback(async () => {
146 if (isSaved) {
147 removePromptControl.open()
148 } else {
149 await onSave()
150 }
151 }, [isSaved, removePromptControl, onSave])
152
153 /*
154 * LOAD STATE
155 *
156 * This state also captures the scenario where a feed can't load for whatever
157 * reason.
158 */
159 if (!feed || !preferences)
160 return (
161 <View
162 style={[
163 pal.border,
164 {
165 borderTopWidth: showMinimalPlaceholder || hideTopBorder ? 0 : 1,
166 flexDirection: 'row',
167 alignItems: 'center',
168 flex: 1,
169 paddingRight: 18,
170 },
171 ]}>
172 {showMinimalPlaceholder ? (
173 <FeedLoadingPlaceholder
174 style={{flex: 1}}
175 showTopBorder={false}
176 showLowerPlaceholder={false}
177 />
178 ) : (
179 <FeedLoadingPlaceholder style={{flex: 1}} showTopBorder={false} />
180 )}
181
182 {showSaveBtn && (
183 <Pressable
184 testID={`feed-${feedUri}-toggleSave`}
185 disabled={isRemovePending}
186 accessibilityRole="button"
187 accessibilityLabel={_(msg`Remove from my feeds`)}
188 accessibilityHint=""
189 onPress={onUnsave}
190 hitSlop={15}
191 style={styles.btn}>
192 <FontAwesomeIcon
193 icon={['far', 'trash-can']}
194 size={19}
195 color={pal.colors.icon}
196 />
197 </Pressable>
198 )}
199 </View>
200 )
201
202 return (
203 <>
204 <Pressable
205 testID={`feed-${feed.displayName}`}
206 accessibilityRole="button"
207 style={[
208 styles.container,
209 pal.border,
210 style,
211 {borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth},
212 ]}
213 onPress={e => {
214 const shouldOpenInNewTab = shouldClickOpenNewTab(e)
215 if (feed.type === 'feed') {
216 if (shouldOpenInNewTab) {
217 Linking.openURL(
218 `/profile/${feed.creatorDid}/feed/${new AtUri(feed.uri).rkey}`,
219 )
220 } else {
221 navigation.push('ProfileFeed', {
222 name: feed.creatorDid,
223 rkey: new AtUri(feed.uri).rkey,
224 })
225 }
226 } else if (feed.type === 'list') {
227 if (shouldOpenInNewTab) {
228 Linking.openURL(
229 `/profile/${feed.creatorDid}/lists/${new AtUri(feed.uri).rkey}`,
230 )
231 } else {
232 navigation.push('ProfileList', {
233 name: feed.creatorDid,
234 rkey: new AtUri(feed.uri).rkey,
235 })
236 }
237 }
238 }}
239 key={feed.uri}>
240 <View style={[styles.headerContainer, a.align_center]}>
241 <View style={[s.mr10]}>
242 <UserAvatar type="algo" size={36} avatar={feed.avatar} />
243 </View>
244 <View style={[styles.headerTextContainer]}>
245 <Text style={[pal.text, s.bold]} numberOfLines={1}>
246 {feed.displayName}
247 </Text>
248 <Text style={[pal.textLight]} numberOfLines={1}>
249 {feed.type === 'feed' ? (
250 <Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
251 ) : (
252 <Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
253 )}
254 </Text>
255 </View>
256
257 {showSaveBtn && (
258 <View style={{alignSelf: 'center'}}>
259 <Pressable
260 testID={`feed-${feed.displayName}-toggleSave`}
261 disabled={isAddSavedFeedPending || isRemovePending}
262 accessibilityRole="button"
263 accessibilityLabel={
264 isSaved
265 ? _(msg`Remove from my feeds`)
266 : _(msg`Add to my feeds`)
267 }
268 accessibilityHint=""
269 onPress={onToggleSaved}
270 hitSlop={15}
271 style={styles.btn}>
272 {isSaved ? (
273 <FontAwesomeIcon
274 icon={['far', 'trash-can']}
275 size={19}
276 color={pal.colors.icon}
277 />
278 ) : (
279 <FontAwesomeIcon
280 icon="plus"
281 size={18}
282 color={pal.colors.link}
283 />
284 )}
285 </Pressable>
286 </View>
287 )}
288 </View>
289
290 {showDescription && feed.description ? (
291 <RichText
292 style={[t.atoms.text_contrast_high, styles.description]}
293 value={feed.description}
294 numberOfLines={3}
295 />
296 ) : null}
297
298 {showLikes && feed.type === 'feed' ? (
299 <Text type="sm-medium" style={[pal.text, pal.textLight]}>
300 <Plural
301 value={feed.likeCount || 0}
302 one="Liked by # user"
303 other="Liked by # users"
304 />
305 </Text>
306 ) : null}
307 </Pressable>
308
309 <Prompt.Basic
310 control={removePromptControl}
311 title={_(msg`Remove from my feeds?`)}
312 description={_(
313 msg`Are you sure you want to remove ${feed.displayName} from your feeds?`,
314 )}
315 onConfirm={onUnsave}
316 confirmButtonCta={_(msg`Remove`)}
317 confirmButtonColor="negative"
318 />
319 </>
320 )
321}
322
323const styles = StyleSheet.create({
324 container: {
325 paddingHorizontal: 18,
326 paddingVertical: 20,
327 flexDirection: 'column',
328 flex: 1,
329 gap: 14,
330 },
331 border: {
332 borderTopWidth: StyleSheet.hairlineWidth,
333 },
334 headerContainer: {
335 flexDirection: 'row',
336 },
337 headerTextContainer: {
338 flexDirection: 'column',
339 columnGap: 4,
340 flex: 1,
341 },
342 description: {
343 flex: 1,
344 flexWrap: 'wrap',
345 },
346 btn: {
347 paddingVertical: 6,
348 },
349})