Merge branch 'main' into app-2066

This commit is contained in:
vineyardbovines
2026-04-13 08:26:28 -04:00
64 changed files with 1108 additions and 719 deletions
+1 -1
View File
@@ -235,7 +235,7 @@ function App() {
<Geo.Provider>
<AppConfigProvider>
<A11yProvider>
<KeyboardControllerProvider>
<KeyboardControllerProvider preload={false}>
<OnboardingProvider>
<AnalyticsContext>
<SessionProvider>
+10 -6
View File
@@ -183,7 +183,7 @@ export async function getServerState({agent}: {agent: AtpAgent}) {
const geolocation = device.get(['mergedGeolocation'])
if (!geolocation || !geolocation.countryCode) {
logger.error(`getServerState: missing geolocation countryCode`)
return
return null
}
const {data} = await agent.app.bsky.ageassurance.getState({
countryCode: geolocation.countryCode,
@@ -225,7 +225,9 @@ export async function prefetchServerState({agent}: {agent: AtpAgent}) {
try {
logger.debug(`prefetchServerState: resolving...`)
const res = await networkRetry(3, () => getServerState({agent}))
qc.setQueryData<AppBskyAgeassuranceGetState.OutputSchema>(qk, res)
if (res) {
qc.setQueryData<AppBskyAgeassuranceGetState.OutputSchema>(qk, res)
}
} catch (err) {
const e = err as Error
logger.warn(`prefetchServerState: failed`, {
@@ -238,10 +240,12 @@ export async function refetchServerState({agent}: {agent: AtpAgent}) {
if (!did) return
logger.debug(`refetchServerState: fetching...`)
const res = await networkRetry(3, () => getServerState({agent}))
qc.setQueryData<AppBskyAgeassuranceGetState.OutputSchema>(
createServerStateQueryKey({did}),
res,
)
if (res) {
qc.setQueryData<AppBskyAgeassuranceGetState.OutputSchema>(
createServerStateQueryKey({did}),
res,
)
}
return res
}
export function usePatchServerState() {
+1 -1
View File
@@ -20,7 +20,7 @@ export const atoms = {
*/
util_screen_outer: [
web({
minHeight: '100vh',
minHeight: '100dvh',
}),
native({
height: '100%',
+2
View File
@@ -338,6 +338,8 @@ export function Composer({
web({
caretColor: textStyle.color ?? 'black',
overscrollBehavior: 'none',
scrollbarWidth: 'thin',
scrollbarColor: `${t.palette.contrast_200} transparent`,
}),
]}
{...rest}
+35
View File
@@ -0,0 +1,35 @@
import {type StyleProp, View, type ViewStyle} from 'react-native'
import {
GlassView as ExpoGlassView,
type GlassViewProps as ExpoGlassViewProps,
isGlassEffectAPIAvailable,
isLiquidGlassAvailable,
} from 'expo-glass-effect'
import {useTheme} from '#/alf'
export const IS_GLASS_AVAILABLE =
isLiquidGlassAvailable() && isGlassEffectAPIAvailable()
/**
* Liquid Glass View that uses `expo-glass-effect`
*
* If unavailable, falls back to a regular `View`. Use `fallbackStyle` to customize the fallback appearance.
*/
export const GlassView = IS_GLASS_AVAILABLE ? InnerGlassView : FallbackView
export type GlassViewProps = ExpoGlassViewProps & {
fallbackStyle?: StyleProp<ViewStyle>
}
function InnerGlassView({
fallbackStyle: _fallbackStyle,
...props
}: GlassViewProps) {
const t = useTheme()
return <ExpoGlassView colorScheme={t.scheme} {...props} />
}
function FallbackView({fallbackStyle, style, ...props}: GlassViewProps) {
return <View style={[fallbackStyle, style]} {...props} />
}
@@ -22,7 +22,13 @@ import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import type * as bsky from '#/types/bsky'
export function RecentChats({postUri}: {postUri: string}) {
export function RecentChats({
postUri,
onBeforePress,
}: {
postUri: string
onBeforePress?: () => void
}) {
const ax = useAnalytics()
const control = useDialogContext()
const {currentAccount} = useSession()
@@ -32,6 +38,7 @@ export function RecentChats({postUri}: {postUri: string}) {
const navigation = useNavigation<NavigationProp>()
const onSelectChat = (convoId: string) => {
onBeforePress?.()
control.close(() => {
ax.metric('share:press:recentDm', {})
navigation.navigate('MessagesConversation', {
@@ -5,12 +5,14 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {makeProfileLink} from '#/lib/routes/links'
import {type NavigationProp} from '#/lib/routes/types'
import {shareText, shareUrl} from '#/lib/sharing'
import {toShareUrl} from '#/lib/strings/url-helpers'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {precachePost} from '#/state/queries/post'
import {useSession} from '#/state/session'
import {atoms as a} from '#/alf'
import {Admonition} from '#/components/Admonition'
@@ -40,6 +42,7 @@ let ShareMenuItems = ({
const sendViaChatControl = useDialogControl()
const [devModeEnabled] = useDevMode()
const aa = useAgeAssurance()
const queryClient = useQueryClient()
const postUri = post.uri
const postAuthor = useProfileShadow(post.author)
@@ -77,7 +80,12 @@ let ShareMenuItems = ({
onShareProp()
}
const onBeforeShareViaChat = () => {
precachePost(queryClient, postUri, post)
}
const onSelectChatToShareTo = (conversation: string) => {
onBeforeShareViaChat()
navigation.navigate('MessagesConversation', {
conversation,
embed: postUri,
@@ -98,7 +106,10 @@ let ShareMenuItems = ({
{hasSession && aa.state.access === aa.Access.Full && (
<Menu.Group>
<Menu.ContainerItem>
<RecentChats postUri={postUri} />
<RecentChats
postUri={postUri}
onBeforePress={onBeforeShareViaChat}
/>
</Menu.ContainerItem>
<Menu.Item
testID="postDropdownSendViaDMBtn"
+1 -1
View File
@@ -303,7 +303,7 @@ let PostControls = ({
isToggled={Boolean(post.viewer?.like)}
hasBeenToggled={hasLikeIconBeenToggled}
renderCount={({count}) => (
<PostControlButtonText>
<PostControlButtonText testID="likeCount">
{formatPostStatCount(count)}
</PostControlButtonText>
)}
+2 -1
View File
@@ -91,7 +91,8 @@ export function ChatEmptyPill() {
onPressOut={onPressOut}>
<Text
style={[a.font_semi_bold, a.pointer_events_none]}
selectable={false}>
selectable={false}
emoji>
{prompts[promptIndex]}
</Text>
</AnimatedPressable>
+5
View File
@@ -3,3 +3,8 @@ import {createSinglePathSVG} from './TEMPLATE'
export const PaperPlane_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3.374 3.22a1 1 0 0 1 1.073-.114l16 8a1 1 0 0 1 0 1.788l-16 8a1 1 0 0 1-1.417-1.136L4.97 12 3.03 4.243a1 1 0 0 1 .344-1.023ZM6.781 13l-1.284 5.133L17.764 12 5.497 5.867 6.781 11H9a1 1 0 1 1 0 2H6.78Z',
})
export const PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded =
createSinglePathSVG({
path: 'M10.655 3.718c.55-1.116 2.14-1.116 2.69 0l7.548 15.317c.578 1.172-.515 2.471-1.768 2.103L13 19.336V15a1 1 0 0 0-2 0v4.336l-6.124 1.802c-1.254.369-2.346-.93-1.769-2.103l7.548-15.317Z',
})
@@ -117,13 +117,14 @@ export function LiveStatus({
const reportDialogControl = useGlobalReportDialogControl()
const dialogContext = Dialog.useDialogContext()
const moderation = useMemo(() => {
return moderateStatus(profile, moderationOpts!)
if (!moderationOpts) return undefined
return moderateStatus(profile, moderationOpts)
}, [profile, moderationOpts])
return (
<>
{embed.external.thumb && (
<Hider.Outer modui={moderation.ui('contentMedia')}>
<Hider.Outer modui={moderation?.ui('contentMedia')}>
<Hider.Mask>
<ModeratedImage />
</Hider.Mask>
@@ -279,7 +280,6 @@ function ModeratedImage() {
return (
<View
style={[
a.flex_1,
a.p_lg,
a.py_xl,
a.align_center,
+2 -1
View File
@@ -100,7 +100,8 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
const moderation = useMemo(() => {
if (!actor || !('status' in actor && actor.status)) return undefined
return moderateStatus(actor, moderationOpts!)
if (!moderationOpts) return undefined
return moderateStatus(actor, moderationOpts)
}, [actor, moderationOpts])
return useMemo(() => {
+114 -105
View File
@@ -694,8 +694,8 @@ msgstr ""
#: src/Navigation.tsx:544
#: src/screens/Settings/AboutSettings.tsx:74
#: src/screens/Settings/Settings.tsx:259
#: src/screens/Settings/Settings.tsx:262
#: src/screens/Settings/Settings.tsx:255
#: src/screens/Settings/Settings.tsx:258
msgid "About"
msgstr ""
@@ -722,8 +722,8 @@ msgid "Accept this language suggestion"
msgstr ""
#: src/screens/Settings/AccessibilitySettings.tsx:45
#: src/screens/Settings/Settings.tsx:235
#: src/screens/Settings/Settings.tsx:238
#: src/screens/Settings/Settings.tsx:233
#: src/screens/Settings/Settings.tsx:236
msgid "Accessibility"
msgstr ""
@@ -734,8 +734,8 @@ msgstr ""
#: src/Navigation.tsx:403
#: src/screens/Login/LoginForm.tsx:192
#: src/screens/Settings/AccountSettings.tsx:56
#: src/screens/Settings/Settings.tsx:177
#: src/screens/Settings/Settings.tsx:180
#: src/screens/Settings/Settings.tsx:173
#: src/screens/Settings/Settings.tsx:176
msgid "Account"
msgstr ""
@@ -774,7 +774,7 @@ msgstr ""
msgid "Account Muted by List"
msgstr ""
#: src/screens/Settings/Settings.tsx:641
#: src/screens/Settings/Settings.tsx:637
msgid "Account options"
msgstr ""
@@ -782,7 +782,7 @@ msgstr ""
msgid "Account provider"
msgstr ""
#: src/screens/Settings/Settings.tsx:677
#: src/screens/Settings/Settings.tsx:671
msgid "Account removed from quick access"
msgstr ""
@@ -868,8 +868,8 @@ msgstr ""
msgid "Add alt text (optional)"
msgstr ""
#: src/screens/Settings/Settings.tsx:579
#: src/screens/Settings/Settings.tsx:582
#: src/screens/Settings/Settings.tsx:575
#: src/screens/Settings/Settings.tsx:578
#: src/view/shell/desktop/LeftNav.tsx:264
#: src/view/shell/desktop/LeftNav.tsx:268
msgid "Add another account"
@@ -1437,8 +1437,8 @@ msgstr ""
#: src/Navigation.tsx:395
#: src/screens/Settings/AppearanceSettings.tsx:73
#: src/screens/Settings/Settings.tsx:227
#: src/screens/Settings/Settings.tsx:230
#: src/screens/Settings/Settings.tsx:225
#: src/screens/Settings/Settings.tsx:228
msgid "Appearance"
msgstr ""
@@ -1447,8 +1447,8 @@ msgstr ""
msgid "Apply default recommended feeds"
msgstr ""
#: src/screens/Settings/Settings.tsx:510
#: src/screens/Settings/Settings.tsx:512
#: src/screens/Settings/Settings.tsx:506
#: src/screens/Settings/Settings.tsx:508
msgid "Apply Pull Request"
msgstr ""
@@ -1619,7 +1619,7 @@ msgstr ""
#: src/components/dms/dialogs/NewChatDialog.tsx:66
#: src/components/dms/MessageProfileButton.tsx:60
#: src/screens/Messages/ChatList.tsx:376
#: src/screens/Messages/Conversation.tsx:226
#: src/screens/Messages/Conversation.tsx:230
msgid "Before you can message another user, you must first verify your email."
msgstr ""
@@ -1960,7 +1960,7 @@ msgstr ""
#: src/screens/Settings/components/ChangeHandleDialog.tsx:87
#: src/screens/Settings/components/ChangePasswordDialog.tsx:248
#: src/screens/Settings/components/ChangePasswordDialog.tsx:254
#: src/screens/Settings/Settings.tsx:304
#: src/screens/Settings/Settings.tsx:300
#: src/screens/Takendown.tsx:102
#: src/screens/Takendown.tsx:105
#: src/view/com/composer/Composer.tsx:1547
@@ -2205,11 +2205,11 @@ msgstr ""
msgid "Choose your username"
msgstr ""
#: src/screens/Settings/Settings.tsx:502
#: src/screens/Settings/Settings.tsx:498
msgid "Clear all storage data"
msgstr ""
#: src/screens/Settings/Settings.tsx:504
#: src/screens/Settings/Settings.tsx:500
msgid "Clear all storage data (restart after this)"
msgstr ""
@@ -2536,8 +2536,8 @@ msgstr ""
msgid "Content & Media"
msgstr ""
#: src/screens/Settings/Settings.tsx:207
#: src/screens/Settings/Settings.tsx:210
#: src/screens/Settings/Settings.tsx:205
#: src/screens/Settings/Settings.tsx:208
msgid "Content and media"
msgstr ""
@@ -2619,7 +2619,7 @@ msgstr "Continue to group name"
msgid "Continue to next step"
msgstr ""
#: src/screens/Messages/Conversation.tsx:57
#: src/screens/Messages/Conversation.tsx:60
msgid "Conversation"
msgstr ""
@@ -2640,7 +2640,7 @@ msgstr ""
#: src/components/dms/MessageContextMenu.tsx:61
#: src/components/PostControls/DiscoverDebug.tsx:36
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:272
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:74
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:77
#: src/lib/sharing.ts:24
#: src/lib/sharing.ts:42
msgid "Copied to clipboard"
@@ -2702,8 +2702,8 @@ msgstr ""
msgid "Copy link to list"
msgstr ""
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:131
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:134
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:142
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:145
#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:88
#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:91
msgid "Copy link to post"
@@ -2990,7 +2990,7 @@ msgstr ""
msgid "Deactivate account"
msgstr ""
#: src/screens/Settings/Settings.tsx:467
#: src/screens/Settings/Settings.tsx:463
msgid "Debug Moderation"
msgstr ""
@@ -3044,7 +3044,7 @@ msgstr ""
msgid "Delete chat"
msgstr ""
#: src/screens/Settings/Settings.tsx:474
#: src/screens/Settings/Settings.tsx:470
msgid "Delete chat declaration record"
msgstr ""
@@ -3157,8 +3157,8 @@ msgctxt "toast"
msgid "Developer mode enabled"
msgstr ""
#: src/screens/Settings/Settings.tsx:286
#: src/screens/Settings/Settings.tsx:289
#: src/screens/Settings/Settings.tsx:282
#: src/screens/Settings/Settings.tsx:285
msgid "Developer options"
msgstr ""
@@ -4329,8 +4329,8 @@ msgstr ""
msgid "Find Friends"
msgstr ""
#: src/screens/Settings/Settings.tsx:218
#: src/screens/Settings/Settings.tsx:221
#: src/screens/Settings/Settings.tsx:216
#: src/screens/Settings/Settings.tsx:219
msgid "Find friends from contacts"
msgstr ""
@@ -4938,8 +4938,8 @@ msgstr ""
msgid "Held by Bluesky for 7 days to prevent abuse, then deleted"
msgstr ""
#: src/screens/Settings/Settings.tsx:247
#: src/screens/Settings/Settings.tsx:251
#: src/screens/Settings/Settings.tsx:255
#: src/view/shell/desktop/RightNav.tsx:129
#: src/view/shell/desktop/RightNav.tsx:132
#: src/view/shell/Drawer.tsx:369
@@ -5554,8 +5554,8 @@ msgid "Language Settings"
msgstr ""
#: src/screens/Settings/LanguageSettings.tsx:81
#: src/screens/Settings/Settings.tsx:243
#: src/screens/Settings/Settings.tsx:246
#: src/screens/Settings/Settings.tsx:239
#: src/screens/Settings/Settings.tsx:242
msgid "Languages"
msgstr ""
@@ -5926,7 +5926,7 @@ msgstr ""
msgid "Live events appear occasionally when something exciting is happening. If you'd like, you can hide this particular event, or all events for this placement in your app interface."
msgstr ""
#: src/features/liveNow/components/LiveStatusDialog.tsx:243
#: src/features/liveNow/components/LiveStatusDialog.tsx:244
msgid "Live feature is in beta"
msgstr ""
@@ -6094,6 +6094,11 @@ msgstr ""
msgid "Menu"
msgstr ""
#: src/screens/Messages/components/MessageComposer.tsx:198
#: src/screens/Messages/components/MessageInput.tsx:174
msgid "Message"
msgstr "Message"
#. placeholder {0}: profile.handle
#: src/components/dms/MessageProfileButton.tsx:99
msgid "Message {0}"
@@ -6119,17 +6124,20 @@ msgstr ""
msgid "Message from server: {0}"
msgstr ""
#: src/screens/Messages/components/MessageComposer.tsx:166
#: src/screens/Messages/components/MessageInput.tsx:154
#: src/screens/Messages/components/MessageComposer.tsx:197
#: src/screens/Messages/components/MessageInput.tsx:172
msgid "Message input field"
msgstr ""
#: src/screens/Messages/components/MessageComposer.tsx:69
#: src/screens/Messages/components/MessageInput.tsx:79
#: src/screens/Messages/components/MessageInput.tsx:85
#: src/screens/Messages/components/MessageInput.web.tsx:60
msgid "Message is too long"
msgstr ""
#: src/screens/Messages/components/MessageComposer.tsx:98
msgid "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})"
msgstr "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})"
#: src/components/dms/MessageContextMenu.tsx:129
msgid "Message options"
msgstr ""
@@ -6161,11 +6169,14 @@ msgstr ""
#: src/Navigation.tsx:181
#: src/screens/Moderation/index.tsx:102
#: src/screens/Settings/Settings.tsx:191
#: src/screens/Settings/Settings.tsx:194
msgid "Moderation"
msgstr ""
#: src/screens/Settings/Settings.tsx:189
#: src/screens/Settings/Settings.tsx:192
msgid "Moderation and content filters"
msgstr "Moderation and content filters"
#: src/components/moderation/ModerationDetailsDialog.tsx:141
msgid "Moderation details"
msgstr ""
@@ -6835,8 +6846,8 @@ msgstr ""
#: src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx:30
#: src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx:30
#: src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx:30
#: src/screens/Settings/Settings.tsx:199
#: src/screens/Settings/Settings.tsx:202
#: src/screens/Settings/Settings.tsx:197
#: src/screens/Settings/Settings.tsx:200
#: src/view/screens/Notifications.tsx:130
#: src/view/shell/bottom-bar/BottomBar.tsx:255
#: src/view/shell/desktop/LeftNav.tsx:710
@@ -6908,7 +6919,7 @@ msgstr "On"
msgid "on<0><1/><2><3/></2></0>"
msgstr ""
#: src/screens/Settings/Settings.tsx:400
#: src/screens/Settings/Settings.tsx:396
msgid "Onboarding reset"
msgstr ""
@@ -6989,7 +7000,7 @@ msgstr ""
msgid "Open drawer menu"
msgstr ""
#: src/screens/Messages/components/MessageComposer.tsx:143
#: src/screens/Messages/components/MessageComposer.tsx:177
#: src/screens/Messages/components/MessageInput.web.tsx:180
#: src/view/com/composer/Composer.tsx:1982
msgid "Open emoji picker"
@@ -7020,7 +7031,7 @@ msgstr ""
msgid "Open message options"
msgstr ""
#: src/screens/Settings/Settings.tsx:465
#: src/screens/Settings/Settings.tsx:461
msgid "Open moderation debug page"
msgstr ""
@@ -7036,8 +7047,8 @@ msgstr ""
msgid "Open post options menu"
msgstr ""
#: src/features/liveNow/components/LiveStatusDialog.tsx:217
#: src/features/liveNow/components/LiveStatusDialog.tsx:227
#: src/features/liveNow/components/LiveStatusDialog.tsx:218
#: src/features/liveNow/components/LiveStatusDialog.tsx:228
msgid "Open profile"
msgstr ""
@@ -7054,12 +7065,12 @@ msgstr ""
msgid "Open starter pack menu"
msgstr ""
#: src/screens/Settings/Settings.tsx:458
#: src/screens/Settings/Settings.tsx:472
#: src/screens/Settings/Settings.tsx:454
#: src/screens/Settings/Settings.tsx:468
msgid "Open storybook page"
msgstr ""
#: src/screens/Settings/Settings.tsx:451
#: src/screens/Settings/Settings.tsx:447
msgid "Open system log"
msgstr ""
@@ -7127,7 +7138,7 @@ msgstr ""
msgid "Opens GIF select dialog"
msgstr ""
#: src/screens/Settings/Settings.tsx:252
#: src/screens/Settings/Settings.tsx:248
msgid "Opens helpdesk in browser"
msgstr ""
@@ -7762,8 +7773,8 @@ msgstr ""
msgid "Privacy"
msgstr ""
#: src/screens/Settings/Settings.tsx:185
#: src/screens/Settings/Settings.tsx:188
#: src/screens/Settings/Settings.tsx:181
#: src/screens/Settings/Settings.tsx:184
msgid "Privacy and security"
msgstr ""
@@ -8055,7 +8066,7 @@ msgstr ""
#: src/components/StarterPack/Wizard/WizardListCard.tsx:113
#: src/screens/Bookmarks/index.tsx:268
#: src/screens/Moderation/index.tsx:486
#: src/screens/Settings/Settings.tsx:679
#: src/screens/Settings/Settings.tsx:673
#: src/view/com/modals/UserAddRemoveLists.tsx:236
#: src/view/com/posts/PostFeedErrorMessage.tsx:221
msgid "Remove"
@@ -8073,8 +8084,8 @@ msgstr ""
msgid "Remove {historyItem}"
msgstr ""
#: src/screens/Settings/Settings.tsx:658
#: src/screens/Settings/Settings.tsx:661
#: src/screens/Settings/Settings.tsx:654
#: src/screens/Settings/Settings.tsx:657
msgid "Remove account"
msgstr ""
@@ -8123,7 +8134,7 @@ msgstr ""
msgid "Remove from my feeds"
msgstr ""
#: src/screens/Settings/Settings.tsx:671
#: src/screens/Settings/Settings.tsx:667
msgid "Remove from quick access?"
msgstr ""
@@ -8331,7 +8342,7 @@ msgstr ""
#: src/components/dms/MessageContextMenu.tsx:172
#: src/components/dms/MessagesListBlockedFooter.tsx:86
#: src/components/dms/MessagesListBlockedFooter.tsx:93
#: src/features/liveNow/components/LiveStatusDialog.tsx:265
#: src/features/liveNow/components/LiveStatusDialog.tsx:266
msgid "Report"
msgstr ""
@@ -8395,7 +8406,7 @@ msgid "Report this list"
msgstr ""
#: src/components/moderation/ReportDialog/copy.ts:19
#: src/features/liveNow/components/LiveStatusDialog.tsx:248
#: src/features/liveNow/components/LiveStatusDialog.tsx:249
msgid "Report this livestream"
msgstr ""
@@ -8525,8 +8536,8 @@ msgstr ""
msgid "Resend Verification Email"
msgstr ""
#: src/screens/Settings/Settings.tsx:494
#: src/screens/Settings/Settings.tsx:496
#: src/screens/Settings/Settings.tsx:490
#: src/screens/Settings/Settings.tsx:492
msgid "Reset activity subscription nudge"
msgstr ""
@@ -8534,8 +8545,8 @@ msgstr ""
msgid "Reset code"
msgstr ""
#: src/screens/Settings/Settings.tsx:479
#: src/screens/Settings/Settings.tsx:481
#: src/screens/Settings/Settings.tsx:475
#: src/screens/Settings/Settings.tsx:477
msgid "Reset onboarding state"
msgstr ""
@@ -9093,13 +9104,13 @@ msgstr ""
msgid "Send feedback"
msgstr ""
#: src/screens/Messages/components/MessageComposer.tsx:216
#: src/screens/Messages/components/MessageInput.tsx:194
#: src/screens/Messages/components/MessageComposer.tsx:264
#: src/screens/Messages/components/MessageInput.tsx:228
#: src/screens/Messages/components/MessageInput.web.tsx:234
msgid "Send message"
msgstr ""
#: src/components/PostControls/ShareMenu/RecentChats.tsx:121
#: src/components/PostControls/ShareMenu/RecentChats.tsx:128
msgid "Send post to {name}"
msgstr ""
@@ -9121,8 +9132,8 @@ msgstr ""
msgid "Send verification email"
msgstr ""
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:105
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:111
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:116
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:122
#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:105
#: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:111
msgid "Send via direct message"
@@ -9166,7 +9177,7 @@ msgid "Sets email for password reset"
msgstr ""
#: src/Navigation.tsx:217
#: src/screens/Settings/Settings.tsx:100
#: src/screens/Settings/Settings.tsx:98
#: src/view/shell/desktop/LeftNav.tsx:806
#: src/view/shell/Drawer.tsx:597
msgid "Settings"
@@ -9253,8 +9264,8 @@ msgstr ""
msgid "Share anyway"
msgstr ""
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:165
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:168
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:176
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:179
msgid "Share author DID"
msgstr ""
@@ -9269,8 +9280,8 @@ msgstr ""
msgid "Share link dialog"
msgstr ""
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:156
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:159
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:167
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:170
msgid "Share post at:// URI"
msgstr ""
@@ -9291,8 +9302,8 @@ msgstr ""
msgid "Share this starter pack and help people join your community on Bluesky."
msgstr ""
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:121
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:124
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:132
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:135
#: src/screens/ProfileList/components/MoreOptionsMenu.tsx:160
#: src/screens/ProfileList/components/MoreOptionsMenu.tsx:166
#: src/screens/StarterPack/StarterPackScreen.tsx:640
@@ -9433,7 +9444,7 @@ msgstr ""
msgid "Shows information about when this post was created"
msgstr ""
#: src/screens/Settings/Settings.tsx:126
#: src/screens/Settings/Settings.tsx:123
msgid "Shows other accounts you can switch to"
msgstr ""
@@ -9497,9 +9508,9 @@ msgstr ""
msgid "Sign in to view post"
msgstr ""
#: src/screens/Settings/Settings.tsx:269
#: src/screens/Settings/Settings.tsx:271
#: src/screens/Settings/Settings.tsx:303
#: src/screens/Settings/Settings.tsx:265
#: src/screens/Settings/Settings.tsx:267
#: src/screens/Settings/Settings.tsx:299
#: src/screens/SignupQueued.tsx:94
#: src/screens/SignupQueued.tsx:97
#: src/screens/Takendown.tsx:88
@@ -9513,7 +9524,7 @@ msgstr ""
msgid "Sign Out"
msgstr ""
#: src/screens/Settings/Settings.tsx:300
#: src/screens/Settings/Settings.tsx:296
#: src/view/shell/desktop/LeftNav.tsx:211
msgid "Sign out?"
msgstr ""
@@ -9607,7 +9618,7 @@ msgstr ""
msgid "Something wasn't quite right with the data you're trying to report. Please contact support."
msgstr ""
#: src/screens/Messages/Conversation.tsx:142
#: src/screens/Messages/Conversation.tsx:144
msgid "Something went wrong"
msgstr ""
@@ -9679,7 +9690,7 @@ msgstr ""
msgid "Sports"
msgstr ""
#: src/components/PostControls/ShareMenu/RecentChats.tsx:199
#: src/components/PostControls/ShareMenu/RecentChats.tsx:206
msgid "Start a conversation, and it will appear here."
msgstr ""
@@ -9761,7 +9772,7 @@ msgstr ""
msgid "Step {0} of {1}"
msgstr ""
#: src/screens/Settings/Settings.tsx:405
#: src/screens/Settings/Settings.tsx:401
msgid "Storage cleared, you need to restart the app now."
msgstr ""
@@ -9770,7 +9781,7 @@ msgid "Stored as part of a secure code for matching with others"
msgstr ""
#: src/Navigation.tsx:310
#: src/screens/Settings/Settings.tsx:460
#: src/screens/Settings/Settings.tsx:456
msgid "Storybook"
msgstr ""
@@ -9883,9 +9894,9 @@ msgstr ""
#: src/components/dialogs/SwitchAccount.tsx:47
#: src/components/dialogs/SwitchAccount.tsx:50
#: src/screens/Settings/Settings.tsx:124
#: src/screens/Settings/Settings.tsx:138
#: src/screens/Settings/Settings.tsx:619
#: src/screens/Settings/Settings.tsx:122
#: src/screens/Settings/Settings.tsx:134
#: src/screens/Settings/Settings.tsx:615
#: src/view/shell/desktop/LeftNav.tsx:249
msgid "Switch account"
msgstr ""
@@ -9909,7 +9920,7 @@ msgstr ""
#: src/screens/Log.tsx:57
#: src/screens/Settings/AboutSettings.tsx:106
#: src/screens/Settings/AboutSettings.tsx:109
#: src/screens/Settings/Settings.tsx:453
#: src/screens/Settings/Settings.tsx:449
msgid "System log"
msgstr ""
@@ -10469,7 +10480,7 @@ msgstr ""
msgid "This post has an unknown type of threadgate on it. Your app may be out of date."
msgstr ""
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:146
#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:157
msgid "This post is only visible to logged-in users."
msgstr ""
@@ -10497,7 +10508,7 @@ msgstr ""
msgid "This service has not provided terms of service or a privacy policy."
msgstr ""
#: src/features/liveNow/index.tsx:199
#: src/features/liveNow/index.tsx:200
msgid "This service is not supported while the Live feature is in beta. Allowed services: {formatted}."
msgstr ""
@@ -10562,7 +10573,7 @@ msgid "This will irreversibly delete your Bluesky account <0>{currentHandle}</0>
msgstr ""
#. placeholder {0}: account.handle
#: src/screens/Settings/Settings.tsx:673
#: src/screens/Settings/Settings.tsx:668
msgid "This will remove @{0} from the quick access list."
msgstr ""
@@ -10735,7 +10746,7 @@ msgstr ""
msgid "Two-factor authentication (2FA)"
msgstr ""
#: src/screens/Messages/components/MessageInput.tsx:155
#: src/screens/Messages/components/MessageInput.tsx:173
msgid "Type your message here"
msgstr ""
@@ -10773,11 +10784,11 @@ msgstr ""
msgid "Unable to resolve handle"
msgstr ""
#: src/screens/Settings/Settings.tsx:519
#: src/screens/Settings/Settings.tsx:515
msgid "Unapply Pull Request"
msgstr ""
#: src/screens/Settings/Settings.tsx:521
#: src/screens/Settings/Settings.tsx:517
msgid "Unapply Pull Request {currentChannel}"
msgstr ""
@@ -10982,8 +10993,8 @@ msgstr ""
msgid "Unpinned list"
msgstr ""
#: src/screens/Settings/Settings.tsx:486
#: src/screens/Settings/Settings.tsx:488
#: src/screens/Settings/Settings.tsx:482
#: src/screens/Settings/Settings.tsx:484
msgid "Unsnooze email reminder"
msgstr ""
@@ -11555,8 +11566,8 @@ msgstr ""
msgid "Warn content and filter from feeds"
msgstr ""
#: src/features/liveNow/components/LiveStatusDialog.tsx:188
#: src/features/liveNow/components/LiveStatusDialog.tsx:197
#: src/features/liveNow/components/LiveStatusDialog.tsx:189
#: src/features/liveNow/components/LiveStatusDialog.tsx:198
msgid "Watch now"
msgstr ""
@@ -11580,7 +11591,7 @@ msgstr ""
msgid "We couldn't find any results for that topic."
msgstr ""
#: src/screens/Messages/Conversation.tsx:143
#: src/screens/Messages/Conversation.tsx:145
msgid "We couldn't load this conversation"
msgstr ""
@@ -11856,8 +11867,6 @@ msgstr ""
msgid "Would you like to save this as a draft to edit later?"
msgstr ""
#: src/screens/Messages/components/MessageComposer.tsx:167
#: src/screens/Messages/components/MessageInput.tsx:156
#: src/screens/Messages/components/MessageInput.web.tsx:213
msgid "Write a message"
msgstr ""
@@ -11953,7 +11962,7 @@ msgstr ""
msgid "You are Live"
msgstr ""
#: src/features/liveNow/index.tsx:367
#: src/features/liveNow/index.tsx:368
msgid "You are no longer live"
msgstr ""
@@ -11965,7 +11974,7 @@ msgstr ""
msgid "You are not following anyone yet"
msgstr ""
#: src/features/liveNow/index.tsx:311
#: src/features/liveNow/index.tsx:312
msgid "You are now live!"
msgstr ""
@@ -12241,7 +12250,7 @@ msgstr ""
msgid "You previously deactivated @{0}."
msgstr ""
#: src/screens/Settings/Settings.tsx:416
#: src/screens/Settings/Settings.tsx:412
msgid "You probably want to restart the app now."
msgstr ""
@@ -12261,7 +12270,7 @@ msgstr ""
msgid "You recently changed your birthdate"
msgstr ""
#: src/screens/Settings/Settings.tsx:301
#: src/screens/Settings/Settings.tsx:297
#: src/view/shell/desktop/LeftNav.tsx:212
msgid "You will be signed out of all your accounts."
msgstr ""
+18 -14
View File
@@ -5,16 +5,19 @@ import {
moderateProfile,
type ModerationDecision,
} from '@atproto/api'
import {ScrollEdgeEffectProvider} from '@bsky.app/expo-scroll-edge-effect'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {
type RouteProp,
useFocusEffect,
useIsFocused,
useNavigation,
useRoute,
} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {RemoveScrollBar} from 'react-remove-scroll-bar'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {
@@ -30,7 +33,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfileQuery} from '#/state/queries/profile'
import {useSetMinimalShellMode} from '#/state/shell'
import {MessagesList} from '#/screens/Messages/components/MessagesList'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {atoms as a, useTheme, web} from '#/alf'
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
import {
@@ -62,7 +65,6 @@ export function MessagesConversationScreen(props: Props) {
}
export function MessagesConversationScreenInner({route}: Props) {
const {gtMobile} = useBreakpoints()
const setMinimalShellMode = useSetMinimalShellMode()
const convoId = route.params.conversation
@@ -71,25 +73,22 @@ export function MessagesConversationScreenInner({route}: Props) {
useFocusEffect(
useCallback(() => {
setCurrentConvoId(convoId)
if (IS_WEB && !gtMobile) {
setMinimalShellMode(true)
} else {
setMinimalShellMode(false)
}
setMinimalShellMode(true)
return () => {
setCurrentConvoId(undefined)
setMinimalShellMode(false)
}
}, [gtMobile, convoId, setCurrentConvoId, setMinimalShellMode]),
}, [convoId, setCurrentConvoId, setMinimalShellMode]),
)
return (
<Layout.Screen testID="convoScreen" style={web([{minHeight: 0}, a.flex_1])}>
<ConvoProvider key={convoId} convoId={convoId}>
<Inner />
</ConvoProvider>
<ScrollEdgeEffectProvider>
<ConvoProvider key={convoId} convoId={convoId}>
<Inner />
</ConvoProvider>
</ScrollEdgeEffectProvider>
</Layout.Screen>
)
}
@@ -98,6 +97,7 @@ function Inner() {
const t = useTheme()
const convoState = useConvo()
const {_} = useLingui()
const isFocused = useIsFocused()
const moderationOpts = useModerationOpts()
const {data: recipientUnshadowed} = useProfileQuery({
@@ -122,11 +122,13 @@ function Inner() {
// Any time that we re-render the `Initializing` state, we have to reset `hasScrolled` to false. After entering this
// state, we know that we're resetting the list of messages and need to re-scroll to the bottom when they get added.
useEffect(() => {
const [prevState, setPrevState] = useState(convoState.status)
if (prevState !== convoState.status) {
setPrevState(convoState.status)
if (convoState.status === ConvoStatus.Initializing) {
setHasScrolled(false)
}
}, [convoState.status])
}
if (convoState.status === ConvoStatus.Error) {
return (
@@ -150,6 +152,8 @@ function Inner() {
return (
<Layout.Center style={[a.flex_1]}>
{/* MessagesList does not use the body scroll */}
{isFocused && IS_WEB && <RemoveScrollBar />}
{!readyToShow &&
(moderation ? (
<MessagesListHeader moderation={moderation} profile={recipient} />
@@ -1,5 +1,19 @@
import {useEffect, useState} from 'react'
import {Pressable, View} from 'react-native'
import {
useKeyboardHandler,
useReanimatedKeyboardAnimation,
} from 'react-native-keyboard-controller'
import Animated, {
Extrapolation,
interpolate,
runOnJS,
useAnimatedStyle,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {GlassContainer} from 'expo-glass-effect'
import {LinearGradient} from 'expo-linear-gradient'
import {ScrollEdgeEffect} from '@bsky.app/expo-scroll-edge-effect'
import {useLingui} from '@lingui/react/macro'
import {countGraphemes} from 'unicode-segmenter/grapheme'
@@ -17,20 +31,24 @@ import {
EmojiPicker,
type EmojiPickerState,
} from '#/view/com/composer/text-input/web/EmojiPicker'
import {atoms as a, useTheme} from '#/alf'
import {atoms as a, native, platform, tokens, useTheme, utils} from '#/alf'
import {Composer, useComposerInternalApiRef} from '#/components/Composer'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
import {GlassView} from '#/components/GlassView'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
import * as Toast from '#/components/Toast'
import {IS_WEB} from '#/env'
import {IS_ANDROID, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
const MIN_HEIGHT = 40
export function MessageComposer({
textInputId,
onSendMessage,
hasEmbed,
setEmbed,
children,
}: {
textInputId?: string
onSendMessage: (message: string) => void
hasEmbed: boolean
setEmbed: (embedUrl: string | undefined) => void
@@ -48,16 +66,25 @@ export function MessageComposer({
})
const composerInternalApiRef = useComposerInternalApiRef()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {
state: hovered,
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
const [text, setText] = useState(getDraft)
useSaveMessageDraft(text)
// Android interactive dismiss sometimes doesn't blur the input
const blur = () => {
composerInternalApiRef.current?.input?.blur()
}
useKeyboardHandler({
onEnd: evt => {
'worklet'
if (IS_ANDROID && evt.progress === 0) {
runOnJS(blur)()
}
},
})
const submitDisabled = !editable || (!hasEmbed && text.trim().length === 0)
const openEmojiPicker = (pos: any) => {
setEmojiPickerState({isOpen: true, pos})
}
@@ -65,10 +92,12 @@ export function MessageComposer({
const onSubmit = () => {
if (!editable) return
if (!hasEmbed && text.trim() === '') return
if (countGraphemes(text) > MAX_DM_GRAPHEME_LENGTH) {
Toast.show(l`Message is too long`, {
type: 'error',
})
const graphemeCount = countGraphemes(text)
if (graphemeCount > MAX_DM_GRAPHEME_LENGTH) {
Toast.show(
l`Message is too long (${graphemeCount}/${MAX_DM_GRAPHEME_LENGTH})`,
{type: 'error'},
)
return
}
@@ -91,152 +120,111 @@ export function MessageComposer({
return () => {
textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted)
}
}, [])
}, [composerInternalApiRef])
return (
<>
<View style={[a.px_md, a.pb_sm, a.pt_xs]}>
{children}
<ComposerContainer>
{children}
<View
collapsable={false}
ref={
IS_WEB
? undefined
: node => {
composerInternalApiRef.current?.setAutocompleteAnchor(node)
}
}
// @ts-expect-error web only
onMouseEnter={onHoverIn}
onMouseLeave={onHoverOut}
style={[a.w_full, a.flex_row, a.gap_sm]}>
{IS_WEB && (
<Pressable
onPress={e => {
e.currentTarget.measure((_fx, _fy, _width, _height, px, py) => {
openEmojiPicker?.({
top: py,
left: px,
right: px,
bottom: py,
nextFocusRef: {
current: composerInternalApiRef.current?.input?.element,
<View
collapsable={false}
ref={native(
(node: View) =>
void composerInternalApiRef.current?.setAutocompleteAnchor(node),
)}>
<GlassContainer
style={[a.w_full, a.flex_row, a.gap_sm, a.align_end]}
spacing={tokens.space.sm}>
<GlassView
isInteractive
glassEffectStyle="regular"
style={[a.flex_1, a.rounded_xl, {minHeight: MIN_HEIGHT}]}
tintColor={t.palette.contrast_50}
fallbackStyle={[t.atoms.bg_contrast_50]}>
{IS_WEB && (
<Pressable
onPress={e => {
e.currentTarget.measure(
(_fx, _fy, _width, _height, px, py) => {
// TODO: rip this horrible system out
openEmojiPicker?.({
top: py,
left: px - 400,
right: px - 400,
bottom: py,
nextFocusRef: {
current:
composerInternalApiRef.current?.input?.element,
},
})
},
})
})
)
}}
style={[
a.overflow_hidden,
a.absolute,
a.rounded_full,
a.align_center,
a.justify_center,
a.z_30,
{
height: 20,
width: 20,
top: 10,
right: 10,
},
]}
accessibilityLabel={l`Open emoji picker`}
accessibilityHint="">
{state => (
<EmojiSmileIcon
size="md"
style={
state.hovered ||
state.focused ||
state.pressed ||
emojiPickerState.isOpen
? {color: t.palette.primary_500}
: t.atoms.text_contrast_high
}
/>
)}
</Pressable>
)}
<Composer
nativeID={textInputId}
label={l`Message input field`}
placeholder={l`Message`}
autocompletePlacement="top-start"
internalApiRef={composerInternalApiRef}
defaultValue={text}
editable={editable}
autoFocus={IS_WEB}
maxRows={12}
outerStyle={[a.flex_1]}
contentTextStyle={[a.text_md, a.leading_snug]}
contentPaddingStyle={{
paddingLeft: 16,
paddingTop: 10,
paddingBottom: 10,
paddingRight: 16 + platform({web: 20, default: 0}),
}}
style={[
a.overflow_hidden,
a.absolute,
a.rounded_full,
a.align_center,
a.justify_center,
a.z_30,
{
height: 30,
width: 30,
top: 8,
left: 8,
},
]}
accessibilityLabel={l`Open emoji picker`}
accessibilityHint="">
{state => (
<View
style={[
a.absolute,
a.inset_0,
a.align_center,
a.justify_center,
{
backgroundColor:
state.hovered || state.focused || state.pressed
? t.atoms.bg.backgroundColor
: undefined,
},
]}>
<EmojiSmile size="lg" />
</View>
)}
</Pressable>
)}
<Composer
label={l`Message input field`}
placeholder={l`Write a message`}
autocompletePlacement="top-start"
internalApiRef={composerInternalApiRef}
defaultValue={text}
editable={editable}
autoFocus={IS_WEB}
maxRows={12}
outerStyle={[
a.flex_1,
t.atoms.bg_contrast_25,
{
borderWidth: 1,
borderColor: 'transparent',
borderRadius: 22,
},
editable &&
hovered && {
borderColor: t.atoms.border_contrast_medium.borderColor,
},
editable &&
focused && {
borderColor: t.palette.primary_500,
},
]}
contentTextStyle={[a.text_md, a.leading_snug]}
contentPaddingStyle={{
paddingLeft: IS_WEB ? 30 + 12 : 12,
paddingTop: 12,
paddingBottom: 12,
paddingRight: 12,
}}
onFocus={onFocus}
onBlur={onBlur}
onChange={setText}
onFacetCommitted={facet => {
if (facet.type === 'url' && isBskyPostUrl(facet.value)) {
setEmbed(facet.value)
}
}}
onRequestSubmit={req => {
if (req.platform === 'web' && req.shiftKey) return
req.nativeEvent.preventDefault()
onSubmit()
}}
/>
{focused || text.length ? (
<Pressable
accessibilityRole="button"
accessibilityLabel={l`Send message`}
accessibilityHint=""
hitSlop={HITSLOP_10}
style={[
a.rounded_full,
a.align_center,
a.justify_center,
a.self_end,
a.z_30,
{
height: 44,
width: 44,
backgroundColor: t.palette.primary_500,
},
]}
onPress={onSubmit}
disabled={!editable}>
<PaperPlane
fill={t.palette.white}
style={[a.relative, {left: 1}]}
/>
</Pressable>
) : null}
</View>
onChange={setText}
onFacetCommitted={facet => {
if (facet.type === 'url' && isBskyPostUrl(facet.value)) {
setEmbed(facet.value)
}
}}
onRequestSubmit={req => {
if (req.platform === 'web' && req.shiftKey) return
req.nativeEvent.preventDefault()
onSubmit()
}}
/>
</GlassView>
<SubmitButton onPress={onSubmit} disabled={submitDisabled} />
</GlassContainer>
</View>
{IS_WEB && (
@@ -246,6 +234,114 @@ export function MessageComposer({
close={() => setEmojiPickerState(prev => ({...prev, isOpen: false}))}
/>
)}
</>
</ComposerContainer>
)
}
function SubmitButton({
onPress,
disabled,
}: {
onPress: () => void
disabled: boolean
}) {
const {t: l} = useLingui()
const t = useTheme()
return (
<GlassView
isInteractive
glassEffectStyle="regular"
style={[a.rounded_full]}
tintColor={disabled ? t.palette.contrast_100 : t.palette.primary_500}
fallbackStyle={{
backgroundColor: disabled
? t.palette.contrast_100
: t.palette.primary_500,
}}>
<Pressable
accessibilityRole="button"
accessibilityLabel={l`Send message`}
accessibilityHint=""
hitSlop={HITSLOP_10}
style={[
a.rounded_full,
a.align_center,
a.justify_center,
{height: MIN_HEIGHT, width: MIN_HEIGHT},
]}
onPress={onPress}
disabled={disabled}>
<PaperPlaneIcon size="md" fill={t.palette.white} style={[a.mb_2xs]} />
</Pressable>
</GlassView>
)
}
// TODO: remove export when MessageInput is deleted
export function ComposerContainer({children}: {children: React.ReactNode}) {
const {bottom: bottomInset} = useSafeAreaInsets()
const {progress} = useReanimatedKeyboardAnimation()
const t = useTheme()
const animatedContainerStyle = useAnimatedStyle(() => ({
paddingHorizontal: interpolate(
progress.get(),
[0, 1],
[bottomInset, tokens.space.sm],
{
extrapolateRight: Extrapolation.CLAMP,
extrapolateLeft: Extrapolation.CLAMP,
},
),
}))
if (IS_LIQUID_GLASS) {
return (
<ScrollEdgeEffect edge="bottom">
<Animated.View style={[a.w_full, animatedContainerStyle, a.pb_lg]}>
{children}
</Animated.View>
</ScrollEdgeEffect>
)
} else {
return (
<>
<LinearGradient
style={platform({
native: [a.pt_sm, a.px_lg, , a.pb_lg, a.w_full],
web: [
a.pt_xs,
a.pl_lg,
a.pb_lg,
// prevent overlap with the scrollbar, which looks ugly
a.pr_xs, // xs + md = lg
{width: `calc(100% - ${tokens.space.md}px)` as '100%'},
],
})}
key={t.name} // android does not update when you change the colors. sigh.
start={[0.5, 0]}
end={[0.5, 1]}
colors={[
utils.alpha(t.atoms.bg.backgroundColor, 0),
utils.alpha(t.atoms.bg.backgroundColor, 0.8),
t.atoms.bg.backgroundColor,
]}>
{children}
</LinearGradient>
{/* covers the gap between the keyboard and the input during keyboard animation */}
{IS_NATIVE && (
<View
style={[
t.atoms.bg,
a.absolute,
a.left_0,
a.right_0,
{top: '100%', height: bottomInset + 1, marginTop: -1},
]}
/>
)}
</>
)
}
}
+120 -78
View File
@@ -1,17 +1,20 @@
import {useCallback, useState} from 'react'
import {Pressable, TextInput, useWindowDimensions, View} from 'react-native'
import {Pressable, TextInput, useWindowDimensions} from 'react-native'
import {
useFocusedInputHandler,
useKeyboardHandler,
useReanimatedKeyboardAnimation,
} from 'react-native-keyboard-controller'
import Animated, {
measure,
runOnJS,
useAnimatedProps,
useAnimatedRef,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {GlassContainer} from 'expo-glass-effect'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {countGraphemes} from 'unicode-segmenter/grapheme'
@@ -24,21 +27,26 @@ import {
useSaveMessageDraft,
} from '#/state/messages/message-drafts'
import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker'
import {android, atoms as a, useTheme} from '#/alf'
import {useSharedInputStyles} from '#/components/forms/TextField'
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
import {atoms as a, platform, tokens, useTheme} from '#/alf'
import {GlassView} from '#/components/GlassView'
import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
import * as Toast from '#/components/Toast'
import {IS_IOS, IS_WEB} from '#/env'
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
import {ComposerContainer} from './MessageComposer'
import {useExtractEmbedFromFacets} from './MessageInputEmbed'
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
const MIN_HEIGHT = 40
export function MessageInput({
textInputId,
onSendMessage,
hasEmbed,
setEmbed,
children,
}: {
textInputId?: string
onSendMessage: (message: string) => void
hasEmbed: boolean
setEmbed: (embedUrl: string | undefined) => void
@@ -57,8 +65,6 @@ export function MessageInput({
const maxHeight = useSharedValue<undefined | number>(undefined)
const isInputScrollable = useSharedValue(false)
const inputStyles = useSharedInputStyles()
const [isFocused, setIsFocused] = useState(false)
const [message, setMessage] = useState(getDraft)
const inputRef = useAnimatedRef<TextInput>()
const [shouldEnforceClear, setShouldEnforceClear] = useState(false)
@@ -133,78 +139,114 @@ export function MessageInput({
scrollEnabled: isInputScrollable.get(),
}))
const submitDisabled = needsEmailVerification || message.trim().length === 0
const blur = useCallback(() => {
inputRef.current?.blur()
}, [inputRef])
useKeyboardHandler({
onEnd: evt => {
'worklet'
// small hack: interactive dismiss on Android sometimes doesn't blur the input
if (IS_ANDROID && evt.progress === 0) {
runOnJS(blur)()
}
},
})
return (
<View style={[a.px_md, a.pb_sm, a.pt_xs]}>
<ComposerContainer>
{children}
<View
style={[
a.w_full,
a.flex_row,
t.atoms.bg_contrast_25,
{
padding: a.p_sm.padding - 2,
paddingLeft: a.p_md.padding - 2,
borderWidth: 1,
borderRadius: 23,
borderColor: 'transparent',
},
isFocused && inputStyles.chromeFocus,
]}>
<AnimatedTextInput
accessibilityLabel={_(msg`Message input field`)}
accessibilityHint={_(msg`Type your message here`)}
placeholder={_(msg`Write a message`)}
placeholderTextColor={t.palette.contrast_500}
value={message}
onChange={evt => {
// bit of a hack: iOS automatically accepts autocomplete suggestions when you tap anywhere on the screen
// including the button we just pressed - and this overrides clearing the input! so we watch for the
// next change and double make sure the input is cleared. It should *always* send an onChange event after
// clearing via setMessage('') that happens in onSubmit()
// -sfn
if (IS_IOS && shouldEnforceClear) {
setShouldEnforceClear(false)
setMessage('')
return
}
const text = evt.nativeEvent.text
setMessage(text)
}}
multiline={true}
style={[
a.flex_1,
a.text_md,
a.px_sm,
t.atoms.text,
android({paddingTop: 0}),
{paddingBottom: IS_IOS ? 5 : 0},
animatedStyle,
]}
keyboardAppearance={t.scheme}
submitBehavior="newline"
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
ref={inputRef}
hitSlop={HITSLOP_10}
animatedProps={animatedProps}
editable={!needsEmailVerification}
/>
<Pressable
accessibilityRole="button"
accessibilityLabel={_(msg`Send message`)}
accessibilityHint=""
hitSlop={HITSLOP_10}
style={[
a.rounded_full,
a.align_center,
a.justify_center,
{height: 30, width: 30, backgroundColor: t.palette.primary_500},
]}
onPress={onSubmit}
disabled={needsEmailVerification}>
<PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} />
</Pressable>
</View>
</View>
<GlassContainer
style={[a.flex_row, a.align_end, a.gap_sm]}
spacing={tokens.space.xs}>
<GlassView
isInteractive
glassEffectStyle="regular"
style={[a.flex_1, a.rounded_xl, {minHeight: MIN_HEIGHT}]}
tintColor={t.palette.contrast_50}
fallbackStyle={[t.atoms.bg_contrast_50]}>
<AnimatedTextInput
nativeID={textInputId}
accessibilityLabel={_(msg`Message input field`)}
accessibilityHint={_(msg`Type your message here`)}
placeholder={_(msg`Message`)}
placeholderTextColor={t.palette.contrast_500}
value={message}
onChange={evt => {
// bit of a hack: iOS automatically accepts autocomplete suggestions when you tap anywhere on the screen
// including the button we just pressed - and this overrides clearing the input! so we watch for the
// next change and double make sure the input is cleared. It should *always* send an onChange event after
// clearing via setMessage('') that happens in onSubmit()
// -sfn
if (IS_IOS && shouldEnforceClear) {
setShouldEnforceClear(false)
setMessage('')
return
}
const text = evt.nativeEvent.text
setMessage(text)
}}
multiline={true}
style={[
{flexBasis: 'auto', minHeight: MIN_HEIGHT},
a.flex_shrink_0,
a.flex_grow,
a.text_md,
a.px_lg,
t.atoms.text,
platform({
android: {paddingTop: 2, paddingBottom: 3},
ios: {paddingTop: 10, paddingBottom: 5},
}),
animatedStyle,
]}
verticalAlign="middle"
keyboardAppearance={t.scheme}
submitBehavior="newline"
ref={inputRef}
hitSlop={HITSLOP_10}
animatedProps={animatedProps}
editable={!needsEmailVerification}
/>
</GlassView>
<GlassView
isInteractive
glassEffectStyle="regular"
style={[a.rounded_full]}
tintColor={
submitDisabled ? t.palette.contrast_100 : t.palette.primary_500
}
fallbackStyle={{
backgroundColor: submitDisabled
? t.palette.contrast_100
: t.palette.primary_500,
}}>
<Pressable
accessibilityRole="button"
accessibilityLabel={_(msg`Send message`)}
accessibilityHint=""
hitSlop={HITSLOP_10}
style={[
a.rounded_full,
a.align_center,
a.justify_center,
{
height: MIN_HEIGHT,
width: MIN_HEIGHT,
},
]}
onPress={onSubmit}
disabled={submitDisabled}>
<PaperPlaneIcon
size="md"
fill={t.palette.white}
style={[a.mb_2xs]}
/>
</Pressable>
</GlassView>
</GlassContainer>
</ComposerContainer>
)
}
+171 -139
View File
@@ -1,22 +1,28 @@
import {useCallback, useEffect, useRef, useState} from 'react'
import {type LayoutChangeEvent, View} from 'react-native'
import {useKeyboardHandler} from 'react-native-keyboard-controller'
import {useCallback, useEffect, useId, useRef, useState} from 'react'
import {type LayoutChangeEvent, type ScrollViewProps, View} from 'react-native'
import {
KeyboardChatScrollView,
type KeyboardChatScrollViewProps,
KeyboardGestureArea,
} from 'react-native-keyboard-controller'
import Animated, {
runOnJS,
scrollTo,
type ScrollEvent,
type SharedValue,
useAnimatedRef,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
} from 'react-native-reanimated'
import {type ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {
type $Typed,
type AppBskyEmbedRecord,
AppBskyRichtextFacet,
RichText,
} from '@atproto/api'
import {useScrollEdgeEffectRef} from '@bsky.app/expo-scroll-edge-effect'
import {useHideBottomBarBorderForScreen} from '#/lib/hooks/useHideBottomBarBorder'
import {mergeRefs} from '#/lib/merge-refs'
import {ScrollProvider} from '#/lib/ScrollContext'
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
import {
@@ -36,7 +42,6 @@ import {
} from '#/state/messages/convo/types'
import {useGetPost} from '#/state/queries/post'
import {useAgent} from '#/state/session'
import {useShellLayout} from '#/state/shell/shell-layout'
import {
EmojiPicker,
type EmojiPickerState,
@@ -46,15 +51,17 @@ import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled'
import {MessageComposer} from '#/screens/Messages/components/MessageComposer'
import {MessageInput} from '#/screens/Messages/components/MessageInput'
import {MessageListError} from '#/screens/Messages/components/MessageListError'
import {atoms as a, platform, tokens, useTheme, web} from '#/alf'
import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
import {MessageItem} from '#/components/dms/MessageItem'
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE, IS_WEB} from '#/env'
import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env'
import {ChatStatusInfo} from './ChatStatusInfo'
import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
import {KeyboardStickyView} from './vendor/KeyboardStickyView'
function MaybeLoader({isLoading}: {isLoading: boolean}) {
return (
@@ -108,9 +115,9 @@ export function MessagesList({
const agent = useAgent()
const getPost = useGetPost()
const {embedUri, setEmbed} = useMessageEmbed()
const t = useTheme()
useHideBottomBarBorderForScreen()
const textInputId = 'chat-input-' + useId()
const flatListRef = useAnimatedRef<ListMethods>()
const [newMessagesPill, setNewMessagesPill] = useState({
@@ -123,6 +130,18 @@ export function MessagesList({
pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null},
})
const inputHeightUI = useSharedValue(0)
const [inputHeightJS, setInputHeightJS] = useState(0)
const onInputLayout = useCallback(
(event: LayoutChangeEvent) => {
const inputHeight = event.nativeEvent.layout.height
inputHeightUI.set(inputHeight)
setInputHeightJS(inputHeight)
},
[inputHeightUI],
)
// We need to keep track of when the scroll offset is at the bottom of the list to know when to scroll as new items
// are added to the list. For example, if the user is scrolled up to 1iew older messages, we don't want to scroll to
// the bottom.
@@ -226,12 +245,12 @@ export function MessagesList({
const onStartReached = useCallback(() => {
if (hasScrolled && prevContentHeight.current > layoutHeight.get()) {
convoState.fetchMessageHistory()
void convoState.fetchMessageHistory()
}
}, [convoState, hasScrolled, layoutHeight])
const onScroll = useCallback(
(e: ReanimatedScrollEvent) => {
(e: ScrollEvent) => {
'worklet'
layoutHeight.set(e.layoutMeasurement.height)
const bottomOffset = e.contentOffset.y + e.layoutMeasurement.height
@@ -256,56 +275,8 @@ export function MessagesList({
)
// -- Keyboard animation handling
const {footerHeight} = useShellLayout()
const keyboardHeight = useSharedValue(0)
const keyboardIsOpening = useSharedValue(false)
// In some cases - like when the emoji piker opens - we don't want to animate the scroll in the list onLayout event.
// We use this value to keep track of when we want to disable the animation.
const layoutScrollWithoutAnimation = useSharedValue(false)
useKeyboardHandler(
{
onStart: e => {
'worklet'
// Immediate updates - like opening the emoji picker - will have a duration of zero. In those cases, we should
// just update the height here instead of having the `onMove` event do it (that event will not fire!)
if (e.duration === 0) {
layoutScrollWithoutAnimation.set(true)
keyboardHeight.set(e.height)
} else {
keyboardIsOpening.set(true)
}
},
onMove: e => {
'worklet'
keyboardHeight.set(e.height)
if (e.height > footerHeight.get()) {
scrollTo(flatListRef, 0, 1e7, false)
}
},
onEnd: e => {
'worklet'
keyboardHeight.set(e.height)
if (e.height > footerHeight.get()) {
scrollTo(flatListRef, 0, 1e7, false)
}
keyboardIsOpening.set(false)
},
},
[footerHeight],
)
const animatedListStyle = useAnimatedStyle(() => ({
marginBottom: Math.max(keyboardHeight.get(), footerHeight.get()),
}))
const animatedStickyViewStyle = useAnimatedStyle(() => ({
transform: [
{translateY: -Math.max(keyboardHeight.get(), footerHeight.get())},
],
}))
const {bottom: bottomInset} = useSafeAreaInsets()
// -- Message sending
const onSendMessage = useCallback(
@@ -387,26 +358,6 @@ export function MessagesList({
[agent, convoState, embedUri, getPost, hasScrolled, setHasScrolled],
)
// -- List layout changes (opening emoji keyboard, etc.)
const onListLayout = useCallback(
(e: LayoutChangeEvent) => {
layoutHeight.set(e.nativeEvent.layout.height)
if (IS_WEB || !keyboardIsOpening.get()) {
flatListRef.current?.scrollToEnd({
animated: !layoutScrollWithoutAnimation.get(),
})
layoutScrollWithoutAnimation.set(false)
}
},
[
flatListRef,
keyboardIsOpening,
layoutScrollWithoutAnimation,
layoutHeight,
],
)
const scrollToEndOnPress = useCallback(() => {
flatListRef.current?.scrollToOffset({
offset: prevContentHeight.current,
@@ -418,66 +369,100 @@ export function MessagesList({
setEmojiPickerState({isOpen: true, pos})
}, [])
const renderScrollComponent = useCallback(
(props: ScrollViewProps) => (
<ChatScrollComponent {...props} inputHeight={inputHeightUI} />
),
[inputHeightUI],
)
return (
<>
{/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
<ScrollProvider onScroll={onScroll}>
<List
ref={flatListRef}
data={convoState.items}
renderItem={renderItem}
keyExtractor={keyExtractor}
disableFullWindowScroll={true}
disableVirtualization={true}
style={animatedListStyle}
// The extra two items account for the header and the footer components
initialNumToRender={IS_NATIVE ? 32 : 62}
maxToRenderPerBatch={IS_WEB ? 32 : 62}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
maintainVisibleContentPosition={{
minIndexForVisible: 0,
}}
removeClippedSubviews={false}
sideBorders={false}
onContentSizeChange={onContentSizeChange}
onLayout={onListLayout}
onStartReached={onStartReached}
onScrollToIndexFailed={onScrollToIndexFailed}
scrollEventThrottle={100}
ListHeaderComponent={
<MaybeLoader isLoading={convoState.isFetchingHistory} />
}
/>
</ScrollProvider>
<Animated.View style={animatedStickyViewStyle}>
{convoState.status === ConvoStatus.Disabled ? (
<ChatDisabled />
) : blocked ? (
footer
) : (
<ConversationFooter
convoState={convoState}
hasAcceptOverride={hasAcceptOverride}>
{ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? (
<MessageComposer
onSendMessage={onSendMessage}
hasEmbed={!!embedUri}
setEmbed={setEmbed}>
<MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
</MessageComposer>
) : (
<MessageInput
onSendMessage={onSendMessage}
hasEmbed={!!embedUri}
setEmbed={setEmbed}
openEmojiPicker={onOpenEmojiPicker}>
<MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
</MessageInput>
<KeyboardGestureArea
interpolator="ios"
// HACKFIX: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1419
offset={Math.round(inputHeightJS)}
textInputNativeID={textInputId}
style={[a.flex_1]}>
{/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */}
<ScrollProvider onScroll={onScroll}>
<List
ref={flatListRef}
data={convoState.items}
renderItem={renderItem}
keyExtractor={keyExtractor}
disableFullWindowScroll={true}
disableVirtualization={true}
// The extra two items account for the header and the footer components
initialNumToRender={IS_NATIVE ? 32 : 62}
maxToRenderPerBatch={IS_WEB ? 32 : 62}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
maintainVisibleContentPosition={{minIndexForVisible: 0}}
removeClippedSubviews={false}
sideBorders={false}
onContentSizeChange={onContentSizeChange}
onStartReached={onStartReached}
onScrollToIndexFailed={onScrollToIndexFailed}
showsVerticalScrollIndicator={!IS_ANDROID}
scrollEventThrottle={100}
ListHeaderComponent={
<MaybeLoader isLoading={convoState.isFetchingHistory} />
}
// native only (prop is not supported on web)
renderScrollComponent={renderScrollComponent}
// pushes up the content under the input on web (renderScrollComponent handles it on native)
ListFooterComponent={web(
<WebInputSpacer inputHeight={inputHeightJS} />,
)}
</ConversationFooter>
)}
</Animated.View>
style={web({
scrollbarWidth: 'thin',
scrollbarColor: `${t.palette.contrast_100} transparent`,
scrollbarGutter: 'stable both-edges',
})}
/>
</ScrollProvider>
<KeyboardStickyView
style={[a.absolute, a.bottom_0, a.left_0, a.right_0]}
onLayout={onInputLayout}
minimumOffset={bottomInset}
offset={{
closed: platform({
ios: tokens.space.lg, // hide bottom padding when closed
default: 0,
}),
opened: 0,
}}>
{convoState.status === ConvoStatus.Disabled ? (
<ChatDisabled />
) : blocked ? (
footer
) : (
<ConversationFooter
convoState={convoState}
hasAcceptOverride={hasAcceptOverride}>
{ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? (
<MessageComposer
textInputId={textInputId}
onSendMessage={onSendMessage}
hasEmbed={!!embedUri}
setEmbed={setEmbed}>
<MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
</MessageComposer>
) : (
<MessageInput
textInputId={textInputId}
onSendMessage={onSendMessage}
hasEmbed={!!embedUri}
setEmbed={setEmbed}
openEmojiPicker={onOpenEmojiPicker}>
<MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
</MessageInput>
)}
</ConversationFooter>
)}
</KeyboardStickyView>
</KeyboardGestureArea>
{IS_WEB && (
<EmojiPicker
@@ -492,6 +477,53 @@ export function MessagesList({
)
}
/** Note: native only */
function ChatScrollComponent({
ref,
inputHeight,
...props
}: ScrollViewProps & {
ref?: React.RefObject<KeyboardChatScrollViewProps>
inputHeight: SharedValue<number>
}) {
const scrollEdgeRef = useScrollEdgeEffectRef()
const {bottom: bottomInset} = useSafeAreaInsets()
const offset = platform({
ios: bottomInset - tokens.space.lg,
android: bottomInset,
default: 0,
})
const inputOffset = platform({
ios: bottomInset - tokens.space.lg,
android: bottomInset,
default: 0,
})
const extraContentPadding = useDerivedValue(
() => inputHeight.get() + inputOffset,
)
return (
<KeyboardChatScrollView
ref={mergeRefs([scrollEdgeRef, ref])}
automaticallyAdjustContentInsets={false}
keyboardDismissMode="interactive"
keyboardLiftBehavior="always"
extraContentPadding={extraContentPadding}
offset={offset}
{...props}
/>
)
}
function WebInputSpacer({inputHeight}: {inputHeight: number}) {
if (!IS_WEB) return null
return <Animated.View style={{height: inputHeight}} />
}
type FooterState = 'loading' | 'new-chat' | 'request' | 'standard'
function getFooterState(
@@ -0,0 +1,48 @@
import {
type KeyboardStickyViewProps,
useReanimatedKeyboardAnimation,
} from 'react-native-keyboard-controller'
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
// Vendored from https://github.com/kirillzyusko/react-native-keyboard-controller/blob/main/src/components/KeyboardStickyView/index.tsx
// Converted to Reanimated to support `minimumOffset` clamping.
export function KeyboardStickyView({
children,
offset: {closed = 0, opened = 0} = {},
style,
enabled = true,
minimumOffset,
...props
}: KeyboardStickyViewProps & {
/**
* Stop the stickyview going lower than this (i.e. bottom safe area)
*/
minimumOffset?: number
}) {
const {height, progress} = useReanimatedKeyboardAnimation()
const animatedStyle = useAnimatedStyle(() => {
const offset = closed + (opened - closed) * progress.get()
let translateY: number
if (enabled) {
let h = height.get()
if (minimumOffset != null) {
h = Math.min(h, -minimumOffset)
}
translateY = h + offset
} else {
translateY = closed
}
return {
transform: [{translateY}],
}
})
return (
<Animated.View style={[animatedStyle, style]} {...props}>
{children}
</Animated.View>
)
}
+49 -55
View File
@@ -2,9 +2,7 @@ import {useState} from 'react'
import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native'
import {useReducedMotion} from 'react-native-reanimated'
import {type AppBskyActorDefs, moderateProfile} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -73,7 +71,7 @@ import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscript
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
export function SettingsScreen({}: Props) {
const ax = useAnalytics()
const {_} = useLingui()
const {t: l} = useLingui()
const reducedMotion = useReducedMotion()
const {logoutEveryAccount} = useSessionApi()
const {accounts, currentAccount} = useSession()
@@ -121,10 +119,8 @@ export function SettingsScreen({}: Props) {
{accounts.length > 1 ? (
<>
<SettingsList.PressableItem
label={_(msg`Switch account`)}
accessibilityHint={_(
msg`Shows other accounts you can switch to`,
)}
label={l`Switch account`}
accessibilityHint={l`Shows other accounts you can switch to`}
onPress={() => {
if (!reducedMotion) {
LayoutAnimation.configureNext(
@@ -174,7 +170,7 @@ export function SettingsScreen({}: Props) {
<AddAccountRow />
)}
<SettingsList.Divider />
<SettingsList.LinkItem to="/settings/account" label={_(msg`Account`)}>
<SettingsList.LinkItem to="/settings/account" label={l`Account`}>
<SettingsList.ItemIcon icon={PersonIcon} />
<SettingsList.ItemText>
<Trans>Account</Trans>
@@ -182,21 +178,23 @@ export function SettingsScreen({}: Props) {
</SettingsList.LinkItem>
<SettingsList.LinkItem
to="/settings/privacy-and-security"
label={_(msg`Privacy and security`)}>
label={l`Privacy and security`}>
<SettingsList.ItemIcon icon={LockIcon} />
<SettingsList.ItemText>
<Trans>Privacy and security</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
<SettingsList.LinkItem to="/moderation" label={_(msg`Moderation`)}>
<SettingsList.LinkItem
to="/moderation"
label={l`Moderation and content filters`}>
<SettingsList.ItemIcon icon={HandIcon} />
<SettingsList.ItemText>
<Trans>Moderation</Trans>
<Trans>Moderation and content filters</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
<SettingsList.LinkItem
to="/settings/notifications"
label={_(msg`Notifications`)}>
label={l`Notifications`}>
<SettingsList.ItemIcon icon={NotificationIcon} />
<SettingsList.ItemText>
<Trans>Notifications</Trans>
@@ -204,7 +202,7 @@ export function SettingsScreen({}: Props) {
</SettingsList.LinkItem>
<SettingsList.LinkItem
to="/settings/content-and-media"
label={_(msg`Content and media`)}>
label={l`Content and media`}>
<SettingsList.ItemIcon icon={WindowIcon} />
<SettingsList.ItemText>
<Trans>Content and media</Trans>
@@ -215,7 +213,7 @@ export function SettingsScreen({}: Props) {
!ax.features.enabled(ax.features.ImportContactsSettingsDisable) && (
<SettingsList.LinkItem
to="/settings/find-contacts"
label={_(msg`Find friends from contacts`)}>
label={l`Find friends from contacts`}>
<SettingsList.ItemIcon icon={ContactsIcon} />
<SettingsList.ItemText>
<Trans>Find friends from contacts</Trans>
@@ -224,7 +222,7 @@ export function SettingsScreen({}: Props) {
)}
<SettingsList.LinkItem
to="/settings/appearance"
label={_(msg`Appearance`)}>
label={l`Appearance`}>
<SettingsList.ItemIcon icon={PaintRollerIcon} />
<SettingsList.ItemText>
<Trans>Appearance</Trans>
@@ -232,15 +230,13 @@ export function SettingsScreen({}: Props) {
</SettingsList.LinkItem>
<SettingsList.LinkItem
to="/settings/accessibility"
label={_(msg`Accessibility`)}>
label={l`Accessibility`}>
<SettingsList.ItemIcon icon={AccessibilityIcon} />
<SettingsList.ItemText>
<Trans>Accessibility</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
<SettingsList.LinkItem
to="/settings/language"
label={_(msg`Languages`)}>
<SettingsList.LinkItem to="/settings/language" label={l`Languages`}>
<SettingsList.ItemIcon icon={EarthIcon} />
<SettingsList.ItemText>
<Trans>Languages</Trans>
@@ -248,15 +244,15 @@ export function SettingsScreen({}: Props) {
</SettingsList.LinkItem>
<SettingsList.PressableItem
onPress={() => void Linking.openURL(HELP_DESK_URL)}
label={_(msg`Help`)}
accessibilityHint={_(msg`Opens helpdesk in browser`)}>
label={l`Help`}
accessibilityHint={l`Opens helpdesk in browser`}>
<SettingsList.ItemIcon icon={CircleQuestionIcon} />
<SettingsList.ItemText>
<Trans>Help</Trans>
</SettingsList.ItemText>
<SettingsList.Chevron />
</SettingsList.PressableItem>
<SettingsList.LinkItem to="/settings/about" label={_(msg`About`)}>
<SettingsList.LinkItem to="/settings/about" label={l`About`}>
<SettingsList.ItemIcon icon={BubbleInfoIcon} />
<SettingsList.ItemText>
<Trans>About</Trans>
@@ -266,7 +262,7 @@ export function SettingsScreen({}: Props) {
<SettingsList.PressableItem
destructive
onPress={() => signOutPromptControl.open()}
label={_(msg`Sign out`)}>
label={l`Sign out`}>
<SettingsList.ItemText>
<Trans>Sign out</Trans>
</SettingsList.ItemText>
@@ -283,7 +279,7 @@ export function SettingsScreen({}: Props) {
}
setShowDevOptions(d => !d)
}}
label={_(msg`Developer options`)}>
label={l`Developer options`}>
<SettingsList.ItemIcon icon={CodeBracketsIcon} />
<SettingsList.ItemText>
<Trans>Developer options</Trans>
@@ -297,11 +293,11 @@ export function SettingsScreen({}: Props) {
<Prompt.Basic
control={signOutPromptControl}
title={_(msg`Sign out?`)}
description={_(msg`You will be signed out of all your accounts.`)}
title={l`Sign out?`}
description={l`You will be signed out of all your accounts.`}
onConfirm={() => logoutEveryAccount('Settings')}
confirmButtonCta={_(msg`Sign out`)}
cancelButtonCta={_(msg`Cancel`)}
confirmButtonCta={l`Sign out`}
cancelButtonCta={l`Cancel`}
confirmButtonColor="negative"
/>
@@ -378,7 +374,7 @@ function ProfilePreview({
}
function DevOptions() {
const {_} = useLingui()
const {t: l} = useLingui()
const agent = useAgent()
const [override, setOverride] = useStorage(device, [
'policyUpdateDebugOverride',
@@ -397,12 +393,12 @@ function DevOptions() {
const resetOnboarding = () => {
navigation.navigate('Home')
onboardingDispatch({type: 'start'})
Toast.show(_(msg`Onboarding reset`))
Toast.show(l`Onboarding reset`)
}
const clearAllStorage = async () => {
await clearStorage()
Toast.show(_(msg`Storage cleared, you need to restart the app now.`))
Toast.show(l`Storage cleared, you need to restart the app now.`)
}
const onPressUnsnoozeReminder = () => {
@@ -413,7 +409,7 @@ function DevOptions() {
...persisted.get('reminders'),
lastEmailConfirm: lastEmailConfirm.toISOString(),
})
Toast.show(_(msg`You probably want to restart the app now.`))
Toast.show(l`You probably want to restart the app now.`)
}
const onPressActySubsUnNudge = () => {
@@ -448,42 +444,42 @@ function DevOptions() {
<>
<SettingsList.PressableItem
onPress={() => navigation.navigate('Log')}
label={_(msg`Open system log`)}>
label={l`Open system log`}>
<SettingsList.ItemText>
<Trans>System log</Trans>
</SettingsList.ItemText>
</SettingsList.PressableItem>
<SettingsList.PressableItem
onPress={() => navigation.navigate('Debug')}
label={_(msg`Open storybook page`)}>
label={l`Open storybook page`}>
<SettingsList.ItemText>
<Trans>Storybook</Trans>
</SettingsList.ItemText>
</SettingsList.PressableItem>
<SettingsList.PressableItem
onPress={() => navigation.navigate('DebugMod')}
label={_(msg`Open moderation debug page`)}>
label={l`Open moderation debug page`}>
<SettingsList.ItemText>
<Trans>Debug Moderation</Trans>
</SettingsList.ItemText>
</SettingsList.PressableItem>
<SettingsList.PressableItem
onPress={() => deleteChatDeclarationRecord()}
label={_(msg`Open storybook page`)}>
label={l`Open storybook page`}>
<SettingsList.ItemText>
<Trans>Delete chat declaration record</Trans>
</SettingsList.ItemText>
</SettingsList.PressableItem>
<SettingsList.PressableItem
onPress={() => void resetOnboarding()}
label={_(msg`Reset onboarding state`)}>
label={l`Reset onboarding state`}>
<SettingsList.ItemText>
<Trans>Reset onboarding state</Trans>
</SettingsList.ItemText>
</SettingsList.PressableItem>
<SettingsList.PressableItem
onPress={onPressUnsnoozeReminder}
label={_(msg`Unsnooze email reminder`)}>
label={l`Unsnooze email reminder`}>
<SettingsList.ItemText>
<Trans>Unsnooze email reminder</Trans>
</SettingsList.ItemText>
@@ -491,7 +487,7 @@ function DevOptions() {
{actyNotifNudged && (
<SettingsList.PressableItem
onPress={onPressActySubsUnNudge}
label={_(msg`Reset activity subscription nudge`)}>
label={l`Reset activity subscription nudge`}>
<SettingsList.ItemText>
<Trans>Reset activity subscription nudge</Trans>
</SettingsList.ItemText>
@@ -499,7 +495,7 @@ function DevOptions() {
)}
<SettingsList.PressableItem
onPress={() => void clearAllStorage()}
label={_(msg`Clear all storage data`)}>
label={l`Clear all storage data`}>
<SettingsList.ItemText>
<Trans>Clear all storage data (restart after this)</Trans>
</SettingsList.ItemText>
@@ -507,7 +503,7 @@ function DevOptions() {
{IS_IOS ? (
<SettingsList.PressableItem
onPress={onPressApplyOta}
label={_(msg`Apply Pull Request`)}>
label={l`Apply Pull Request`}>
<SettingsList.ItemText>
<Trans>Apply Pull Request</Trans>
</SettingsList.ItemText>
@@ -516,7 +512,7 @@ function DevOptions() {
{IS_NATIVE && isCurrentlyRunningPullRequestDeployment ? (
<SettingsList.PressableItem
onPress={() => void revertToEmbedded()}
label={_(msg`Unapply Pull Request`)}>
label={l`Unapply Pull Request`}>
<SettingsList.ItemText>
<Trans>Unapply Pull Request {currentChannel}</Trans>
</SettingsList.ItemText>
@@ -564,7 +560,7 @@ function DevOptions() {
}
function AddAccountRow() {
const {_} = useLingui()
const {t: l} = useLingui()
const {setShowLoggedOut} = useLoggedOutViewControls()
const closeEverything = useCloseAllActiveElements()
@@ -576,7 +572,7 @@ function AddAccountRow() {
return (
<SettingsList.PressableItem
onPress={onAddAnotherAccount}
label={_(msg`Add another account`)}>
label={l`Add another account`}>
<SettingsList.ItemIcon icon={PersonPlusIcon} />
<SettingsList.ItemText>
<Trans>Add another account</Trans>
@@ -599,7 +595,7 @@ function AccountRow({
logContext: 'Settings',
) => void
}) {
const {_} = useLingui()
const {t: l} = useLingui()
const t = useTheme()
const moderationOpts = useModerationOpts()
@@ -616,7 +612,7 @@ function AccountRow({
<View style={[a.relative]}>
<SettingsList.PressableItem
onPress={onSwitchAccount}
label={_(msg`Switch account`)}>
label={l`Switch account`}>
{moderationOpts && profile ? (
<UserAvatar
size={28}
@@ -638,7 +634,7 @@ function AccountRow({
</SettingsList.PressableItem>
{!pendingDid && (
<Menu.Root>
<Menu.Trigger label={_(msg`Account options`)}>
<Menu.Trigger label={l`Account options`}>
{({props, state}) => (
<Pressable
{...props}
@@ -655,7 +651,7 @@ function AccountRow({
</Menu.Trigger>
<Menu.Outer showCancel>
<Menu.Item
label={_(msg`Remove account`)}
label={l`Remove account`}
onPress={() => removePromptControl.open()}>
<Menu.ItemText>
<Trans>Remove account</Trans>
@@ -668,15 +664,13 @@ function AccountRow({
<Prompt.Basic
control={removePromptControl}
title={_(msg`Remove from quick access?`)}
description={_(
msg`This will remove @${account.handle} from the quick access list.`,
)}
title={l`Remove from quick access?`}
description={l`This will remove @${account.handle} from the quick access list.`}
onConfirm={() => {
removeAccount(account)
Toast.show(_(msg`Account removed from quick access`))
Toast.show(l`Account removed from quick access`)
}}
confirmButtonCta={_(msg`Remove`)}
confirmButtonCta={l`Remove`}
confirmButtonColor="negative"
/>
</View>
+14 -1
View File
@@ -1,6 +1,11 @@
import {useCallback} from 'react'
import {type AppBskyActorDefs, type AppBskyFeedDefs, AtUri} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {
type QueryClient,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {updatePostShadow} from '#/state/cache/post-shadow'
@@ -43,6 +48,14 @@ export function usePostQuery(uri: string | undefined) {
})
}
export function precachePost(
queryClient: QueryClient,
uri: string,
post: AppBskyFeedDefs.PostView,
) {
queryClient.setQueryData(RQKEY(uri), post)
}
export function useGetPost() {
const queryClient = useQueryClient()
const agent = useAgent()