Merge branch 'main' into app-2066
This commit is contained in:
+1
-1
@@ -235,7 +235,7 @@ function App() {
|
||||
<Geo.Provider>
|
||||
<AppConfigProvider>
|
||||
<A11yProvider>
|
||||
<KeyboardControllerProvider>
|
||||
<KeyboardControllerProvider preload={false}>
|
||||
<OnboardingProvider>
|
||||
<AnalyticsContext>
|
||||
<SessionProvider>
|
||||
|
||||
@@ -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
@@ -20,7 +20,7 @@ export const atoms = {
|
||||
*/
|
||||
util_screen_outer: [
|
||||
web({
|
||||
minHeight: '100vh',
|
||||
minHeight: '100dvh',
|
||||
}),
|
||||
native({
|
||||
height: '100%',
|
||||
|
||||
@@ -338,6 +338,8 @@ export function Composer({
|
||||
web({
|
||||
caretColor: textStyle.color ?? 'black',
|
||||
overscrollBehavior: 'none',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${t.palette.contrast_200} transparent`,
|
||||
}),
|
||||
]}
|
||||
{...rest}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -303,7 +303,7 @@ let PostControls = ({
|
||||
isToggled={Boolean(post.viewer?.like)}
|
||||
hasBeenToggled={hasLikeIconBeenToggled}
|
||||
renderCount={({count}) => (
|
||||
<PostControlButtonText>
|
||||
<PostControlButtonText testID="likeCount">
|
||||
{formatPostStatCount(count)}
|
||||
</PostControlButtonText>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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 ""
|
||||
|
||||
@@ -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},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user