Bluesky app fork with some witchin' additions 💫

Cleaner sidebar layout (#9603)

authored by

Alex Benzer and committed by
GitHub
c4fd9980 966ae68d

+419 -126
+8 -1
src/components/FeedInterstitials.tsx
··· 933 933 934 934 export function ProgressGuide() { 935 935 const t = useTheme() 936 + const {gtMobile} = useBreakpoints() 936 937 return ( 937 - <View style={[t.atoms.border_contrast_low, a.px_lg, a.py_lg, a.pb_lg]}> 938 + <View 939 + style={[ 940 + t.atoms.border_contrast_low, 941 + a.px_lg, 942 + a.py_lg, 943 + !gtMobile && {marginTop: 4}, 944 + ]}> 938 945 <ProgressGuideList /> 939 946 </View> 940 947 )
+12 -7
src/components/ProgressGuide/FollowDialog.tsx
··· 31 31 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 32 32 import * as Dialog from '#/components/Dialog' 33 33 import {useInteractionState} from '#/components/hooks/useInteractionState' 34 + import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' 34 35 import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass' 35 - import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person' 36 36 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 37 37 import {boostInterests, InterestTabs} from '#/components/InterestTabs' 38 38 import * as ProfileCard from '#/components/ProfileCard' ··· 60 60 key: string 61 61 } 62 62 63 - export function FollowDialog({guide}: {guide: Follow10ProgressGuide}) { 63 + export function FollowDialog({ 64 + guide, 65 + showArrow, 66 + }: { 67 + guide: Follow10ProgressGuide 68 + showArrow?: boolean 69 + }) { 64 70 const {_} = useLingui() 65 71 const control = Dialog.useDialogControl() 66 - const {gtMobile} = useBreakpoints() 72 + const {gtPhone} = useBreakpoints() 67 73 const {height: minHeight} = useWindowDimensions() 68 74 69 75 return ( ··· 74 80 control.open() 75 81 logEvent('progressGuide:followDialog:open', {}) 76 82 }} 77 - size={gtMobile ? 'small' : 'large'} 78 - color="primary" 79 - variant="solid"> 80 - <ButtonIcon icon={PersonGroupIcon} /> 83 + size={gtPhone ? 'small' : 'large'} 84 + color="primary"> 81 85 <ButtonText> 82 86 <Trans>Find people to follow</Trans> 83 87 </ButtonText> 88 + {showArrow && <ButtonIcon icon={ArrowRightIcon} />} 84 89 </Button> 85 90 <Dialog.Outer control={control} nativeOptions={{minHeight}}> 86 91 <Dialog.Handle />
+124 -21
src/components/ProgressGuide/List.tsx
··· 2 2 import {msg, Trans} from '@lingui/macro' 3 3 import {useLingui} from '@lingui/react' 4 4 5 + import {useProfileFollowsQuery} from '#/state/queries/profile-follows' 6 + import {useSession} from '#/state/session' 5 7 import { 6 8 useProgressGuide, 7 9 useProgressGuideControls, 8 10 } from '#/state/shell/progress-guide' 9 - import {atoms as a, useTheme} from '#/alf' 11 + import {UserAvatar} from '#/view/com/util/UserAvatar' 12 + import {atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme} from '#/alf' 10 13 import {Button, ButtonIcon} from '#/components/Button' 14 + import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/Person' 11 15 import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times' 12 16 import {Text} from '#/components/Typography' 17 + import type * as bsky from '#/types/bsky' 13 18 import {FollowDialog} from './FollowDialog' 14 19 import {ProgressGuideTask} from './Task' 15 20 21 + const TOTAL_AVATARS = 10 22 + 16 23 export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) { 17 24 const t = useTheme() 18 25 const {_} = useLingui() 26 + const {gtPhone} = useBreakpoints() 27 + const {rightNavVisible} = useLayoutBreakpoints() 28 + const {currentAccount} = useSession() 19 29 const followProgressGuide = useProgressGuide('follow-10') 20 30 const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7') 21 31 const guide = followProgressGuide || followAndLikeProgressGuide 22 32 const {endProgressGuide} = useProgressGuideControls() 33 + const {data: follows} = useProfileFollowsQuery(currentAccount?.did, { 34 + limit: TOTAL_AVATARS, 35 + }) 36 + 37 + const actualFollowsCount = follows?.pages?.[0]?.follows?.length ?? 0 38 + 39 + // Hide if user already follows 10+ people 40 + if (guide?.guide === 'follow-10' && actualFollowsCount >= TOTAL_AVATARS) { 41 + return null 42 + } 43 + 44 + // Inline layout when left nav visible but no right sidebar (800-1100px) 45 + const inlineLayout = gtPhone && !rightNavVisible 23 46 24 47 if (guide) { 25 48 return ( 26 - <View style={[a.flex_col, a.gap_md, style]}> 49 + <View 50 + style={[ 51 + a.flex_col, 52 + a.gap_md, 53 + a.rounded_md, 54 + t.atoms.bg_contrast_25, 55 + a.p_lg, 56 + style, 57 + ]}> 27 58 <View style={[a.flex_row, a.align_center, a.justify_between]}> 28 - <Text 29 - style={[ 30 - t.atoms.text_contrast_medium, 31 - a.font_semi_bold, 32 - a.text_sm, 33 - {textTransform: 'uppercase'}, 34 - ]}> 35 - <Trans>Getting started</Trans> 59 + <Text style={[t.atoms.text, a.font_semi_bold, a.text_md]}> 60 + <Trans>Follow 10 people to get started</Trans> 36 61 </Text> 37 62 <Button 38 63 variant="ghost" ··· 40 65 color="secondary" 41 66 shape="round" 42 67 label={_(msg`Dismiss getting started guide`)} 43 - onPress={endProgressGuide}> 44 - <ButtonIcon icon={Times} size="sm" /> 68 + onPress={endProgressGuide} 69 + style={[a.bg_transparent, {marginTop: -6, marginRight: -6}]}> 70 + <ButtonIcon icon={Times} size="xs" /> 45 71 </Button> 46 72 </View> 47 73 {guide.guide === 'follow-10' && ( 48 - <> 49 - <ProgressGuideTask 50 - current={guide.numFollows + 1} 51 - total={10 + 1} 52 - title={_(msg`Follow 10 accounts`)} 53 - subtitle={_(msg`Bluesky is better with friends!`)} 54 - /> 55 - <FollowDialog guide={guide} /> 56 - </> 74 + <View 75 + style={[ 76 + inlineLayout 77 + ? [ 78 + a.flex_row, 79 + a.flex_wrap, 80 + a.align_center, 81 + a.justify_between, 82 + a.gap_sm, 83 + ] 84 + : a.flex_col, 85 + !inlineLayout && a.gap_md, 86 + ]}> 87 + <StackedAvatars follows={follows?.pages?.[0]?.follows} /> 88 + <FollowDialog guide={guide} showArrow={inlineLayout} /> 89 + </View> 57 90 )} 58 91 {guide.guide === 'like-10-and-follow-7' && ( 59 92 <> ··· 76 109 } 77 110 return null 78 111 } 112 + 113 + function StackedAvatars({follows}: {follows?: bsky.profile.AnyProfileView[]}) { 114 + const t = useTheme() 115 + const {centerColumnOffset} = useLayoutBreakpoints() 116 + 117 + // Smaller avatars for narrower viewport 118 + const avatarSize = centerColumnOffset ? 30 : 37 119 + const overlap = centerColumnOffset ? 9 : 11 120 + const iconSize = centerColumnOffset ? 14 : 18 121 + 122 + // Use actual follows count, not the guide's event counter 123 + const followedAvatars = follows?.slice(0, TOTAL_AVATARS) ?? [] 124 + const remainingSlots = TOTAL_AVATARS - followedAvatars.length 125 + 126 + // Total width calculation: first avatar + (remaining * visible portion) 127 + const totalWidth = avatarSize + (TOTAL_AVATARS - 1) * (avatarSize - overlap) 128 + 129 + return ( 130 + <View style={[a.flex_row, a.self_start, {width: totalWidth}]}> 131 + {/* Show followed user avatars */} 132 + {followedAvatars.map((follow, i) => ( 133 + <View 134 + key={follow.did} 135 + style={[ 136 + a.rounded_full, 137 + { 138 + marginLeft: i === 0 ? 0 : -overlap, 139 + zIndex: TOTAL_AVATARS - i, 140 + borderWidth: 2, 141 + borderColor: t.atoms.bg_contrast_25.backgroundColor, 142 + }, 143 + ]}> 144 + <UserAvatar 145 + type="user" 146 + size={avatarSize - 4} 147 + avatar={follow.avatar} 148 + /> 149 + </View> 150 + ))} 151 + {/* Show placeholder avatars for remaining slots */} 152 + {Array(remainingSlots) 153 + .fill(0) 154 + .map((_, i) => ( 155 + <View 156 + key={`placeholder-${i}`} 157 + style={[ 158 + a.align_center, 159 + a.justify_center, 160 + a.rounded_full, 161 + t.atoms.bg_contrast_100, 162 + { 163 + width: avatarSize, 164 + height: avatarSize, 165 + marginLeft: 166 + followedAvatars.length === 0 && i === 0 ? 0 : -overlap, 167 + zIndex: TOTAL_AVATARS - followedAvatars.length - i, 168 + borderWidth: 2, 169 + borderColor: t.atoms.bg_contrast_25.backgroundColor, 170 + }, 171 + ]}> 172 + <PersonIcon 173 + width={iconSize} 174 + height={iconSize} 175 + fill={t.atoms.text_contrast_low.color} 176 + /> 177 + </View> 178 + ))} 179 + </View> 180 + ) 181 + }
+2 -2
src/components/ProgressGuide/Task.tsx
··· 31 31 size={20} 32 32 thickness={3} 33 33 borderWidth={0} 34 - unfilledColor={t.palette.contrast_50} 34 + unfilledColor={t.palette.contrast_100} 35 35 /> 36 36 )} 37 37 38 - <View style={[a.flex_col, a.gap_2xs, subtitle && {marginTop: -2}]}> 38 + <View style={[a.flex_col, a.gap_xs, subtitle && {marginTop: -2}]}> 39 39 <Text 40 40 style={[ 41 41 a.text_sm,
+10 -9
src/components/TrendingTopics.tsx
··· 20 20 topic: raw, 21 21 size, 22 22 style, 23 - }: {topic: TrendingTopic; size?: 'large' | 'small'} & ViewStyleProp) { 24 - const t = useTheme() 23 + hovered, 24 + }: { 25 + topic: TrendingTopic 26 + size?: 'large' | 'small' 27 + hovered?: boolean 28 + } & ViewStyleProp) { 25 29 const topic = useTopic(raw) 26 30 27 31 const isSmall = size === 'small' ··· 33 37 style={[ 34 38 a.flex_row, 35 39 a.align_center, 36 - a.rounded_full, 37 - a.border, 38 - t.atoms.border_contrast_medium, 39 - t.atoms.bg, 40 40 isSmall 41 41 ? [ 42 42 { 43 - paddingVertical: 5, 44 - paddingHorizontal: 10, 43 + paddingVertical: 2, 44 + paddingHorizontal: 4, 45 45 }, 46 46 ] 47 - : [a.py_sm, a.px_md], 47 + : [a.py_xs, a.px_sm], 48 48 hasIcon && {gap: 6}, 49 49 style, 50 50 ]}> ··· 93 93 a.font_semi_bold, 94 94 a.leading_tight, 95 95 isSmall ? [a.text_sm] : [a.text_md, {paddingBottom: 1}], 96 + hovered && {textDecorationLine: 'underline'}, 96 97 ]} 97 98 numberOfLines={1}> 98 99 {topic.displayName}
+1 -2
src/components/interstitials/Trending.tsx
··· 99 99 <View style={[a.py_lg]}> 100 100 <Text 101 101 style={[ 102 - t.atoms.text, 102 + t.atoms.text_contrast_medium, 103 103 a.text_sm, 104 104 a.font_semi_bold, 105 - {opacity: 0.7}, // NOTE: we use opacity 0.7 instead of a color to match the color of the home pager tab bar 106 105 ]}> 107 106 {topic.topic} 108 107 </Text>
+45
src/state/queries/profile.ts
··· 4 4 type AppBskyActorGetProfile, 5 5 type AppBskyActorGetProfiles, 6 6 type AppBskyActorProfile, 7 + type AppBskyGraphGetFollows, 7 8 AtUri, 8 9 type BskyAgent, 9 10 type ComAtprotoRepoUploadBlob, 10 11 type Un$Typed, 11 12 } from '@atproto/api' 12 13 import { 14 + type InfiniteData, 13 15 keepPreviousData, 14 16 type QueryClient, 15 17 useMutation, ··· 26 28 import {type ImageMeta} from '#/state/gallery' 27 29 import {STALE} from '#/state/queries' 28 30 import {resetProfilePostsQueries} from '#/state/queries/post-feed' 31 + import {RQKEY as PROFILE_FOLLOWS_RQKEY} from '#/state/queries/profile-follows' 29 32 import { 30 33 unstableCacheProfileView, 31 34 useUnstableProfileViewCache, ··· 247 250 ) { 248 251 const agent = useAgent() 249 252 const queryClient = useQueryClient() 253 + const {currentAccount} = useSession() 250 254 const did = profile.did 251 255 const initialFollowingUri = profile.viewer?.following 252 256 const followMutation = useProfileFollowMutation( ··· 282 286 updateProfileShadow(queryClient, did, { 283 287 followingUri: finalFollowingUri, 284 288 }) 289 + 290 + // Optimistically update profile follows cache for avatar displays 291 + if (currentAccount?.did) { 292 + type FollowsQueryData = 293 + InfiniteData<AppBskyGraphGetFollows.OutputSchema> 294 + queryClient.setQueryData<FollowsQueryData>( 295 + PROFILE_FOLLOWS_RQKEY(currentAccount.did), 296 + old => { 297 + if (!old?.pages?.[0]) return old 298 + if (finalFollowingUri) { 299 + // Add the followed profile to the beginning 300 + const alreadyExists = old.pages[0].follows.some( 301 + f => f.did === profile.did, 302 + ) 303 + if (alreadyExists) return old 304 + return { 305 + ...old, 306 + pages: [ 307 + { 308 + ...old.pages[0], 309 + follows: [ 310 + profile as AppBskyActorDefs.ProfileView, 311 + ...old.pages[0].follows, 312 + ], 313 + }, 314 + ...old.pages.slice(1), 315 + ], 316 + } 317 + } else { 318 + // Remove the unfollowed profile 319 + return { 320 + ...old, 321 + pages: old.pages.map(page => ({ 322 + ...page, 323 + follows: page.follows.filter(f => f.did !== profile.did), 324 + })), 325 + } 326 + } 327 + }, 328 + ) 329 + } 285 330 286 331 if (finalFollowingUri) { 287 332 agent.app.bsky.graph
+145 -33
src/view/shell/desktop/Feeds.tsx
··· 1 - import {View} from 'react-native' 1 + import {Pressable, View} from 'react-native' 2 2 import {msg} from '@lingui/macro' 3 3 import {useLingui} from '@lingui/react' 4 4 import {useNavigation, useNavigationState} from '@react-navigation/native' ··· 7 7 import {type NavigationProp} from '#/lib/routes/types' 8 8 import {logger} from '#/logger' 9 9 import {emitSoftReset} from '#/state/events' 10 - import {usePinnedFeedsInfos} from '#/state/queries/feed' 10 + import { 11 + type SavedFeedSourceInfo, 12 + usePinnedFeedsInfos, 13 + } from '#/state/queries/feed' 11 14 import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed' 15 + import {UserAvatar} from '#/view/com/util/UserAvatar' 12 16 import {atoms as a, useTheme, web} from '#/alf' 13 - import {createStaticClick, InlineLinkText} from '#/components/Link' 17 + import {useInteractionState} from '#/components/hooks/useInteractionState' 18 + import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' 19 + import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 20 + import {Link} from '#/components/Link' 21 + import {Text} from '#/components/Typography' 14 22 15 23 export function DesktopFeeds() { 16 24 const t = useTheme() ··· 57 65 style={[ 58 66 a.flex_1, 59 67 web({ 60 - gap: 10, 68 + gap: 2, 61 69 /* 62 70 * Small padding prevents overflow prior to actually overflowing the 63 71 * height of the screen with lots of feeds. 64 72 */ 65 - paddingVertical: 2, 66 - marginHorizontal: -2, 73 + paddingTop: 2, 67 74 overflowY: 'auto', 68 75 }), 69 76 ]}> ··· 72 79 const current = route.name === 'Home' && feed === selectedFeed 73 80 74 81 return ( 75 - <InlineLinkText 82 + <FeedItem 76 83 key={feedInfo.uri} 77 - label={feedInfo.displayName} 78 - {...createStaticClick(() => { 84 + feedInfo={feedInfo} 85 + current={current} 86 + onPress={() => { 79 87 logger.metric( 80 88 'desktopFeeds:feed:click', 81 89 { ··· 89 97 if (route.name === 'Home' && feed === selectedFeed) { 90 98 emitSoftReset() 91 99 } 92 - })} 93 - style={[ 94 - a.text_md, 95 - a.leading_snug, 96 - a.flex_shrink_0, 97 - current 98 - ? [a.font_semi_bold, t.atoms.text] 99 - : [t.atoms.text_contrast_medium], 100 - web({ 101 - marginHorizontal: 2, 102 - width: 'calc(100% - 4px)', 103 - }), 104 - ]} 105 - numberOfLines={1}> 106 - {feedInfo.displayName} 107 - </InlineLinkText> 100 + }} 101 + /> 108 102 ) 109 103 })} 110 104 111 - <InlineLinkText 105 + <Link 112 106 to="/feeds" 113 107 label={_(msg`More feeds`)} 114 108 style={[ 109 + a.flex_row, 110 + a.align_center, 111 + a.gap_sm, 112 + a.self_start, 113 + a.rounded_sm, 114 + {paddingVertical: 6, paddingHorizontal: 8}, 115 + route.name === 'Feeds' && {backgroundColor: t.palette.primary_50}, 116 + ]}> 117 + {({hovered}) => { 118 + const isActive = route.name === 'Feeds' 119 + return ( 120 + <> 121 + <View 122 + style={[ 123 + a.align_center, 124 + a.justify_center, 125 + a.rounded_xs, 126 + isActive 127 + ? {backgroundColor: t.palette.primary_100} 128 + : t.atoms.bg_contrast_50, 129 + { 130 + width: 20, 131 + height: 20, 132 + }, 133 + ]}> 134 + <Plus 135 + style={{width: 16, height: 16}} 136 + fill={ 137 + isActive || hovered 138 + ? t.atoms.text.color 139 + : t.atoms.text_contrast_medium.color 140 + } 141 + /> 142 + </View> 143 + <Text 144 + style={[ 145 + a.text_md, 146 + a.leading_snug, 147 + isActive 148 + ? [t.atoms.text, a.font_semi_bold] 149 + : hovered 150 + ? t.atoms.text 151 + : t.atoms.text_contrast_medium, 152 + ]} 153 + numberOfLines={1}> 154 + {_(msg`More feeds`)} 155 + </Text> 156 + </> 157 + ) 158 + }} 159 + </Link> 160 + </View> 161 + ) 162 + } 163 + 164 + function FeedItem({ 165 + feedInfo, 166 + current, 167 + onPress, 168 + }: { 169 + feedInfo: SavedFeedSourceInfo 170 + current: boolean 171 + onPress: () => void 172 + }) { 173 + const t = useTheme() 174 + const {_} = useLingui() 175 + const { 176 + state: hovered, 177 + onIn: onHoverIn, 178 + onOut: onHoverOut, 179 + } = useInteractionState() 180 + const isFollowing = feedInfo.feedDescriptor === 'following' 181 + 182 + return ( 183 + <Pressable 184 + accessibilityRole="link" 185 + accessibilityLabel={feedInfo.displayName} 186 + accessibilityHint={_(msg`Opens ${feedInfo.displayName} feed`)} 187 + onPress={onPress} 188 + onHoverIn={onHoverIn} 189 + onHoverOut={onHoverOut} 190 + style={[ 191 + a.flex_row, 192 + a.align_center, 193 + a.gap_sm, 194 + a.self_start, 195 + a.rounded_sm, 196 + {paddingVertical: 6, paddingHorizontal: 8}, 197 + current && {backgroundColor: t.palette.primary_50}, 198 + ]}> 199 + {isFollowing ? ( 200 + <View 201 + style={[ 202 + a.align_center, 203 + a.justify_center, 204 + a.rounded_xs, 205 + { 206 + width: 20, 207 + height: 20, 208 + backgroundColor: t.palette.primary_500, 209 + }, 210 + ]}> 211 + <FilterTimeline 212 + style={{width: 14, height: 14}} 213 + fill={t.palette.white} 214 + /> 215 + </View> 216 + ) : ( 217 + <UserAvatar 218 + type={feedInfo.type === 'list' ? 'list' : 'algo'} 219 + size={20} 220 + avatar={feedInfo.avatar} 221 + noBorder 222 + /> 223 + )} 224 + <Text 225 + style={[ 115 226 a.text_md, 116 227 a.leading_snug, 117 - web({ 118 - marginHorizontal: 2, 119 - width: 'calc(100% - 4px)', 120 - }), 228 + current 229 + ? [t.atoms.text, a.font_semi_bold] 230 + : hovered 231 + ? t.atoms.text 232 + : t.atoms.text_contrast_medium, 121 233 ]} 122 234 numberOfLines={1}> 123 - {_(msg`More feeds`)} 124 - </InlineLinkText> 125 - </View> 235 + {feedInfo.displayName} 236 + </Text> 237 + </Pressable> 126 238 ) 127 239 }
+11 -7
src/view/shell/desktop/RightNav.tsx
··· 18 18 web, 19 19 } from '#/alf' 20 20 import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 21 - import {Divider} from '#/components/Divider' 22 21 import {CENTER_COLUMN_OFFSET} from '#/components/Layout' 23 22 import {InlineLinkText} from '#/components/Link' 24 23 import {ProgressGuideList} from '#/components/ProgressGuide/List' ··· 86 85 87 86 {hasSession && ( 88 87 <> 89 - <ProgressGuideList /> 90 88 <DesktopFeeds /> 91 - <Divider /> 89 + <ProgressGuideList /> 92 90 </> 93 91 )} 94 92 ··· 102 100 email: currentAccount?.email, 103 101 handle: currentAccount?.handle, 104 102 })} 103 + style={[t.atoms.text_contrast_medium]} 105 104 label={_(msg`Feedback`)}> 106 105 {_(msg`Feedback`)} 107 106 </InlineLinkText> 108 - {' • '} 107 + <Text style={[t.atoms.text_contrast_low]}>{' ∙ '}</Text> 109 108 </> 110 109 )} 111 110 <InlineLinkText 112 111 to="https://bsky.social/about/support/privacy-policy" 112 + style={[t.atoms.text_contrast_medium]} 113 113 label={_(msg`Privacy`)}> 114 114 {_(msg`Privacy`)} 115 115 </InlineLinkText> 116 - {' • '} 116 + <Text style={[t.atoms.text_contrast_low]}>{' ∙ '}</Text> 117 117 <InlineLinkText 118 118 to="https://bsky.social/about/support/tos" 119 + style={[t.atoms.text_contrast_medium]} 119 120 label={_(msg`Terms`)}> 120 121 {_(msg`Terms`)} 121 122 </InlineLinkText> 122 - {' • '} 123 - <InlineLinkText label={_(msg`Help`)} to={HELP_DESK_URL}> 123 + <Text style={[t.atoms.text_contrast_low]}>{' ∙ '}</Text> 124 + <InlineLinkText 125 + label={_(msg`Help`)} 126 + to={HELP_DESK_URL} 127 + style={[t.atoms.text_contrast_medium]}> 124 128 {_(msg`Help`)} 125 129 </InlineLinkText> 126 130 </Text>
+61 -44
src/view/shell/desktop/SidebarTrendingTopics.tsx
··· 1 - import React from 'react' 2 1 import {View} from 'react-native' 3 2 import {msg, Trans} from '@lingui/macro' 4 3 import {useLingui} from '@lingui/react' 5 4 6 - import {logEvent} from '#/lib/statsig/statsig' 5 + import {logger} from '#/logger' 7 6 import { 8 7 useTrendingSettings, 9 8 useTrendingSettingsApi, ··· 12 11 import {useTrendingConfig} from '#/state/service-config' 13 12 import {atoms as a, useTheme} from '#/alf' 14 13 import {Button, ButtonIcon} from '#/components/Button' 15 - import {Divider} from '#/components/Divider' 16 - import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 17 - import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending' 14 + import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' 15 + import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending' 18 16 import * as Prompt from '#/components/Prompt' 19 - import { 20 - TrendingTopic, 21 - TrendingTopicLink, 22 - TrendingTopicSkeleton, 23 - } from '#/components/TrendingTopics' 17 + import {TrendingTopicLink} from '#/components/TrendingTopics' 24 18 import {Text} from '#/components/Typography' 25 19 26 - const TRENDING_LIMIT = 6 20 + const TRENDING_LIMIT = 5 27 21 28 22 export function SidebarTrendingTopics() { 29 23 const {enabled} = useTrendingConfig() ··· 39 33 const {data: trending, error, isLoading} = useTrendingTopics() 40 34 const noTopics = !isLoading && !error && !trending?.topics?.length 41 35 42 - const onConfirmHide = React.useCallback(() => { 43 - logEvent('trendingTopics:hide', {context: 'sidebar'}) 36 + const onConfirmHide = () => { 37 + logger.metric('trendingTopics:hide', {context: 'sidebar'}) 44 38 setTrendingDisabled(true) 45 - }, [setTrendingDisabled]) 39 + } 46 40 47 41 return error || noTopics ? null : ( 48 42 <> 49 - <View style={[a.gap_sm, {paddingBottom: 2}]}> 50 - <View style={[a.flex_row, a.align_center, a.gap_xs]}> 51 - <Graph size="sm" /> 52 - <Text 53 - style={[ 54 - a.flex_1, 55 - a.text_sm, 56 - a.font_semi_bold, 57 - t.atoms.text_contrast_medium, 58 - ]}> 43 + <View 44 + style={[a.p_lg, a.rounded_md, a.border, t.atoms.border_contrast_low]}> 45 + <View style={[a.flex_row, a.align_center, a.gap_xs, a.pb_md]}> 46 + <TrendingIcon width={16} height={16} fill={t.atoms.text.color} /> 47 + <Text style={[a.flex_1, a.text_md, a.font_semi_bold, t.atoms.text]}> 59 48 <Trans>Trending</Trans> 60 49 </Text> 61 50 <Button 62 - label={_(msg`Hide trending topics`)} 51 + variant="ghost" 63 52 size="tiny" 64 - variant="ghost" 65 53 color="secondary" 66 54 shape="round" 67 - onPress={() => trendingPrompt.open()}> 68 - <ButtonIcon icon={X} /> 55 + label={_(msg`Trending options`)} 56 + onPress={() => trendingPrompt.open()} 57 + style={[a.bg_transparent, {marginTop: -6, marginRight: -6}]}> 58 + <ButtonIcon icon={Ellipsis} size="xs" /> 69 59 </Button> 70 60 </View> 71 61 72 - <View style={[a.flex_row, a.flex_wrap, {gap: '6px 4px'}]}> 62 + <View style={[a.gap_xs]}> 73 63 {isLoading ? ( 74 64 Array(TRENDING_LIMIT) 75 65 .fill(0) 76 66 .map((_n, i) => ( 77 - <TrendingTopicSkeleton key={i} size="small" index={i} /> 67 + <View key={i} style={[a.flex_row, a.align_center, a.gap_sm]}> 68 + <Text 69 + style={[ 70 + a.text_sm, 71 + t.atoms.text_contrast_low, 72 + {minWidth: 16}, 73 + ]}> 74 + {i + 1}. 75 + </Text> 76 + <View 77 + style={[ 78 + a.rounded_xs, 79 + t.atoms.bg_contrast_50, 80 + {height: 14, width: i % 2 === 0 ? 80 : 100}, 81 + ]} 82 + /> 83 + </View> 78 84 )) 79 85 ) : !trending?.topics ? null : ( 80 86 <> 81 - {trending.topics.slice(0, TRENDING_LIMIT).map(topic => ( 87 + {trending.topics.slice(0, TRENDING_LIMIT).map((topic, i) => ( 82 88 <TrendingTopicLink 83 89 key={topic.link} 84 90 topic={topic} 85 - style={a.rounded_full} 91 + style={[a.self_start]} 86 92 onPress={() => { 87 - logEvent('trendingTopic:click', {context: 'sidebar'}) 93 + logger.metric('trendingTopic:click', {context: 'sidebar'}) 88 94 }}> 89 95 {({hovered}) => ( 90 - <TrendingTopic 91 - size="small" 92 - topic={topic} 93 - style={[ 94 - hovered && [ 95 - t.atoms.border_contrast_high, 96 - t.atoms.bg_contrast_25, 97 - ], 98 - ]} 99 - /> 96 + <View style={[a.flex_row, a.align_center, a.gap_xs]}> 97 + <Text 98 + style={[ 99 + a.text_sm, 100 + a.leading_snug, 101 + t.atoms.text_contrast_low, 102 + {minWidth: 16}, 103 + ]}> 104 + {i + 1}. 105 + </Text> 106 + <Text 107 + style={[ 108 + a.text_sm, 109 + a.leading_snug, 110 + hovered 111 + ? [t.atoms.text, a.underline] 112 + : t.atoms.text_contrast_medium, 113 + ]} 114 + numberOfLines={1}> 115 + {topic.displayName ?? topic.topic} 116 + </Text> 117 + </View> 100 118 )} 101 119 </TrendingTopicLink> 102 120 ))} ··· 111 129 confirmButtonCta={_(msg`Hide`)} 112 130 onConfirm={onConfirmHide} 113 131 /> 114 - <Divider /> 115 132 </> 116 133 ) 117 134 }