empty states

This commit is contained in:
Samuel Newman
2026-05-11 16:04:34 +03:00
parent 5ea12b6adb
commit 73092633a0
11 changed files with 97 additions and 43 deletions
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 64 64"><path fill="#111822" d="M55 32C55 19.298 44.703 9 32 9S9 19.298 9 32c0 3.463.765 6.745 2.134 9.688.358.769.479 1.658.267 2.525l-2.076 8.51a1.667 1.667 0 0 0 1.987 2.02l8.876-2.005a3.84 3.84 0 0 1 2.412.26A22.9 22.9 0 0 0 32 55c12.703 0 23-10.297 23-23Zm2 0c0 13.807-11.193 25-25 25-3.638 0-7.098-.778-10.219-2.177a1.84 1.84 0 0 0-1.152-.133l-8.877 2.004c-2.655.6-5.015-1.802-4.37-4.446l2.077-8.51a1.84 1.84 0 0 0-.139-1.207A24.9 24.9 0 0 1 7 32C7 18.193 18.193 7 32 7s25 11.193 25 25Z"/><path fill="#111822" d="M17.667 32a2.333 2.333 0 1 0 4.667 0 2.333 2.333 0 0 0-4.667 0Zm24 0a2.334 2.334 0 1 0 4.667 0 2.334 2.334 0 0 0-4.667 0Z"/><path fill="#000" d="M35.137 37.215a1 1 0 0 1 1.414 1.414 6.143 6.143 0 0 1-8.687 0 1 1 0 0 1 1.415-1.414 4.14 4.14 0 0 0 5.858 0Z"/></svg>

After

Width:  |  Height:  |  Size: 847 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 64 64"><path fill="#111822" d="M57.666 33.416q-.001-.21-.022-.419L57.6 33H44a4.33 4.33 0 0 0-3.466 1.734l-.8 1.066a6.34 6.34 0 0 1-5.068 2.533h-5.333a6.33 6.33 0 0 1-5.066-2.533l-.8-1.066A4.33 4.33 0 0 0 20 33H6.4l-.045-.003a4 4 0 0 0-.022.42v11.916a4.333 4.333 0 0 0 4.333 4.333h42.667a4.333 4.333 0 0 0 4.333-4.333V33.416Zm-38.57-19.083a4.33 4.33 0 0 0-3.764 2.184L7.057 31H20c1.994 0 3.87.939 5.067 2.533l.8 1.067a4.33 4.33 0 0 0 3.466 1.733h5.333a4.33 4.33 0 0 0 3.467-1.733l.8-1.067A6.34 6.34 0 0 1 44 31h12.944l-8.277-14.483a4.33 4.33 0 0 0-3.762-2.184h-25.81Zm40.57 31a6.333 6.333 0 0 1-6.333 6.333H10.666a6.333 6.333 0 0 1-6.333-6.333V33.416c0-1.102.288-2.185.835-3.142l8.428-14.75a6.33 6.33 0 0 1 5.5-3.19h25.809a6.33 6.33 0 0 1 5.499 3.19l8.427 14.75h.001a6.33 6.33 0 0 1 .834 3.142v11.917Z"/></svg>

After

Width:  |  Height:  |  Size: 875 B

