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

Combine actions, convert to new menu (#3174)

* Combine actions, convert to new menu

* remove about tab and move content to header

* Tweak alignment

* fix missing rkey

* hog the like button

* Add a little more whitespace

* Improve a11y

* Yeah toast

* Update usage

* Pin to Home

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

authored by

Eric Bailey
Samuel Newman
and committed by
GitHub
c9d821c5 81232991

+180 -200
+1
assets/icons/dotGrid1x3Horizontal_stroke2_corner2_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M2 12a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm16 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm-6-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z" clip-rule="evenodd"/></svg>
+3 -1
src/components/Link.tsx
··· 228 228 onPress: outerOnPress, 229 229 download, 230 230 selectable, 231 + label, 231 232 ...rest 232 233 }: InlineLinkProps) { 233 234 const t = useTheme() ··· 255 256 return ( 256 257 <Text 257 258 selectable={selectable} 258 - label={href} 259 + accessibilityHint="" 260 + accessibilityLabel={label || href} 259 261 {...rest} 260 262 style={[ 261 263 {color: t.palette.primary_500},
+5
src/components/icons/DotGrid.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const DotGrid_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + path: 'M2 12a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm16 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm-6-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z', 5 + })
+9
src/components/icons/Heart2.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const Heart2_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + path: 'M16.734 5.091c-1.238-.276-2.708.047-4.022 1.38a1 1 0 0 1-1.424 0C9.974 5.137 8.504 4.814 7.266 5.09c-1.263.282-2.379 1.206-2.92 2.556C3.33 10.18 4.252 14.84 12 19.348c7.747-4.508 8.67-9.168 7.654-11.7-.541-1.351-1.657-2.275-2.92-2.557Zm4.777 1.812c1.604 4-.494 9.69-9.022 14.47a1 1 0 0 1-.978 0C2.983 16.592.885 10.902 2.49 6.902c.779-1.942 2.414-3.334 4.342-3.764 1.697-.378 3.552.003 5.169 1.286 1.617-1.283 3.472-1.664 5.17-1.286 1.927.43 3.562 1.822 4.34 3.764Z', 5 + }) 6 + 7 + export const Heart2_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ 8 + path: 'M12.489 21.372c8.528-4.78 10.626-10.47 9.022-14.47-.779-1.941-2.414-3.333-4.342-3.763-1.697-.378-3.552.003-5.169 1.287-1.617-1.284-3.472-1.665-5.17-1.287-1.927.43-3.562 1.822-4.34 3.764-1.605 4 .493 9.69 9.021 14.47a1 1 0 0 0 .978 0Z', 9 + })
+162 -199
src/view/screens/ProfileFeed.tsx
··· 1 1 import React, {useMemo, useCallback} from 'react' 2 - import {Dimensions, StyleSheet, View} from 'react-native' 2 + import {StyleSheet, View, Pressable} from 'react-native' 3 3 import {NativeStackScreenProps} from '@react-navigation/native-stack' 4 4 import {useIsFocused, useNavigation} from '@react-navigation/native' 5 5 import {useQueryClient} from '@tanstack/react-query' 6 6 import {usePalette} from 'lib/hooks/usePalette' 7 - import {HeartIcon, HeartIconSolid} from 'lib/icons' 8 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 9 7 import {CommonNavigatorParams} from 'lib/routes/types' 10 8 import {makeRecordUri} from 'lib/strings/url-helpers' 11 9 import {s} from 'lib/styles' ··· 13 11 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' 14 12 import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' 15 13 import {Feed} from 'view/com/posts/Feed' 16 - import {TextLink} from 'view/com/util/Link' 14 + import {InlineLink} from '#/components/Link' 17 15 import {ListRef} from 'view/com/util/List' 18 16 import {Button} from 'view/com/util/forms/Button' 19 17 import {Text} from 'view/com/util/text/Text' ··· 29 27 import {toShareUrl} from 'lib/strings/url-helpers' 30 28 import {Haptics} from 'lib/haptics' 31 29 import {useAnalytics} from 'lib/analytics/analytics' 32 - import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' 33 - import {useScrollHandlers} from '#/lib/ScrollContext' 34 - import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 35 30 import {makeCustomFeedLink} from 'lib/routes/links' 36 31 import {pluralize} from 'lib/strings/helpers' 37 - import {CenteredView, ScrollView} from 'view/com/util/Views' 32 + import {CenteredView} from 'view/com/util/Views' 38 33 import {NavigationProp} from 'lib/routes/types' 39 - import {sanitizeHandle} from 'lib/strings/handles' 40 - import {makeProfileLink} from 'lib/routes/links' 41 34 import {ComposeIcon2} from 'lib/icons' 42 35 import {logger} from '#/logger' 43 36 import {Trans, msg} from '@lingui/macro' ··· 59 52 import {truncateAndInvalidate} from '#/state/queries/util' 60 53 import {isNative} from '#/platform/detection' 61 54 import {listenSoftReset} from '#/state/events' 62 - import {atoms as a} from '#/alf' 55 + import {atoms as a, useTheme} from '#/alf' 56 + import * as Menu from '#/components/Menu' 57 + import {HITSLOP_20} from '#/lib/constants' 58 + import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' 59 + import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' 60 + import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 61 + import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 62 + import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' 63 + import { 64 + Heart2_Stroke2_Corner0_Rounded as HeartOutline, 65 + Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, 66 + } from '#/components/icons/Heart2' 67 + import {Button as NewButton, ButtonText} from '#/components/Button' 63 68 64 - const SECTION_TITLES = ['Posts', 'About'] 69 + const SECTION_TITLES = ['Posts'] 65 70 66 71 interface SectionRef { 67 72 scrollToTop: () => void ··· 148 153 feedInfo: FeedSourceFeedInfo 149 154 }) { 150 155 const {_} = useLingui() 151 - const pal = usePalette('default') 156 + const t = useTheme() 152 157 const {hasSession, currentAccount} = useSession() 153 158 const {openModal} = useModalControls() 154 159 const {openComposer} = useComposerControls() ··· 200 205 if (isSaved) { 201 206 await removeFeed({uri: feedInfo.uri}) 202 207 resetRemoveFeed() 208 + Toast.show(_(msg`Removed from your feeds`)) 203 209 } else { 204 210 await saveFeed({uri: feedInfo.uri}) 205 211 resetSaveFeed() 212 + Toast.show(_(msg`Saved to your feeds`)) 206 213 } 207 214 } catch (err) { 208 215 Toast.show( ··· 263 270 [feedSectionRef], 264 271 ) 265 272 266 - // render 267 - // = 273 + const renderHeader = useCallback(() => { 274 + return ( 275 + <> 276 + <ProfileSubpageHeader 277 + isLoading={false} 278 + href={feedInfo.route.href} 279 + title={feedInfo?.displayName} 280 + avatar={feedInfo?.avatar} 281 + isOwner={feedInfo.creatorDid === currentAccount?.did} 282 + creator={ 283 + feedInfo 284 + ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle} 285 + : undefined 286 + } 287 + avatarType="algo"> 288 + <View style={[a.flex_row, a.align_center, a.gap_sm]}> 289 + {feedInfo && hasSession && ( 290 + <NewButton 291 + testID={isPinned ? 'unpinBtn' : 'pinBtn'} 292 + disabled={isPinPending || isUnpinPending} 293 + size="small" 294 + variant="solid" 295 + color={isPinned ? 'secondary' : 'primary'} 296 + label={isPinned ? _(msg`Unpin from home`) : _(msg`Pin to home`)} 297 + onPress={onTogglePinned}> 298 + <ButtonText> 299 + {isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)} 300 + </ButtonText> 301 + </NewButton> 302 + )} 303 + <Menu.Root> 304 + <Menu.Trigger label={_(msg`Open feed options menu`)}> 305 + {({props, state}) => { 306 + return ( 307 + <Pressable 308 + {...props} 309 + hitSlop={HITSLOP_20} 310 + style={[ 311 + a.justify_center, 312 + a.align_center, 313 + a.rounded_full, 314 + {height: 36, width: 36}, 315 + t.atoms.bg_contrast_50, 316 + (state.hovered || state.pressed) && [ 317 + t.atoms.bg_contrast_100, 318 + ], 319 + ]} 320 + testID="headerDropdownBtn"> 321 + <Ellipsis 322 + size="lg" 323 + fill={t.atoms.text_contrast_medium.color} 324 + /> 325 + </Pressable> 326 + ) 327 + }} 328 + </Menu.Trigger> 329 + 330 + <Menu.Outer> 331 + <Menu.Group> 332 + {hasSession && ( 333 + <> 334 + <Menu.Item 335 + disabled={isSavePending || isRemovePending} 336 + testID="feedHeaderDropdownToggleSavedBtn" 337 + label={ 338 + isSaved 339 + ? _(msg`Remove from my feeds`) 340 + : _(msg`Save to my feeds`) 341 + } 342 + onPress={onToggleSaved}> 343 + <Menu.ItemText> 344 + {isSaved 345 + ? _(msg`Remove from my feeds`) 346 + : _(msg`Save to my feeds`)} 347 + </Menu.ItemText> 348 + <Menu.ItemIcon 349 + icon={isSaved ? Trash : Plus} 350 + position="right" 351 + /> 352 + </Menu.Item> 268 353 269 - const dropdownItems: DropdownItem[] = React.useMemo(() => { 270 - return [ 271 - hasSession && { 272 - testID: 'feedHeaderDropdownToggleSavedBtn', 273 - label: isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`), 274 - onPress: isSavePending || isRemovePending ? undefined : onToggleSaved, 275 - icon: isSaved 276 - ? { 277 - ios: { 278 - name: 'trash', 279 - }, 280 - android: 'ic_delete', 281 - web: ['far', 'trash-can'], 282 - } 283 - : { 284 - ios: { 285 - name: 'plus', 286 - }, 287 - android: '', 288 - web: 'plus', 289 - }, 290 - }, 291 - hasSession && { 292 - testID: 'feedHeaderDropdownReportBtn', 293 - label: _(msg`Report feed`), 294 - onPress: onPressReport, 295 - icon: { 296 - ios: { 297 - name: 'exclamationmark.triangle', 298 - }, 299 - android: 'ic_menu_report_image', 300 - web: 'circle-exclamation', 301 - }, 302 - }, 303 - { 304 - testID: 'feedHeaderDropdownShareBtn', 305 - label: _(msg`Share feed`), 306 - onPress: onPressShare, 307 - icon: { 308 - ios: { 309 - name: 'square.and.arrow.up', 310 - }, 311 - android: 'ic_menu_share', 312 - web: 'share', 313 - }, 314 - }, 315 - ].filter(Boolean) as DropdownItem[] 316 - }, [ 317 - hasSession, 318 - onToggleSaved, 319 - onPressReport, 320 - onPressShare, 321 - isSaved, 322 - isSavePending, 323 - isRemovePending, 324 - _, 325 - ]) 354 + <Menu.Item 355 + testID="feedHeaderDropdownReportBtn" 356 + label={_(msg`Report feed`)} 357 + onPress={onPressReport}> 358 + <Menu.ItemText>{_(msg`Report feed`)}</Menu.ItemText> 359 + <Menu.ItemIcon icon={CircleInfo} position="right" /> 360 + </Menu.Item> 361 + </> 362 + )} 326 363 327 - const renderHeader = useCallback(() => { 328 - return ( 329 - <ProfileSubpageHeader 330 - isLoading={false} 331 - href={feedInfo.route.href} 332 - title={feedInfo?.displayName} 333 - avatar={feedInfo?.avatar} 334 - isOwner={feedInfo.creatorDid === currentAccount?.did} 335 - creator={ 336 - feedInfo 337 - ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle} 338 - : undefined 339 - } 340 - avatarType="algo"> 341 - {feedInfo && hasSession && ( 342 - <> 343 - <Button 344 - disabled={isSavePending || isRemovePending} 345 - type="default" 346 - label={isSaved ? _(msg`Unsave`) : _(msg`Save`)} 347 - onPress={onToggleSaved} 348 - style={styles.btn} 349 - /> 350 - <Button 351 - testID={isPinned ? 'unpinBtn' : 'pinBtn'} 352 - disabled={isPinPending || isUnpinPending} 353 - type={isPinned ? 'default' : 'inverted'} 354 - label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)} 355 - onPress={onTogglePinned} 356 - style={styles.btn} 357 - /> 358 - </> 359 - )} 360 - <NativeDropdown 361 - testID="headerDropdownBtn" 362 - items={dropdownItems} 363 - accessibilityLabel={_(msg`More options`)} 364 - accessibilityHint=""> 365 - <View style={[pal.viewLight, styles.btn]}> 366 - <FontAwesomeIcon 367 - icon="ellipsis" 368 - size={20} 369 - color={pal.colors.text} 370 - /> 364 + <Menu.Item 365 + testID="feedHeaderDropdownShareBtn" 366 + label={_(msg`Share feed`)} 367 + onPress={onPressShare}> 368 + <Menu.ItemText>{_(msg`Share feed`)}</Menu.ItemText> 369 + <Menu.ItemIcon icon={Share} position="right" /> 370 + </Menu.Item> 371 + </Menu.Group> 372 + </Menu.Outer> 373 + </Menu.Root> 371 374 </View> 372 - </NativeDropdown> 373 - </ProfileSubpageHeader> 375 + </ProfileSubpageHeader> 376 + <AboutSection 377 + feedOwnerDid={feedInfo.creatorDid} 378 + feedRkey={feedInfo.route.params.rkey} 379 + feedInfo={feedInfo} 380 + /> 381 + </> 374 382 ) 375 383 }, [ 376 384 _, 377 385 hasSession, 378 - pal, 379 386 feedInfo, 380 387 isPinned, 381 388 onTogglePinned, 382 389 onToggleSaved, 383 - dropdownItems, 384 390 currentAccount?.did, 385 391 isPinPending, 386 392 isRemovePending, 387 393 isSavePending, 388 394 isSaved, 389 395 isUnpinPending, 396 + onPressReport, 397 + onPressShare, 398 + t, 390 399 ]) 391 400 392 401 return ( ··· 405 414 isFocused={isScreenFocused && isFocused} 406 415 /> 407 416 )} 408 - {({headerHeight, scrollElRef}) => ( 409 - <AboutSection 410 - feedOwnerDid={feedInfo.creatorDid} 411 - feedRkey={feedInfo.route.params.rkey} 412 - feedInfo={feedInfo} 413 - headerHeight={headerHeight} 414 - scrollElRef={ 415 - scrollElRef as React.MutableRefObject<ScrollView | null> 416 - } 417 - isOwner={feedInfo.creatorDid === currentAccount?.did} 418 - /> 419 - )} 420 417 </PagerWithHeader> 421 418 {hasSession && ( 422 419 <FAB ··· 505 502 feedOwnerDid, 506 503 feedRkey, 507 504 feedInfo, 508 - headerHeight, 509 - scrollElRef, 510 - isOwner, 511 505 }: { 512 506 feedOwnerDid: string 513 507 feedRkey: string 514 508 feedInfo: FeedSourceFeedInfo 515 - headerHeight: number 516 - scrollElRef: React.MutableRefObject<ScrollView | null> 517 - isOwner: boolean 518 509 }) { 510 + const t = useTheme() 519 511 const pal = usePalette('default') 520 512 const {_} = useLingui() 521 - const scrollHandlers = useScrollHandlers() 522 - const onScroll = useAnimatedScrollHandler(scrollHandlers) 523 513 const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri) 524 514 const {hasSession} = useSession() 525 515 const {track} = useAnalytics() ··· 555 545 }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _]) 556 546 557 547 return ( 558 - <ScrollView 559 - ref={scrollElRef} 560 - onScroll={onScroll} 561 - scrollEventThrottle={1} 562 - contentContainerStyle={{ 563 - paddingTop: headerHeight, 564 - minHeight: Dimensions.get('window').height * 1.5, 565 - }}> 566 - <View 567 - style={[ 568 - { 569 - borderTopWidth: 1, 570 - paddingVertical: 20, 571 - paddingHorizontal: 20, 572 - gap: 12, 573 - }, 574 - pal.border, 575 - ]}> 548 + <View style={[styles.aboutSectionContainer]}> 549 + <View style={[a.pt_sm]}> 576 550 {feedInfo.description ? ( 577 551 <RichText 578 552 testID="listDescription" ··· 584 558 <Trans>No description</Trans> 585 559 </Text> 586 560 )} 587 - <View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}> 588 - <Button 589 - type="default" 590 - testID="toggleLikeBtn" 591 - accessibilityLabel={_(msg`Like this feed`)} 592 - accessibilityHint="" 593 - disabled={!hasSession || isLikePending || isUnlikePending} 594 - onPress={onToggleLiked} 595 - style={{paddingHorizontal: 10}}> 596 - {isLiked ? ( 597 - <HeartIconSolid size={19} style={s.likeColor} /> 598 - ) : ( 599 - <HeartIcon strokeWidth={3} size={19} style={pal.textLight} /> 600 - )} 601 - </Button> 602 - {typeof likeCount === 'number' && ( 603 - <TextLink 604 - href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')} 605 - text={_( 606 - msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`, 607 - )} 608 - style={[pal.textLight, s.semiBold]} 609 - /> 610 - )} 611 - </View> 612 - <Text type="md" style={[pal.textLight]} numberOfLines={1}> 613 - {isOwner ? ( 614 - <Trans>Created by you</Trans> 561 + </View> 562 + 563 + <View style={[a.flex_row, a.gap_sm, a.align_center, a.pb_sm]}> 564 + <NewButton 565 + size="small" 566 + variant="solid" 567 + color="secondary" 568 + shape="round" 569 + label={isLiked ? _(msg`Unlike this feed`) : _(msg`Like this feed`)} 570 + testID="toggleLikeBtn" 571 + disabled={!hasSession || isLikePending || isUnlikePending} 572 + onPress={onToggleLiked}> 573 + {isLiked ? ( 574 + <HeartFilled size="md" fill={s.likeColor.color} /> 615 575 ) : ( 616 - <Trans> 617 - Created by{' '} 618 - <TextLink 619 - text={sanitizeHandle(feedInfo.creatorHandle, '@')} 620 - href={makeProfileLink({ 621 - did: feedInfo.creatorDid, 622 - handle: feedInfo.creatorHandle, 623 - })} 624 - style={pal.textLight} 625 - /> 626 - </Trans> 576 + <HeartOutline size="md" fill={t.atoms.text_contrast_medium.color} /> 627 577 )} 628 - </Text> 578 + </NewButton> 579 + {typeof likeCount === 'number' && ( 580 + <InlineLink 581 + label={_(msg`View users who like this feed`)} 582 + to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')} 583 + style={[t.atoms.text_contrast_medium, a.font_bold]}> 584 + {_(msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`)} 585 + </InlineLink> 586 + )} 629 587 </View> 630 - </ScrollView> 588 + </View> 631 589 ) 632 590 } 633 591 ··· 646 604 paddingHorizontal: 18, 647 605 paddingVertical: 14, 648 606 borderRadius: 6, 607 + }, 608 + aboutSectionContainer: { 609 + paddingVertical: 4, 610 + paddingHorizontal: 16, 611 + gap: 12, 649 612 }, 650 613 })