From d0649e9a9e4145c7682e77e1dd6cd6adffbe89c4 Mon Sep 17 00:00:00 2001 From: Anastasiya Uraleva Date: Thu, 27 Nov 2025 03:22:03 -0800 Subject: [PATCH] [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 --- __tests__/lib/link-meta.test.ts | 2 +- .../bulletlist_stroke1_corner0_rounded.svg | 1 + ..._square_stroke1_corner0_rounded_filled.svg | 1 + .../icons/editbig_stroke1_corner0_rounded.svg | 1 + .../hashtagwide_stroke1_corner0_rounded.svg | 1 + .../icons/heart2_stroke1_corner0_rounded.svg | 1 + .../icons/image_stroke1_corner0_rounded.svg | 1 + ...message_stroke1_corner0_rounded_filled.svg | 1 + .../peopleremove2_stroke1_corner0_rounded.svg | 1 + .../videoclip_stroke1_corner0_rounded.svg | 1 + src/components/Button.tsx | 3 +- src/components/Lists.tsx | 27 ++++ src/components/StarterPack/Main/PostsList.tsx | 9 +- .../StarterPack/ProfileStarterPacks.tsx | 34 ++++- src/components/icons/BulletList.tsx | 8 ++ src/components/icons/CircleAndSquare.tsx | 7 + src/components/icons/EditBig.tsx | 5 + src/components/icons/Hashtag.tsx | 8 ++ src/components/icons/Heart2.tsx | 7 + src/components/icons/Image.tsx | 7 + src/components/icons/Message.tsx | 8 ++ src/components/icons/PeopleRemove2.tsx | 8 ++ src/components/icons/TEMPLATE.tsx | 45 +++++- src/components/icons/VideoClip.tsx | 8 ++ src/components/icons/common.tsx | 1 + src/locale/locales/en/messages.po | 2 +- src/screens/Bookmarks/index.tsx | 34 ++++- src/screens/Notifications/ActivityList.tsx | 7 +- src/screens/Profile/ProfileFeed/index.tsx | 9 +- src/screens/Profile/Sections/Feed.tsx | 34 ++++- src/screens/ProfileList/AboutSection.tsx | 5 +- src/screens/ProfileList/FeedSection.tsx | 13 +- src/screens/Settings/AppPasswords.tsx | 3 +- .../Settings/components/SettingsList.tsx | 1 + src/view/com/feeds/ProfileFeedgens.tsx | 19 ++- src/view/com/lists/MyLists.tsx | 13 +- src/view/com/lists/ProfileLists.tsx | 83 ++++++----- .../com/notifications/NotificationFeed.tsx | 3 +- src/view/com/posts/PostFeedErrorMessage.tsx | 5 +- src/view/com/profile/ProfileFollowers.tsx | 14 +- src/view/com/profile/ProfileFollows.tsx | 25 +++- src/view/com/util/EmptyState.tsx | 133 +++++++++++------- src/view/screens/Debug.tsx | 11 +- src/view/screens/Profile.tsx | 65 ++++++++- 44 files changed, 551 insertions(+), 124 deletions(-) create mode 100644 assets/icons/bulletlist_stroke1_corner0_rounded.svg create mode 100644 assets/icons/circle_and_square_stroke1_corner0_rounded_filled.svg create mode 100644 assets/icons/editbig_stroke1_corner0_rounded.svg create mode 100644 assets/icons/hashtagwide_stroke1_corner0_rounded.svg create mode 100644 assets/icons/heart2_stroke1_corner0_rounded.svg create mode 100644 assets/icons/image_stroke1_corner0_rounded.svg create mode 100644 assets/icons/message_stroke1_corner0_rounded_filled.svg create mode 100644 assets/icons/peopleremove2_stroke1_corner0_rounded.svg create mode 100644 assets/icons/videoclip_stroke1_corner0_rounded.svg create mode 100644 src/components/icons/CircleAndSquare.tsx diff --git a/__tests__/lib/link-meta.test.ts b/__tests__/lib/link-meta.test.ts index 504b11c22e..bcd3acff47 100644 --- a/__tests__/lib/link-meta.test.ts +++ b/__tests__/lib/link-meta.test.ts @@ -1,4 +1,4 @@ -import {LikelyType, getLikelyType} from '../../src/lib/link-meta/link-meta' +import {getLikelyType, LikelyType} from '../../src/lib/link-meta/link-meta' describe('getLikelyType', () => { it('correctly handles non-parsed url', async () => { diff --git a/assets/icons/bulletlist_stroke1_corner0_rounded.svg b/assets/icons/bulletlist_stroke1_corner0_rounded.svg new file mode 100644 index 0000000000..a626238600 --- /dev/null +++ b/assets/icons/bulletlist_stroke1_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/circle_and_square_stroke1_corner0_rounded_filled.svg b/assets/icons/circle_and_square_stroke1_corner0_rounded_filled.svg new file mode 100644 index 0000000000..cd1465c9b7 --- /dev/null +++ b/assets/icons/circle_and_square_stroke1_corner0_rounded_filled.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/editbig_stroke1_corner0_rounded.svg b/assets/icons/editbig_stroke1_corner0_rounded.svg new file mode 100644 index 0000000000..3c978b04dd --- /dev/null +++ b/assets/icons/editbig_stroke1_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/hashtagwide_stroke1_corner0_rounded.svg b/assets/icons/hashtagwide_stroke1_corner0_rounded.svg new file mode 100644 index 0000000000..97cc78d8dc --- /dev/null +++ b/assets/icons/hashtagwide_stroke1_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/heart2_stroke1_corner0_rounded.svg b/assets/icons/heart2_stroke1_corner0_rounded.svg new file mode 100644 index 0000000000..788c97168b --- /dev/null +++ b/assets/icons/heart2_stroke1_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/image_stroke1_corner0_rounded.svg b/assets/icons/image_stroke1_corner0_rounded.svg new file mode 100644 index 0000000000..68c3215de4 --- /dev/null +++ b/assets/icons/image_stroke1_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/message_stroke1_corner0_rounded_filled.svg b/assets/icons/message_stroke1_corner0_rounded_filled.svg new file mode 100644 index 0000000000..e45f2b2449 --- /dev/null +++ b/assets/icons/message_stroke1_corner0_rounded_filled.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/peopleremove2_stroke1_corner0_rounded.svg b/assets/icons/peopleremove2_stroke1_corner0_rounded.svg new file mode 100644 index 0000000000..a94ed73053 --- /dev/null +++ b/assets/icons/peopleremove2_stroke1_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/videoclip_stroke1_corner0_rounded.svg b/assets/icons/videoclip_stroke1_corner0_rounded.svg new file mode 100644 index 0000000000..44679a173c --- /dev/null +++ b/assets/icons/videoclip_stroke1_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/Button.tsx b/src/components/Button.tsx index efac8468d0..810442fea0 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -798,13 +798,14 @@ export function ButtonIcon({ * also so that we can calculate transforms. */ const iconSize = { - '2xs': 8, xs: 12, sm: 16, md: 18, lg: 24, xl: 28, + '2xs': 8, '2xl': 32, + '3xl': 40, }[iconSizeShorthand] /* diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx index fdfaec64de..24dc0017a4 100644 --- a/src/components/Lists.tsx +++ b/src/components/Lists.tsx @@ -4,6 +4,10 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {cleanError} from '#/lib/strings/errors' +import { + EmptyState, + type EmptyStateButtonProps, +} from '#/view/com/util/EmptyState' import {CenteredView} from '#/view/com/util/Views' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' @@ -129,6 +133,9 @@ let ListMaybePlaceholder = ({ hideBackButton, sideBorders, topBorder = false, + emptyStateIcon, + emptyStateButton, + useEmptyState = false, }: { isLoading: boolean noEmpty?: boolean @@ -143,6 +150,9 @@ let ListMaybePlaceholder = ({ hideBackButton?: boolean sideBorders?: boolean topBorder?: boolean + emptyStateIcon?: React.ComponentType | React.ReactElement + emptyStateButton?: EmptyStateButtonProps + useEmptyState?: boolean }): React.ReactNode => { const t = useTheme() const {_} = useLingui() @@ -180,6 +190,23 @@ let ListMaybePlaceholder = ({ ) } + if (useEmptyState) { + return ( + + + + ) + } + if (!noEmpty) { return ( ( })) const renderPostsEmpty = useCallback(() => { - return + return ( + + ) }, [_]) return ( diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx index ecb5357e0e..b2a0f02989 100644 --- a/src/components/StarterPack/ProfileStarterPacks.tsx +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -21,6 +21,10 @@ import {parseStarterPackUri} from '#/lib/strings/starter-pack' import {logger} from '#/logger' import {isIOS} from '#/platform/detection' import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' +import { + EmptyState, + type EmptyStateButtonProps, +} from '#/view/com/util/EmptyState' import {List, type ListRef} from '#/view/com/util/List' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {atoms as a, ios, useTheme} from '#/alf' @@ -47,6 +51,9 @@ interface ProfileFeedgensProps { testID?: string setScrollViewTag: (tag: number | null) => void isMe: boolean + emptyStateMessage?: string + emptyStateButton?: EmptyStateButtonProps + emptyStateIcon?: React.ComponentType | React.ReactElement } function keyExtractor(item: AppBskyGraphDefs.StarterPackView) { @@ -63,6 +70,9 @@ export function ProfileStarterPacks({ testID, setScrollViewTag, isMe, + emptyStateMessage, + emptyStateButton, + emptyStateIcon, }: ProfileFeedgensProps) { const t = useTheme() const bottomBarOffset = useBottomBarOffset(100) @@ -79,6 +89,28 @@ export function ProfileStarterPacks({ const {isTabletOrDesktop} = useWebMediaQueries() const items = data?.pages.flatMap(page => page.starterPacks) + const {_} = useLingui() + + const EmptyComponent = useCallback(() => { + if (emptyStateMessage || emptyStateButton || emptyStateIcon) { + return ( + + + + ) + } + return + }, [_, emptyStateMessage, emptyStateButton, emptyStateIcon]) useImperativeHandle(ref, () => ({ scrollToTop: () => {}, @@ -146,7 +178,7 @@ export function ProfileStarterPacks({ onEndReached={onEndReached} onRefresh={onRefresh} ListEmptyComponent={ - data ? (isMe ? Empty : undefined) : FeedLoadingPlaceholder + data ? (isMe ? EmptyComponent : undefined) : FeedLoadingPlaceholder } ListFooterComponent={ !!data && items?.length !== 0 && isMe ? CreateAnother : undefined diff --git a/src/components/icons/BulletList.tsx b/src/components/icons/BulletList.tsx index 58847d9d96..120b3a6cb8 100644 --- a/src/components/icons/BulletList.tsx +++ b/src/components/icons/BulletList.tsx @@ -1,5 +1,13 @@ import {createSinglePathSVG} from './TEMPLATE' +export const BulletList_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '0 0 47 38', + strokeLinecap: 'round', + strokeLinejoin: 'round', + strokeWidth: 2, + 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', +}) + export const BulletList_Stroke2_Corner0_Rounded = createSinglePathSVG({ 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', }) diff --git a/src/components/icons/CircleAndSquare.tsx b/src/components/icons/CircleAndSquare.tsx new file mode 100644 index 0000000000..a15a5390e5 --- /dev/null +++ b/src/components/icons/CircleAndSquare.tsx @@ -0,0 +1,7 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Circle_And_Square_Stroke1_Corner0_Rounded_Filled = + createSinglePathSVG({ + viewBox: '0 0 62 53', + 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', + }) diff --git a/src/components/icons/EditBig.tsx b/src/components/icons/EditBig.tsx index 571f38b3e1..3065f99a74 100644 --- a/src/components/icons/EditBig.tsx +++ b/src/components/icons/EditBig.tsx @@ -1,5 +1,10 @@ import {createSinglePathSVG} from './TEMPLATE' +export const EditBig_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '0 0 48 48', + 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', +}) + export const EditBig_Stroke2_Corner0_Rounded = createSinglePathSVG({ 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', }) diff --git a/src/components/icons/Hashtag.tsx b/src/components/icons/Hashtag.tsx index 930484fb21..763266210e 100644 --- a/src/components/icons/Hashtag.tsx +++ b/src/components/icons/Hashtag.tsx @@ -1,5 +1,13 @@ import {createSinglePathSVG} from './TEMPLATE' +export const HashtagWide_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '0 0 46 46', + strokeLinecap: 'round', + strokeLinejoin: 'round', + strokeWidth: 2, + path: 'M14.333 1 9 45M37 1l-5.333 44M1 11.667h44m0 22.666H1', +}) + export const Hashtag_Stroke2_Corner0_Rounded = createSinglePathSVG({ 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', }) diff --git a/src/components/icons/Heart2.tsx b/src/components/icons/Heart2.tsx index 9c9b0be5ba..09b81102db 100644 --- a/src/components/icons/Heart2.tsx +++ b/src/components/icons/Heart2.tsx @@ -1,5 +1,12 @@ import {createSinglePathSVG} from './TEMPLATE' +export const Heart2_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '0 0 51 46', + strokeLinejoin: 'round', + strokeWidth: 2, + 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', +}) + export const Heart2_Stroke2_Corner0_Rounded = createSinglePathSVG({ 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', }) diff --git a/src/components/icons/Image.tsx b/src/components/icons/Image.tsx index eac296ad42..7fc9311a2f 100644 --- a/src/components/icons/Image.tsx +++ b/src/components/icons/Image.tsx @@ -1,5 +1,12 @@ import {createSinglePathSVG} from './TEMPLATE' +export const Image_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '0 0 46 46', + strokeLinecap: 'round', + strokeWidth: 1.5, + 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', +}) + export const Image_Stroke2_Corner0_Rounded = createSinglePathSVG({ 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', }) diff --git a/src/components/icons/Message.tsx b/src/components/icons/Message.tsx index 6d0dd01e76..e3ca70f01b 100644 --- a/src/components/icons/Message.tsx +++ b/src/components/icons/Message.tsx @@ -1,5 +1,13 @@ import {createSinglePathSVG} from './TEMPLATE' +export const Message_Stroke1_Corner0_Rounded_Filled = createSinglePathSVG({ + viewBox: '0 0 51 51', + strokeWidth: 2, + strokeLinecap: 'square', + strokeLinejoin: 'round', + 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', +}) + export const Message_Stroke2_Corner0_Rounded_Filled = createSinglePathSVG({ 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', }) diff --git a/src/components/icons/PeopleRemove2.tsx b/src/components/icons/PeopleRemove2.tsx index 3d16ed9682..39b5386678 100644 --- a/src/components/icons/PeopleRemove2.tsx +++ b/src/components/icons/PeopleRemove2.tsx @@ -1,5 +1,13 @@ import {createSinglePathSVG} from './TEMPLATE' +export const PeopleRemove2_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '-15 0 65 64', + strokeWidth: 2, + strokeLinecap: 'round', + strokeLinejoin: 'round', + 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', +}) + export const PeopleRemove2_Stroke2_Corner0_Rounded = createSinglePathSVG({ 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', }) diff --git a/src/components/icons/TEMPLATE.tsx b/src/components/icons/TEMPLATE.tsx index 7fa9b68100..6bde25d76a 100644 --- a/src/components/icons/TEMPLATE.tsx +++ b/src/components/icons/TEMPLATE.tsx @@ -28,7 +28,50 @@ export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef( }, ) -export function createSinglePathSVG({path}: {path: string}) { +export function createSinglePathSVG({ + path, + viewBox, + strokeWidth = 0, + strokeLinecap = 'butt', + strokeLinejoin = 'miter', +}: { + path: string + viewBox?: string + strokeWidth?: number + strokeLinecap?: 'butt' | 'round' | 'square' + strokeLinejoin?: 'miter' | 'round' | 'bevel' +}) { + return React.forwardRef(function LogoImpl(props, ref) { + const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) + + const hasStroke = strokeWidth > 0 + + return ( + + {gradient} + + + ) + }) +} + +export function createSinglePathSVG2({path}: {path: string}) { return React.forwardRef(function LogoImpl(props, ref) { const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) diff --git a/src/components/icons/VideoClip.tsx b/src/components/icons/VideoClip.tsx index 0a541a418f..0b7d7e1bd0 100644 --- a/src/components/icons/VideoClip.tsx +++ b/src/components/icons/VideoClip.tsx @@ -1,5 +1,13 @@ import {createSinglePathSVG} from './TEMPLATE' +export const VideoClip_Stroke1_Corner0_Rounded = createSinglePathSVG({ + viewBox: '0 0 46 46', + strokeLinecap: 'square', + strokeLinejoin: 'round', + strokeWidth: 2, + 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', +}) + export const VideoClip_Stroke2_Corner0_Rounded = createSinglePathSVG({ 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', }) diff --git a/src/components/icons/common.tsx b/src/components/icons/common.tsx index 0f208240f3..46df59207c 100644 --- a/src/components/icons/common.tsx +++ b/src/components/icons/common.tsx @@ -20,6 +20,7 @@ export const sizes = { lg: 24, xl: 28, '2xl': 32, + '3xl': 48, } as const export function useCommonSVGProps(props: Props) { diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 8122fc565b..10bbf2c542 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -11013,4 +11013,4 @@ msgstr "" #: src/components/verification/VerificationsDialog.tsx:65 msgid "Your verifications" -msgstr "" +msgstr "" \ No newline at end of file diff --git a/src/screens/Bookmarks/index.tsx b/src/screens/Bookmarks/index.tsx index 72ad1f1677..a98756a392 100644 --- a/src/screens/Bookmarks/index.tsx +++ b/src/screens/Bookmarks/index.tsx @@ -7,7 +7,11 @@ import { } from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useFocusEffect} from '@react-navigation/native' +import { + type NavigationProp, + useFocusEffect, + useNavigation, +} from '@react-navigation/native' import {useCleanError} from '#/lib/hooks/useCleanError' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' @@ -21,12 +25,12 @@ import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery' import {useSetMinimalShellMode} from '#/state/shell' import {Post} from '#/view/com/post/Post' +import {EmptyState} from '#/view/com/util/EmptyState' import {List} from '#/view/com/util/List' import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' -import {EmptyState} from '#/screens/Bookmarks/components/EmptyState' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import {BookmarkFilled} from '#/components/icons/Bookmark' +import {BookmarkDeleteLarge, BookmarkFilled} from '#/components/icons/Bookmark' import {CircleQuestion_Stroke2_Corner2_Rounded as QuestionIcon} from '#/components/icons/CircleQuestion' import * as Layout from '#/components/Layout' import {ListFooter} from '#/components/Lists' @@ -259,13 +263,35 @@ function BookmarkNotFound({ ) } +function BookmarksEmpty() { + const t = useTheme() + const {_} = useLingui() + const navigation = useNavigation>() + + return ( + navigation.navigate('Home' as never), + size: 'small', + color: 'secondary', + }} + style={[a.pt_3xl]} + /> + ) +} + function renderItem({item, index}: {item: ListItem; index: number}) { switch (item.type) { case 'loading': { return } case 'empty': { - return + return } case 'bookmark': { return ( diff --git a/src/screens/Notifications/ActivityList.tsx b/src/screens/Notifications/ActivityList.tsx index f87e340082..0b57ccde75 100644 --- a/src/screens/Notifications/ActivityList.tsx +++ b/src/screens/Notifications/ActivityList.tsx @@ -5,6 +5,7 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type AllNavigatorParams} from '#/lib/routes/types' import {PostFeed} from '#/view/com/posts/PostFeed' import {EmptyState} from '#/view/com/util/EmptyState' +import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig' import * as Layout from '#/components/Layout' import {ListFooter} from '#/components/Lists' @@ -35,7 +36,11 @@ export function NotificationsActivityListScreen({ feed={`posts|${uris}`} disablePoll renderEmptyState={() => ( - + )} renderEndOfFeed={() => } /> diff --git a/src/screens/Profile/ProfileFeed/index.tsx b/src/screens/Profile/ProfileFeed/index.tsx index b97fc4ed58..14f7709c0e 100644 --- a/src/screens/Profile/ProfileFeed/index.tsx +++ b/src/screens/Profile/ProfileFeed/index.tsx @@ -45,6 +45,7 @@ import { ProfileFeedHeader, ProfileFeedHeaderSkeleton, } from '#/screens/Profile/components/ProfileFeedHeader' +import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' import * as Layout from '#/components/Layout' type Props = NativeStackScreenProps @@ -189,7 +190,13 @@ export function ProfileFeedScreenInner({ }, [onScrollToTop, isScreenFocused]) const renderPostsEmpty = useCallback(() => { - return + return ( + + ) }, [_]) const isVideoFeed = React.useMemo(() => { diff --git a/src/screens/Profile/Sections/Feed.tsx b/src/screens/Profile/Sections/Feed.tsx index 2f54eda7b3..1591218b78 100644 --- a/src/screens/Profile/Sections/Feed.tsx +++ b/src/screens/Profile/Sections/Feed.tsx @@ -6,14 +6,20 @@ import {useQueryClient} from '@tanstack/react-query' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {isIOS, isNative} from '#/platform/detection' -import {type FeedDescriptor} from '#/state/queries/post-feed' -import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' +import { + type FeedDescriptor, + RQKEY as FEED_RQKEY, +} from '#/state/queries/post-feed' import {truncateAndInvalidate} from '#/state/queries/util' import {PostFeed} from '#/view/com/posts/PostFeed' -import {EmptyState} from '#/view/com/util/EmptyState' +import { + EmptyState, + type EmptyStateButtonProps, +} from '#/view/com/util/EmptyState' import {type ListRef} from '#/view/com/util/List' import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {atoms as a, ios, useTheme} from '#/alf' +import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig' import {Text} from '#/components/Typography' import {type SectionRef} from './types' @@ -25,7 +31,11 @@ interface FeedSectionProps { scrollElRef: ListRef ignoreFilterFor?: string setScrollViewTag: (tag: number | null) => void + emptyStateMessage?: string + emptyStateButton?: EmptyStateButtonProps + emptyStateIcon?: React.ComponentType | React.ReactElement } + export function ProfileFeedSection({ ref, feed, @@ -34,6 +44,9 @@ export function ProfileFeedSection({ scrollElRef, ignoreFilterFor, setScrollViewTag, + emptyStateMessage, + emptyStateButton, + emptyStateIcon, }: FeedSectionProps) { const {_} = useLingui() const queryClient = useQueryClient() @@ -44,7 +57,6 @@ export function ProfileFeedSection({ const adjustedInitialNumToRender = useInitialNumToRender({ screenHeightOffset: headerHeight, }) - const onScrollToTop = useCallback(() => { scrollElRef.current?.scrollToOffset({ animated: isNative, @@ -59,8 +71,18 @@ export function ProfileFeedSection({ })) const renderPostsEmpty = useCallback(() => { - return - }, [_]) + return ( + + + + ) + }, [_, emptyStateButton, emptyStateIcon, emptyStateMessage]) useEffect(() => { if (isIOS && isFocused && scrollElRef.current) { diff --git a/src/screens/ProfileList/AboutSection.tsx b/src/screens/ProfileList/AboutSection.tsx index 47f29b8386..24de3011a3 100644 --- a/src/screens/ProfileList/AboutSection.tsx +++ b/src/screens/ProfileList/AboutSection.tsx @@ -12,6 +12,7 @@ import {type ListRef} from '#/view/com/util/List' import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {atoms as a, useBreakpoints} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' interface SectionRef { @@ -95,7 +96,7 @@ export function AboutSection({ const renderEmptyState = useCallback(() => { return ( - + {isOwner && ( + + )} ) } - -const styles = StyleSheet.create({ - iconContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - height: 80, - width: 80, - marginLeft: 'auto', - marginRight: 'auto', - borderRadius: 80, - marginTop: 30, - }, - iconContainerBig: { - width: 100, - height: 100, - marginTop: 50, - }, - text: { - textAlign: 'center', - paddingTop: 20, - }, -}) diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx index 8b81cee10b..127bdddd76 100644 --- a/src/view/screens/Debug.tsx +++ b/src/view/screens/Debug.tsx @@ -20,6 +20,7 @@ import {Text} from '#/view/com/util/text/Text' import * as Toast from '#/view/com/util/Toast' import {ViewHeader} from '#/view/com/util/ViewHeader' import {ViewSelector} from '#/view/com/util/ViewSelector' +import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' import * as Layout from '#/components/Layout' const MAIN_VIEWS = ['Base', 'Controls', 'Error', 'Notifs'] @@ -333,7 +334,15 @@ function TypographyView() { } function EmptyStateView() { - return + const {_} = useLingui() + + return ( + + ) } function LoadingPlaceholderView() { diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index cc339bb03d..fddde55e19 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -7,17 +7,19 @@ import { type ModerationOpts, RichText as RichTextAPI, } from '@atproto/api' -import {msg} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useFocusEffect} from '@react-navigation/native' +import {useFocusEffect, useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' +import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' import {useSetTitle} from '#/lib/hooks/useSetTitle' import {ComposeIcon2} from '#/lib/icons' import { type CommonNavigatorParams, type NativeStackScreenProps, + type NavigationProp, } from '#/lib/routes/types' import {combinedDisplayName} from '#/lib/strings/display-names' import {cleanError} from '#/lib/strings/errors' @@ -42,6 +44,11 @@ import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' import {atoms as a} from '#/alf' +import {Circle_And_Square_Stroke1_Corner0_Rounded_Filled as CircleAndSquareIcon} from '#/components/icons/CircleAndSquare' +import {Heart2_Stroke1_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2' +import {Image_Stroke1_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' +import {Message_Stroke1_Corner0_Rounded_Filled as MessageIcon} from '#/components/icons/Message' +import {VideoClip_Stroke1_Corner0_Rounded as VideoIcon} from '#/components/icons/VideoClip' import * as Layout from '#/components/Layout' import {ScreenHider} from '#/components/moderation/ScreenHider' import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks' @@ -169,6 +176,8 @@ function ProfileScreenLoaded({ const {hasSession, currentAccount} = useSession() const setMinimalShellMode = useSetMinimalShellMode() const {openComposer} = useOpenComposer() + const navigation = useNavigation() + const requireEmailVerification = useRequireEmailVerification() const { data: labelerInfo, error: labelerError, @@ -334,6 +343,17 @@ function ProfileScreenLoaded({ scrollSectionToTop(index) } + const navToWizard = useCallback(() => { + navigation.navigate('StarterPackWizard', {}) + }, [navigation]) + const wrappedNavToWizard = requireEmailVerification(navToWizard, { + instructions: [ + + Before creating a starter pack, you must first verify your email. + , + ], + }) + // rendering // = @@ -408,6 +428,14 @@ function ProfileScreenLoaded({ scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} setScrollViewTag={setScrollViewTag} + emptyStateMessage={_(msg`No posts yet`)} + emptyStateButton={{ + label: _(msg`Write a post`), + text: _(msg`Write a post`), + onPress: () => openComposer({}), + size: 'small', + color: 'primary', + }} /> ) : null} @@ -421,6 +449,8 @@ function ProfileScreenLoaded({ scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} setScrollViewTag={setScrollViewTag} + emptyStateMessage={_(msg`No replies yet`)} + emptyStateIcon={MessageIcon} /> ) : null} @@ -434,6 +464,15 @@ function ProfileScreenLoaded({ scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} setScrollViewTag={setScrollViewTag} + emptyStateMessage={_(msg`No media yet`)} + emptyStateButton={{ + label: _(msg`Post a photo`), + text: _(msg`Post a photo`), + onPress: () => openComposer({}), + size: 'small', + color: 'primary', + }} + emptyStateIcon={ImageIcon} /> ) : null} @@ -447,6 +486,15 @@ function ProfileScreenLoaded({ scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} setScrollViewTag={setScrollViewTag} + emptyStateMessage={_(msg`No video posts yet`)} + emptyStateButton={{ + label: _(msg`Post a video`), + text: _(msg`Post a video`), + onPress: () => openComposer({}), + size: 'small', + color: 'primary', + }} + emptyStateIcon={VideoIcon} /> ) : null} @@ -460,6 +508,8 @@ function ProfileScreenLoaded({ scrollElRef={scrollElRef as ListRef} ignoreFilterFor={profile.did} setScrollViewTag={setScrollViewTag} + emptyStateMessage={_(msg`No likes yet`)} + emptyStateIcon={HeartIcon} /> ) : null} @@ -485,6 +535,17 @@ function ProfileScreenLoaded({ headerOffset={headerHeight} enabled={isFocused} setScrollViewTag={setScrollViewTag} + emptyStateMessage={_( + msg`Starter packs let you share your favorite feeds and people with your friends.`, + )} + emptyStateButton={{ + label: _(msg`Create a Starter Pack`), + text: _(msg`Create a Starter Pack`), + onPress: wrappedNavToWizard, + color: 'primary', + size: 'small', + }} + emptyStateIcon={CircleAndSquareIcon} /> ) : null}