+2 -1
View File
@@ -811,14 +811,15 @@ 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,
'4xl': 48,
}[iconSizeShorthand]
/*
+10 -1
View File
@@ -1,4 +1,4 @@
import {createSinglePathSVG} from './TEMPLATE'
import {createMultiPathSVG, createSinglePathSVG} from './TEMPLATE'
export const BubbleQuestion_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z',
@@ -15,3 +15,12 @@ export const Bubble_Stroke2_Corner3_Rounded = createSinglePathSVG({
export const Bubbles_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M6.002 6a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-1v1a3 3 0 0 1-3 3h-4.24l-4.274 2.374a1 1 0 0 1-1.486-.874V19a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3h1V6Zm-1 3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1a1 1 0 0 1 1 1v.8l3.015-1.674a1 1 0 0 1 .485-.126h4.5a1 1 0 0 0 1-1v-1.933a1 1 0 0 1 0-.134V10a1 1 0 0 0-1-1h-10Zm13 4v-3a3 3 0 0 0-3-3h-7V6a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-1Z',
})
export const BubbleSmile_Stroke2_Corner2_Rounded_Large = createMultiPathSVG({
viewBox: '0 0 64 64',
paths: [
'M55 32C55 19.298 44.703 9 32 9S9 19.298 9 32c0 3.463.765 6.745 2.134 9.688.358.769.479 1.658.267 2.525l-2.076 8.51a1.667 1.667 0 0 0 1.987 2.02l8.876-2.005a3.84 3.84 0 0 1 2.412.26A22.9 22.9 0 0 0 32 55c12.703 0 23-10.297 23-23Zm2 0c0 13.807-11.193 25-25 25-3.638 0-7.098-.778-10.219-2.177a1.84 1.84 0 0 0-1.152-.133l-8.877 2.004c-2.655.6-5.015-1.802-4.37-4.446l2.077-8.51c.094-.384.046-.809-.139-1.207A24.9 24.9 0 0 1 7 32C7 18.193 18.193 7 32 7s25 11.193 25 25Z',
'M17.667 32a2.333 2.333 0 1 0 4.667 0 2.333 2.333 0 0 0-4.667 0Zm24 0a2.334 2.334 0 1 0 4.667 0 2.334 2.334 0 0 0-4.667 0Z',
'M35.137 37.215a1 1 0 0 1 1.414 1.414 6.143 6.143 0 0 1-8.687 0 1 1 0 0 1 1.415-1.414 4.14 4.14 0 0 0 5.858 0Z',
],
})
+5
View File
@@ -3,3 +3,8 @@ import {createSinglePathSVG} from './TEMPLATE'
export const Inbox_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M19 14h-2.417a5 5 0 0 1-9.166 0H5v4a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-4Zm0-8a1 1 0 0 0-.898-.995L18 5H6a1 1 0 0 0-1 1v6h3.126a1 1 0 0 1 .969.751 3.001 3.001 0 0 0 5.81 0l.056-.16a1 1 0 0 1 .913-.591H19V6Zm2 12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12l.154.004A3 3 0 0 1 21 6v12Z',
})
export const Inbox_Stroke2_Corner2_Rounded_Large = createSinglePathSVG({
viewBox: '0 0 64 64',
path: 'M57.666 33.416q-.001-.21-.022-.419L57.6 33H44a4.33 4.33 0 0 0-3.466 1.734l-.8 1.066a6.34 6.34 0 0 1-5.068 2.533h-5.333a6.33 6.33 0 0 1-5.066-2.533l-.8-1.066A4.33 4.33 0 0 0 20 33H6.4l-.045-.003a4 4 0 0 0-.022.42v11.916a4.333 4.333 0 0 0 4.333 4.333h42.667a4.333 4.333 0 0 0 4.333-4.333V33.416Zm-38.57-19.083a4.33 4.33 0 0 0-3.764 2.184L7.057 31H20c1.994 0 3.87.939 5.067 2.533l.8 1.067a4.33 4.33 0 0 0 3.466 1.733h5.333a4.33 4.33 0 0 0 3.467-1.733l.8-1.067A6.34 6.34 0 0 1 44 31h12.944l-8.277-14.483a4.33 4.33 0 0 0-3.762-2.184h-25.81Zm40.57 31a6.333 6.333 0 0 1-6.333 6.333H10.666a6.333 6.333 0 0 1-6.333-6.333V33.416c0-1.102.288-2.185.835-3.142l8.428-14.75a6.33 6.33 0 0 1 5.5-3.19h25.809a6.33 6.33 0 0 1 5.499 3.19l8.427 14.75h.001c.547.957.834 2.04.834 3.142v11.917Z',
})
+9 -23
View File
@@ -51,7 +51,7 @@ export function createSinglePathSVG({
fill="none"
{...rest}
ref={ref}
viewBox={viewBox || '0 0 24 24'}
viewBox={viewBox ?? '0 0 24 24'}
width={size}
height={size}
style={[style]}>
@@ -71,7 +71,13 @@ export function createSinglePathSVG({
})
}
export function createSinglePathSVG2({path}: {path: string}) {
export function createMultiPathSVG({
paths,
viewBox,
}: {
paths: string[]
viewBox?: string
}) {
return forwardRef<Svg, Props>(function LogoImpl(props, ref) {
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
@@ -80,27 +86,7 @@ export function createSinglePathSVG2({path}: {path: string}) {
fill="none"
{...rest}
ref={ref}
viewBox="0 0 24 24"
width={size}
height={size}
style={style}>
{gradient}
<Path fill={fill} fillRule="evenodd" clipRule="evenodd" d={path} />
</Svg>
)
})
}
export function createMultiPathSVG({paths}: {paths: string[]}) {
return forwardRef<Svg, Props>(function LogoImpl(props, ref) {
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
return (
<Svg
fill="none"
{...rest}
ref={ref}
viewBox="0 0 24 24"
viewBox={viewBox ?? '0 0 24 24'}
width={size}
height={size}
style={[style]}>
+1
View File
@@ -21,6 +21,7 @@ export const sizes = {
xl: 28,
'2xl': 32,
'3xl': 48,
'4xl': 64,
} as const
export function useCommonSVGProps(props: Props) {
+49 -11
View File
@@ -28,9 +28,11 @@ import {type DialogControlProps, useDialogControl} from '#/components/Dialog'
import {NewChat} from '#/components/dms/dialogs/NewChatDialog'
import {useRefreshOnFocus} from '#/components/hooks/useRefreshOnFocus'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate'
import {BubbleSmile_Stroke2_Corner2_Rounded_Large as BubbleSmileIcon} from '#/components/icons/Bubble'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
import {Inbox_Stroke2_Corner2_Rounded_Large as InboxLargeIcon} from '#/components/icons/Inbox'
import {
Message_Stroke2_Corner0_Rounded as MessageIcon,
MessagePlus_Stroke2_Corner0_Rounded as MessagePlusIcon,
MessagePlus_Stroke2_Corner0_Rounded as NewChatIcon,
} from '#/components/icons/Message'
import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
@@ -90,6 +92,7 @@ export function MessagesScreen(props: Props) {
export function MessagesScreenInner({navigation, route}: Props) {
const {isWithinSplitView} = useIsWithinSplitView()
const {t: l} = useLingui()
const t = useTheme()
const newChatControl = useDialogControl()
const pushToConversation = route.params?.pushToConversation
@@ -132,15 +135,18 @@ export function MessagesScreenInner({navigation, route}: Props) {
return (
<>
<EmptyState
message={l`Start a conversation`}
icon={MessageIcon}
iconSize="3xl"
message={l`Say hi to someone`}
icon={BubbleSmileIcon}
textStyle={t.atoms.text}
iconColor={t.atoms.text.color}
iconSize="4xl"
button={{
label: l`New chat`,
text: l`New chat`,
onPress: newChatControl.open,
size: 'small',
color: 'secondary_inverted',
color: 'primary',
icon: MessagePlusIcon,
}}
style={[a.h_full, a.justify_center, a.pb_5xl]}
/>
@@ -152,18 +158,37 @@ export function MessagesScreenInner({navigation, route}: Props) {
return (
<Layout.Screen testID="messagesScreen">
<Header newChatControl={newChatControl} />
<ChatList />
<ChatList newChatControl={newChatControl} />
<NewChat onNewChat={onNewChat} control={newChatControl} />
</Layout.Screen>
)
}
export function ChatList({selectedChat}: {selectedChat?: string}) {
export function ChatList({
selectedChat,
newChatControl,
}: {
selectedChat?: string
newChatControl: DialogControlProps
}) {
const t = useTheme()
const {t: l} = useLingui()
const scrollElRef: ListRef = useAnimatedRef()
const {isWithinSplitView} = useIsWithinSplitView()
const openChatControl = useCallback(() => {
newChatControl.open()
}, [newChatControl])
const requireEmailVerification = useRequireEmailVerification()
const wrappedOpenChatControl = requireEmailVerification(openChatControl, {
instructions: [
<Trans key="new-chat">
Before you can message another user, you must first verify your email.
</Trans>,
],
})
const initialNumToRender = useInitialNumToRender({minItemHeight: 80})
const [isPTRing, setIsPTRing] = useState(false)
@@ -289,14 +314,27 @@ export function ChatList({selectedChat}: {selectedChat?: string}) {
</>
) : isWithinSplitView ? (
<EmptyState
message={l`Your conversations will appear here`}
icon={MessageIcon}
message={l`Inbox empty`}
icon={InboxLargeIcon}
iconSize="4xl"
textStyle={t.atoms.text}
iconColor={t.atoms.text.color}
/>
) : (
<EmptyState
message={l`No chats yet`}
icon={MessageIcon}
iconSize="3xl"
icon={InboxLargeIcon}
iconSize="4xl"
textStyle={t.atoms.text}
iconColor={t.atoms.text.color}
button={{
label: l`New chat`,
text: l`New chat`,
onPress: wrappedOpenChatControl,
size: 'small',
color: 'primary',
icon: MessagePlusIcon,
}}
/>
)}
</>
@@ -94,7 +94,10 @@ function MessagesSplitViewLayout({children, navigation, route}: LayoutProps) {
{width: centerColumnOffset ? 300 : 350},
]}>
<ChatListHeader newChatControl={newChatControl} />
<ChatList selectedChat={selectedChat} />
<ChatList
newChatControl={newChatControl}
selectedChat={selectedChat}
/>
<NewChat onNewChat={onNewChat} control={newChatControl} />
</View>
</SplitViewProvider>
@@ -197,11 +197,12 @@ export function ItemIcon({
'2xs': 8,
xs: 12,
sm: 16,
md: 20,
md: 18,
lg: 24,
xl: 28,
'2xl': 32,
'3xl': 40,
'4xl': 48,
}[size]
const color =
+13 -5
View File
@@ -3,19 +3,26 @@ import {type StyleProp, type TextStyle, type ViewStyle} from 'react-native'
import {View} from 'react-native'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, type ButtonProps, ButtonText} from '#/components/Button'
import {
Button,
ButtonIcon,
type ButtonProps,
ButtonText,
} from '#/components/Button'
import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
import {Text} from '#/components/Typography'
export type EmptyStateButtonProps = Omit<ButtonProps, 'children' | 'label'> & {
label: string
text: string
icon?: React.ComponentProps<typeof ButtonIcon>['icon']
}
export function EmptyState({
testID,
icon,
iconSize = '3xl',
iconColor,
message,
style,
textStyle,
@@ -23,7 +30,8 @@ export function EmptyState({
}: {
testID?: string
icon?: React.ComponentType<any> | React.ReactElement | null
iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'
iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'
iconColor?: string
message: string
style?: StyleProp<ViewStyle>
textStyle?: StyleProp<TextStyle>
@@ -57,8 +65,7 @@ export function EmptyState({
return (
<IconComponent
size={iconSize}
fill={t.atoms.text_contrast_medium.color}
style={{color: t.atoms.text_contrast_low.color}}
style={{color: iconColor ?? t.atoms.text_contrast_low.color}}
/>
)
}
@@ -99,8 +106,9 @@ export function EmptyState({
{message}
</Text>
{button && (
<View style={[a.flex_shrink, a.mt_xl, a.self_center, a.mb_5xl]}>
<View style={[a.flex_shrink, a.mt_md, a.self_center, a.mb_5xl]}>
<Button {...button}>
{button.icon && <ButtonIcon icon={button.icon} />}
<ButtonText>{button.text}</ButtonText>
</Button>
</View>