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