Merge remote-tracking branch 'origin/main' into app-1310/design

* origin/main:
  [APP-1310] Button cleanup (#8754)
  Some toasts cleanup and reorg (#8748)
  tweak case of labels in SearchResults.tsx (#8759)
  Make proper extension of `Button` more clearly defined (#8753)
  Nightly source-language update
  build
  fix echoprom config
  Fix bad substitution (#8752)
This commit is contained in:
Eric Bailey
2025-07-31 11:01:54 -05:00
21 changed files with 756 additions and 724 deletions
@@ -3,6 +3,7 @@ on:
push: push:
branches: branches:
- main - main
- echoprom_fix
env: env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
+7 -1
View File
@@ -154,7 +154,13 @@ func serve(cctx *cli.Context) error {
RedirectCode: http.StatusFound, RedirectCode: http.StatusFound,
})) }))
e.Use(echoprometheus.NewMiddleware("")) echoprom := echoprometheus.NewMiddlewareWithConfig(
echoprometheus.MiddlewareConfig{
DoNotUseRequestPathFor404: true,
},
)
e.Use(echoprom)
// //
// configure routes // configure routes
+1 -1
View File
@@ -50,7 +50,6 @@ import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index' import {Shell} from '#/view/shell/index'
import {ThemeProvider as Alf} from '#/alf' import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
@@ -61,6 +60,7 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext' import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {ToastContainer} from '#/components/Toast'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
+12
View File
@@ -19,6 +19,18 @@ import {atoms as a, flatten, select, useTheme} from '#/alf'
import {type Props as SVGIconProps} from '#/components/icons/common' import {type Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
/**
* The `Button` component, and some extensions of it like `Link` are intended
* to be generic and therefore apply no styles by default. These `VariantProps`
* are what control the `Button`'s presentation, and are intended only use cases where the buttons appear as, well, buttons.
*
* If `Button` or an extension of it are used for other compound components, use this property to avoid misuse of these variant props further down the line.
*
* @example
* type MyComponentProps = Omit<ButtonProps, UninheritableButtonProps> & {...}
*/
export type UninheritableButtonProps = 'variant' | 'color' | 'size' | 'shape'
export type ButtonVariant = 'solid' | 'outline' | 'ghost' export type ButtonVariant = 'solid' | 'outline' | 'ghost'
export type ButtonColor = export type ButtonColor =
| 'primary' | 'primary'
+205
View File
@@ -0,0 +1,205 @@
import {createContext, useContext, useMemo} from 'react'
import {View} from 'react-native'
import {atoms as a, select, useTheme} from '#/alf'
import {Check_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {type ToastType} from '#/components/Toast/types'
import {Text} from '#/components/Typography'
type ContextType = {
type: ToastType
}
export const ICONS = {
default: SuccessIcon,
success: SuccessIcon,
error: ErrorIcon,
warning: WarningIcon,
info: CircleInfo,
}
const Context = createContext<ContextType>({
type: 'default',
})
export function Toast({
type,
content,
}: {
type: ToastType
content: React.ReactNode
}) {
const t = useTheme()
const styles = useToastStyles({type})
const Icon = ICONS[type]
return (
<Context.Provider value={useMemo(() => ({type}), [type])}>
<View
style={[
a.flex_1,
a.py_lg,
a.pl_xl,
a.pr_2xl,
a.rounded_md,
a.border,
a.flex_row,
a.gap_sm,
t.atoms.shadow_sm,
{
backgroundColor: styles.backgroundColor,
borderColor: styles.borderColor,
},
]}>
<Icon size="md" fill={styles.iconColor} />
<View style={[a.flex_1]}>
{typeof content === 'string' ? (
<ToastText>{content}</ToastText>
) : (
content
)}
</View>
</View>
</Context.Provider>
)
}
export function ToastText({children}: {children: React.ReactNode}) {
const {type} = useContext(Context)
const {textColor} = useToastStyles({type})
return (
<Text
style={[
a.text_md,
a.font_bold,
a.leading_snug,
{
color: textColor,
},
]}>
{children}
</Text>
)
}
function useToastStyles({type}: {type: ToastType}) {
const t = useTheme()
return useMemo(() => {
return {
default: {
backgroundColor: select(t.name, {
light: t.atoms.bg_contrast_25.backgroundColor,
dim: t.atoms.bg_contrast_100.backgroundColor,
dark: t.atoms.bg_contrast_100.backgroundColor,
}),
borderColor: select(t.name, {
light: t.atoms.border_contrast_low.borderColor,
dim: t.atoms.border_contrast_high.borderColor,
dark: t.atoms.border_contrast_high.borderColor,
}),
iconColor: select(t.name, {
light: t.atoms.text_contrast_medium.color,
dim: t.atoms.text_contrast_medium.color,
dark: t.atoms.text_contrast_medium.color,
}),
textColor: select(t.name, {
light: t.atoms.text_contrast_medium.color,
dim: t.atoms.text_contrast_medium.color,
dark: t.atoms.text_contrast_medium.color,
}),
},
success: {
backgroundColor: select(t.name, {
light: t.palette.primary_100,
dim: t.palette.primary_100,
dark: t.palette.primary_50,
}),
borderColor: select(t.name, {
light: t.palette.primary_500,
dim: t.palette.primary_500,
dark: t.palette.primary_500,
}),
iconColor: select(t.name, {
light: t.palette.primary_500,
dim: t.palette.primary_600,
dark: t.palette.primary_600,
}),
textColor: select(t.name, {
light: t.palette.primary_500,
dim: t.palette.primary_600,
dark: t.palette.primary_600,
}),
},
error: {
backgroundColor: select(t.name, {
light: t.palette.negative_200,
dim: t.palette.negative_25,
dark: t.palette.negative_25,
}),
borderColor: select(t.name, {
light: t.palette.negative_300,
dim: t.palette.negative_300,
dark: t.palette.negative_300,
}),
iconColor: select(t.name, {
light: t.palette.negative_600,
dim: t.palette.negative_600,
dark: t.palette.negative_600,
}),
textColor: select(t.name, {
light: t.palette.negative_600,
dim: t.palette.negative_600,
dark: t.palette.negative_600,
}),
},
warning: {
backgroundColor: select(t.name, {
light: t.atoms.bg_contrast_25.backgroundColor,
dim: t.atoms.bg_contrast_100.backgroundColor,
dark: t.atoms.bg_contrast_100.backgroundColor,
}),
borderColor: select(t.name, {
light: t.atoms.border_contrast_low.borderColor,
dim: t.atoms.border_contrast_high.borderColor,
dark: t.atoms.border_contrast_high.borderColor,
}),
iconColor: select(t.name, {
light: t.atoms.text_contrast_medium.color,
dim: t.atoms.text_contrast_medium.color,
dark: t.atoms.text_contrast_medium.color,
}),
textColor: select(t.name, {
light: t.atoms.text_contrast_medium.color,
dim: t.atoms.text_contrast_medium.color,
dark: t.atoms.text_contrast_medium.color,
}),
},
info: {
backgroundColor: select(t.name, {
light: t.atoms.bg_contrast_25.backgroundColor,
dim: t.atoms.bg_contrast_100.backgroundColor,
dark: t.atoms.bg_contrast_100.backgroundColor,
}),
borderColor: select(t.name, {
light: t.atoms.border_contrast_low.borderColor,
dim: t.atoms.border_contrast_high.borderColor,
dark: t.atoms.border_contrast_high.borderColor,
}),
iconColor: select(t.name, {
light: t.atoms.text_contrast_medium.color,
dim: t.atoms.text_contrast_medium.color,
dark: t.atoms.text_contrast_medium.color,
}),
textColor: select(t.name, {
light: t.atoms.text_contrast_medium.color,
dim: t.atoms.text_contrast_medium.color,
dark: t.atoms.text_contrast_medium.color,
}),
},
}[type]
}, [t, type])
}
+1
View File
@@ -0,0 +1 @@
export const DEFAULT_TOAST_DURATION = 3000
+5
View File
@@ -0,0 +1,5 @@
export function ToastContainer() {
return null
}
export function show() {}
+197
View File
@@ -0,0 +1,197 @@
import {useEffect, useMemo, useRef, useState} from 'react'
import {AccessibilityInfo} from 'react-native'
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
} from 'react-native-gesture-handler'
import Animated, {
Easing,
runOnJS,
SlideInUp,
SlideOutUp,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withDecay,
withSpring,
} from 'react-native-reanimated'
import RootSiblings from 'react-native-root-siblings'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {atoms as a} from '#/alf'
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
import {Toast} from '#/components/Toast/Toast'
import {type ToastApi, type ToastType} from '#/components/Toast/types'
const TOAST_ANIMATION_DURATION = 300
export function ToastContainer() {
return null
}
export const toast: ToastApi = {
show(props) {
if (process.env.NODE_ENV === 'test') {
return
}
AccessibilityInfo.announceForAccessibility(props.a11yLabel)
const item = new RootSiblings(
(
<AnimatedToast
type={props.type}
content={props.content}
a11yLabel={props.a11yLabel}
duration={props.duration ?? DEFAULT_TOAST_DURATION}
destroy={() => item.destroy()}
/>
),
)
},
}
function AnimatedToast({
type,
content,
a11yLabel,
duration,
destroy,
}: {
type: ToastType
content: React.ReactNode
a11yLabel: string
duration: number
destroy: () => void
}) {
const {top} = useSafeAreaInsets()
const isPanning = useSharedValue(false)
const dismissSwipeTranslateY = useSharedValue(0)
const [cardHeight, setCardHeight] = useState(0)
// for the exit animation to work on iOS the animated component
// must not be the root component
// so we need to wrap it in a view and unmount the toast ahead of time
const [alive, setAlive] = useState(true)
const hideAndDestroyImmediately = () => {
setAlive(false)
setTimeout(() => {
destroy()
}, 1e3)
}
const destroyTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
const hideAndDestroyAfterTimeout = useNonReactiveCallback(() => {
clearTimeout(destroyTimeoutRef.current)
destroyTimeoutRef.current = setTimeout(hideAndDestroyImmediately, duration)
})
const pauseDestroy = useNonReactiveCallback(() => {
clearTimeout(destroyTimeoutRef.current)
})
useEffect(() => {
hideAndDestroyAfterTimeout()
}, [hideAndDestroyAfterTimeout])
const panGesture = useMemo(() => {
return Gesture.Pan()
.activeOffsetY([-10, 10])
.failOffsetX([-10, 10])
.maxPointers(1)
.onStart(() => {
'worklet'
if (!alive) return
isPanning.set(true)
runOnJS(pauseDestroy)()
})
.onUpdate(e => {
'worklet'
if (!alive) return
dismissSwipeTranslateY.value = e.translationY
})
.onEnd(e => {
'worklet'
if (!alive) return
runOnJS(hideAndDestroyAfterTimeout)()
isPanning.set(false)
if (e.velocityY < -100) {
if (dismissSwipeTranslateY.value === 0) {
// HACK: If the initial value is 0, withDecay() animation doesn't start.
// This is a bug in Reanimated, but for now we'll work around it like this.
dismissSwipeTranslateY.value = 1
}
dismissSwipeTranslateY.value = withDecay({
velocity: e.velocityY,
velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1),
deceleration: 1,
})
} else {
dismissSwipeTranslateY.value = withSpring(0, {
stiffness: 500,
damping: 50,
})
}
})
}, [
dismissSwipeTranslateY,
isPanning,
alive,
hideAndDestroyAfterTimeout,
pauseDestroy,
])
const topOffset = top + 10
useAnimatedReaction(
() =>
!isPanning.get() &&
dismissSwipeTranslateY.get() < -topOffset - cardHeight,
(isSwipedAway, prevIsSwipedAway) => {
'worklet'
if (isSwipedAway && !prevIsSwipedAway) {
runOnJS(destroy)()
}
},
)
const animatedStyle = useAnimatedStyle(() => {
const translation = dismissSwipeTranslateY.get()
return {
transform: [
{
translateY: translation > 0 ? translation ** 0.7 : translation,
},
],
}
})
return (
<GestureHandlerRootView
style={[a.absolute, {top: topOffset, left: 16, right: 16}]}
pointerEvents="box-none">
{alive && (
<Animated.View
entering={SlideInUp.easing(Easing.out(Easing.exp)).duration(
TOAST_ANIMATION_DURATION,
)}
exiting={SlideOutUp.easing(Easing.in(Easing.exp)).duration(
TOAST_ANIMATION_DURATION * 0.7,
)}
onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)}
accessibilityRole="alert"
accessible={true}
accessibilityLabel={a11yLabel}
accessibilityHint=""
onAccessibilityEscape={hideAndDestroyImmediately}
style={[a.flex_1, animatedStyle]}>
<GestureDetector gesture={panGesture}>
<Toast content={content} type={type} />
</GestureDetector>
</Animated.View>
)}
</GestureHandlerRootView>
)
}
+107
View File
@@ -0,0 +1,107 @@
/*
* Note: relies on styles in #/styles.css
*/
import {useEffect, useState} from 'react'
import {AccessibilityInfo, Pressable, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useBreakpoints} from '#/alf'
import {DEFAULT_TOAST_DURATION} from '#/components/Toast/const'
import {Toast} from '#/components/Toast/Toast'
import {type ToastApi, type ToastType} from '#/components/Toast/types'
const TOAST_ANIMATION_STYLES = {
entering: {
animation: 'toastFadeIn 0.3s ease-out forwards',
},
exiting: {
animation: 'toastFadeOut 0.2s ease-in forwards',
},
}
interface ActiveToast {
type: ToastType
content: React.ReactNode
a11yLabel: string
}
type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
let globalSetActiveToast: GlobalSetActiveToast | undefined
let toastTimeout: NodeJS.Timeout | undefined
type ToastContainerProps = {}
export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
const {_} = useLingui()
const {gtPhone} = useBreakpoints()
const [activeToast, setActiveToast] = useState<ActiveToast | undefined>()
const [isExiting, setIsExiting] = useState(false)
useEffect(() => {
globalSetActiveToast = (t: ActiveToast | undefined) => {
if (!t && activeToast) {
setIsExiting(true)
setTimeout(() => {
setActiveToast(t)
setIsExiting(false)
}, 200)
} else {
if (t) {
AccessibilityInfo.announceForAccessibility(t.a11yLabel)
}
setActiveToast(t)
setIsExiting(false)
}
}
}, [activeToast])
return (
<>
{activeToast && (
<View
style={[
a.fixed,
{
left: a.px_xl.paddingLeft,
right: a.px_xl.paddingLeft,
bottom: a.px_xl.paddingLeft,
...(isExiting
? TOAST_ANIMATION_STYLES.exiting
: TOAST_ANIMATION_STYLES.entering),
},
gtPhone && [
{
maxWidth: 380,
},
],
]}>
<Toast content={activeToast.content} type={activeToast.type} />
<Pressable
style={[a.absolute, a.inset_0]}
accessibilityLabel={_(msg`Dismiss toast`)}
accessibilityHint=""
onPress={() => setActiveToast(undefined)}
/>
</View>
)}
</>
)
}
export const toast: ToastApi = {
show(props) {
if (toastTimeout) {
clearTimeout(toastTimeout)
}
globalSetActiveToast?.({
type: props.type,
content: props.content,
a11yLabel: props.a11yLabel,
})
toastTimeout = setTimeout(() => {
globalSetActiveToast?.(undefined)
}, props.duration || DEFAULT_TOAST_DURATION)
},
}
+24
View File
@@ -0,0 +1,24 @@
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info'
export type ToastApi = {
show: (props: {
/**
* The type of toast to show. This determines the styling and icon used.
*/
type: ToastType
/**
* A string, `Text`, or `Span` components to render inside the toast. This
* allows additional formatting of the content, but should not be used for
* interactive elements link links or buttons.
*/
content: React.ReactNode | string
/**
* Accessibility label for the toast, used for screen readers.
*/
a11yLabel: string
/**
* Defaults to `DEFAULT_TOAST_DURATION` from `#components/Toast/const`.
*/
duration?: number
}) => void
}
+4 -3
View File
@@ -43,9 +43,10 @@ export const BUNDLE_IDENTIFIER: string =
* for each build. This should only be used for StatSig reporting and shouldn't * for each build. This should only be used for StatSig reporting and shouldn't
* be used to identify a specific bundle. * be used to identify a specific bundle.
*/ */
export const BUNDLE_DATE: number = !process.env.EXPO_PUBLIC_BUNDLE_DATE export const BUNDLE_DATE: number =
? 0 process.env.EXPO_PUBLIC_BUNDLE_DATE === undefined
: Number(process.env.EXPO_PUBLIC_BUNDLE_DATE) ? 0
: Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
/** /**
* The log level for the app. * The log level for the app.
+18 -18
View File
@@ -1239,7 +1239,7 @@ msgstr ""
#: src/components/dms/dialogs/NewChatDialog.tsx:54 #: src/components/dms/dialogs/NewChatDialog.tsx:54
#: src/components/dms/MessageProfileButton.tsx:58 #: src/components/dms/MessageProfileButton.tsx:58
#: src/screens/Messages/ChatList.tsx:362 #: src/screens/Messages/ChatList.tsx:358
#: src/screens/Messages/Conversation.tsx:228 #: src/screens/Messages/Conversation.tsx:228
msgid "Before you can message another user, you must first verify your email." msgid "Before you can message another user, you must first verify your email."
msgstr "" msgstr ""
@@ -1663,11 +1663,11 @@ msgid "Chat muted"
msgstr "" msgstr ""
#: src/Navigation.tsx:558 #: src/Navigation.tsx:558
#: src/screens/Messages/components/InboxPreview.tsx:24 #: src/screens/Messages/components/InboxPreview.tsx:22
msgid "Chat request inbox" msgid "Chat request inbox"
msgstr "" msgstr ""
#: src/screens/Messages/components/InboxPreview.tsx:64 #: src/screens/Messages/components/InboxPreview.tsx:62
#: src/screens/Messages/Inbox.tsx:56 #: src/screens/Messages/Inbox.tsx:56
#: src/screens/Messages/Inbox.tsx:98 #: src/screens/Messages/Inbox.tsx:98
msgid "Chat requests" msgid "Chat requests"
@@ -1675,7 +1675,7 @@ msgstr ""
#: src/components/dms/ConvoMenu.tsx:75 #: src/components/dms/ConvoMenu.tsx:75
#: src/Navigation.tsx:553 #: src/Navigation.tsx:553
#: src/screens/Messages/ChatList.tsx:371 #: src/screens/Messages/ChatList.tsx:367
msgid "Chat settings" msgid "Chat settings"
msgstr "" msgstr ""
@@ -1690,8 +1690,8 @@ msgid "Chat unmuted"
msgstr "" msgstr ""
#: src/screens/Messages/ChatList.tsx:76 #: src/screens/Messages/ChatList.tsx:76
#: src/screens/Messages/ChatList.tsx:387 #: src/screens/Messages/ChatList.tsx:383
#: src/screens/Messages/ChatList.tsx:411 #: src/screens/Messages/ChatList.tsx:407
msgid "Chats" msgid "Chats"
msgstr "" msgstr ""
@@ -3339,7 +3339,7 @@ msgstr ""
msgid "Failed to delete starter pack" msgid "Failed to delete starter pack"
msgstr "" msgstr ""
#: src/screens/Messages/ChatList.tsx:274 #: src/screens/Messages/ChatList.tsx:270
#: src/screens/Messages/Inbox.tsx:208 #: src/screens/Messages/Inbox.tsx:208
msgid "Failed to load conversations" msgid "Failed to load conversations"
msgstr "" msgstr ""
@@ -5300,8 +5300,8 @@ msgid "New {postsCount, plural, one {post} other {posts}} from {firstAuthorName}
msgstr "" msgstr ""
#: src/components/dms/dialogs/NewChatDialog.tsx:67 #: src/components/dms/dialogs/NewChatDialog.tsx:67
#: src/screens/Messages/ChatList.tsx:394 #: src/screens/Messages/ChatList.tsx:390
#: src/screens/Messages/ChatList.tsx:401 #: src/screens/Messages/ChatList.tsx:397
msgid "New chat" msgid "New chat"
msgstr "" msgstr ""
@@ -5592,7 +5592,7 @@ msgstr ""
msgid "Note: This post is only visible to logged-in users." msgid "Note: This post is only visible to logged-in users."
msgstr "" msgstr ""
#: src/screens/Messages/ChatList.tsx:295 #: src/screens/Messages/ChatList.tsx:291
msgid "Nothing here" msgid "Nothing here"
msgstr "" msgstr ""
@@ -5834,7 +5834,7 @@ msgstr ""
msgid "Open system log" msgid "Open system log"
msgstr "" msgstr ""
#: src/view/com/util/forms/DropdownButton.tsx:162 #: src/view/com/util/forms/DropdownButton.tsx:167
msgid "Opens {numItems} options" msgid "Opens {numItems} options"
msgstr "" msgstr ""
@@ -6688,7 +6688,7 @@ msgstr ""
msgid "Reject chat request" msgid "Reject chat request"
msgstr "" msgstr ""
#: src/screens/Messages/ChatList.tsx:278 #: src/screens/Messages/ChatList.tsx:274
#: src/screens/Messages/Inbox.tsx:212 #: src/screens/Messages/Inbox.tsx:212
msgid "Reload conversations" msgid "Reload conversations"
msgstr "" msgstr ""
@@ -7158,7 +7158,7 @@ msgstr ""
#: src/components/StarterPack/ProfileStarterPacks.tsx:346 #: src/components/StarterPack/ProfileStarterPacks.tsx:346
#: src/screens/Login/LoginForm.tsx:323 #: src/screens/Login/LoginForm.tsx:323
#: src/screens/Login/LoginForm.tsx:330 #: src/screens/Login/LoginForm.tsx:330
#: src/screens/Messages/ChatList.tsx:284 #: src/screens/Messages/ChatList.tsx:280
#: src/screens/Messages/components/MessageListError.tsx:25 #: src/screens/Messages/components/MessageListError.tsx:25
#: src/screens/Messages/Inbox.tsx:218 #: src/screens/Messages/Inbox.tsx:218
#: src/screens/Onboarding/StepInterests/index.tsx:217 #: src/screens/Onboarding/StepInterests/index.tsx:217
@@ -7538,7 +7538,7 @@ msgstr ""
msgid "Select your preferred notification channels" msgid "Select your preferred notification channels"
msgstr "" msgstr ""
#: src/view/com/util/forms/DropdownButton.tsx:297 #: src/view/com/util/forms/DropdownButton.tsx:302
msgid "Selects option {0} of {numItems}" msgid "Selects option {0} of {numItems}"
msgstr "" msgstr ""
@@ -8933,7 +8933,7 @@ msgstr ""
msgid "Today" msgid "Today"
msgstr "" msgstr ""
#: src/view/com/util/forms/DropdownButton.tsx:258 #: src/view/com/util/forms/DropdownButton.tsx:263
msgid "Toggle dropdown" msgid "Toggle dropdown"
msgstr "" msgstr ""
@@ -9517,7 +9517,7 @@ msgstr ""
#: src/screens/Settings/AboutSettings.tsx:126 #: src/screens/Settings/AboutSettings.tsx:126
#: src/screens/Settings/AboutSettings.tsx:155 #: src/screens/Settings/AboutSettings.tsx:155
msgid "Version {appVersion}" msgid "Version {0}"
msgstr "" msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:83 #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:83
@@ -9901,7 +9901,7 @@ msgid "Who can verify?"
msgstr "" msgstr ""
#: src/screens/Home/NoFeedsPinned.tsx:79 #: src/screens/Home/NoFeedsPinned.tsx:79
#: src/screens/Messages/ChatList.tsx:262 #: src/screens/Messages/ChatList.tsx:258
#: src/screens/Messages/Inbox.tsx:197 #: src/screens/Messages/Inbox.tsx:197
msgid "Whoops!" msgid "Whoops!"
msgstr "" msgstr ""
@@ -10173,7 +10173,7 @@ msgstr ""
msgid "You have muted this user" msgid "You have muted this user"
msgstr "" msgstr ""
#: src/screens/Messages/ChatList.tsx:305 #: src/screens/Messages/ChatList.tsx:301
msgid "You have no conversations yet. Start one!" msgid "You have no conversations yet. Start one!"
msgstr "" msgstr ""
+2 -2
View File
@@ -255,7 +255,7 @@ let SearchScreenPostResults = ({
<Trans> <Trans>
<InlineLinkText <InlineLinkText
style={[pal.link]} style={[pal.link]}
label={_(msg`sign in`)} label={_(msg`Sign in`)}
to={'#'} to={'#'}
onPress={showSignIn}> onPress={showSignIn}>
Sign in Sign in
@@ -263,7 +263,7 @@ let SearchScreenPostResults = ({
<Text style={t.atoms.text_contrast_medium}> or </Text> <Text style={t.atoms.text_contrast_medium}> or </Text>
<InlineLinkText <InlineLinkText
style={[pal.link]} style={[pal.link]}
label={_(msg`create an account`)} label={_(msg`Create an account`)}
to={'#'} to={'#'}
onPress={showCreateAccount}> onPress={showCreateAccount}>
create an account create an account
@@ -124,7 +124,7 @@ export function LinkItem({
contentContainerStyle, contentContainerStyle,
chevronColor, chevronColor,
...props ...props
}: LinkProps & { }: Omit<LinkProps, Button.UninheritableButtonProps> & {
contentContainerStyle?: StyleProp<ViewStyle> contentContainerStyle?: StyleProp<ViewStyle>
destructive?: boolean destructive?: boolean
chevronColor?: string chevronColor?: string
@@ -132,7 +132,7 @@ export function LinkItem({
const t = useTheme() const t = useTheme()
return ( return (
<Link color="secondary" {...props}> <Link {...props}>
{args => ( {args => (
<Item <Item
destructive={destructive} destructive={destructive}
@@ -154,7 +154,7 @@ export function PressableItem({
contentContainerStyle, contentContainerStyle,
hoverStyle, hoverStyle,
...props ...props
}: Button.ButtonProps & { }: Omit<Button.ButtonProps, Button.UninheritableButtonProps> & {
contentContainerStyle?: StyleProp<ViewStyle> contentContainerStyle?: StyleProp<ViewStyle>
destructive?: boolean destructive?: boolean
}) { }) {
+20
View File
@@ -369,3 +369,23 @@ input[type='range'][orient='vertical']::-moz-range-thumb {
transform: translateY(0); transform: translateY(0);
} }
} }
/*
* #/components/Toast/index.web.tsx
*/
@keyframes toastFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes toastFadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
-1
View File
@@ -1 +0,0 @@
export function show() {}
-201
View File
@@ -1,201 +0,0 @@
import {select, type Theme} from '#/alf'
import {Check_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info'
export type LegacyToastType =
| 'xmark'
| 'exclamation-circle'
| 'check'
| 'clipboard-check'
| 'circle-exclamation'
export const convertLegacyToastType = (
type: ToastType | LegacyToastType,
): ToastType => {
switch (type) {
// these ones are fine
case 'default':
case 'success':
case 'error':
case 'warning':
case 'info':
return type
// legacy ones need conversion
case 'xmark':
return 'error'
case 'exclamation-circle':
return 'warning'
case 'check':
return 'success'
case 'clipboard-check':
return 'success'
case 'circle-exclamation':
return 'warning'
default:
return 'default'
}
}
export const TOAST_ANIMATION_CONFIG = {
duration: 300,
damping: 15,
stiffness: 150,
mass: 0.8,
overshootClamping: false,
restSpeedThreshold: 0.01,
restDisplacementThreshold: 0.01,
}
export const TOAST_TYPE_TO_ICON = {
default: SuccessIcon,
success: SuccessIcon,
error: ErrorIcon,
warning: WarningIcon,
info: CircleInfo,
}
export const getToastTypeStyles = (t: Theme) => ({
default: {
backgroundColor: select(t.name, {
light: t.atoms.bg_contrast_25.backgroundColor,
dim: t.atoms.bg_contrast_100.backgroundColor,
dark: t.atoms.bg_contrast_100.backgroundColor,
}),
borderColor: select(t.name, {
light: t.atoms.border_contrast_low.borderColor,
dim: t.atoms.border_contrast_high.borderColor,
dark: t.atoms.border_contrast_high.borderColor,
}),
iconColor: select(t.name, {
light: t.atoms.text_contrast_medium.color,
dim: t.atoms.text_contrast_medium.color,
dark: t.atoms.text_contrast_medium.color,
}),
textColor: select(t.name, {
light: t.atoms.text_contrast_medium.color,
dim: t.atoms.text_contrast_medium.color,
dark: t.atoms.text_contrast_medium.color,
}),
},
success: {
backgroundColor: select(t.name, {
light: t.palette.primary_100,
dim: t.palette.primary_100,
dark: t.palette.primary_50,
}),
borderColor: select(t.name, {
light: t.palette.primary_500,
dim: t.palette.primary_500,
dark: t.palette.primary_500,
}),
iconColor: select(t.name, {
light: t.palette.primary_500,
dim: t.palette.primary_600,
dark: t.palette.primary_600,
}),
textColor: select(t.name, {
light: t.palette.primary_500,
dim: t.palette.primary_600,
dark: t.palette.primary_600,
}),
},
error: {
backgroundColor: select(t.name, {
light: t.palette.negative_200,
dim: t.palette.negative_25,
dark: t.palette.negative_25,
}),
borderColor: select(t.name, {
light: t.palette.negative_300,
dim: t.palette.negative_300,
dark: t.palette.negative_300,
}),
iconColor: select(t.name, {
light: t.palette.negative_600,
dim: t.palette.negative_600,
dark: t.palette.negative_600,
}),
textColor: select(t.name, {
light: t.palette.negative_600,
dim: t.palette.negative_600,
dark: t.palette.negative_600,
}),
},
warning: {
backgroundColor: select(t.name, {
light: t.atoms.bg_contrast_25.backgroundColor,
dim: t.atoms.bg_contrast_100.backgroundColor,
dark: t.atoms.bg_contrast_100.backgroundColor,
}),
borderColor: select(t.name, {
light: t.atoms.border_contrast_low.borderColor,
dim: t.atoms.border_contrast_high.borderColor,
dark: t.atoms.border_contrast_high.borderColor,
}),
iconColor: select(t.name, {
light: t.atoms.text_contrast_medium.color,
dim: t.atoms.text_contrast_medium.color,
dark: t.atoms.text_contrast_medium.color,
}),
textColor: select(t.name, {
light: t.atoms.text_contrast_medium.color,
dim: t.atoms.text_contrast_medium.color,
dark: t.atoms.text_contrast_medium.color,
}),
},
info: {
backgroundColor: select(t.name, {
light: t.atoms.bg_contrast_25.backgroundColor,
dim: t.atoms.bg_contrast_100.backgroundColor,
dark: t.atoms.bg_contrast_100.backgroundColor,
}),
borderColor: select(t.name, {
light: t.atoms.border_contrast_low.borderColor,
dim: t.atoms.border_contrast_high.borderColor,
dark: t.atoms.border_contrast_high.borderColor,
}),
iconColor: select(t.name, {
light: t.atoms.text_contrast_medium.color,
dim: t.atoms.text_contrast_medium.color,
dark: t.atoms.text_contrast_medium.color,
}),
textColor: select(t.name, {
light: t.atoms.text_contrast_medium.color,
dim: t.atoms.text_contrast_medium.color,
dark: t.atoms.text_contrast_medium.color,
}),
},
})
export const getToastWebAnimationStyles = () => ({
entering: {
animation: 'toastFadeIn 0.3s ease-out forwards',
},
exiting: {
animation: 'toastFadeOut 0.2s ease-in forwards',
},
})
export const TOAST_WEB_KEYFRAMES = `
@keyframes toastFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes toastFadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
`
+46 -226
View File
@@ -1,234 +1,54 @@
import {useEffect, useMemo, useRef, useState} from 'react' import {toast} from '#/components/Toast'
import {AccessibilityInfo, View} from 'react-native' import {type ToastType} from '#/components/Toast/types'
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
} from 'react-native-gesture-handler'
import Animated, {
FadeIn,
FadeOut,
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withDecay,
withSpring,
} from 'react-native-reanimated'
import RootSiblings from 'react-native-root-siblings'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {
convertLegacyToastType,
getToastTypeStyles,
type LegacyToastType,
TOAST_ANIMATION_CONFIG,
TOAST_TYPE_TO_ICON,
type ToastType,
} from '#/view/com/util/Toast.style'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
const TIMEOUT = 2e3
// Use type overloading to mark certain types as deprecated -sfn
// https://stackoverflow.com/a/78325851/13325987
export function show(message: string, type?: ToastType): void
/** /**
* @deprecated type is deprecated - use one of `'default' | 'success' | 'error' | 'warning' | 'info'` * @deprecated use {@link ToastType} and {@link toast} instead
*/
export type LegacyToastType =
| 'xmark'
| 'exclamation-circle'
| 'check'
| 'clipboard-check'
| 'circle-exclamation'
export const convertLegacyToastType = (
type: ToastType | LegacyToastType,
): ToastType => {
switch (type) {
// these ones are fine
case 'default':
case 'success':
case 'error':
case 'warning':
case 'info':
return type
// legacy ones need conversion
case 'xmark':
return 'error'
case 'exclamation-circle':
return 'warning'
case 'check':
return 'success'
case 'clipboard-check':
return 'success'
case 'circle-exclamation':
return 'warning'
default:
return 'default'
}
}
/**
* @deprecated use {@link toast} instead
*/ */
export function show(message: string, type?: LegacyToastType): void
export function show( export function show(
message: string, message: string,
type: ToastType | LegacyToastType = 'default', type: ToastType | LegacyToastType = 'default',
): void { ): void {
if (process.env.NODE_ENV === 'test') { const convertedType = convertLegacyToastType(type)
return toast.show({
} type: convertedType,
content: message,
AccessibilityInfo.announceForAccessibility(message) a11yLabel: message,
const item = new RootSiblings( })
(
<Toast
message={message}
type={convertLegacyToastType(type)}
destroy={() => item.destroy()}
/>
),
)
}
function Toast({
message,
type,
destroy,
}: {
message: string
type: ToastType
destroy: () => void
}) {
const t = useTheme()
const {top} = useSafeAreaInsets()
const isPanning = useSharedValue(false)
const dismissSwipeTranslateY = useSharedValue(0)
const [cardHeight, setCardHeight] = useState(0)
const toastStyles = getToastTypeStyles(t)
const colors = toastStyles[type]
const IconComponent = TOAST_TYPE_TO_ICON[type]
// for the exit animation to work on iOS the animated component
// must not be the root component
// so we need to wrap it in a view and unmount the toast ahead of time
const [alive, setAlive] = useState(true)
const hideAndDestroyImmediately = () => {
setAlive(false)
setTimeout(() => {
destroy()
}, 1e3)
}
const destroyTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
const hideAndDestroyAfterTimeout = useNonReactiveCallback(() => {
clearTimeout(destroyTimeoutRef.current)
destroyTimeoutRef.current = setTimeout(hideAndDestroyImmediately, TIMEOUT)
})
const pauseDestroy = useNonReactiveCallback(() => {
clearTimeout(destroyTimeoutRef.current)
})
useEffect(() => {
hideAndDestroyAfterTimeout()
}, [hideAndDestroyAfterTimeout])
const panGesture = useMemo(() => {
return Gesture.Pan()
.activeOffsetY([-10, 10])
.failOffsetX([-10, 10])
.maxPointers(1)
.onStart(() => {
'worklet'
if (!alive) return
isPanning.set(true)
runOnJS(pauseDestroy)()
})
.onUpdate(e => {
'worklet'
if (!alive) return
dismissSwipeTranslateY.value = e.translationY
})
.onEnd(e => {
'worklet'
if (!alive) return
runOnJS(hideAndDestroyAfterTimeout)()
isPanning.set(false)
if (e.velocityY < -100) {
if (dismissSwipeTranslateY.value === 0) {
// HACK: If the initial value is 0, withDecay() animation doesn't start.
// This is a bug in Reanimated, but for now we'll work around it like this.
dismissSwipeTranslateY.value = 1
}
dismissSwipeTranslateY.value = withDecay({
velocity: e.velocityY,
velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1),
deceleration: 1,
})
} else {
dismissSwipeTranslateY.value = withSpring(0, {
stiffness: 500,
damping: 50,
})
}
})
}, [
dismissSwipeTranslateY,
isPanning,
alive,
hideAndDestroyAfterTimeout,
pauseDestroy,
])
const topOffset = top + 10
useAnimatedReaction(
() =>
!isPanning.get() &&
dismissSwipeTranslateY.get() < -topOffset - cardHeight,
(isSwipedAway, prevIsSwipedAway) => {
'worklet'
if (isSwipedAway && !prevIsSwipedAway) {
runOnJS(destroy)()
}
},
)
const animatedStyle = useAnimatedStyle(() => {
const translation = dismissSwipeTranslateY.get()
return {
transform: [
{
translateY: translation > 0 ? translation ** 0.7 : translation,
},
],
}
})
return (
<GestureHandlerRootView
style={[a.absolute, {top: topOffset, left: 16, right: 16}]}
pointerEvents="box-none">
{alive && (
<Animated.View
entering={FadeIn.duration(TOAST_ANIMATION_CONFIG.duration)}
exiting={FadeOut.duration(TOAST_ANIMATION_CONFIG.duration * 0.7)}
onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)}
accessibilityRole="alert"
accessible={true}
accessibilityLabel={message}
accessibilityHint=""
onAccessibilityEscape={hideAndDestroyImmediately}
style={[
a.flex_1,
{backgroundColor: colors.backgroundColor},
a.shadow_sm,
{borderColor: colors.borderColor, borderWidth: 1},
a.rounded_sm,
animatedStyle,
]}>
<GestureDetector gesture={panGesture}>
<View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}>
<View
style={[
a.flex_shrink_0,
a.rounded_full,
{width: 32, height: 32},
a.align_center,
a.justify_center,
{
backgroundColor: colors.backgroundColor,
},
]}>
<IconComponent fill={colors.iconColor} size="sm" />
</View>
<View
style={[
a.h_full,
a.justify_center,
a.flex_1,
a.justify_center,
]}>
<Text
style={[a.text_md, a.font_bold, {color: colors.textColor}]}
emoji>
{message}
</Text>
</View>
</View>
</GestureDetector>
</Animated.View>
)}
</GestureHandlerRootView>
)
} }
-180
View File
@@ -1,180 +0,0 @@
/*
* Note: the dataSet properties are used to leverage custom CSS in public/index.html
*/
import {useEffect, useState} from 'react'
import {Pressable, StyleSheet, Text, View} from 'react-native'
import {
convertLegacyToastType,
getToastTypeStyles,
getToastWebAnimationStyles,
type LegacyToastType,
TOAST_TYPE_TO_ICON,
TOAST_WEB_KEYFRAMES,
type ToastType,
} from '#/view/com/util/Toast.style'
import {atoms as a, useTheme} from '#/alf'
const DURATION = 3500
interface ActiveToast {
text: string
type: ToastType
}
type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
// globals
// =
let globalSetActiveToast: GlobalSetActiveToast | undefined
let toastTimeout: NodeJS.Timeout | undefined
// components
// =
type ToastContainerProps = {}
export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
const [activeToast, setActiveToast] = useState<ActiveToast | undefined>()
const [isExiting, setIsExiting] = useState(false)
useEffect(() => {
globalSetActiveToast = (t: ActiveToast | undefined) => {
if (!t && activeToast) {
setIsExiting(true)
setTimeout(() => {
setActiveToast(t)
setIsExiting(false)
}, 200)
} else {
setActiveToast(t)
setIsExiting(false)
}
}
}, [activeToast])
useEffect(() => {
const styleId = 'toast-animations'
if (!document.getElementById(styleId)) {
const style = document.createElement('style')
style.id = styleId
style.textContent = TOAST_WEB_KEYFRAMES
document.head.appendChild(style)
}
}, [])
const t = useTheme()
const toastTypeStyles = getToastTypeStyles(t)
const toastStyles = activeToast
? toastTypeStyles[activeToast.type]
: toastTypeStyles.default
const IconComponent = activeToast
? TOAST_TYPE_TO_ICON[activeToast.type]
: TOAST_TYPE_TO_ICON.default
const animationStyles = getToastWebAnimationStyles()
return (
<>
{activeToast && (
<View
style={[
styles.container,
{
backgroundColor: toastStyles.backgroundColor,
borderColor: toastStyles.borderColor,
...(isExiting
? animationStyles.exiting
: animationStyles.entering),
},
]}>
<View
style={[
styles.iconContainer,
{
backgroundColor: 'transparent',
},
]}>
<IconComponent
fill={toastStyles.iconColor}
size="sm"
style={styles.icon}
/>
</View>
<Text
style={[
styles.text,
a.text_sm,
a.font_bold,
{color: toastStyles.textColor},
]}>
{activeToast.text}
</Text>
<Pressable
style={styles.dismissBackdrop}
accessibilityLabel="Dismiss"
accessibilityHint=""
onPress={() => {
setActiveToast(undefined)
}}
/>
</View>
)}
</>
)
}
// methods
// =
export function show(
text: string,
type: ToastType | LegacyToastType = 'default',
) {
if (toastTimeout) {
clearTimeout(toastTimeout)
}
globalSetActiveToast?.({text, type: convertLegacyToastType(type)})
toastTimeout = setTimeout(() => {
globalSetActiveToast?.(undefined)
}, DURATION)
}
const styles = StyleSheet.create({
container: {
// @ts-ignore web only
position: 'fixed',
left: 20,
bottom: 20,
// @ts-ignore web only
width: 'calc(100% - 40px)',
maxWidth: 380,
padding: 20,
flexDirection: 'row',
alignItems: 'center',
borderRadius: 10,
borderWidth: 1,
},
dismissBackdrop: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
},
iconContainer: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
},
icon: {
flexShrink: 0,
},
text: {
marginLeft: 10,
},
})
+101 -87
View File
@@ -1,65 +1,11 @@
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import * as Toast from '#/view/com/util/Toast' import {show as deprecatedShow} from '#/view/com/util/Toast'
import { import {atoms as a} from '#/alf'
getToastTypeStyles, import {Button, ButtonText} from '#/components/Button'
TOAST_TYPE_TO_ICON, import {toast} from '#/components/Toast'
type ToastType, import {Toast} from '#/components/Toast/Toast'
} from '#/view/com/util/Toast.style' import {H1} from '#/components/Typography'
import {atoms as a, useTheme} from '#/alf'
import {H1, Text} from '#/components/Typography'
function ToastPreview({message, type}: {message: string; type: ToastType}) {
const t = useTheme()
const toastStyles = getToastTypeStyles(t)
const colors = toastStyles[type as keyof typeof toastStyles]
const IconComponent =
TOAST_TYPE_TO_ICON[type as keyof typeof TOAST_TYPE_TO_ICON]
return (
<Pressable
accessibilityRole="button"
onPress={() => Toast.show(message, type)}
style={[
{backgroundColor: colors.backgroundColor},
a.shadow_sm,
{borderColor: colors.borderColor},
a.rounded_sm,
a.border,
a.px_sm,
a.py_sm,
a.flex_row,
a.gap_sm,
a.align_center,
]}>
<View
style={[
a.flex_shrink_0,
a.rounded_full,
{width: 24, height: 24},
a.align_center,
a.justify_center,
{
backgroundColor: colors.backgroundColor,
},
]}>
<IconComponent fill={colors.iconColor} size="xs" />
</View>
<View style={[a.flex_1]}>
<Text
style={[
a.text_sm,
a.font_bold,
a.leading_snug,
{color: colors.textColor},
]}
emoji>
{message}
</Text>
</View>
</Pressable>
)
}
export function Toasts() { export function Toasts() {
return ( return (
@@ -67,35 +13,103 @@ export function Toasts() {
<H1>Toast Examples</H1> <H1>Toast Examples</H1>
<View style={[a.gap_md]}> <View style={[a.gap_md]}>
<View style={[a.gap_xs]}> <Pressable
<ToastPreview message="Default Toast" type="default" /> accessibilityRole="button"
</View> onPress={() =>
toast.show({
<View style={[a.gap_xs]}> type: 'default',
<ToastPreview content: 'Default toast',
message="Operation completed successfully!" a11yLabel: 'Default toast',
type="success" })
}>
<Toast content="Default toast" type="default" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'default',
content: 'Default toast, 6 seconds',
a11yLabel: 'Default toast, 6 seconds',
duration: 6e3,
})
}>
<Toast content="Default toast, 6 seconds" type="default" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'default',
content:
'This is a longer message to test how the toast handles multiple lines of text content.',
a11yLabel:
'This is a longer message to test how the toast handles multiple lines of text content.',
})
}>
<Toast
content="This is a longer message to test how the toast handles multiple lines of text content."
type="default"
/> />
</View> </Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'success',
content: 'Success toast',
a11yLabel: 'Success toast',
})
}>
<Toast content="Success toast" type="success" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'info',
content: 'Info toast',
a11yLabel: 'Info toast',
})
}>
<Toast content="Info" type="info" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'warning',
content: 'Warning toast',
a11yLabel: 'Warning toast',
})
}>
<Toast content="Warning" type="warning" />
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() =>
toast.show({
type: 'error',
content: 'Error toast',
a11yLabel: 'Error toast',
})
}>
<Toast content="Error" type="error" />
</Pressable>
<View style={[a.gap_xs]}> <Button
<ToastPreview message="Something went wrong!" type="error" /> label="Deprecated toast example"
</View> onPress={() =>
deprecatedShow(
<View style={[a.gap_xs]}> 'This is a deprecated toast example',
<ToastPreview message="Please check your input" type="warning" /> 'exclamation-circle',
</View> )
}
<View style={[a.gap_xs]}> size="large"
<ToastPreview message="Here's some helpful information" type="info" /> variant="solid"
</View> color="secondary">
<ButtonText>Deprecated toast example</ButtonText>
<View style={[a.gap_xs]}> </Button>
<ToastPreview
message="This is a longer message to test how the toast handles multiple lines of text content."
type="info"
/>
</View>
</View> </View>
</View> </View>
) )
+2 -1
View File
@@ -87,6 +87,8 @@ function StorybookInner() {
</Button> </Button>
</View> </View>
<Toasts />
<Button <Button
color="primary" color="primary"
size="small" size="small"
@@ -118,7 +120,6 @@ function StorybookInner() {
<Breakpoints /> <Breakpoints />
<Dialogs /> <Dialogs />
<Admonitions /> <Admonitions />
<Toasts />
<Settings /> <Settings />
<Button <Button