Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

Merge branch 'main' of https://github.com/bluesky-social/social-app

+605 -509
+2 -9
src/components/Divider.tsx
··· 1 1 import {View} from 'react-native' 2 2 3 - import {atoms as a, flatten, useTheme, ViewStyleProp} from '#/alf' 3 + import {atoms as a, useTheme, type ViewStyleProp} from '#/alf' 4 4 5 5 export function Divider({style}: ViewStyleProp) { 6 6 const t = useTheme() 7 7 8 8 return ( 9 - <View 10 - style={[ 11 - a.w_full, 12 - a.border_t, 13 - t.atoms.border_contrast_low, 14 - flatten(style), 15 - ]} 16 - /> 9 + <View style={[a.w_full, a.border_t, t.atoms.border_contrast_low, style]} /> 17 10 ) 18 11 }
+1 -1
src/components/FeedCard.tsx
··· 214 214 export function Likes({count}: {count: number}) { 215 215 const t = useTheme() 216 216 return ( 217 - <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 217 + <Text style={[a.text_sm, t.atoms.text_contrast_medium, a.font_bold]}> 218 218 <Trans> 219 219 Liked by <Plural value={count || 0} one="# user" other="# users" /> 220 220 </Trans>
+7 -5
src/components/ListCard.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 3 import { 4 - AppBskyGraphDefs, 4 + type AppBskyGraphDefs, 5 5 AtUri, 6 6 moderateUserList, 7 - ModerationUI, 7 + type ModerationUI, 8 8 } from '@atproto/api' 9 9 import {msg, Trans} from '@lingui/macro' 10 10 import {useLingui} from '@lingui/react' ··· 22 22 Outer, 23 23 SaveButton, 24 24 } from '#/components/FeedCard' 25 - import {Link as InternalLink, LinkProps} from '#/components/Link' 25 + import {Link as InternalLink, type LinkProps} from '#/components/Link' 26 26 import * as Hider from '#/components/moderation/Hider' 27 27 import {Text} from '#/components/Typography' 28 - import * as bsky from '#/types/bsky' 28 + import type * as bsky from '#/types/bsky' 29 29 30 30 /* 31 31 * This component is based on `FeedCard` and is tightly coupled with that ··· 50 50 showPinButton?: boolean 51 51 } 52 52 53 - export function Default(props: Props) { 53 + export function Default( 54 + props: Props & Omit<LinkProps, 'to' | 'label' | 'children'>, 55 + ) { 54 56 const {view, showPinButton} = props 55 57 const moderationOpts = useModerationOpts() 56 58 const moderation = moderationOpts
+22 -21
src/components/Post/Embed/FeedEmbed.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet} from 'react-native' 1 + import {useMemo} from 'react' 3 2 import {moderateFeedGenerator} from '@atproto/api' 4 3 5 - import {usePalette} from '#/lib/hooks/usePalette' 6 4 import {useModerationOpts} from '#/state/preferences/moderation-opts' 7 - import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' 5 + import {atoms as a, useTheme} from '#/alf' 6 + import * as FeedCard from '#/components/FeedCard' 8 7 import {ContentHider} from '#/components/moderation/ContentHider' 9 8 import {type EmbedType} from '#/types/bsky/post' 10 9 import {type CommonProps} from './types' ··· 14 13 }: CommonProps & { 15 14 embed: EmbedType<'feed'> 16 15 }) { 17 - const pal = usePalette('default') 16 + const t = useTheme() 18 17 return ( 19 - <FeedSourceCard 20 - feedUri={embed.view.uri} 21 - style={[pal.view, pal.border, styles.customFeedOuter]} 22 - showLikes 23 - /> 18 + <FeedCard.Link 19 + view={embed.view} 20 + style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}> 21 + <FeedCard.Outer> 22 + <FeedCard.Header> 23 + <FeedCard.Avatar src={embed.view.avatar} /> 24 + <FeedCard.TitleAndByline 25 + title={embed.view.displayName} 26 + creator={embed.view.creator} 27 + /> 28 + </FeedCard.Header> 29 + <FeedCard.Likes count={embed.view.likeCount || 0} /> 30 + </FeedCard.Outer> 31 + </FeedCard.Link> 24 32 ) 25 33 } 26 34 ··· 30 38 embed: EmbedType<'feed'> 31 39 }) { 32 40 const moderationOpts = useModerationOpts() 33 - const moderation = React.useMemo(() => { 41 + const moderation = useMemo(() => { 34 42 return moderationOpts 35 43 ? moderateFeedGenerator(embed.view, moderationOpts) 36 44 : undefined 37 45 }, [embed.view, moderationOpts]) 38 46 return ( 39 - <ContentHider modui={moderation?.ui('contentList')}> 47 + <ContentHider 48 + modui={moderation?.ui('contentList')} 49 + childContainerStyle={[a.pt_xs]}> 40 50 <FeedEmbed embed={embed} /> 41 51 </ContentHider> 42 52 ) 43 53 } 44 - 45 - const styles = StyleSheet.create({ 46 - customFeedOuter: { 47 - borderWidth: StyleSheet.hairlineWidth, 48 - borderRadius: 8, 49 - paddingHorizontal: 12, 50 - paddingVertical: 12, 51 - }, 52 - })
+9 -8
src/components/Post/Embed/ListEmbed.tsx
··· 1 - import React from 'react' 2 - import {View} from 'react-native' 1 + import {useMemo} from 'react' 3 2 import {moderateUserList} from '@atproto/api' 4 3 5 4 import {useModerationOpts} from '#/state/preferences/moderation-opts' ··· 16 15 }) { 17 16 const t = useTheme() 18 17 return ( 19 - <View 20 - style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}> 21 - <ListCard.Default view={embed.view} /> 22 - </View> 18 + <ListCard.Default 19 + view={embed.view} 20 + style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]} 21 + /> 23 22 ) 24 23 } 25 24 ··· 29 28 embed: EmbedType<'list'> 30 29 }) { 31 30 const moderationOpts = useModerationOpts() 32 - const moderation = React.useMemo(() => { 31 + const moderation = useMemo(() => { 33 32 return moderationOpts 34 33 ? moderateUserList(embed.view, moderationOpts) 35 34 : undefined 36 35 }, [embed.view, moderationOpts]) 37 36 return ( 38 - <ContentHider modui={moderation?.ui('contentList')}> 37 + <ContentHider 38 + modui={moderation?.ui('contentList')} 39 + childContainerStyle={[a.pt_xs]}> 39 40 <ListEmbed embed={embed} /> 40 41 </ContentHider> 41 42 )
+51 -75
src/components/Post/Embed/index.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 2 + import {View} from 'react-native' 3 3 import { 4 4 type $Typed, 5 5 type AppBskyFeedDefs, ··· 21 21 import {useSession} from '#/state/session' 22 22 import {Link} from '#/view/com/util/Link' 23 23 import {PostMeta} from '#/view/com/util/PostMeta' 24 - import {Text} from '#/view/com/util/text/Text' 25 24 import {atoms as a, useTheme} from '#/alf' 26 - import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash' 27 25 import {ContentHider} from '#/components/moderation/ContentHider' 28 26 import {PostAlerts} from '#/components/moderation/PostAlerts' 29 27 import {RichText} from '#/components/RichText' ··· 308 306 style, 309 307 isWithinQuote: parentIsWithinQuote, 310 308 allowNestedQuotes: parentAllowNestedQuotes, 311 - visibilityLabel, 312 309 }: Omit<CommonProps, 'viewContext'> & { 313 310 embed: EmbedType<'post'> 314 311 viewContext?: QuoteEmbedViewContext ··· 357 354 const [hover, setHover] = React.useState(false) 358 355 return ( 359 356 <View 360 - onPointerEnter={() => { 361 - setHover(true) 362 - }} 363 - onPointerLeave={() => { 364 - setHover(false) 365 - }}> 357 + style={[a.mt_sm]} 358 + onPointerEnter={() => setHover(true)} 359 + onPointerLeave={() => setHover(false)}> 366 360 <ContentHider 367 361 modui={moderation?.ui('contentList')} 368 - style={[ 369 - a.rounded_md, 370 - a.p_md, 371 - a.mt_sm, 372 - a.border, 373 - t.atoms.border_contrast_low, 374 - style, 375 - ]} 362 + style={[a.rounded_md, a.border, t.atoms.border_contrast_low, style]} 363 + activeStyle={[a.p_md, a.pt_sm]} 376 364 childContainerStyle={[a.pt_sm]}> 377 - <SubtleWebHover hover={hover} /> 378 - <Link 379 - hoverStyle={{borderColor: pal.colors.borderLinkHover}} 380 - href={itemHref} 381 - title={itemTitle} 382 - onBeforePress={onBeforePress}> 383 - <View pointerEvents="none"> 384 - {visibilityLabel !== undefined ? ( 385 - <View style={[styles.blockHeader, t.atoms.border_contrast_low]}> 386 - <EyeSlashIcon size="sm" style={pal.text} /> 387 - <Text type="lg" style={pal.text}> 388 - {visibilityLabel} 389 - </Text> 365 + {({active}) => ( 366 + <> 367 + {!active && <SubtleWebHover hover={hover} style={[a.rounded_md]} />} 368 + <Link 369 + style={[!active && a.p_md]} 370 + hoverStyle={{borderColor: pal.colors.borderLinkHover}} 371 + href={itemHref} 372 + title={itemTitle} 373 + onBeforePress={onBeforePress}> 374 + <View pointerEvents="none"> 375 + <PostMeta 376 + author={quote.author} 377 + moderation={moderation} 378 + showAvatar 379 + postHref={itemHref} 380 + timestamp={quote.indexedAt} 381 + /> 390 382 </View> 391 - ) : undefined} 392 - <PostMeta 393 - author={quote.author} 394 - moderation={moderation} 395 - showAvatar 396 - postHref={itemHref} 397 - timestamp={quote.indexedAt} 398 - /> 399 - </View> 400 - {moderation ? ( 401 - <PostAlerts 402 - modui={moderation.ui('contentView')} 403 - style={[a.py_xs]} 404 - /> 405 - ) : null} 406 - {richText ? ( 407 - <RichText 408 - value={richText} 409 - style={a.text_md} 410 - numberOfLines={20} 411 - disableLinks 412 - /> 413 - ) : null} 414 - {quote.embed && ( 415 - <Embed 416 - embed={quote.embed} 417 - moderation={moderation} 418 - isWithinQuote={parentIsWithinQuote ?? true} 419 - // already within quote? override nested 420 - allowNestedQuotes={ 421 - parentIsWithinQuote ? false : parentAllowNestedQuotes 422 - } 423 - /> 424 - )} 425 - </Link> 383 + {moderation ? ( 384 + <PostAlerts 385 + modui={moderation.ui('contentView')} 386 + style={[a.py_xs]} 387 + /> 388 + ) : null} 389 + {richText ? ( 390 + <RichText 391 + value={richText} 392 + style={a.text_md} 393 + numberOfLines={20} 394 + disableLinks 395 + /> 396 + ) : null} 397 + {quote.embed && ( 398 + <Embed 399 + embed={quote.embed} 400 + moderation={moderation} 401 + isWithinQuote={parentIsWithinQuote ?? true} 402 + // already within quote? override nested 403 + allowNestedQuotes={ 404 + parentIsWithinQuote ? false : parentAllowNestedQuotes 405 + } 406 + /> 407 + )} 408 + </Link> 409 + </> 410 + )} 426 411 </ContentHider> 427 412 </View> 428 413 ) 429 414 } 430 - 431 - const styles = StyleSheet.create({ 432 - blockHeader: { 433 - flexDirection: 'row', 434 - alignItems: 'center', 435 - gap: 4, 436 - marginBottom: 8, 437 - }, 438 - })
+8 -5
src/components/moderation/ContentHider.tsx
··· 23 23 modui, 24 24 ignoreMute, 25 25 style, 26 + activeStyle, 26 27 childContainerStyle, 27 28 children, 28 - }: React.PropsWithChildren<{ 29 + }: { 29 30 testID?: string 30 31 modui: ModerationUI | undefined 31 32 ignoreMute?: boolean 32 33 style?: StyleProp<ViewStyle> 34 + activeStyle?: StyleProp<ViewStyle> 33 35 childContainerStyle?: StyleProp<ViewStyle> 34 - }>) { 36 + children?: React.ReactNode | ((props: {active: boolean}) => React.ReactNode) 37 + }) { 35 38 const blur = modui?.blurs[0] 36 39 if (!blur || (ignoreMute && isJustAMute(modui))) { 37 40 return ( 38 41 <View testID={testID} style={style}> 39 - {children} 42 + {typeof children === 'function' ? children({active: false}) : children} 40 43 </View> 41 44 ) 42 45 } ··· 44 47 <ContentHiderActive 45 48 testID={testID} 46 49 modui={modui} 47 - style={style} 50 + style={[style, activeStyle]} 48 51 childContainerStyle={childContainerStyle}> 49 - {children} 52 + {typeof children === 'function' ? children({active: true}) : children} 50 53 </ContentHiderActive> 51 54 ) 52 55 }
+109 -61
src/locale/locales/en/messages.po
··· 162 162 msgid "{0} reacted {1} to {2}" 163 163 msgstr "" 164 164 165 + #: src/view/com/feeds/FeedSourceCard.tsx:187 166 + msgid "{0}, a feed by {1}, liked by {2}" 167 + msgstr "" 168 + 169 + #: src/view/com/feeds/FeedSourceCard.tsx:188 170 + msgid "{0}, a list by {1}" 171 + msgstr "" 172 + 165 173 #: src/view/com/util/UserAvatar.tsx:570 166 174 #: src/view/com/util/UserAvatar.tsx:588 167 175 msgid "{0}'s avatar" ··· 733 741 msgid "Add to lists" 734 742 msgstr "" 735 743 736 - #: src/view/com/feeds/FeedSourceCard.tsx:269 737 - msgid "Add to my feeds" 738 - msgstr "" 739 - 740 744 #: src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx:156 741 745 msgid "Add user to list" 742 746 msgstr "" ··· 746 750 msgid "Added to list" 747 751 msgstr "" 748 752 749 - #: src/view/com/feeds/FeedSourceCard.tsx:125 750 - msgid "Added to my feeds" 751 - msgstr "" 752 - 753 753 #: src/components/moderation/ReportDialog/index.tsx:418 754 754 msgid "Additional details (limit 300 characters)" 755 755 msgstr "" ··· 758 758 msgid "Adult" 759 759 msgstr "" 760 760 761 - #: src/components/moderation/ContentHider.tsx:113 761 + #: src/components/moderation/ContentHider.tsx:116 762 762 #: src/lib/moderation/useGlobalLabelStrings.ts:34 763 763 #: src/lib/moderation/useModerationCauseDescription.ts:148 764 764 #: src/view/com/composer/labels/LabelsBtn.tsx:127 ··· 1109 1109 1110 1110 #: src/components/dms/LeaveConvoPrompt.tsx:45 1111 1111 msgid "Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant." 1112 - msgstr "" 1113 - 1114 - #: src/view/com/feeds/FeedSourceCard.tsx:319 1115 - msgid "Are you sure you want to remove {0} from your feeds?" 1116 1112 msgstr "" 1117 1113 1118 1114 #: src/components/FeedCard.tsx:340 ··· 1718 1714 msgid "Clear search query" 1719 1715 msgstr "" 1720 1716 1717 + #: src/view/com/feeds/MissingFeed.tsx:87 1718 + msgid "Click for information" 1719 + msgstr "" 1720 + 1721 1721 #: src/view/screens/Support.tsx:41 1722 1722 msgid "click here" 1723 1723 msgstr "" ··· 1775 1775 #: src/components/verification/VerifierDialog.tsx:144 1776 1776 #: src/components/WhoCanReply.tsx:200 1777 1777 #: src/components/WhoCanReply.tsx:207 1778 + #: src/view/com/feeds/MissingFeed.tsx:208 1779 + #: src/view/com/feeds/MissingFeed.tsx:215 1778 1780 #: src/view/com/modals/ChangePassword.tsx:279 1779 1781 #: src/view/com/modals/ChangePassword.tsx:282 1780 1782 msgid "Close" ··· 1851 1853 msgid "Collapse list of users" 1852 1854 msgstr "" 1853 1855 1854 - #: src/view/com/notifications/NotificationFeedItem.tsx:799 1856 + #: src/view/com/notifications/NotificationFeedItem.tsx:801 1855 1857 msgid "Collapses list of users for a given notification" 1856 1858 msgstr "" 1857 1859 ··· 2157 2159 msgid "Copyright Policy" 2158 2160 msgstr "" 2159 2161 2162 + #: src/view/com/feeds/MissingFeed.tsx:41 2163 + msgid "Could not connect to custom feed" 2164 + msgstr "" 2165 + 2166 + #: src/view/com/feeds/MissingFeed.tsx:133 2167 + msgid "Could not connect to feed service" 2168 + msgstr "" 2169 + 2170 + #: src/view/com/feeds/MissingFeed.tsx:182 2171 + msgid "Could not find profile" 2172 + msgstr "" 2173 + 2160 2174 #: src/components/dms/LeaveConvoPrompt.tsx:35 2161 2175 #: src/components/dms/ReportDialog.tsx:310 2162 2176 msgid "Could not leave chat" ··· 2423 2437 msgid "Deleted Account" 2424 2438 msgstr "" 2425 2439 2440 + #: src/view/com/feeds/MissingFeed.tsx:42 2441 + #: src/view/com/feeds/MissingFeed.tsx:75 2442 + #: src/view/com/feeds/MissingFeed.tsx:127 2443 + #: src/view/com/feeds/MissingFeed.tsx:135 2444 + msgid "Deleted list" 2445 + msgstr "" 2446 + 2426 2447 #: src/view/com/post-thread/PostThread.tsx:474 2427 2448 msgid "Deleted post." 2428 2449 msgstr "" ··· 3014 3035 msgid "Error loading preference" 3015 3036 msgstr "" 3016 3037 3038 + #: src/view/com/feeds/MissingFeed.tsx:198 3039 + msgid "Error message" 3040 + msgstr "" 3041 + 3017 3042 #: src/screens/Settings/components/ExportCarDialog.tsx:47 3018 3043 msgid "Error occurred while saving file" 3019 3044 msgstr "" ··· 3367 3392 msgstr "" 3368 3393 3369 3394 #: src/components/FeedCard.tsx:139 3370 - #: src/view/com/feeds/FeedSourceCard.tsx:253 3395 + #: src/view/com/feeds/FeedSourceCard.tsx:150 3371 3396 msgid "Feed by {0}" 3372 3397 msgstr "" 3373 3398 3399 + #: src/view/com/feeds/MissingFeed.tsx:152 3400 + msgid "Feed creator" 3401 + msgstr "" 3402 + 3403 + #: src/view/com/feeds/MissingFeed.tsx:188 3404 + msgid "Feed identifier" 3405 + msgstr "" 3406 + 3374 3407 #: src/screens/Profile/components/ProfileFeedHeader.tsx:354 3375 3408 msgid "Feed menu" 3376 3409 msgstr "" ··· 3379 3412 msgid "Feed toggle" 3380 3413 msgstr "" 3381 3414 3415 + #: src/view/com/feeds/MissingFeed.tsx:73 3416 + msgid "Feed unavailable" 3417 + msgstr "" 3418 + 3382 3419 #: src/view/shell/desktop/RightNav.tsx:102 3383 3420 #: src/view/shell/desktop/RightNav.tsx:103 3384 3421 #: src/view/shell/Drawer.tsx:357 ··· 3396 3433 #: src/screens/StarterPack/StarterPackScreen.tsx:190 3397 3434 #: src/view/screens/Feeds.tsx:511 3398 3435 #: src/view/screens/Profile.tsx:230 3399 - #: src/view/screens/SavedFeeds.tsx:103 3436 + #: src/view/screens/SavedFeeds.tsx:104 3400 3437 #: src/view/shell/desktop/LeftNav.tsx:673 3401 3438 #: src/view/shell/Drawer.tsx:519 3402 3439 msgid "Feeds" 3403 3440 msgstr "" 3404 3441 3405 - #: src/view/screens/SavedFeeds.tsx:205 3442 + #: src/view/screens/SavedFeeds.tsx:206 3406 3443 msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information." 3407 3444 msgstr "" 3408 3445 3409 3446 #: src/components/FeedCard.tsx:282 3410 - #: src/view/screens/SavedFeeds.tsx:85 3447 + #: src/view/screens/SavedFeeds.tsx:86 3411 3448 msgctxt "toast" 3412 3449 msgid "Feeds updated!" 3413 3450 msgstr "" ··· 3577 3614 #: src/screens/VideoFeed/index.tsx:848 3578 3615 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:134 3579 3616 #: src/view/screens/Feeds.tsx:602 3580 - #: src/view/screens/SavedFeeds.tsx:429 3617 + #: src/view/screens/SavedFeeds.tsx:420 3581 3618 msgid "Following" 3582 3619 msgstr "" 3583 3620 ··· 3924 3961 msgid "Hidden by your moderation settings." 3925 3962 msgstr "" 3926 3963 3927 - #: src/components/ListCard.tsx:130 3964 + #: src/components/ListCard.tsx:132 3928 3965 msgid "Hidden list" 3929 3966 msgstr "" 3930 3967 3931 3968 #: src/components/interstitials/Trending.tsx:131 3932 3969 #: src/components/interstitials/TrendingVideos.tsx:140 3933 - #: src/components/moderation/ContentHider.tsx:200 3970 + #: src/components/moderation/ContentHider.tsx:203 3934 3971 #: src/components/moderation/LabelPreference.tsx:135 3935 3972 #: src/components/moderation/PostHider.tsx:134 3936 3973 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:712 ··· 3942 3979 msgid "Hide" 3943 3980 msgstr "" 3944 3981 3945 - #: src/view/com/notifications/NotificationFeedItem.tsx:806 3982 + #: src/view/com/notifications/NotificationFeedItem.tsx:808 3946 3983 msgctxt "action" 3947 3984 msgid "Hide" 3948 3985 msgstr "" ··· 3993 4030 msgid "Hide trending videos?" 3994 4031 msgstr "" 3995 4032 3996 - #: src/view/com/notifications/NotificationFeedItem.tsx:797 4033 + #: src/view/com/notifications/NotificationFeedItem.tsx:799 3997 4034 msgid "Hide user list" 3998 4035 msgstr "" 3999 4036 ··· 4002 4039 msgid "Hide verification badges" 4003 4040 msgstr "" 4004 4041 4005 - #: src/components/moderation/ContentHider.tsx:151 4042 + #: src/components/moderation/ContentHider.tsx:154 4006 4043 #: src/components/moderation/PostHider.tsx:89 4007 4044 msgid "Hides the content" 4008 4045 msgstr "" ··· 4332 4369 msgid "Keep me posted" 4333 4370 msgstr "" 4334 4371 4335 - #: src/components/moderation/ContentHider.tsx:231 4372 + #: src/components/moderation/ContentHider.tsx:234 4336 4373 msgid "Labeled by {0}." 4337 4374 msgstr "" 4338 4375 4339 - #: src/components/moderation/ContentHider.tsx:229 4376 + #: src/components/moderation/ContentHider.tsx:232 4340 4377 msgid "Labeled by the author." 4341 4378 msgstr "" 4342 4379 ··· 4409 4446 msgid "Learn more about self hosting your PDS." 4410 4447 msgstr "" 4411 4448 4412 - #: src/components/moderation/ContentHider.tsx:149 4449 + #: src/components/moderation/ContentHider.tsx:152 4413 4450 msgid "Learn more about the moderation applied to this content" 4414 4451 msgstr "" 4415 4452 4416 - #: src/components/moderation/ContentHider.tsx:215 4453 + #: src/components/moderation/ContentHider.tsx:218 4417 4454 msgid "Learn more about the moderation applied to this content." 4418 4455 msgstr "" 4419 4456 ··· 4432 4469 msgid "Learn more about what is public on Bluesky." 4433 4470 msgstr "" 4434 4471 4435 - #: src/components/moderation/ContentHider.tsx:239 4472 + #: src/components/moderation/ContentHider.tsx:242 4436 4473 #: src/view/com/auth/server-input/index.tsx:220 4437 4474 msgid "Learn more." 4438 4475 msgstr "" ··· 4533 4570 msgstr "" 4534 4571 4535 4572 #: src/components/FeedCard.tsx:218 4536 - #: src/view/com/feeds/FeedSourceCard.tsx:303 4573 + #: src/view/com/feeds/FeedSourceCard.tsx:172 4537 4574 msgid "Liked by {0, plural, one {# user} other {# users}}" 4538 4575 msgstr "" 4539 4576 ··· 4586 4623 msgid "List blocked" 4587 4624 msgstr "" 4588 4625 4589 - #: src/components/ListCard.tsx:150 4590 - #: src/view/com/feeds/FeedSourceCard.tsx:255 4626 + #: src/components/ListCard.tsx:152 4627 + #: src/view/com/feeds/FeedSourceCard.tsx:152 4591 4628 msgid "List by {0}" 4592 4629 msgstr "" 4593 4630 ··· 4597 4634 4598 4635 #: src/view/com/profile/ProfileSubpageHeader.tsx:158 4599 4636 msgid "List by you" 4637 + msgstr "" 4638 + 4639 + #: src/view/com/feeds/MissingFeed.tsx:154 4640 + msgid "List creator" 4600 4641 msgstr "" 4601 4642 4602 4643 #: src/view/screens/ProfileList.tsx:485 ··· 4866 4907 msgid "Moderation details" 4867 4908 msgstr "" 4868 4909 4869 - #: src/components/ListCard.tsx:149 4910 + #: src/components/ListCard.tsx:151 4870 4911 #: src/view/com/modals/UserAddRemoveLists.tsx:222 4871 4912 msgid "Moderation list by {0}" 4872 4913 msgstr "" ··· 5743 5784 msgid "Opens password reset form" 5744 5785 msgstr "" 5745 5786 5746 - #: src/view/com/notifications/NotificationFeedItem.tsx:900 5787 + #: src/view/com/notifications/NotificationFeedItem.tsx:902 5747 5788 #: src/view/com/util/UserAvatar.tsx:592 5748 5789 msgid "Opens this profile" 5749 5790 msgstr "" ··· 5926 5967 msgid "Pinned {0} to Home" 5927 5968 msgstr "" 5928 5969 5929 - #: src/view/screens/SavedFeeds.tsx:130 5970 + #: src/view/screens/SavedFeeds.tsx:131 5930 5971 msgid "Pinned Feeds" 5931 5972 msgstr "" 5932 5973 ··· 6508 6549 #: src/components/StarterPack/Wizard/WizardListCard.tsx:102 6509 6550 #: src/components/StarterPack/Wizard/WizardListCard.tsx:109 6510 6551 #: src/screens/Settings/Settings.tsx:567 6511 - #: src/view/com/feeds/FeedSourceCard.tsx:322 6512 6552 #: src/view/com/modals/UserAddRemoveLists.tsx:235 6513 6553 #: src/view/com/posts/PostFeedErrorMessage.tsx:217 6514 6554 msgid "Remove" ··· 6557 6597 6558 6598 #: src/screens/Profile/components/ProfileFeedHeader.tsx:319 6559 6599 #: src/screens/Profile/components/ProfileFeedHeader.tsx:325 6560 - #: src/view/com/feeds/FeedSourceCard.tsx:190 6561 - #: src/view/com/feeds/FeedSourceCard.tsx:268 6562 6600 #: src/view/screens/ProfileList.tsx:528 6563 - #: src/view/screens/SavedFeeds.tsx:349 6601 + #: src/view/screens/SavedFeeds.tsx:350 6564 6602 msgid "Remove from my feeds" 6565 6603 msgstr "" 6566 6604 ··· 6574 6612 msgstr "" 6575 6613 6576 6614 #: src/components/FeedCard.tsx:338 6577 - #: src/view/com/feeds/FeedSourceCard.tsx:317 6578 6615 msgid "Remove from your feeds?" 6579 6616 msgstr "" 6580 6617 ··· 6636 6673 msgid "Removed from list" 6637 6674 msgstr "" 6638 6675 6639 - #: src/view/com/feeds/FeedSourceCard.tsx:138 6640 - msgid "Removed from my feeds" 6641 - msgstr "" 6642 - 6643 6676 #: src/screens/List/ListHiddenScreen.tsx:94 6644 6677 msgid "Removed from saved feeds" 6645 6678 msgstr "" ··· 7033 7066 #: src/view/com/composer/photos/ImageAltTextDialog.tsx:153 7034 7067 #: src/view/com/composer/photos/ImageAltTextDialog.tsx:163 7035 7068 #: src/view/com/modals/CreateOrEditList.tsx:315 7036 - #: src/view/screens/SavedFeeds.tsx:116 7069 + #: src/view/screens/SavedFeeds.tsx:117 7037 7070 msgid "Save" 7038 7071 msgstr "" 7039 7072 ··· 7049 7082 7050 7083 #: src/components/activity-notifications/SubscribeProfileDialog.tsx:191 7051 7084 #: src/components/activity-notifications/SubscribeProfileDialog.tsx:200 7052 - #: src/view/screens/SavedFeeds.tsx:112 7053 - #: src/view/screens/SavedFeeds.tsx:116 7085 + #: src/view/screens/SavedFeeds.tsx:113 7086 + #: src/view/screens/SavedFeeds.tsx:117 7054 7087 msgid "Save changes" 7055 7088 msgstr "" 7056 7089 ··· 7076 7109 msgid "Save to my feeds" 7077 7110 msgstr "" 7078 7111 7079 - #: src/view/screens/SavedFeeds.tsx:171 7112 + #: src/view/screens/SavedFeeds.tsx:172 7080 7113 msgid "Saved Feeds" 7081 7114 msgstr "" 7082 7115 ··· 7091 7124 7092 7125 #: src/components/dms/ChatEmptyPill.tsx:33 7093 7126 #: src/components/NewskieDialog.tsx:105 7094 - #: src/view/com/notifications/NotificationFeedItem.tsx:745 7095 - #: src/view/com/notifications/NotificationFeedItem.tsx:770 7127 + #: src/view/com/notifications/NotificationFeedItem.tsx:747 7128 + #: src/view/com/notifications/NotificationFeedItem.tsx:772 7096 7129 msgid "Say hello!" 7097 7130 msgstr "" 7098 7131 ··· 7211 7244 msgid "See jobs at Bluesky" 7212 7245 msgstr "" 7213 7246 7214 - #: src/view/screens/SavedFeeds.tsx:212 7247 + #: src/view/screens/SavedFeeds.tsx:213 7215 7248 msgid "See this guide" 7216 7249 msgstr "" 7217 7250 ··· 7574 7607 msgid "Shared Preferences Tester" 7575 7608 msgstr "" 7576 7609 7577 - #: src/components/moderation/ContentHider.tsx:200 7610 + #: src/components/moderation/ContentHider.tsx:203 7578 7611 #: src/components/moderation/LabelPreference.tsx:137 7579 7612 #: src/components/moderation/PostHider.tsx:134 7580 7613 msgid "Show" ··· 7695 7728 msgid "Shows other accounts you can switch to" 7696 7729 msgstr "" 7697 7730 7698 - #: src/components/moderation/ContentHider.tsx:152 7731 + #: src/components/moderation/ContentHider.tsx:155 7699 7732 #: src/components/moderation/PostHider.tsx:89 7700 7733 msgid "Shows the content" 7701 7734 msgstr "" ··· 8095 8128 msgid "Tags only" 8096 8129 msgstr "" 8097 8130 8131 + #: src/view/com/feeds/MissingFeed.tsx:89 8132 + msgid "Tap for information" 8133 + msgstr "" 8134 + 8135 + #: src/view/com/feeds/MissingFeed.tsx:44 8136 + msgid "Tap for more information" 8137 + msgstr "" 8138 + 8098 8139 #: src/components/ContextMenu/Backdrop.ios.tsx:54 8099 8140 #: src/components/ContextMenu/Backdrop.ios.tsx:80 8100 8141 #: src/components/ContextMenu/Backdrop.tsx:46 ··· 8317 8358 #: src/screens/Profile/components/ProfileFeedHeader.tsx:178 8318 8359 #: src/view/screens/ProfileList.tsx:375 8319 8360 #: src/view/screens/ProfileList.tsx:394 8320 - #: src/view/screens/SavedFeeds.tsx:92 8361 + #: src/view/screens/SavedFeeds.tsx:93 8321 8362 msgid "There was an issue contacting the server" 8322 8363 msgstr "" 8323 8364 ··· 8326 8367 msgid "There was an issue contacting the server, please check your internet connection and try again." 8327 8368 msgstr "" 8328 8369 8329 - #: src/view/com/feeds/FeedSourceCard.tsx:127 8330 - #: src/view/com/feeds/FeedSourceCard.tsx:140 8331 - msgid "There was an issue contacting your server" 8332 - msgstr "" 8333 - 8334 8370 #: src/view/com/notifications/NotificationFeed.tsx:124 8335 8371 msgid "There was an issue fetching notifications. Tap here to try again." 8336 8372 msgstr "" ··· 8803 8839 8804 8840 #: src/screens/StarterPack/StarterPackScreen.tsx:673 8805 8841 msgid "Unable to delete" 8842 + msgstr "" 8843 + 8844 + #: src/view/com/feeds/MissingFeed.tsx:126 8845 + msgid "Unavailable feed information" 8806 8846 msgstr "" 8807 8847 8808 8848 #: src/components/dms/MessagesListBlockedFooter.tsx:97 ··· 9455 9495 msgid "Watch now" 9456 9496 msgstr "" 9457 9497 9498 + #: src/view/com/feeds/MissingFeed.tsx:140 9499 + msgid "We could not connect to the service that provides this custom feed. It may be temporarily unavailable and experiencing issues, or permanently unavailable." 9500 + msgstr "" 9501 + 9502 + #: src/view/com/feeds/MissingFeed.tsx:146 9503 + msgid "We could not find this list. It was probably deleted." 9504 + msgstr "" 9505 + 9458 9506 #: src/screens/Hashtag.tsx:205 9459 9507 msgid "We couldn't find any results for that hashtag." 9460 9508 msgstr "" ··· 9822 9870 msgid "You don't have any invite codes yet! We'll send you some when you've been on Bluesky for a little longer." 9823 9871 msgstr "" 9824 9872 9825 - #: src/view/screens/SavedFeeds.tsx:144 9873 + #: src/view/screens/SavedFeeds.tsx:145 9826 9874 msgid "You don't have any pinned feeds." 9827 9875 msgstr "" 9828 9876 9829 - #: src/view/screens/SavedFeeds.tsx:184 9877 + #: src/view/screens/SavedFeeds.tsx:185 9830 9878 msgid "You don't have any saved feeds." 9831 9879 msgstr "" 9832 9880
+158 -293
src/view/com/feeds/FeedSourceCard.tsx
··· 1 - import React from 'react' 1 + import {type StyleProp, View, type ViewStyle} from 'react-native' 2 2 import { 3 - Linking, 4 - Pressable, 5 - StyleProp, 6 - StyleSheet, 7 - View, 8 - ViewStyle, 9 - } from 'react-native' 10 - import {AtUri} from '@atproto/api' 11 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 3 + type $Typed, 4 + AppBskyFeedDefs, 5 + type AppBskyGraphDefs, 6 + AtUri, 7 + } from '@atproto/api' 12 8 import {msg, Plural, Trans} from '@lingui/macro' 13 9 import {useLingui} from '@lingui/react' 14 10 15 - import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped' 16 - import {usePalette} from '#/lib/hooks/usePalette' 17 11 import {sanitizeHandle} from '#/lib/strings/handles' 18 - import {s} from '#/lib/styles' 19 - import {logger} from '#/logger' 20 - import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' 21 12 import { 22 - useAddSavedFeedsMutation, 23 - usePreferencesQuery, 24 - UsePreferencesQueryResponse, 25 - useRemoveFeedMutation, 26 - } from '#/state/queries/preferences' 13 + type FeedSourceInfo, 14 + hydrateFeedGenerator, 15 + hydrateList, 16 + useFeedSourceInfoQuery, 17 + } from '#/state/queries/feed' 27 18 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 28 - import * as Toast from '#/view/com/util/Toast' 29 - import {useTheme} from '#/alf' 30 - import {atoms as a} from '#/alf' 31 - import {shouldClickOpenNewTab} from '#/components/Link' 32 - import * as Prompt from '#/components/Prompt' 19 + import {UserAvatar} from '#/view/com/util/UserAvatar' 20 + import {atoms as a, useTheme} from '#/alf' 21 + import {Link} from '#/components/Link' 33 22 import {RichText} from '#/components/RichText' 34 - import {Text} from '../util/text/Text' 35 - import {UserAvatar} from '../util/UserAvatar' 23 + import {Text} from '#/components/Typography' 24 + import {MissingFeed} from './MissingFeed' 36 25 37 - export function FeedSourceCard({ 38 - feedUri, 39 - style, 40 - showSaveBtn = false, 41 - showDescription = false, 42 - showLikes = false, 43 - pinOnSave = false, 44 - showMinimalPlaceholder, 45 - hideTopBorder, 46 - }: { 26 + type FeedSourceCardProps = { 47 27 feedUri: string 28 + feedData?: 29 + | $Typed<AppBskyFeedDefs.GeneratorView> 30 + | $Typed<AppBskyGraphDefs.ListView> 48 31 style?: StyleProp<ViewStyle> 49 32 showSaveBtn?: boolean 50 33 showDescription?: boolean ··· 52 35 pinOnSave?: boolean 53 36 showMinimalPlaceholder?: boolean 54 37 hideTopBorder?: boolean 55 - }) { 56 - const {data: preferences} = usePreferencesQuery() 57 - const {data: feed} = useFeedSourceInfoQuery({uri: feedUri}) 38 + link?: boolean 39 + } 40 + 41 + export 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 + 59 + export function FeedSourceCardWithoutData({ 60 + feedUri, 61 + ...props 62 + }: Omit<FeedSourceCardProps, 'feedData'>) { 63 + const {data: feed, error} = useFeedSourceInfoQuery({ 64 + uri: feedUri, 65 + }) 58 66 59 67 return ( 60 68 <FeedSourceCardLoaded 61 69 feedUri={feedUri} 62 70 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 + error={error} 72 + {...props} 71 73 /> 72 74 ) 73 75 } ··· 75 77 export function FeedSourceCardLoaded({ 76 78 feedUri, 77 79 feed, 78 - preferences, 79 80 style, 80 - showSaveBtn = false, 81 81 showDescription = false, 82 82 showLikes = false, 83 - pinOnSave = false, 84 83 showMinimalPlaceholder, 85 84 hideTopBorder, 85 + link = true, 86 + error, 86 87 }: { 87 88 feedUri: string 88 89 feed?: FeedSourceInfo 89 - preferences?: UsePreferencesQueryResponse 90 90 style?: StyleProp<ViewStyle> 91 - showSaveBtn?: boolean 92 91 showDescription?: boolean 93 92 showLikes?: boolean 94 - pinOnSave?: boolean 95 93 showMinimalPlaceholder?: boolean 96 94 hideTopBorder?: boolean 95 + link?: boolean 96 + error?: unknown 97 97 }) { 98 98 const t = useTheme() 99 - const pal = usePalette('default') 100 99 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 100 153 101 /* 154 102 * LOAD STATE ··· 156 104 * This state also captures the scenario where a feed can't load for whatever 157 105 * reason. 158 106 */ 159 - if (!feed || !preferences) 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 + 160 119 return ( 161 - <View 120 + <FeedLoadingPlaceholder 162 121 style={[ 163 - pal.border, 164 - { 165 - borderTopWidth: 166 - showMinimalPlaceholder || hideTopBorder 167 - ? 0 168 - : StyleSheet.hairlineWidth, 169 - flexDirection: 'row', 170 - alignItems: 'center', 171 - flex: 1, 172 - paddingRight: 18, 173 - }, 174 - ]}> 175 - {showMinimalPlaceholder ? ( 176 - <FeedLoadingPlaceholder 177 - style={{flex: 1}} 178 - showTopBorder={false} 179 - showLowerPlaceholder={false} 180 - /> 181 - ) : ( 182 - <FeedLoadingPlaceholder style={{flex: 1}} showTopBorder={false} /> 183 - )} 184 - 185 - {showSaveBtn && ( 186 - <Pressable 187 - testID={`feed-${feedUri}-toggleSave`} 188 - disabled={isRemovePending} 189 - accessibilityRole="button" 190 - accessibilityLabel={_(msg`Remove from my feeds`)} 191 - accessibilityHint="" 192 - onPress={onUnsave} 193 - hitSlop={15} 194 - style={styles.btn}> 195 - <FontAwesomeIcon 196 - icon={['far', 'trash-can']} 197 - size={19} 198 - color={pal.colors.icon} 199 - /> 200 - </Pressable> 201 - )} 202 - </View> 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 + /> 203 130 ) 131 + } 204 132 205 - return ( 133 + const inner = ( 206 134 <> 207 - <Pressable 208 - testID={`feed-${feed.displayName}`} 209 - accessibilityRole="button" 210 - style={[ 211 - styles.container, 212 - pal.border, 213 - style, 214 - {borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth}, 215 - ]} 216 - onPress={e => { 217 - const shouldOpenInNewTab = shouldClickOpenNewTab(e) 218 - if (feed.type === 'feed') { 219 - if (shouldOpenInNewTab) { 220 - Linking.openURL( 221 - `/profile/${feed.creatorDid}/feed/${new AtUri(feed.uri).rkey}`, 222 - ) 223 - } else { 224 - navigation.push('ProfileFeed', { 225 - name: feed.creatorDid, 226 - rkey: new AtUri(feed.uri).rkey, 227 - }) 228 - } 229 - } else if (feed.type === 'list') { 230 - if (shouldOpenInNewTab) { 231 - Linking.openURL( 232 - `/profile/${feed.creatorDid}/lists/${new AtUri(feed.uri).rkey}`, 233 - ) 234 - } else { 235 - navigation.push('ProfileList', { 236 - name: feed.creatorDid, 237 - rkey: new AtUri(feed.uri).rkey, 238 - }) 239 - } 240 - } 241 - }} 242 - key={feed.uri}> 243 - <View style={[styles.headerContainer, a.align_center]}> 244 - <View style={[s.mr10]}> 245 - <UserAvatar type="algo" size={36} avatar={feed.avatar} /> 246 - </View> 247 - <View style={[styles.headerTextContainer]}> 248 - <Text emoji style={[pal.text, s.bold]} numberOfLines={1}> 249 - {feed.displayName} 250 - </Text> 251 - <Text style={[pal.textLight]} numberOfLines={1}> 252 - {feed.type === 'feed' ? ( 253 - <Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> 254 - ) : ( 255 - <Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> 256 - )} 257 - </Text> 258 - </View> 259 - 260 - {showSaveBtn && ( 261 - <View style={{alignSelf: 'center'}}> 262 - <Pressable 263 - testID={`feed-${feed.displayName}-toggleSave`} 264 - disabled={isAddSavedFeedPending || isRemovePending} 265 - accessibilityRole="button" 266 - accessibilityLabel={ 267 - isSaved 268 - ? _(msg`Remove from my feeds`) 269 - : _(msg`Add to my feeds`) 270 - } 271 - accessibilityHint="" 272 - onPress={onToggleSaved} 273 - hitSlop={15} 274 - style={styles.btn}> 275 - {isSaved ? ( 276 - <FontAwesomeIcon 277 - icon={['far', 'trash-can']} 278 - size={19} 279 - color={pal.colors.icon} 280 - /> 281 - ) : ( 282 - <FontAwesomeIcon 283 - icon="plus" 284 - size={18} 285 - color={pal.colors.link} 286 - /> 287 - )} 288 - </Pressable> 289 - </View> 290 - )} 135 + <View style={[a.flex_row, a.align_center]}> 136 + <View style={[a.mr_md]}> 137 + <UserAvatar type="algo" size={36} avatar={feed.avatar} /> 291 138 </View> 292 - 293 - {showDescription && feed.description ? ( 294 - <RichText 295 - style={[t.atoms.text_contrast_high, styles.description]} 296 - value={feed.description} 297 - numberOfLines={3} 298 - /> 299 - ) : null} 300 - 301 - {showLikes && feed.type === 'feed' ? ( 302 - <Text type="sm-medium" style={[pal.text, pal.textLight]}> 303 - <Trans> 304 - Liked by{' '} 305 - <Plural 306 - value={feed.likeCount || 0} 307 - one="# user" 308 - other="# users" 309 - /> 310 - </Trans> 139 + <View style={[a.flex_1]}> 140 + <Text 141 + emoji 142 + style={[a.text_sm, a.font_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 + )} 311 154 </Text> 312 - ) : null} 313 - </Pressable> 314 - 315 - <Prompt.Basic 316 - control={removePromptControl} 317 - title={_(msg`Remove from your feeds?`)} 318 - description={_( 319 - msg`Are you sure you want to remove ${feed.displayName} from your feeds?`, 320 - )} 321 - onConfirm={onUnsave} 322 - confirmButtonCta={_(msg`Remove`)} 323 - confirmButtonColor="negative" 324 - /> 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_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} 325 178 </> 326 179 ) 327 - } 328 180 329 - const styles = StyleSheet.create({ 330 - container: { 331 - paddingHorizontal: 18, 332 - paddingVertical: 20, 333 - flexDirection: 'column', 334 - flex: 1, 335 - gap: 14, 336 - }, 337 - border: { 338 - borderTopWidth: StyleSheet.hairlineWidth, 339 - }, 340 - headerContainer: { 341 - flexDirection: 'row', 342 - }, 343 - headerTextContainer: { 344 - flexDirection: 'column', 345 - columnGap: 4, 346 - flex: 1, 347 - }, 348 - description: { 349 - flex: 1, 350 - flexWrap: 'wrap', 351 - }, 352 - btn: { 353 - paddingVertical: 6, 354 - }, 355 - }) 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 + }
+222
src/view/com/feeds/MissingFeed.tsx
··· 1 + import {type StyleProp, View, type ViewStyle} from 'react-native' 2 + import {AtUri} from '@atproto/api' 3 + import {msg, Trans} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {cleanError} from '#/lib/strings/errors' 7 + import {isNative, isWeb} from '#/platform/detection' 8 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 9 + import {getFeedTypeFromUri} from '#/state/queries/feed' 10 + import {useProfileQuery} from '#/state/queries/profile' 11 + import {atoms as a, useTheme, web} from '#/alf' 12 + import {Button, ButtonText} from '#/components/Button' 13 + import * as Dialog from '#/components/Dialog' 14 + import {Divider} from '#/components/Divider' 15 + import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 16 + import * as ProfileCard from '#/components/ProfileCard' 17 + import {Text} from '#/components/Typography' 18 + 19 + export function MissingFeed({ 20 + style, 21 + hideTopBorder, 22 + uri, 23 + error, 24 + }: { 25 + style?: StyleProp<ViewStyle> 26 + hideTopBorder?: boolean 27 + uri: string 28 + error?: unknown 29 + }) { 30 + const t = useTheme() 31 + const {_} = useLingui() 32 + const control = Dialog.useDialogControl() 33 + 34 + const type = getFeedTypeFromUri(uri) 35 + 36 + return ( 37 + <> 38 + <Button 39 + label={ 40 + type === 'feed' 41 + ? _(msg`Could not connect to custom feed`) 42 + : _(msg`Deleted list`) 43 + } 44 + accessibilityHint={_(msg`Tap for more information`)} 45 + onPress={control.open} 46 + style={[ 47 + a.flex_1, 48 + a.p_lg, 49 + a.gap_md, 50 + !hideTopBorder && !a.border_t, 51 + t.atoms.border_contrast_low, 52 + a.justify_start, 53 + style, 54 + ]}> 55 + <View style={[a.flex_row, a.align_center]}> 56 + <View 57 + style={[ 58 + {width: 36, height: 36}, 59 + t.atoms.bg_contrast_25, 60 + a.rounded_sm, 61 + a.mr_md, 62 + a.align_center, 63 + a.justify_center, 64 + ]}> 65 + <WarningIcon size="lg" /> 66 + </View> 67 + <View style={[a.flex_1]}> 68 + <Text 69 + emoji 70 + style={[a.text_sm, a.font_bold, a.leading_snug, a.italic]} 71 + numberOfLines={1}> 72 + {type === 'feed' ? ( 73 + <Trans>Feed unavailable</Trans> 74 + ) : ( 75 + <Trans>Deleted list</Trans> 76 + )} 77 + </Text> 78 + <Text 79 + style={[ 80 + a.text_sm, 81 + t.atoms.text_contrast_medium, 82 + a.leading_snug, 83 + a.italic, 84 + ]} 85 + numberOfLines={1}> 86 + {isWeb ? ( 87 + <Trans>Click for information</Trans> 88 + ) : ( 89 + <Trans>Tap for information</Trans> 90 + )} 91 + </Text> 92 + </View> 93 + </View> 94 + </Button> 95 + 96 + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 97 + <Dialog.Handle /> 98 + <DialogInner uri={uri} type={type} error={error} /> 99 + </Dialog.Outer> 100 + </> 101 + ) 102 + } 103 + 104 + function DialogInner({ 105 + uri, 106 + type, 107 + error, 108 + }: { 109 + uri: string 110 + type: 'feed' | 'list' 111 + error: unknown 112 + }) { 113 + const control = Dialog.useDialogContext() 114 + const t = useTheme() 115 + const {_} = useLingui() 116 + const atUri = new AtUri(uri) 117 + const {data: profile, isError: isProfileError} = useProfileQuery({ 118 + did: atUri.host, 119 + }) 120 + const moderationOpts = useModerationOpts() 121 + 122 + return ( 123 + <Dialog.ScrollableInner 124 + label={ 125 + type === 'feed' 126 + ? _(msg`Unavailable feed information`) 127 + : _(msg`Deleted list`) 128 + } 129 + style={web({maxWidth: 500})}> 130 + <View style={[a.gap_sm]}> 131 + <Text style={[a.font_heavy, a.text_2xl]}> 132 + {type === 'feed' ? ( 133 + <Trans>Could not connect to feed service</Trans> 134 + ) : ( 135 + <Trans>Deleted list</Trans> 136 + )} 137 + </Text> 138 + <Text style={[t.atoms.text_contrast_high, a.leading_snug]}> 139 + {type === 'feed' ? ( 140 + <Trans> 141 + We could not connect to the service that provides this custom 142 + feed. It may be temporarily unavailable and experiencing issues, 143 + or permanently unavailable. 144 + </Trans> 145 + ) : ( 146 + <Trans>We could not find this list. It was probably deleted.</Trans> 147 + )} 148 + </Text> 149 + <Divider style={[a.my_md]} /> 150 + <Text style={[a.font_bold, t.atoms.text_contrast_high]}> 151 + {type === 'feed' ? ( 152 + <Trans>Feed creator</Trans> 153 + ) : ( 154 + <Trans>List creator</Trans> 155 + )} 156 + </Text> 157 + {profile && moderationOpts && ( 158 + <View style={[a.w_full, a.align_start]}> 159 + <ProfileCard.Link profile={profile} onPress={() => control.close()}> 160 + <ProfileCard.Header> 161 + <ProfileCard.Avatar 162 + profile={profile} 163 + moderationOpts={moderationOpts} 164 + disabledPreview 165 + /> 166 + <ProfileCard.NameAndHandle 167 + profile={profile} 168 + moderationOpts={moderationOpts} 169 + /> 170 + </ProfileCard.Header> 171 + </ProfileCard.Link> 172 + </View> 173 + )} 174 + {isProfileError && ( 175 + <Text 176 + style={[ 177 + t.atoms.text_contrast_high, 178 + a.italic, 179 + a.text_center, 180 + a.w_full, 181 + ]}> 182 + <Trans>Could not find profile</Trans> 183 + </Text> 184 + )} 185 + {type === 'feed' && ( 186 + <> 187 + <Text style={[a.font_bold, t.atoms.text_contrast_high, a.mt_md]}> 188 + <Trans>Feed identifier</Trans> 189 + </Text> 190 + <Text style={[a.text_md, t.atoms.text_contrast_high, a.italic]}> 191 + {atUri.rkey} 192 + </Text> 193 + </> 194 + )} 195 + {error instanceof Error && ( 196 + <> 197 + <Text style={[a.font_bold, t.atoms.text_contrast_high, a.mt_md]}> 198 + <Trans>Error message</Trans> 199 + </Text> 200 + <Text style={[a.text_md, t.atoms.text_contrast_high, a.italic]}> 201 + {cleanError(error.message)} 202 + </Text> 203 + </> 204 + )} 205 + </View> 206 + {isNative && ( 207 + <Button 208 + label={_(msg`Close`)} 209 + onPress={() => control.close()} 210 + size="small" 211 + variant="solid" 212 + color="secondary" 213 + style={[a.mt_5xl]}> 214 + <ButtonText> 215 + <Trans>Close</Trans> 216 + </ButtonText> 217 + </Button> 218 + )} 219 + <Dialog.Close /> 220 + </Dialog.ScrollableInner> 221 + ) 222 + }
+2 -1
src/view/com/notifications/NotificationFeedItem.tsx
··· 671 671 {item.type === 'feedgen-like' && item.subjectUri ? ( 672 672 <FeedSourceCard 673 673 feedUri={item.subjectUri} 674 + link={false} 674 675 style={[ 675 676 t.atoms.bg, 676 677 t.atoms.border_contrast_low, 677 678 a.border, 679 + a.p_md, 678 680 styles.feedcard, 679 681 ]} 680 682 showLikes ··· 1000 1002 }, 1001 1003 feedcard: { 1002 1004 borderRadius: 8, 1003 - paddingVertical: 12, 1004 1005 marginTop: 6, 1005 1006 }, 1006 1007 addedContainer: {
+7 -14
src/view/com/util/LoadingPlaceholder.tsx
··· 1 1 import {useMemo} from 'react' 2 2 import { 3 - DimensionValue, 4 - StyleProp, 3 + type DimensionValue, 4 + type StyleProp, 5 5 StyleSheet, 6 6 View, 7 - ViewStyle, 7 + type ViewStyle, 8 8 } from 'react-native' 9 9 10 10 import {usePalette} from '#/lib/hooks/usePalette' ··· 233 233 <View 234 234 style={[ 235 235 { 236 - paddingHorizontal: 12, 237 - paddingVertical: 18, 236 + padding: 16, 238 237 borderTopWidth: showTopBorder ? StyleSheet.hairlineWidth : 0, 239 238 }, 240 239 pal.border, ··· 244 243 <LoadingPlaceholder 245 244 width={36} 246 245 height={36} 247 - style={[styles.avatar, {borderRadius: 6}]} 246 + style={[styles.avatar, {borderRadius: 8}]} 248 247 /> 249 248 <View style={[s.flex1]}> 250 249 <LoadingPlaceholder width={100} height={8} style={[s.mt5, s.mb10]} /> ··· 252 251 </View> 253 252 </View> 254 253 {showLowerPlaceholder && ( 255 - <View style={{paddingHorizontal: 5, marginTop: 10}}> 256 - <LoadingPlaceholder 257 - width={260} 258 - height={8} 259 - style={{marginVertical: 12}} 260 - /> 254 + <View style={{marginTop: 12}}> 261 255 <LoadingPlaceholder width={120} height={8} /> 262 256 </View> 263 257 )} ··· 352 346 }, 353 347 avatar: { 354 348 borderRadius: 999, 355 - marginRight: 10, 356 - marginLeft: 8, 349 + marginRight: 12, 357 350 }, 358 351 notification: { 359 352 flexDirection: 'row',
+7 -16
src/view/screens/SavedFeeds.tsx
··· 36 36 import {FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon} from '#/components/icons/FloppyDisk' 37 37 import * as Layout from '#/components/Layout' 38 38 import {Loader} from '#/components/Loader' 39 + import {Text as NewText} from '#/components/Typography' 39 40 40 41 type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'> 41 42 export function SavedFeeds({}: Props) { ··· 296 297 <FeedSourceCard 297 298 key={feedUri} 298 299 feedUri={feedUri} 299 - style={[isPinned && {paddingRight: 8}]} 300 + style={[isPinned && a.pr_sm]} 300 301 showMinimalPlaceholder 301 302 hideTopBorder={true} 302 303 /> ··· 391 392 function FollowingFeedCard() { 392 393 const t = useTheme() 393 394 return ( 394 - <View 395 - style={[ 396 - a.flex_row, 397 - a.align_center, 398 - a.flex_1, 399 - { 400 - paddingHorizontal: 18, 401 - paddingVertical: 20, 402 - }, 403 - ]}> 395 + <View style={[a.flex_row, a.align_center, a.flex_1, a.p_lg]}> 404 396 <View 405 397 style={[ 406 398 a.align_center, 407 399 a.justify_center, 408 400 a.rounded_sm, 401 + a.mr_md, 409 402 { 410 403 width: 36, 411 404 height: 36, 412 405 backgroundColor: t.palette.primary_500, 413 - marginRight: 10, 414 406 }, 415 407 ]}> 416 408 <FilterTimeline ··· 423 415 fill={t.palette.white} 424 416 /> 425 417 </View> 426 - <View 427 - style={{flex: 1, flexDirection: 'row', gap: 8, alignItems: 'center'}}> 428 - <Text type="lg-medium" style={[t.atoms.text]} numberOfLines={1}> 418 + <View style={[a.flex_1, a.flex_row, a.gap_sm, a.align_center]}> 419 + <NewText style={[a.text_sm, a.font_bold, a.leading_snug]}> 429 420 <Trans>Following</Trans> 430 - </Text> 421 + </NewText> 431 422 </View> 432 423 </View> 433 424 )