Bluesky app fork with some witchin' additions 💫

[APP-1403] Profile empty states (#8969)

* update: Empty state component

* update type error fixes

* add empty state icon

* update translations on labels

* update: lint error for empty state

* remove unused prompt

* update type errors

* fix lint errors and update profile followers and profile follows

* updated starterpack

* fix lint errors

* address feedback

* update icons to be a react element

* update icons

* update viewbox for icons

* optimize: icons and update profile lists

* optimize: icons and update profile lists

* lint error fix

* update iconSize from the rebase

authored by Anastasiya Uraleva and committed by GitHub d0649e9a 2d056e5b

+1
assets/icons/bulletlist_stroke1_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 46 38"><path stroke="#405168" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M22.333 31.667H45M22.333 6.333H45m-33.333 0A5.333 5.333 0 1 1 1 6.333a5.333 5.333 0 0 1 10.667 0Zm0 25.334a5.333 5.333 0 1 1-10.667 0 5.333 5.333 0 0 1 10.667 0Z"/></svg>
+1
assets/icons/circle_and_square_stroke1_corner0_rounded_filled.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 62 53"><path fill="#405168" d="M28.173.231a5.653 5.653 0 0 1 7.018 3.83l2.66 9.046a20 20 0 0 1 3.986-.397c11.026 0 19.964 8.937 19.964 19.962l-.006.516c-.274 10.787-9.104 19.448-19.958 19.448l-.514-.007c-8.332-.21-15.394-5.528-18.178-12.938l-8.805 2.59a5.654 5.654 0 0 1-7.02-3.83L.232 14.34a5.654 5.654 0 0 1 3.83-7.018L28.172.23ZM41.838 14.71c-1.17 0-2.313.111-3.42.325l3.863 13.137a5.653 5.653 0 0 1-3.83 7.019L25.07 39.126c2.593 6.732 9.122 11.51 16.768 11.51 9.92 0 17.963-8.043 17.963-17.964S51.758 14.71 41.837 14.71ZM33.271 4.624a3.653 3.653 0 0 0-4.535-2.474L4.624 9.24a3.653 3.653 0 0 0-2.475 4.535l7.09 24.113a3.654 3.654 0 0 0 4.536 2.475l8.762-2.577a20 20 0 0 1-.662-5.114c0-8.961 5.905-16.544 14.037-19.069l-2.64-8.98Zm3.204 10.899c-7.302 2.28-12.601 9.096-12.601 17.15 0 1.571.203 3.095.582 4.548l13.431-3.948a3.654 3.654 0 0 0 2.474-4.536l-3.886-13.214Z"/></svg>
+1
assets/icons/editbig_stroke1_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48"><path fill="#405168" d="M19.667 4.458a1 1 0 1 0 0-2v2Zm25 23a1 1 0 0 0-2 0h2ZM3.912 45.543l.454-.891-.454.89Zm-2.33-2.33.89-.455h0l-.89.454Zm39.173 2.33-.454-.891h0l.454.89Zm2.33-2.33-.89-.455.89.454ZM1.581 6.37l-.89-.454h0l.89.454Zm2.331-2.331-.454-.891.454.89ZM14.333 32.79h-1a1 1 0 0 0 1 1v-1Zm.781-8.781.707.707-.707-.707ZM36.562 2.562l-.707-.707v0l.707.707Zm7.543 0-.707.707v0l.707-.707Zm.457.458.707-.707v0l-.707.707Zm0 7.542.707.707-.707-.707ZM23.114 32.01l.707.707-.707-.707Zm12.02 14.114v-1h-25.6v2h25.6v-1ZM1 37.591h1v-25.6H0v25.6h1ZM9.533 3.458v1h10.134v-2H9.533v1Zm34.134 24h-1V37.59h2V27.457h-1ZM9.533 46.124v-1c-1.51 0-2.582 0-3.421-.07-.828-.067-1.34-.195-1.746-.402l-.454.89-.454.892c.735.374 1.54.537 2.491.614.94.077 2.107.076 3.584.076v-1ZM1 37.591H0c0 1.477 0 2.645.076 3.584.078.951.24 1.756.614 2.491l.891-.454.891-.454c-.207-.406-.335-.918-.403-1.746C2.001 40.173 2 39.101 2 37.591H1Zm2.912 7.952.454-.891a4.33 4.33 0 0 1-1.894-1.894l-.89.454-.892.454a6.33 6.33 0 0 0 2.768 2.768l.454-.891Zm31.221.581v1c1.477 0 2.645.001 3.585-.076.95-.078 1.756-.24 2.49-.614l-.453-.891-.454-.891c-.406.207-.919.335-1.746.403-.84.068-1.912.07-3.422.07v1Zm8.534-8.533h-1c0 1.51-.001 2.582-.07 3.421-.067.828-.196 1.34-.403 1.746l.891.454.891.454c.375-.735.537-1.54.615-2.49.076-.94.076-2.108.076-3.585h-1Zm-2.912 7.952.454.89a6.33 6.33 0 0 0 2.767-2.767l-.89-.454-.892-.454a4.33 4.33 0 0 1-1.893 1.894l.454.89ZM1 11.99h1c0-1.51 0-2.582.07-3.422.067-.827.195-1.34.402-1.745l-.89-.454-.892-.454c-.374.734-.536 1.54-.614 2.49C-.001 9.345 0 10.513 0 11.99h1Zm8.533-8.533v-1c-1.477 0-2.645-.001-3.584.076-.951.077-1.756.24-2.49.614l.453.89.454.892c.406-.207.918-.336 1.746-.403.839-.069 1.911-.07 3.421-.07v-1ZM1.581 6.37l.891.454A4.33 4.33 0 0 1 4.366 4.93l-.454-.891-.454-.891A6.33 6.33 0 0 0 .69 5.916l.891.454Zm12.752 19.525h-1v6.896h2v-6.896h-1Zm0 6.896v1h6.896v-2h-6.896v1Zm.781-8.781.707.707L37.27 3.269l-.707-.707-.707-.707-21.448 21.448.707.707Zm28.99-21.448-.706.707.457.458.707-.707.707-.707-.457-.458-.707.707Zm.458 8-.707-.707-21.448 21.448.707.707.707.707L45.27 11.269l-.707-.707Zm0-7.542-.707.707a4.333 4.333 0 0 1 0 6.128l.707.707.707.707a6.333 6.333 0 0 0 0-8.956l-.707.707Zm-8-.458.707.707a4.333 4.333 0 0 1 6.129 0l.707-.707.707-.707a6.333 6.333 0 0 0-8.957 0l.707.707ZM21.23 32.791v1c.972 0 1.905-.386 2.593-1.074l-.708-.707-.707-.707a1.67 1.67 0 0 1-1.178.488v1Zm-6.896-6.896h1c0-.442.176-.866.489-1.178l-.708-.707-.707-.707a3.67 3.67 0 0 0-1.074 2.592h1Z"/></svg>
+1
assets/icons/hashtagwide_stroke1_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 46 46"><path stroke="#405168" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.333 1 9 45M37 1l-5.333 44M1 11.667h44m0 22.666H1"/></svg>
+1
assets/icons/heart2_stroke1_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 50 45"><path stroke="#405168" stroke-linejoin="round" stroke-width="2" d="M49 17c0 15.333-22 26.667-24 26.667S1 32.333 1 17C1 6.333 7.667 1 14.333 1S25 5 25 5s4-4 10.667-4S49 6.333 49 17Z"/></svg>
+1
assets/icons/image_stroke1_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 46 46"><path stroke="#080B12" stroke-linecap="round" stroke-width="1.5" d="m1.417 28.645 7.586-5.676a5.33 5.33 0 0 1 6.867.809c3.98 4.286 8.594 8.182 14.88 8.182 5.794 0 9.633-2.147 13.333-5.847m-38 18.637h33.334a5.333 5.333 0 0 0 5.333-5.333V6.083A5.333 5.333 0 0 0 39.417.75H6.083A5.333 5.333 0 0 0 .75 6.083v33.334a5.333 5.333 0 0 0 5.333 5.333ZM36.75 14.083a5.333 5.333 0 1 1-10.667 0 5.333 5.333 0 0 1 10.667 0Z"/></svg>
+1
assets/icons/message_stroke1_corner0_rounded_filled.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 50 50"><path stroke="#405168" stroke-linecap="square" stroke-linejoin="round" stroke-width="2" d="M9 1h32a8 8 0 0 1 8 8v21.333a8 8 0 0 1-8 8H27.667L14.333 49V38.333H9a8 8 0 0 1-8-8V9a8 8 0 0 1 8-8Z"/></svg>
+1
assets/icons/peopleremove2_stroke1_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 43 48"><path stroke="#405168" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.603 46.333H3.532c-1.572 0-2.816-1.358-2.472-2.891 2.033-9.046 9.421-15.775 19.543-15.775q1.367 0 2.666.16m18.667 7.84L36.603 41m0 0-5.334 5.333M36.603 41l-5.334-5.333M36.603 41l5.333 5.333m-12-36A9.333 9.333 0 1 1 20.603 1a9.333 9.333 0 0 1 9.333 9.333Z"/></svg>
+1
assets/icons/videoclip_stroke1_corner0_rounded.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 46 46"><path stroke="#405168" stroke-linecap="square" stroke-linejoin="round" stroke-width="2" d="M1 23h10.667M1 23V12m0 11v11m10.667-11h22.666m-22.666 0v11m0-11V12m22.666 11H45m-10.667 0v12.222m0-12.222V12M45 23V12m0 11v12.222M34.333 45h5.334A5.333 5.333 0 0 0 45 39.667v-4.445M34.333 45v-9.778m0 9.778H11.667M34.333 1h5.334A5.333 5.333 0 0 1 45 6.333V12M34.333 1v11m0-11H11.667m22.666 11H45M34.333 35.222H45M11.667 45H6.333A5.333 5.333 0 0 1 1 39.667V34m10.667 11V34m0-33H6.333A5.333 5.333 0 0 0 1 6.333V12M11.667 1v11M1 12h10.667M1 34h10.667"/></svg>
+2 -1
src/components/Button.tsx
··· 798 798 * also so that we can calculate transforms. 799 799 */ 800 800 const iconSize = { 801 - '2xs': 8, 802 801 xs: 12, 803 802 sm: 16, 804 803 md: 18, 805 804 lg: 24, 806 805 xl: 28, 806 + '2xs': 8, 807 807 '2xl': 32, 808 + '3xl': 40, 808 809 }[iconSizeShorthand] 809 810 810 811 /*
+27
src/components/Lists.tsx
··· 4 4 import {useLingui} from '@lingui/react' 5 5 6 6 import {cleanError} from '#/lib/strings/errors' 7 + import { 8 + EmptyState, 9 + type EmptyStateButtonProps, 10 + } from '#/view/com/util/EmptyState' 7 11 import {CenteredView} from '#/view/com/util/Views' 8 12 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 9 13 import {Button, ButtonText} from '#/components/Button' ··· 129 133 hideBackButton, 130 134 sideBorders, 131 135 topBorder = false, 136 + emptyStateIcon, 137 + emptyStateButton, 138 + useEmptyState = false, 132 139 }: { 133 140 isLoading: boolean 134 141 noEmpty?: boolean ··· 143 150 hideBackButton?: boolean 144 151 sideBorders?: boolean 145 152 topBorder?: boolean 153 + emptyStateIcon?: React.ComponentType<any> | React.ReactElement 154 + emptyStateButton?: EmptyStateButtonProps 155 + useEmptyState?: boolean 146 156 }): React.ReactNode => { 147 157 const t = useTheme() 148 158 const {_} = useLingui() ··· 177 187 sideBorders={sideBorders} 178 188 hideBackButton={hideBackButton} 179 189 /> 190 + ) 191 + } 192 + 193 + if (useEmptyState) { 194 + return ( 195 + <View style={[t.atoms.border_contrast_low]}> 196 + <EmptyState 197 + icon={emptyStateIcon} 198 + message={ 199 + emptyMessage ?? 200 + (emptyType === 'results' 201 + ? _(msg`No results found`) 202 + : _(msg`Page not found`)) 203 + } 204 + button={emptyStateButton} 205 + /> 206 + </View> 180 207 ) 181 208 } 182 209
+8 -1
src/components/StarterPack/Main/PostsList.tsx
··· 9 9 import {EmptyState} from '#/view/com/util/EmptyState' 10 10 import {type ListRef} from '#/view/com/util/List' 11 11 import {type SectionRef} from '#/screens/Profile/Sections/types' 12 + import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 12 13 13 14 interface ProfilesListProps { 14 15 listUri: string ··· 33 34 })) 34 35 35 36 const renderPostsEmpty = useCallback(() => { 36 - return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} /> 37 + return ( 38 + <EmptyState 39 + icon={HashtagWideIcon} 40 + iconSize="2xl" 41 + message={_(msg`This feed is empty.`)} 42 + /> 43 + ) 37 44 }, [_]) 38 45 39 46 return (
+33 -1
src/components/StarterPack/ProfileStarterPacks.tsx
··· 21 21 import {logger} from '#/logger' 22 22 import {isIOS} from '#/platform/detection' 23 23 import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' 24 + import { 25 + EmptyState, 26 + type EmptyStateButtonProps, 27 + } from '#/view/com/util/EmptyState' 24 28 import {List, type ListRef} from '#/view/com/util/List' 25 29 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 26 30 import {atoms as a, ios, useTheme} from '#/alf' ··· 47 51 testID?: string 48 52 setScrollViewTag: (tag: number | null) => void 49 53 isMe: boolean 54 + emptyStateMessage?: string 55 + emptyStateButton?: EmptyStateButtonProps 56 + emptyStateIcon?: React.ComponentType<any> | React.ReactElement 50 57 } 51 58 52 59 function keyExtractor(item: AppBskyGraphDefs.StarterPackView) { ··· 63 70 testID, 64 71 setScrollViewTag, 65 72 isMe, 73 + emptyStateMessage, 74 + emptyStateButton, 75 + emptyStateIcon, 66 76 }: ProfileFeedgensProps) { 67 77 const t = useTheme() 68 78 const bottomBarOffset = useBottomBarOffset(100) ··· 79 89 const {isTabletOrDesktop} = useWebMediaQueries() 80 90 81 91 const items = data?.pages.flatMap(page => page.starterPacks) 92 + const {_} = useLingui() 93 + 94 + const EmptyComponent = useCallback(() => { 95 + if (emptyStateMessage || emptyStateButton || emptyStateIcon) { 96 + return ( 97 + <View style={[a.px_lg, a.align_center, a.justify_center]}> 98 + <EmptyState 99 + icon={emptyStateIcon} 100 + iconSize="3xl" 101 + message={ 102 + emptyStateMessage ?? 103 + _( 104 + 'Starter packs let you share your favorite feeds and people with your friends.', 105 + ) 106 + } 107 + button={emptyStateButton} 108 + /> 109 + </View> 110 + ) 111 + } 112 + return <Empty /> 113 + }, [_, emptyStateMessage, emptyStateButton, emptyStateIcon]) 82 114 83 115 useImperativeHandle(ref, () => ({ 84 116 scrollToTop: () => {}, ··· 146 178 onEndReached={onEndReached} 147 179 onRefresh={onRefresh} 148 180 ListEmptyComponent={ 149 - data ? (isMe ? Empty : undefined) : FeedLoadingPlaceholder 181 + data ? (isMe ? EmptyComponent : undefined) : FeedLoadingPlaceholder 150 182 } 151 183 ListFooterComponent={ 152 184 !!data && items?.length !== 0 && isMe ? CreateAnother : undefined
+8
src/components/icons/BulletList.tsx
··· 1 1 import {createSinglePathSVG} from './TEMPLATE' 2 2 3 + export const BulletList_Stroke1_Corner0_Rounded = createSinglePathSVG({ 4 + viewBox: '0 0 47 38', 5 + strokeLinecap: 'round', 6 + strokeLinejoin: 'round', 7 + strokeWidth: 2, 8 + path: 'M22.333 31.667H45M22.333 6.333H45m-33.333 0A5.333 5.333 0 1 1 1 6.333a5.333 5.333 0 0 1 10.667 0Zm0 25.334a5.333 5.333 0 1 1-10.667 0 5.333 5.333 0 0 1 10.667 0Z', 9 + }) 10 + 3 11 export const BulletList_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 12 path: 'M6 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM3 7a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm9 0a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Zm-6 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm9 0a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z', 5 13 })
+7
src/components/icons/CircleAndSquare.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const Circle_And_Square_Stroke1_Corner0_Rounded_Filled = 4 + createSinglePathSVG({ 5 + viewBox: '0 0 62 53', 6 + path: 'M28.173.231a5.653 5.653 0 0 1 7.018 3.83l2.66 9.046a20 20 0 0 1 3.986-.397c11.026 0 19.964 8.937 19.964 19.962l-.006.516c-.274 10.787-9.104 19.448-19.958 19.448l-.514-.007c-8.332-.21-15.394-5.528-18.178-12.938l-8.805 2.59a5.654 5.654 0 0 1-7.02-3.83L.232 14.34a5.654 5.654 0 0 1 3.83-7.018L28.172.23ZM41.838 14.71c-1.17 0-2.313.111-3.42.325l3.863 13.137a5.653 5.653 0 0 1-3.83 7.019L25.07 39.126c2.593 6.732 9.122 11.51 16.768 11.51 9.92 0 17.963-8.043 17.963-17.964S51.758 14.71 41.837 14.71ZM33.271 4.624a3.653 3.653 0 0 0-4.535-2.474L4.624 9.24a3.653 3.653 0 0 0-2.475 4.535l7.09 24.113a3.654 3.654 0 0 0 4.536 2.475l8.762-2.577a20 20 0 0 1-.662-5.114c0-8.961 5.905-16.544 14.037-19.069l-2.64-8.98Zm3.204 10.899c-7.302 2.28-12.601 9.096-12.601 17.15 0 1.571.203 3.095.582 4.548l13.431-3.948a3.654 3.654 0 0 0 2.474-4.536l-3.886-13.214Z', 7 + })
+5
src/components/icons/EditBig.tsx
··· 1 1 import {createSinglePathSVG} from './TEMPLATE' 2 2 3 + export const EditBig_Stroke1_Corner0_Rounded = createSinglePathSVG({ 4 + viewBox: '0 0 48 48', 5 + path: 'M19.667 4.458a1 1 0 1 0 0-2v2Zm25 23a1 1 0 0 0-2 0h2ZM3.912 45.543l.454-.891-.454.89Zm-2.33-2.33.89-.455h0l-.89.454Zm39.173 2.33-.454-.891h0l.454.89Zm2.33-2.33-.89-.455.89.454ZM1.581 6.37l-.89-.454h0l.89.454Zm2.331-2.331-.454-.891.454.89ZM14.333 32.79h-1a1 1 0 0 0 1 1v-1Zm.781-8.781.707.707-.707-.707ZM36.562 2.562l-.707-.707v0l.707.707Zm7.543 0-.707.707v0l.707-.707Zm.457.458.707-.707v0l-.707.707Zm0 7.542.707.707-.707-.707ZM23.114 32.01l.707.707-.707-.707Zm12.02 14.114v-1h-25.6v2h25.6v-1ZM1 37.591h1v-25.6H0v25.6h1ZM9.533 3.458v1h10.134v-2H9.533v1Zm34.134 24h-1V37.59h2V27.457h-1ZM9.533 46.124v-1c-1.51 0-2.582 0-3.421-.07-.828-.067-1.34-.195-1.746-.402l-.454.89-.454.892c.735.374 1.54.537 2.491.614.94.077 2.107.076 3.584.076v-1ZM1 37.591H0c0 1.477 0 2.645.076 3.584.078.951.24 1.756.614 2.491l.891-.454.891-.454c-.207-.406-.335-.918-.403-1.746C2.001 40.173 2 39.101 2 37.591H1Zm2.912 7.952.454-.891a4.33 4.33 0 0 1-1.894-1.894l-.89.454-.892.454a6.33 6.33 0 0 0 2.768 2.768l.454-.891Zm31.221.581v1c1.477 0 2.645.001 3.585-.076.95-.078 1.756-.24 2.49-.614l-.453-.891-.454-.891c-.406.207-.919.335-1.746.403-.84.068-1.912.07-3.422.07v1Zm8.534-8.533h-1c0 1.51-.001 2.582-.07 3.421-.067.828-.196 1.34-.403 1.746l.891.454.891.454c.375-.735.537-1.54.615-2.49.076-.94.076-2.108.076-3.585h-1Zm-2.912 7.952.454.89a6.33 6.33 0 0 0 2.767-2.767l-.89-.454-.892-.454a4.33 4.33 0 0 1-1.893 1.894l.454.89ZM1 11.99h1c0-1.51 0-2.582.07-3.422.067-.827.195-1.34.402-1.745l-.89-.454-.892-.454c-.374.734-.536 1.54-.614 2.49C-.001 9.345 0 10.513 0 11.99h1Zm8.533-8.533v-1c-1.477 0-2.645-.001-3.584.076-.951.077-1.756.24-2.49.614l.453.89.454.892c.406-.207.918-.336 1.746-.403.839-.069 1.911-.07 3.421-.07v-1ZM1.581 6.37l.891.454A4.33 4.33 0 0 1 4.366 4.93l-.454-.891-.454-.891A6.33 6.33 0 0 0 .69 5.916l.891.454Zm12.752 19.525h-1v6.896h2v-6.896h-1Zm0 6.896v1h6.896v-2h-6.896v1Zm.781-8.781.707.707L37.27 3.269l-.707-.707-.707-.707-21.448 21.448.707.707Zm28.99-21.448-.706.707.457.458.707-.707.707-.707-.457-.458-.707.707Zm.458 8-.707-.707-21.448 21.448.707.707.707.707L45.27 11.269l-.707-.707Zm0-7.542-.707.707a4.333 4.333 0 0 1 0 6.128l.707.707.707.707a6.333 6.333 0 0 0 0-8.956l-.707.707Zm-8-.458.707.707a4.333 4.333 0 0 1 6.129 0l.707-.707.707-.707a6.333 6.333 0 0 0-8.957 0l.707.707ZM21.23 32.791v1c.972 0 1.905-.386 2.593-1.074l-.708-.707-.707-.707a1.67 1.67 0 0 1-1.178.488v1Zm-6.896-6.896h1c0-.442.176-.866.489-1.178l-.708-.707-.707-.707a3.67 3.67 0 0 0-1.074 2.592h1Z', 6 + }) 7 + 3 8 export const EditBig_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 9 path: 'M17.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-9 9A1 1 0 0 1 12 16H9a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707l9-9ZM10 12.414V14h1.586l8-8L18 4.414l-8 8ZM3 4a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Z', 5 10 })
+8
src/components/icons/Hashtag.tsx
··· 1 1 import {createSinglePathSVG} from './TEMPLATE' 2 2 3 + export const HashtagWide_Stroke1_Corner0_Rounded = createSinglePathSVG({ 4 + viewBox: '0 0 46 46', 5 + strokeLinecap: 'round', 6 + strokeLinejoin: 'round', 7 + strokeWidth: 2, 8 + path: 'M14.333 1 9 45M37 1l-5.333 44M1 11.667h44m0 22.666H1', 9 + }) 10 + 3 11 export const Hashtag_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 12 path: 'M9.124 3.008a1 1 0 0 1 .868 1.116L9.632 7h5.985l.39-3.124a1 1 0 0 1 1.985.248L17.632 7H20a1 1 0 1 1 0 2h-2.617l-.75 6H20a1 1 0 1 1 0 2h-3.617l-.39 3.124a1 1 0 1 1-1.985-.248l.36-2.876H8.382l-.39 3.124a1 1 0 1 1-1.985-.248L6.368 17H4a1 1 0 1 1 0-2h2.617l.75-6H4a1 1 0 1 1 0-2h3.617l.39-3.124a1 1 0 0 1 1.117-.868ZM9.383 9l-.75 6h5.984l.75-6H9.383Z', 5 13 })
+7
src/components/icons/Heart2.tsx
··· 1 1 import {createSinglePathSVG} from './TEMPLATE' 2 2 3 + export const Heart2_Stroke1_Corner0_Rounded = createSinglePathSVG({ 4 + viewBox: '0 0 51 46', 5 + strokeLinejoin: 'round', 6 + strokeWidth: 2, 7 + path: 'M49 17c0 15.333-22 26.667-24 26.667S1 32.333 1 17C1 6.333 7.667 1 14.333 1S25 5 25 5s4-4 10.667-4S49 6.333 49 17Z', 8 + }) 9 + 3 10 export const Heart2_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 11 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 12 })
+7
src/components/icons/Image.tsx
··· 1 1 import {createSinglePathSVG} from './TEMPLATE' 2 2 3 + export const Image_Stroke1_Corner0_Rounded = createSinglePathSVG({ 4 + viewBox: '0 0 46 46', 5 + strokeLinecap: 'round', 6 + strokeWidth: 1.5, 7 + path: 'm1.417 28.645 7.586-5.676a5.33 5.33 0 0 1 6.867.809c3.98 4.286 8.594 8.182 14.88 8.182 5.794 0 9.633-2.147 13.333-5.847m-38 18.637h33.334a5.333 5.333 0 0 0 5.333-5.333V6.083A5.333 5.333 0 0 0 39.417.75H6.083A5.333 5.333 0 0 0 .75 6.083v33.334a5.333 5.333 0 0 0 5.333 5.333ZM36.75 14.083a5.333 5.333 0 1 1-10.667 0 5.333 5.333 0 0 1 10.667 0Z', 8 + }) 9 + 3 10 export const Image_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 11 path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v7.213l1.246-.932.044-.03a3 3 0 0 1 3.863.454c1.468 1.58 2.941 2.749 4.847 2.749 1.703 0 2.855-.555 4-1.618V5H5Zm14 10.357c-1.112.697-2.386 1.097-4 1.097-2.81 0-4.796-1.755-6.313-3.388a1 1 0 0 0-1.269-.164L5 14.712V19h14v-3.643ZM15 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z', 5 12 })
+8
src/components/icons/Message.tsx
··· 1 1 import {createSinglePathSVG} from './TEMPLATE' 2 2 3 + export const Message_Stroke1_Corner0_Rounded_Filled = createSinglePathSVG({ 4 + viewBox: '0 0 51 51', 5 + strokeWidth: 2, 6 + strokeLinecap: 'square', 7 + strokeLinejoin: 'round', 8 + path: 'M9 1h32a8 8 0 0 1 8 8v21.333a8 8 0 0 1-8 8H27.667L14.333 49V38.333H9a8 8 0 0 1-8-8V9a8 8 0 0 1 8-8Z', 9 + }) 10 + 3 11 export const Message_Stroke2_Corner0_Rounded_Filled = createSinglePathSVG({ 4 12 path: 'M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10a9.968 9.968 0 0 1-4.136-.893l-4.68.876a1 1 0 0 1-1.164-1.184l.931-4.537A9.965 9.965 0 0 1 2 12Zm4.25 0a1.25 1.25 0 1 0 2.5 0 1.25 1.25 0 0 0-2.5 0Zm4.5 0a1.25 1.25 0 1 0 2.5 0 1.25 1.25 0 0 0-2.5 0Zm5.75 1.25a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z', 5 13 })
+8
src/components/icons/PeopleRemove2.tsx
··· 1 1 import {createSinglePathSVG} from './TEMPLATE' 2 2 3 + export const PeopleRemove2_Stroke1_Corner0_Rounded = createSinglePathSVG({ 4 + viewBox: '-15 0 65 64', 5 + strokeWidth: 2, 6 + strokeLinecap: 'round', 7 + strokeLinejoin: 'round', 8 + path: 'M20.603 46.333H3.532c-1.572 0-2.816-1.358-2.472-2.891 2.033-9.046 9.421-15.775 19.543-15.775q1.367 0 2.666.16m18.667 7.84L36.603 41m0 0-5.334 5.333M36.603 41l-5.334-5.333M36.603 41l5.333 5.333m-12-36A9.333 9.333 0 1 1 20.603 1a9.333 9.333 0 0 1 9.333 9.333Z', 9 + }) 10 + 3 11 export const PeopleRemove2_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 12 path: 'M10 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM5.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM16 11a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1ZM3.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C1.917 15.521 5.242 12 10 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 17.5 21h-15a1 1 0 0 1-.996-1.094Z', 5 13 })
+44 -1
src/components/icons/TEMPLATE.tsx
··· 28 28 }, 29 29 ) 30 30 31 - export function createSinglePathSVG({path}: {path: string}) { 31 + export function createSinglePathSVG({ 32 + path, 33 + viewBox, 34 + strokeWidth = 0, 35 + strokeLinecap = 'butt', 36 + strokeLinejoin = 'miter', 37 + }: { 38 + path: string 39 + viewBox?: string 40 + strokeWidth?: number 41 + strokeLinecap?: 'butt' | 'round' | 'square' 42 + strokeLinejoin?: 'miter' | 'round' | 'bevel' 43 + }) { 44 + return React.forwardRef<Svg, Props>(function LogoImpl(props, ref) { 45 + const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) 46 + 47 + const hasStroke = strokeWidth > 0 48 + 49 + return ( 50 + <Svg 51 + fill="none" 52 + {...rest} 53 + ref={ref} 54 + viewBox={viewBox || '0 0 24 24'} 55 + width={size} 56 + height={size} 57 + style={[style]}> 58 + {gradient} 59 + <Path 60 + fill={hasStroke ? 'none' : fill} 61 + stroke={hasStroke ? fill : 'none'} 62 + strokeWidth={strokeWidth} 63 + strokeLinecap={strokeLinecap} 64 + strokeLinejoin={strokeLinejoin} 65 + fillRule="evenodd" 66 + clipRule="evenodd" 67 + d={path} 68 + /> 69 + </Svg> 70 + ) 71 + }) 72 + } 73 + 74 + export function createSinglePathSVG2({path}: {path: string}) { 32 75 return React.forwardRef<Svg, Props>(function LogoImpl(props, ref) { 33 76 const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) 34 77
+8
src/components/icons/VideoClip.tsx
··· 1 1 import {createSinglePathSVG} from './TEMPLATE' 2 2 3 + export const VideoClip_Stroke1_Corner0_Rounded = createSinglePathSVG({ 4 + viewBox: '0 0 46 46', 5 + strokeLinecap: 'square', 6 + strokeLinejoin: 'round', 7 + strokeWidth: 2, 8 + path: 'M1 23h10.667M1 23V12m0 11v11m10.667-11h22.666m-22.666 0v11m0-11V12m22.666 11H45m-10.667 0v12.222m0-12.222V12M45 23V12m0 11v12.222M34.333 45h5.334A5.333 5.333 0 0 0 45 39.667v-4.445M34.333 45v-9.778m0 9.778H11.667M34.333 1h5.334A5.333 5.333 0 0 1 45 6.333V12M34.333 1v11m0-11H11.667m22.666 11H45M34.333 35.222H45M11.667 45H6.333A5.333 5.333 0 0 1 1 39.667V34m10.667 11V34m0-33H6.333A5.333 5.333 0 0 0 1 6.333V12M11.667 1v11M1 12h10.667M1 34h10.667', 9 + }) 10 + 3 11 export const VideoClip_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 12 path: 'M3 4a1 1 0 011-1h16a1 1 0 011 1v16a1 1 0 01-1 1H4a1 1 0 01-1-1V4Zm2 1v2h2V5H5Zm4 0v6h6V5H9Zm8 0v2h2V5h-2Zm2 4h-2v2h2V9Zm0 4h-2v2h2V13Zm0 4h-2V19h2ZM15 19v-6H9v6h6Zm-8 0v-2H5v2h2Zm-2-4h2v-2H5v2Zm0-4h2V9H5v2Z', 5 13 })
+1
src/components/icons/common.tsx
··· 20 20 lg: 24, 21 21 xl: 28, 22 22 '2xl': 32, 23 + '3xl': 48, 23 24 } as const 24 25 25 26 export function useCommonSVGProps(props: Props) {
+1 -1
src/locale/locales/en/messages.po
··· 11013 11013 11014 11014 #: src/components/verification/VerificationsDialog.tsx:65 11015 11015 msgid "Your verifications" 11016 - msgstr "" 11016 + msgstr ""
+30 -4
src/screens/Bookmarks/index.tsx
··· 7 7 } from '@atproto/api' 8 8 import {msg, Trans} from '@lingui/macro' 9 9 import {useLingui} from '@lingui/react' 10 - import {useFocusEffect} from '@react-navigation/native' 10 + import { 11 + type NavigationProp, 12 + useFocusEffect, 13 + useNavigation, 14 + } from '@react-navigation/native' 11 15 12 16 import {useCleanError} from '#/lib/hooks/useCleanError' 13 17 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' ··· 21 25 import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery' 22 26 import {useSetMinimalShellMode} from '#/state/shell' 23 27 import {Post} from '#/view/com/post/Post' 28 + import {EmptyState} from '#/view/com/util/EmptyState' 24 29 import {List} from '#/view/com/util/List' 25 30 import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 26 - import {EmptyState} from '#/screens/Bookmarks/components/EmptyState' 27 31 import {atoms as a, useTheme} from '#/alf' 28 32 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 29 - import {BookmarkFilled} from '#/components/icons/Bookmark' 33 + import {BookmarkDeleteLarge, BookmarkFilled} from '#/components/icons/Bookmark' 30 34 import {CircleQuestion_Stroke2_Corner2_Rounded as QuestionIcon} from '#/components/icons/CircleQuestion' 31 35 import * as Layout from '#/components/Layout' 32 36 import {ListFooter} from '#/components/Lists' ··· 259 263 ) 260 264 } 261 265 266 + function BookmarksEmpty() { 267 + const t = useTheme() 268 + const {_} = useLingui() 269 + const navigation = useNavigation<NavigationProp<CommonNavigatorParams>>() 270 + 271 + return ( 272 + <EmptyState 273 + icon={BookmarkDeleteLarge} 274 + message={_(msg`Nothing saved yet`)} 275 + textStyle={[t.atoms.text_contrast_medium, a.font_medium]} 276 + button={{ 277 + label: _(msg`Button to go back to the home timeline`), 278 + text: _(msg`Go home`), 279 + onPress: () => navigation.navigate('Home' as never), 280 + size: 'small', 281 + color: 'secondary', 282 + }} 283 + style={[a.pt_3xl]} 284 + /> 285 + ) 286 + } 287 + 262 288 function renderItem({item, index}: {item: ListItem; index: number}) { 263 289 switch (item.type) { 264 290 case 'loading': { 265 291 return <PostFeedLoadingPlaceholder /> 266 292 } 267 293 case 'empty': { 268 - return <EmptyState /> 294 + return <BookmarksEmpty /> 269 295 } 270 296 case 'bookmark': { 271 297 return (
+6 -1
src/screens/Notifications/ActivityList.tsx
··· 5 5 import {type AllNavigatorParams} from '#/lib/routes/types' 6 6 import {PostFeed} from '#/view/com/posts/PostFeed' 7 7 import {EmptyState} from '#/view/com/util/EmptyState' 8 + import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig' 8 9 import * as Layout from '#/components/Layout' 9 10 import {ListFooter} from '#/components/Lists' 10 11 ··· 35 36 feed={`posts|${uris}`} 36 37 disablePoll 37 38 renderEmptyState={() => ( 38 - <EmptyState icon="growth" message={_(msg`No posts here`)} /> 39 + <EmptyState 40 + icon={EditIcon} 41 + iconSize="2xl" 42 + message={_(msg`No posts here`)} 43 + /> 39 44 )} 40 45 renderEndOfFeed={() => <ListFooter />} 41 46 />
+8 -1
src/screens/Profile/ProfileFeed/index.tsx
··· 45 45 ProfileFeedHeader, 46 46 ProfileFeedHeaderSkeleton, 47 47 } from '#/screens/Profile/components/ProfileFeedHeader' 48 + import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 48 49 import * as Layout from '#/components/Layout' 49 50 50 51 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'> ··· 189 190 }, [onScrollToTop, isScreenFocused]) 190 191 191 192 const renderPostsEmpty = useCallback(() => { 192 - return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} /> 193 + return ( 194 + <EmptyState 195 + icon={HashtagWideIcon} 196 + iconSize="2xl" 197 + message={_(msg`This feed is empty.`)} 198 + /> 199 + ) 193 200 }, [_]) 194 201 195 202 const isVideoFeed = React.useMemo(() => {
+28 -6
src/screens/Profile/Sections/Feed.tsx
··· 6 6 7 7 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 8 8 import {isIOS, isNative} from '#/platform/detection' 9 - import {type FeedDescriptor} from '#/state/queries/post-feed' 10 - import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' 9 + import { 10 + type FeedDescriptor, 11 + RQKEY as FEED_RQKEY, 12 + } from '#/state/queries/post-feed' 11 13 import {truncateAndInvalidate} from '#/state/queries/util' 12 14 import {PostFeed} from '#/view/com/posts/PostFeed' 13 - import {EmptyState} from '#/view/com/util/EmptyState' 15 + import { 16 + EmptyState, 17 + type EmptyStateButtonProps, 18 + } from '#/view/com/util/EmptyState' 14 19 import {type ListRef} from '#/view/com/util/List' 15 20 import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' 16 21 import {atoms as a, ios, useTheme} from '#/alf' 22 + import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig' 17 23 import {Text} from '#/components/Typography' 18 24 import {type SectionRef} from './types' 19 25 ··· 25 31 scrollElRef: ListRef 26 32 ignoreFilterFor?: string 27 33 setScrollViewTag: (tag: number | null) => void 34 + emptyStateMessage?: string 35 + emptyStateButton?: EmptyStateButtonProps 36 + emptyStateIcon?: React.ComponentType<any> | React.ReactElement 28 37 } 38 + 29 39 export function ProfileFeedSection({ 30 40 ref, 31 41 feed, ··· 34 44 scrollElRef, 35 45 ignoreFilterFor, 36 46 setScrollViewTag, 47 + emptyStateMessage, 48 + emptyStateButton, 49 + emptyStateIcon, 37 50 }: FeedSectionProps) { 38 51 const {_} = useLingui() 39 52 const queryClient = useQueryClient() ··· 44 57 const adjustedInitialNumToRender = useInitialNumToRender({ 45 58 screenHeightOffset: headerHeight, 46 59 }) 47 - 48 60 const onScrollToTop = useCallback(() => { 49 61 scrollElRef.current?.scrollToOffset({ 50 62 animated: isNative, ··· 59 71 })) 60 72 61 73 const renderPostsEmpty = useCallback(() => { 62 - return <EmptyState icon="growth" message={_(msg`No posts yet.`)} /> 63 - }, [_]) 74 + return ( 75 + <View style={[a.flex_1, a.justify_center, a.align_center]}> 76 + <EmptyState 77 + style={{width: '100%'}} 78 + icon={emptyStateIcon || EditIcon} 79 + iconSize="3xl" 80 + message={emptyStateMessage || _(msg`No posts yet`)} 81 + button={emptyStateButton} 82 + /> 83 + </View> 84 + ) 85 + }, [_, emptyStateButton, emptyStateIcon, emptyStateMessage]) 64 86 65 87 useEffect(() => { 66 88 if (isIOS && isFocused && scrollElRef.current) {
+3 -2
src/screens/ProfileList/AboutSection.tsx
··· 12 12 import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' 13 13 import {atoms as a, useBreakpoints} from '#/alf' 14 14 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 15 + import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' 15 16 import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' 16 17 17 18 interface SectionRef { ··· 95 96 const renderEmptyState = useCallback(() => { 96 97 return ( 97 98 <View style={[a.gap_xl, a.align_center]}> 98 - <EmptyState icon="users-slash" message={_(msg`This list is empty.`)} /> 99 + <EmptyState icon={ListIcon} message={_(msg`This list is empty.`)} /> 99 100 {isOwner && ( 100 101 <Button 101 102 testID="emptyStateAddUserBtn" ··· 111 112 )} 112 113 </View> 113 114 ) 114 - }, [_, onPressAddUser, isOwner]) 115 + }, [_, isOwner, onPressAddUser]) 115 116 116 117 return ( 117 118 <View>
+10 -3
src/screens/ProfileList/FeedSection.tsx
··· 7 7 8 8 import {isNative} from '#/platform/detection' 9 9 import {listenSoftReset} from '#/state/events' 10 - import {type FeedDescriptor} from '#/state/queries/post-feed' 11 - import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' 10 + import { 11 + type FeedDescriptor, 12 + RQKEY as FEED_RQKEY, 13 + } from '#/state/queries/post-feed' 12 14 import {PostFeed} from '#/view/com/posts/PostFeed' 13 15 import {EmptyState} from '#/view/com/util/EmptyState' 14 16 import {type ListRef} from '#/view/com/util/List' 15 17 import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' 16 18 import {atoms as a} from '#/alf' 17 19 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 20 + import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 18 21 import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' 19 22 20 23 interface SectionRef { ··· 68 71 const renderPostsEmpty = useCallback(() => { 69 72 return ( 70 73 <View style={[a.gap_xl, a.align_center]}> 71 - <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} /> 74 + <EmptyState 75 + icon={HashtagWideIcon} 76 + iconSize="2xl" 77 + message={_(msg`This feed is empty.`)} 78 + /> 72 79 {isOwner && ( 73 80 <Button 74 81 label={_(msg`Start adding people`)}
+2 -1
src/screens/Settings/AppPasswords.tsx
··· 24 24 import {Admonition, colors} from '#/components/Admonition' 25 25 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 26 26 import {useDialogControl} from '#/components/Dialog' 27 + import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth' 27 28 import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 28 29 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 29 30 import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' ··· 102 103 </View> 103 104 ) : ( 104 105 <EmptyState 105 - icon="growth" 106 + icon={Growth} 106 107 message={_(msg`No app passwords yet`)} 107 108 /> 108 109 )
+1
src/screens/Settings/components/SettingsList.tsx
··· 201 201 lg: 24, 202 202 xl: 28, 203 203 '2xl': 32, 204 + '3xl': 40, 204 205 }[size] 205 206 206 207 const color =
+15 -4
src/view/com/feeds/ProfileFeedgens.tsx
··· 15 15 } from 'react-native' 16 16 import {msg} from '@lingui/macro' 17 17 import {useLingui} from '@lingui/react' 18 + import {useNavigation} from '@react-navigation/native' 18 19 import {useQueryClient} from '@tanstack/react-query' 19 20 20 21 import {cleanError} from '#/lib/strings/errors' ··· 29 30 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 30 31 import {atoms as a, ios, useTheme} from '#/alf' 31 32 import * as FeedCard from '#/components/FeedCard' 33 + import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 32 34 import {ListFooter} from '#/components/Lists' 33 35 34 36 const LOADING = {_reactKey: '__loading__'} ··· 78 80 } = useProfileFeedgensQuery(did, opts) 79 81 const isEmpty = !isPending && !data?.pages[0]?.feeds.length 80 82 const {data: preferences} = usePreferencesQuery() 83 + const navigation = useNavigation() 81 84 82 85 const items = useMemo(() => { 83 86 let items: any[] = [] ··· 147 150 if (item === EMPTY) { 148 151 return ( 149 152 <EmptyState 150 - icon="hashtag" 151 - message={_(msg`You have no feeds.`)} 152 - testID="listsEmpty" 153 + style={{width: '100%'}} 154 + icon={HashtagWideIcon} 155 + message={_(msg`You haven't made any custom feeds yet.`)} 156 + textStyle={[t.atoms.text_contrast_medium, a.font_medium]} 157 + button={{ 158 + label: _(msg`Browse custom feeds`), 159 + text: _(msg`Browse custom feeds`), 160 + onPress: () => navigation.navigate('Feeds' as never), 161 + size: 'small', 162 + color: 'secondary', 163 + }} 153 164 /> 154 165 ) 155 166 } else if (item === ERROR_ITEM) { ··· 183 194 } 184 195 return null 185 196 }, 186 - [_, t, error, refetch, onPressRetryLoadMore, preferences], 197 + [_, t, error, refetch, onPressRetryLoadMore, preferences, navigation], 187 198 ) 188 199 189 200 useEffect(() => {
+6 -7
src/view/com/lists/MyLists.tsx
··· 18 18 import {useModerationOpts} from '#/state/preferences/moderation-opts' 19 19 import {type MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists' 20 20 import {atoms as a, useTheme} from '#/alf' 21 - import {BulletList_Stroke2_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' 21 + import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' 22 22 import * as ListCard from '#/components/ListCard' 23 23 import {Text} from '#/components/Typography' 24 24 import {ErrorMessage} from '../util/error/ErrorMessage' ··· 69 69 switch (filter) { 70 70 case 'curate': 71 71 emptyText = _( 72 - msg`Public, sharable lists which can be used to drive feeds.`, 72 + msg`Lists allow you to see content from your favorite people.`, 73 73 ) 74 74 break 75 75 case 'mod': ··· 102 102 ({item, index}: {item: any; index: number}) => { 103 103 if (item === EMPTY) { 104 104 return ( 105 - <View style={[a.flex_1, a.align_center, a.gap_sm, a.px_xl, a.pt_xl]}> 105 + <View style={[a.flex_1, a.align_center, a.gap_sm, a.px_xl, a.pt_3xl]}> 106 106 <View 107 107 style={[ 108 108 a.align_center, 109 109 a.justify_center, 110 110 a.rounded_full, 111 - t.atoms.bg_contrast_25, 112 111 { 113 - width: 32, 114 - height: 32, 112 + width: 64, 113 + height: 64, 115 114 }, 116 115 ]}> 117 - <ListIcon size="md" fill={t.atoms.text_contrast_low.color} /> 116 + <ListIcon size="2xl" fill={t.atoms.text_contrast_medium.color} /> 118 117 </View> 119 118 <Text 120 119 style={[
+50 -33
src/view/com/lists/ProfileLists.tsx
··· 15 15 } from 'react-native' 16 16 import {msg} from '@lingui/macro' 17 17 import {useLingui} from '@lingui/react' 18 + import {useNavigation} from '@react-navigation/native' 18 19 import {useQueryClient} from '@tanstack/react-query' 19 20 20 21 import {cleanError} from '#/lib/strings/errors' 21 22 import {logger} from '#/logger' 22 23 import {isIOS, isNative, isWeb} from '#/platform/detection' 23 - import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists' 24 + import {usePreferencesQuery} from '#/state/queries/preferences' 25 + import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens' 24 26 import {EmptyState} from '#/view/com/util/EmptyState' 25 27 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 26 28 import {List, type ListRef} from '#/view/com/util/List' 27 29 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 28 30 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 29 31 import {atoms as a, ios, useTheme} from '#/alf' 30 - import * as ListCard from '#/components/ListCard' 32 + import * as FeedCard from '#/components/FeedCard' 33 + import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' 31 34 import {ListFooter} from '#/components/Lists' 32 35 33 36 const LOADING = {_reactKey: '__loading__'} ··· 39 42 scrollToTop: () => void 40 43 } 41 44 42 - interface ProfileListsProps { 45 + interface ProfileFeedgensProps { 43 46 ref?: React.Ref<SectionRef> 44 47 did: string 45 48 scrollElRef: ListRef ··· 59 62 style, 60 63 testID, 61 64 setScrollViewTag, 62 - }: ProfileListsProps) { 65 + }: ProfileFeedgensProps) { 66 + const {_} = useLingui() 63 67 const t = useTheme() 64 - const {_} = useLingui() 68 + const [isPTRing, setIsPTRing] = useState(false) 65 69 const {height} = useWindowDimensions() 66 - const [isPTRing, setIsPTRing] = useState(false) 67 70 const opts = useMemo(() => ({enabled}), [enabled]) 68 71 const { 69 72 data, 70 73 isPending, 74 + isFetchingNextPage, 71 75 hasNextPage, 72 76 fetchNextPage, 73 - isFetchingNextPage, 74 77 isError, 75 78 error, 76 79 refetch, 77 - } = useProfileListsQuery(did, opts) 78 - const isEmpty = !isPending && !data?.pages[0]?.lists.length 80 + } = useProfileFeedgensQuery(did, opts) 81 + const isEmpty = !isPending && !data?.pages[0]?.feeds.length 82 + const {data: preferences} = usePreferencesQuery() 83 + const navigation = useNavigation() 79 84 80 85 const items = useMemo(() => { 81 86 let items: any[] = [] ··· 88 93 items = items.concat([EMPTY]) 89 94 } else if (data?.pages) { 90 95 for (const page of data?.pages) { 91 - items = items.concat(page.lists) 96 + items = items.concat(page.feeds) 92 97 } 93 - } 94 - if (isError && !isEmpty) { 98 + } else if (isError && !isEmpty) { 95 99 items = items.concat([LOAD_MORE_ERROR_ITEM]) 96 100 } 97 101 return items ··· 119 123 try { 120 124 await refetch() 121 125 } catch (err) { 122 - logger.error('Failed to refresh lists', {message: err}) 126 + logger.error('Failed to refresh feeds', {message: err}) 123 127 } 124 128 setIsPTRing(false) 125 129 }, [refetch, setIsPTRing]) 126 130 127 131 const onEndReached = useCallback(async () => { 128 132 if (isFetchingNextPage || !hasNextPage || isError) return 133 + 129 134 try { 130 135 await fetchNextPage() 131 136 } catch (err) { 132 - logger.error('Failed to load more lists', {message: err}) 137 + logger.error('Failed to load more feeds', {message: err}) 133 138 } 134 139 }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 135 140 ··· 140 145 // rendering 141 146 // = 142 147 143 - const renderItemInner = useCallback( 148 + const renderItem = useCallback( 144 149 ({item, index}: ListRenderItemInfo<any>) => { 145 150 if (item === EMPTY) { 146 151 return ( 147 152 <EmptyState 148 - icon="list-ul" 149 - message={_(msg`You have no lists.`)} 150 - testID="listsEmpty" 153 + icon={ListIcon} 154 + message={_( 155 + msg`Lists allow you to see content from your favorite people.`, 156 + )} 157 + textStyle={[t.atoms.text_contrast_medium, a.font_medium]} 158 + button={{ 159 + label: _(msg`Create a list`), 160 + text: _(msg`Create a list`), 161 + onPress: () => navigation.navigate('Lists' as never), 162 + size: 'small', 163 + color: 'primary', 164 + }} 151 165 /> 152 166 ) 153 167 } else if (item === ERROR_ITEM) { ··· 166 180 } else if (item === LOADING) { 167 181 return <FeedLoadingPlaceholder /> 168 182 } 169 - return ( 170 - <View 171 - style={[ 172 - (index !== 0 || isWeb) && a.border_t, 173 - t.atoms.border_contrast_low, 174 - a.px_lg, 175 - a.py_lg, 176 - ]}> 177 - <ListCard.Default view={item} /> 178 - </View> 179 - ) 183 + if (preferences) { 184 + return ( 185 + <View 186 + style={[ 187 + (index !== 0 || isWeb) && a.border_t, 188 + t.atoms.border_contrast_low, 189 + a.px_lg, 190 + a.py_lg, 191 + ]}> 192 + <FeedCard.Default view={item} /> 193 + </View> 194 + ) 195 + } 196 + return null 180 197 }, 181 - [error, refetch, onPressRetryLoadMore, _, t.atoms.border_contrast_low], 198 + [_, t, error, refetch, onPressRetryLoadMore, preferences, navigation], 182 199 ) 183 200 184 201 useEffect(() => { ··· 188 205 } 189 206 }, [enabled, scrollElRef, setScrollViewTag]) 190 207 191 - const ProfileListsFooter = useCallback(() => { 208 + const ProfileFeedgensFooter = useCallback(() => { 192 209 if (isEmpty) return null 193 210 return ( 194 211 <ListFooter ··· 215 232 ref={scrollElRef} 216 233 data={items} 217 234 keyExtractor={keyExtractor} 218 - renderItem={renderItemInner} 219 - ListFooterComponent={ProfileListsFooter} 235 + renderItem={renderItem} 236 + ListFooterComponent={ProfileFeedgensFooter} 220 237 refreshing={isPTRing} 221 238 onRefresh={onRefresh} 222 239 headerOffset={headerOffset}
+2 -1
src/view/com/notifications/NotificationFeed.tsx
··· 19 19 import {List, type ListProps, type ListRef} from '#/view/com/util/List' 20 20 import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 21 21 import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 22 + import {Bell_Stroke2_Corner0_Rounded as BellIcon} from '#/components/icons/Bell' 22 23 import {NotificationFeedItem} from './NotificationFeedItem' 23 24 24 25 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} ··· 118 119 if (item === EMPTY_FEED_ITEM) { 119 120 return ( 120 121 <EmptyState 121 - icon="bell" 122 + icon={BellIcon} 122 123 message={_(msg`No notifications yet!`)} 123 124 style={styles.emptyState} 124 125 />
+4 -1
src/view/com/posts/PostFeedErrorMessage.tsx
··· 15 15 import {logger} from '#/logger' 16 16 import {type FeedDescriptor} from '#/state/queries/post-feed' 17 17 import {useRemoveFeedMutation} from '#/state/queries/preferences' 18 + import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 18 19 import * as Prompt from '#/components/Prompt' 19 20 import {EmptyState} from '../util/EmptyState' 20 21 import {ErrorMessage} from '../util/error/ErrorMessage' ··· 50 51 () => detectKnownError(feedDesc, error), 51 52 [feedDesc, error], 52 53 ) 54 + 53 55 if ( 54 56 typeof knownError !== 'undefined' && 55 57 knownError !== KnownError.Unknown && ··· 68 70 if (knownError === KnownError.Block) { 69 71 return ( 70 72 <EmptyState 71 - icon="ban" 73 + icon={WarningIcon} 74 + iconSize="2xl" 72 75 message={_l(msgLingui`Posts hidden`)} 73 76 style={{paddingVertical: 40}} 74 77 />
+13 -1
src/view/com/profile/ProfileFollowers.tsx
··· 2 2 import {type AppBskyActorDefs as ActorDefs} from '@atproto/api' 3 3 import {msg} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 + import {useNavigation} from '@react-navigation/native' 5 6 6 7 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 7 8 import {cleanError} from '#/lib/strings/errors' ··· 9 10 import {useProfileFollowersQuery} from '#/state/queries/profile-followers' 10 11 import {useResolveDidQuery} from '#/state/queries/resolve-uri' 11 12 import {useSession} from '#/state/session' 13 + import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2' 12 14 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 13 15 import {List} from '../util/List' 14 16 import {ProfileCardWithFollowBtn} from './ProfileCard' ··· 39 41 40 42 export function ProfileFollowers({name}: {name: string}) { 41 43 const {_} = useLingui() 44 + const navigation = useNavigation() 42 45 const initialNumToRender = useInitialNumToRender() 43 46 const {currentAccount} = useSession() 44 47 ··· 129 132 emptyType="results" 130 133 emptyMessage={ 131 134 isMe 132 - ? _(msg`You do not have any followers.`) 135 + ? _(msg`No followers yet`) 133 136 : _(msg`This user doesn't have any followers.`) 134 137 } 135 138 errorMessage={cleanError(resolveError || error)} 136 139 onRetry={isError ? refetch : undefined} 137 140 sideBorders={false} 141 + useEmptyState={true} 142 + emptyStateIcon={PeopleRemoveIcon} 143 + emptyStateButton={{ 144 + label: _(msg`Go back`), 145 + text: _(msg`Go back`), 146 + color: 'secondary', 147 + size: 'small', 148 + onPress: () => navigation.goBack(), 149 + }} 138 150 /> 139 151 ) 140 152 }
+24 -1
src/view/com/profile/ProfileFollows.tsx
··· 2 2 import {type AppBskyActorDefs as ActorDefs} from '@atproto/api' 3 3 import {msg} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 + import {useNavigation} from '@react-navigation/native' 5 6 6 7 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 8 + import {type NavigationProp} from '#/lib/routes/types' 7 9 import {cleanError} from '#/lib/strings/errors' 8 10 import {logger} from '#/logger' 11 + import {isWeb} from '#/platform/detection' 9 12 import {useProfileFollowsQuery} from '#/state/queries/profile-follows' 10 13 import {useResolveDidQuery} from '#/state/queries/resolve-uri' 11 14 import {useSession} from '#/state/session' 15 + import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2' 12 16 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 13 17 import {List} from '../util/List' 14 18 import {ProfileCardWithFollowBtn} from './ProfileCard' ··· 41 45 const {_} = useLingui() 42 46 const initialNumToRender = useInitialNumToRender() 43 47 const {currentAccount} = useSession() 48 + const navigation = useNavigation<NavigationProp>() 49 + 50 + const onPressFindAccounts = React.useCallback(() => { 51 + if (isWeb) { 52 + navigation.navigate('Search', {}) 53 + } else { 54 + navigation.navigate('SearchTab') 55 + navigation.popToTop() 56 + } 57 + }, [navigation]) 44 58 45 59 const [isPTRing, setIsPTRing] = React.useState(false) 46 60 const { ··· 129 143 emptyType="results" 130 144 emptyMessage={ 131 145 isMe 132 - ? _(msg`You are not following anyone.`) 146 + ? _(msg`You are not following anyone yet`) 133 147 : _(msg`This user isn't following anyone.`) 134 148 } 135 149 errorMessage={cleanError(resolveError || error)} 136 150 onRetry={isError ? refetch : undefined} 137 151 sideBorders={false} 152 + useEmptyState={true} 153 + emptyStateIcon={PeopleRemoveIcon} 154 + emptyStateButton={{ 155 + label: _(msg`See suggested accounts`), 156 + text: _(msg`See suggested accounts`), 157 + onPress: onPressFindAccounts, 158 + size: 'tiny', 159 + color: 'primary', 160 + }} 138 161 /> 139 162 ) 140 163 }
+84 -49
src/view/com/util/EmptyState.tsx
··· 1 - import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' 2 - import {type IconProp} from '@fortawesome/fontawesome-svg-core' 3 - import { 4 - FontAwesomeIcon, 5 - type FontAwesomeIconStyle, 6 - } from '@fortawesome/react-native-fontawesome' 1 + import React from 'react' 2 + import {type StyleProp, type TextStyle, type ViewStyle} from 'react-native' 3 + import {View} from 'react-native' 7 4 8 5 import {usePalette} from '#/lib/hooks/usePalette' 9 6 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 10 - import {UserGroupIcon} from '#/lib/icons' 11 - import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth' 12 - import {Text} from './text/Text' 7 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 8 + import {Button, type ButtonProps, ButtonText} from '#/components/Button' 9 + import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig' 10 + import {Text} from '#/components/Typography' 11 + 12 + export type EmptyStateButtonProps = Omit<ButtonProps, 'children' | 'label'> & { 13 + label: string 14 + text: string 15 + } 13 16 14 17 export function EmptyState({ 15 18 testID, 16 19 icon, 20 + iconSize = '3xl', 17 21 message, 18 22 style, 23 + textStyle, 24 + button, 19 25 }: { 20 26 testID?: string 21 - icon: IconProp | 'user-group' | 'growth' 27 + icon?: React.ComponentType<any> | React.ReactElement 28 + iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' 22 29 message: string 23 30 style?: StyleProp<ViewStyle> 31 + textStyle?: StyleProp<TextStyle> 32 + button?: EmptyStateButtonProps 24 33 }) { 25 34 const pal = usePalette('default') 26 35 const {isTabletOrDesktop} = useWebMediaQueries() 27 - const iconSize = isTabletOrDesktop ? 64 : 48 36 + const t = useTheme() 37 + const {gtMobile} = useBreakpoints() 38 + 39 + const placeholderIcon = ( 40 + <EditIcon size="2xl" fill={t.atoms.text_contrast_medium.color} /> 41 + ) 42 + 43 + const renderIcon = () => { 44 + if (!icon) { 45 + return placeholderIcon 46 + } 47 + 48 + if (React.isValidElement(icon)) { 49 + return icon 50 + } 51 + 52 + if ( 53 + typeof icon === 'function' || 54 + (typeof icon === 'object' && icon && 'render' in icon) 55 + ) { 56 + const IconComponent = icon 57 + return ( 58 + <IconComponent 59 + size={iconSize} 60 + fill={t.atoms.text_contrast_medium.color} 61 + style={{color: t.atoms.text_contrast_low.color}} 62 + /> 63 + ) 64 + } 65 + 66 + return placeholderIcon 67 + } 68 + 28 69 return ( 29 70 <View testID={testID} style={style}> 30 71 <View 31 72 style={[ 32 - styles.iconContainer, 33 - isTabletOrDesktop && styles.iconContainerBig, 34 - pal.viewLight, 73 + a.flex_row, 74 + a.align_center, 75 + a.justify_center, 76 + a.self_center, 77 + a.rounded_full, 78 + a.mt_5xl, 79 + {height: 64, width: 64}, 80 + React.isValidElement(icon) 81 + ? a.bg_transparent 82 + : [isTabletOrDesktop && {marginTop: 50}], 35 83 ]}> 36 - {icon === 'user-group' ? ( 37 - <UserGroupIcon size={iconSize} /> 38 - ) : icon === 'growth' ? ( 39 - <Growth width={iconSize} fill={pal.colors.emptyStateIcon} /> 40 - ) : ( 41 - <FontAwesomeIcon 42 - icon={icon} 43 - size={iconSize} 44 - style={[{color: pal.colors.emptyStateIcon} as FontAwesomeIconStyle]} 45 - /> 46 - )} 84 + {renderIcon()} 47 85 </View> 48 - <Text type="xl" style={[{color: pal.colors.textLight}, styles.text]}> 86 + <Text 87 + style={[ 88 + { 89 + color: pal.colors.textLight, 90 + maxWidth: gtMobile ? '40%' : '60%', 91 + }, 92 + a.pt_xs, 93 + a.font_medium, 94 + a.text_md, 95 + a.leading_snug, 96 + a.text_center, 97 + a.self_center, 98 + textStyle, 99 + ]}> 49 100 {message} 50 101 </Text> 102 + {button && ( 103 + <View style={[a.flex_shrink, a.mt_xl, a.self_center]}> 104 + <Button {...button}> 105 + <ButtonText>{button.text}</ButtonText> 106 + </Button> 107 + </View> 108 + )} 51 109 </View> 52 110 ) 53 111 } 54 - 55 - const styles = StyleSheet.create({ 56 - iconContainer: { 57 - flexDirection: 'row', 58 - alignItems: 'center', 59 - justifyContent: 'center', 60 - height: 80, 61 - width: 80, 62 - marginLeft: 'auto', 63 - marginRight: 'auto', 64 - borderRadius: 80, 65 - marginTop: 30, 66 - }, 67 - iconContainerBig: { 68 - width: 100, 69 - height: 100, 70 - marginTop: 50, 71 - }, 72 - text: { 73 - textAlign: 'center', 74 - paddingTop: 20, 75 - }, 76 - })
+10 -1
src/view/screens/Debug.tsx
··· 20 20 import * as Toast from '#/view/com/util/Toast' 21 21 import {ViewHeader} from '#/view/com/util/ViewHeader' 22 22 import {ViewSelector} from '#/view/com/util/ViewSelector' 23 + import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 23 24 import * as Layout from '#/components/Layout' 24 25 25 26 const MAIN_VIEWS = ['Base', 'Controls', 'Error', 'Notifs'] ··· 333 334 } 334 335 335 336 function EmptyStateView() { 336 - return <EmptyState icon="bars" message="This is an empty state" /> 337 + const {_} = useLingui() 338 + 339 + return ( 340 + <EmptyState 341 + icon={HashtagWideIcon} 342 + iconSize="2xl" 343 + message={_(msg`This is an empty state`)} 344 + /> 345 + ) 337 346 } 338 347 339 348 function LoadingPlaceholderView() {
+63 -2
src/view/screens/Profile.tsx
··· 7 7 type ModerationOpts, 8 8 RichText as RichTextAPI, 9 9 } from '@atproto/api' 10 - import {msg} from '@lingui/macro' 10 + import {msg, Trans} from '@lingui/macro' 11 11 import {useLingui} from '@lingui/react' 12 - import {useFocusEffect} from '@react-navigation/native' 12 + import {useFocusEffect, useNavigation} from '@react-navigation/native' 13 13 import {useQueryClient} from '@tanstack/react-query' 14 14 15 15 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 16 + import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' 16 17 import {useSetTitle} from '#/lib/hooks/useSetTitle' 17 18 import {ComposeIcon2} from '#/lib/icons' 18 19 import { 19 20 type CommonNavigatorParams, 20 21 type NativeStackScreenProps, 22 + type NavigationProp, 21 23 } from '#/lib/routes/types' 22 24 import {combinedDisplayName} from '#/lib/strings/display-names' 23 25 import {cleanError} from '#/lib/strings/errors' ··· 42 44 import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' 43 45 import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' 44 46 import {atoms as a} from '#/alf' 47 + import {Circle_And_Square_Stroke1_Corner0_Rounded_Filled as CircleAndSquareIcon} from '#/components/icons/CircleAndSquare' 48 + import {Heart2_Stroke1_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2' 49 + import {Image_Stroke1_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' 50 + import {Message_Stroke1_Corner0_Rounded_Filled as MessageIcon} from '#/components/icons/Message' 51 + import {VideoClip_Stroke1_Corner0_Rounded as VideoIcon} from '#/components/icons/VideoClip' 45 52 import * as Layout from '#/components/Layout' 46 53 import {ScreenHider} from '#/components/moderation/ScreenHider' 47 54 import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks' ··· 169 176 const {hasSession, currentAccount} = useSession() 170 177 const setMinimalShellMode = useSetMinimalShellMode() 171 178 const {openComposer} = useOpenComposer() 179 + const navigation = useNavigation<NavigationProp>() 180 + const requireEmailVerification = useRequireEmailVerification() 172 181 const { 173 182 data: labelerInfo, 174 183 error: labelerError, ··· 334 343 scrollSectionToTop(index) 335 344 } 336 345 346 + const navToWizard = useCallback(() => { 347 + navigation.navigate('StarterPackWizard', {}) 348 + }, [navigation]) 349 + const wrappedNavToWizard = requireEmailVerification(navToWizard, { 350 + instructions: [ 351 + <Trans key="nav"> 352 + Before creating a starter pack, you must first verify your email. 353 + </Trans>, 354 + ], 355 + }) 356 + 337 357 // rendering 338 358 // = 339 359 ··· 408 428 scrollElRef={scrollElRef as ListRef} 409 429 ignoreFilterFor={profile.did} 410 430 setScrollViewTag={setScrollViewTag} 431 + emptyStateMessage={_(msg`No posts yet`)} 432 + emptyStateButton={{ 433 + label: _(msg`Write a post`), 434 + text: _(msg`Write a post`), 435 + onPress: () => openComposer({}), 436 + size: 'small', 437 + color: 'primary', 438 + }} 411 439 /> 412 440 ) 413 441 : null} ··· 421 449 scrollElRef={scrollElRef as ListRef} 422 450 ignoreFilterFor={profile.did} 423 451 setScrollViewTag={setScrollViewTag} 452 + emptyStateMessage={_(msg`No replies yet`)} 453 + emptyStateIcon={MessageIcon} 424 454 /> 425 455 ) 426 456 : null} ··· 434 464 scrollElRef={scrollElRef as ListRef} 435 465 ignoreFilterFor={profile.did} 436 466 setScrollViewTag={setScrollViewTag} 467 + emptyStateMessage={_(msg`No media yet`)} 468 + emptyStateButton={{ 469 + label: _(msg`Post a photo`), 470 + text: _(msg`Post a photo`), 471 + onPress: () => openComposer({}), 472 + size: 'small', 473 + color: 'primary', 474 + }} 475 + emptyStateIcon={ImageIcon} 437 476 /> 438 477 ) 439 478 : null} ··· 447 486 scrollElRef={scrollElRef as ListRef} 448 487 ignoreFilterFor={profile.did} 449 488 setScrollViewTag={setScrollViewTag} 489 + emptyStateMessage={_(msg`No video posts yet`)} 490 + emptyStateButton={{ 491 + label: _(msg`Post a video`), 492 + text: _(msg`Post a video`), 493 + onPress: () => openComposer({}), 494 + size: 'small', 495 + color: 'primary', 496 + }} 497 + emptyStateIcon={VideoIcon} 450 498 /> 451 499 ) 452 500 : null} ··· 460 508 scrollElRef={scrollElRef as ListRef} 461 509 ignoreFilterFor={profile.did} 462 510 setScrollViewTag={setScrollViewTag} 511 + emptyStateMessage={_(msg`No likes yet`)} 512 + emptyStateIcon={HeartIcon} 463 513 /> 464 514 ) 465 515 : null} ··· 485 535 headerOffset={headerHeight} 486 536 enabled={isFocused} 487 537 setScrollViewTag={setScrollViewTag} 538 + emptyStateMessage={_( 539 + msg`Starter packs let you share your favorite feeds and people with your friends.`, 540 + )} 541 + emptyStateButton={{ 542 + label: _(msg`Create a Starter Pack`), 543 + text: _(msg`Create a Starter Pack`), 544 + onPress: wrappedNavToWizard, 545 + color: 'primary', 546 + size: 'small', 547 + }} 548 + emptyStateIcon={CircleAndSquareIcon} 488 549 /> 489 550 ) 490 551 : null}