Merge remote-tracking branch 'origin/main' into app-d9-oom-fixes
This commit is contained in:
@@ -134,6 +134,7 @@ jobs:
|
||||
|
||||
- name: 📷 Check fingerprint and install dependencies
|
||||
id: fingerprint
|
||||
timeout-minutes: 5
|
||||
uses: bluesky-social/github-actions/fingerprint-native@b5556913e4aef3964cfd5936d0add3fc0d809bdb # v0.1.0
|
||||
with:
|
||||
profile: pull-request
|
||||
@@ -168,11 +169,13 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: gh pr edit "$PR_NUMBER" --add-label "bot: fingerprint changed" || true
|
||||
run: |
|
||||
gh pr edit "$PR_NUMBER" --add-label "bot: fingerprint changed" || true
|
||||
|
||||
- name: 🏷️ Remove fingerprint changed label
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: gh pr edit "$PR_NUMBER" --remove-label "bot: fingerprint changed" || true
|
||||
run: |
|
||||
gh pr edit "$PR_NUMBER" --remove-label "bot: fingerprint changed" || true
|
||||
|
||||
@@ -10,7 +10,7 @@ Bluesky Social is a cross-platform social media application built with React Nat
|
||||
|
||||
- React 19.1
|
||||
- React Native 0.81 with Expo 54
|
||||
- TypeScript 6
|
||||
- TypeScript 7
|
||||
- React Navigation 7 for routing
|
||||
- TanStack Query (React Query) for data fetching
|
||||
- Lingui 5 for internationalization
|
||||
@@ -32,6 +32,7 @@ pnpm ios # Run on iOS
|
||||
pnpm test # Run Jest tests
|
||||
pnpm lint # Run ESLint
|
||||
pnpm typecheck # Run TypeScript type checking
|
||||
pnpm prettier # Run Prettier for code formatting
|
||||
|
||||
# Internationalization
|
||||
# DO NOT run these commands - extraction and compilation are handled by CI
|
||||
|
||||
@@ -1940,15 +1940,6 @@
|
||||
}
|
||||
},
|
||||
"src/view/com/composer/Composer.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
},
|
||||
"@typescript-eslint/no-misused-promises": {
|
||||
"count": 4
|
||||
},
|
||||
"@typescript-eslint/no-unsafe-member-access": {
|
||||
"count": 2
|
||||
},
|
||||
"react-hooks/immutability": {
|
||||
"count": 2
|
||||
},
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import {useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
AppBskyActorStatus,
|
||||
type AppBskyEmbedExternal,
|
||||
} from '@atproto/api'
|
||||
import {type AppBskyActorDefs, type AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -24,6 +20,7 @@ import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {
|
||||
displayDuration,
|
||||
getValidLiveStatusRecord,
|
||||
useLiveLinkMetaQuery,
|
||||
useRemoveLiveStatusMutation,
|
||||
useUpsertLiveStatusMutation,
|
||||
@@ -74,14 +71,10 @@ function DialogInner({
|
||||
error: linkMetaError,
|
||||
} = useLiveLinkMetaQuery(debouncedUrl)
|
||||
|
||||
const record = useMemo(() => {
|
||||
if (!AppBskyActorStatus.isRecord(status.record)) return null
|
||||
const validation = AppBskyActorStatus.validateRecord(status.record)
|
||||
if (validation.success) {
|
||||
return validation.value
|
||||
}
|
||||
return null
|
||||
}, [status])
|
||||
const record = useMemo(
|
||||
() => getValidLiveStatusRecord(status.record),
|
||||
[status],
|
||||
)
|
||||
|
||||
const {
|
||||
mutate: goLive,
|
||||
|
||||
@@ -20,7 +20,9 @@ import * as Select from '#/components/Select'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {
|
||||
displayDuration,
|
||||
getLiveLinkFromStatusRecord,
|
||||
getLiveServiceNames,
|
||||
useActorStatus,
|
||||
useLiveLinkMetaQuery,
|
||||
useLiveNowConfig,
|
||||
useUpsertLiveStatusMutation,
|
||||
@@ -50,16 +52,20 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
|
||||
const control = Dialog.useDialogContext()
|
||||
const {_, i18n} = useLingui()
|
||||
const t = useTheme()
|
||||
const [liveLink, setLiveLink] = useState('')
|
||||
const [liveLinkError, setLiveLinkError] = useState('')
|
||||
const [duration, setDuration] = useState(60)
|
||||
const moderationOpts = useModerationOpts()
|
||||
const tick = useTickEveryMinute()
|
||||
const liveNowConfig = useLiveNowConfig()
|
||||
const status = useActorStatus(profile)
|
||||
const {formatted: allowedServices} = getLiveServiceNames(
|
||||
liveNowConfig.currentAccountAllowedHosts,
|
||||
)
|
||||
|
||||
const [liveLink, setLiveLink] = useState(() =>
|
||||
getLiveLinkFromStatusRecord(status.record),
|
||||
)
|
||||
|
||||
const time = useCallback(
|
||||
(offset: number) => {
|
||||
void tick
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
import {AppBskyActorStatus, AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {type I18n} from '@lingui/core'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import psl from 'psl'
|
||||
|
||||
/**
|
||||
* Validates a raw status record and returns the typed record, or null if the
|
||||
* value is not a valid `app.bsky.actor.status` record.
|
||||
*/
|
||||
export function getValidLiveStatusRecord(
|
||||
statusRecord: unknown,
|
||||
): AppBskyActorStatus.Record | null {
|
||||
if (!AppBskyActorStatus.isRecord(statusRecord)) return null
|
||||
const validation = AppBskyActorStatus.validateRecord(statusRecord)
|
||||
if (!validation.success) return null
|
||||
return validation.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the external link URI from a status record, if present. Returns an
|
||||
* empty string when the record is invalid or has no external embed.
|
||||
*/
|
||||
export function getLiveLinkFromStatusRecord(statusRecord: unknown): string {
|
||||
const record = getValidLiveStatusRecord(statusRecord)
|
||||
if (!record) return ''
|
||||
if (!AppBskyEmbedExternal.isMain(record.embed)) return ''
|
||||
return record.embed.external.uri
|
||||
}
|
||||
|
||||
export function displayDuration(i18n: I18n, durationInMinutes: number) {
|
||||
const roundedDurationInMinutes = Math.round(durationInMinutes)
|
||||
const hours = Math.floor(roundedDurationInMinutes / 60)
|
||||
|
||||
+274
-311
File diff suppressed because it is too large
Load Diff
@@ -121,7 +121,9 @@ export default function HashtagScreen({
|
||||
<Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText>
|
||||
{author && (
|
||||
<Layout.Header.SubtitleText>
|
||||
{_(msg`From @${sanitizedAuthor}`)}
|
||||
{author.startsWith('did:')
|
||||
? _(msg`From ${sanitizedAuthor}`)
|
||||
: _(msg`From @${sanitizedAuthor}`)}
|
||||
</Layout.Header.SubtitleText>
|
||||
)}
|
||||
</Layout.Header.Content>
|
||||
@@ -172,11 +174,9 @@ function HashtagScreenTab({
|
||||
const isCashtag = fullTag.startsWith('$')
|
||||
|
||||
const queryParam = useMemo(() => {
|
||||
// Cashtags need # prefix for search: "#$BTC" or "#$BTC from:author"
|
||||
const searchTag = isCashtag ? `#${fullTag}` : fullTag
|
||||
if (!author) return searchTag
|
||||
return `${searchTag} from:${author}`
|
||||
}, [fullTag, author, isCashtag])
|
||||
// Cashtags need # prefix for search: "#$BTC"
|
||||
return isCashtag ? `#${fullTag}` : fullTag
|
||||
}, [fullTag, isCashtag])
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -188,7 +188,7 @@ function HashtagScreenTab({
|
||||
refetch,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
} = useSearchPostsQuery({query: queryParam, sort, enabled: active})
|
||||
} = useSearchPostsQuery({query: queryParam, sort, enabled: active, author})
|
||||
|
||||
const posts = useMemo(() => {
|
||||
return data?.pages.flatMap(page => page.posts) || []
|
||||
|
||||
+1
-5
@@ -78,11 +78,7 @@ export function LogScreen({}: NativeStackScreenProps<
|
||||
) : (
|
||||
<CircleInfoIcon size="sm" />
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<View style={[a.flex_1, a.gap_sm]}>
|
||||
{entry.context && (
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
({String(entry.context)})
|
||||
|
||||
@@ -4,7 +4,8 @@ import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {type Shadow} from '#/state/cache/types'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
|
||||
import {ProfileBadges} from '#/components/ProfileBadges'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function ProfileHeaderDisplayName({
|
||||
@@ -18,7 +19,7 @@ export function ProfileHeaderDisplayName({
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
return (
|
||||
<View pointerEvents="none">
|
||||
<View>
|
||||
<Text
|
||||
emoji
|
||||
testID="profileHeaderDisplayName"
|
||||
@@ -27,11 +28,15 @@ export function ProfileHeaderDisplayName({
|
||||
gtMobile ? a.text_4xl : a.text_3xl,
|
||||
a.self_start,
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
]}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
<View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}>
|
||||
<ProfileBadges profile={profile} size="lg" interactive />
|
||||
</View>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {
|
||||
@@ -22,7 +21,7 @@ import {
|
||||
} from '#/state/queries/profile'
|
||||
import {useRequireAuth, useSession} from '#/state/session'
|
||||
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
|
||||
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
|
||||
import {atoms as a, platform} from '#/alf'
|
||||
import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
|
||||
@@ -34,16 +33,15 @@ import {
|
||||
KnownFollowers,
|
||||
shouldShowKnownFollowers,
|
||||
} from '#/components/KnownFollowers'
|
||||
import {ProfileBadges} from '#/components/ProfileBadges'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_IOS, IS_NATIVE} from '#/env'
|
||||
import {InviteFriendsDialog} from '#/features/inviteFriends'
|
||||
import {useActorStatus} from '#/features/liveNow'
|
||||
import {GermButton} from '../components/GermButton'
|
||||
import {ProfileHeaderDisplayName} from './DisplayName'
|
||||
import {EditProfileDialog} from './EditProfileDialog'
|
||||
import {ProfileHeaderHandle} from './Handle'
|
||||
import {ProfileHeaderMetrics} from './Metrics'
|
||||
@@ -65,8 +63,6 @@ let ProfileHeaderStandard = ({
|
||||
hideBackButton = false,
|
||||
isPlaceholderProfile,
|
||||
}: Props): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const profile =
|
||||
useProfileShadow<AppBskyActorDefs.ProfileViewDetailed>(profileUnshadowed)
|
||||
const {currentAccount} = useSession()
|
||||
@@ -138,26 +134,10 @@ let ProfileHeaderStandard = ({
|
||||
</View>
|
||||
<View
|
||||
style={[a.flex_col, a.gap_xs, a.pb_sm, live ? a.pt_sm : a.pt_2xs]}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}>
|
||||
<Text
|
||||
emoji
|
||||
testID="profileHeaderDisplayName"
|
||||
style={[
|
||||
t.atoms.text,
|
||||
gtMobile ? a.text_4xl : a.text_3xl,
|
||||
a.self_start,
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
]}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
<View style={[a.pl_xs, {marginTop: platform({ios: 2})}]}>
|
||||
<ProfileBadges profile={profile} size="lg" interactive />
|
||||
</View>
|
||||
</Text>
|
||||
</View>
|
||||
<ProfileHeaderDisplayName
|
||||
profile={profile}
|
||||
moderation={moderation}
|
||||
/>
|
||||
<ProfileHeaderHandle profile={profile} />
|
||||
</View>
|
||||
{!isPlaceholderProfile && !isBlockedUser && (
|
||||
|
||||
@@ -154,6 +154,24 @@ function DialogInner({
|
||||
const scrollRef = useRef<ScrollView>(null)
|
||||
const filtersSectionRef = useRef<View>(null)
|
||||
|
||||
const suggestions = [
|
||||
{
|
||||
all: l({
|
||||
message: 'cats dogs',
|
||||
comment: 'Advanced search: Example of an “all of these words” search',
|
||||
}),
|
||||
none: l({
|
||||
message: 'cows pigs',
|
||||
comment: 'Advanced search: Example of a “none of these words” search',
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [suggestion] = useState(() =>
|
||||
Math.floor(Math.random() * suggestions.length),
|
||||
)
|
||||
|
||||
function addFilter() {
|
||||
if (filters.length >= MAX_FILTERS) return
|
||||
/*
|
||||
@@ -261,11 +279,7 @@ function DialogInner({
|
||||
<ClearableInput
|
||||
label={l`Search query`}
|
||||
defaultValue={query}
|
||||
placeholder={l({
|
||||
message: 'bluesky atproto',
|
||||
comment:
|
||||
'Advanced search: Example of an “all of these words” search',
|
||||
})}
|
||||
placeholder={suggestions[suggestion].all}
|
||||
onChangeText={setQuery}
|
||||
onSubmitEditing={handlePressSearch}
|
||||
/>
|
||||
@@ -295,11 +309,7 @@ function DialogInner({
|
||||
<ClearableInput
|
||||
label={l`None of these words`}
|
||||
defaultValue={negatedWords}
|
||||
placeholder={l({
|
||||
message: 'cows pigs',
|
||||
comment:
|
||||
'Advanced search: Example of an “none of these words” search',
|
||||
})}
|
||||
placeholder={suggestions[suggestion].none}
|
||||
onChangeText={setNegatedWords}
|
||||
onSubmitEditing={handlePressSearch}
|
||||
/>
|
||||
|
||||
@@ -454,7 +454,7 @@ export function usePinnedFeedsInfos() {
|
||||
pinnedItems.map(f => f.value),
|
||||
),
|
||||
gcTime: GCTIME.INFINITY,
|
||||
staleTime: STALE.INFINITY,
|
||||
staleTime: STALE.MINUTES.FIFTEEN,
|
||||
enabled: !isLoadingPrefs,
|
||||
queryFn: async () => {
|
||||
if (!hasSession) {
|
||||
|
||||
@@ -11,6 +11,7 @@ export const STALE = {
|
||||
ONE: MINUTE,
|
||||
THREE: 3 * MINUTE,
|
||||
FIVE: 5 * MINUTE,
|
||||
FIFTEEN: 15 * MINUTE,
|
||||
THIRTY: 30 * MINUTE,
|
||||
},
|
||||
HOURS: {
|
||||
|
||||
@@ -22,29 +22,35 @@ import {
|
||||
} from './util'
|
||||
|
||||
const searchPostsQueryKeyRoot = 'search-posts'
|
||||
const searchPostsQueryKey = ({query, sort}: {query: string; sort?: string}) => [
|
||||
searchPostsQueryKeyRoot,
|
||||
const searchPostsQueryKey = ({
|
||||
query,
|
||||
sort,
|
||||
]
|
||||
author,
|
||||
}: {
|
||||
query: string
|
||||
sort?: string
|
||||
author?: string
|
||||
}) => [searchPostsQueryKeyRoot, query, sort, author]
|
||||
|
||||
export function useSearchPostsQuery({
|
||||
query,
|
||||
sort,
|
||||
enabled,
|
||||
author,
|
||||
}: {
|
||||
query: string
|
||||
sort?: 'top' | 'latest'
|
||||
enabled?: boolean
|
||||
author?: string
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const selectArgs = useMemo(
|
||||
() => ({
|
||||
isSearchingSpecificUser: /from:(\w+)/.test(query),
|
||||
isSearchingSpecificUser: !!author || /from:(\w+)/.test(query),
|
||||
moderationOpts,
|
||||
}),
|
||||
[query, moderationOpts],
|
||||
[query, author, moderationOpts],
|
||||
)
|
||||
const lastRun = useRef<{
|
||||
data: InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
|
||||
@@ -59,13 +65,14 @@ export function useSearchPostsQuery({
|
||||
QueryKey,
|
||||
string | undefined
|
||||
>({
|
||||
queryKey: searchPostsQueryKey({query, sort}),
|
||||
queryKey: searchPostsQueryKey({query, sort, author}),
|
||||
queryFn: async ({pageParam}) => {
|
||||
const res = await agent.app.bsky.feed.searchPosts({
|
||||
q: query,
|
||||
limit: 25,
|
||||
cursor: pageParam,
|
||||
sort,
|
||||
author,
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ActivityIndicator,
|
||||
BackHandler,
|
||||
Keyboard,
|
||||
KeyboardAvoidingView,
|
||||
type LayoutChangeEvent,
|
||||
ScrollView,
|
||||
type StyleProp,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
|
||||
// @ts-expect-error no type definition
|
||||
import ProgressCircle from 'react-native-progress/Circle'
|
||||
import Animated, {
|
||||
@@ -72,7 +72,6 @@ import {
|
||||
type SupportedMimeTypes,
|
||||
VIDEO_MAX_DURATION_MS,
|
||||
} from '#/lib/constants'
|
||||
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {createVideoTelemetry} from '#/lib/media/video/telemetry'
|
||||
import {mimeToExt} from '#/lib/media/video/util'
|
||||
@@ -287,7 +286,6 @@ export const ComposePost = ({
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
|
||||
const [isPublishing, setIsPublishing] = useState(false)
|
||||
const [publishingStage, setPublishingStage] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
@@ -856,17 +854,9 @@ export const ComposePost = ({
|
||||
const viewStyles = useMemo(
|
||||
() => ({
|
||||
paddingTop: IS_ANDROID ? insets.top : 0,
|
||||
paddingBottom:
|
||||
// iOS - when keyboard is closed, keep the bottom bar in the safe area
|
||||
(IS_IOS && !isKeyboardVisible) ||
|
||||
// Android - Android >=35 KeyboardAvoidingView adds double padding when
|
||||
// keyboard is closed, so we subtract that in the offset and add it back
|
||||
// here when the keyboard is open
|
||||
(IS_ANDROID && isKeyboardVisible)
|
||||
? insets.bottom
|
||||
: 0,
|
||||
paddingBottom: insets.bottom,
|
||||
}),
|
||||
[insets, isKeyboardVisible],
|
||||
[insets.top, insets.bottom],
|
||||
)
|
||||
|
||||
const onPressCancel = useCallback(() => {
|
||||
@@ -1100,13 +1090,13 @@ export const ComposePost = ({
|
||||
posts,
|
||||
}
|
||||
}
|
||||
} catch (waitErr: any) {
|
||||
} catch (waitErr) {
|
||||
logger.info(`composer: waiting for app view failed`, {
|
||||
safeMessage: waitErr,
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e, {
|
||||
} catch (e) {
|
||||
logger.error(e instanceof Error ? e : String(e), {
|
||||
message: `Composer: create post failed`,
|
||||
hasImages: filteredThread.posts.some(
|
||||
p =>
|
||||
@@ -1115,7 +1105,7 @@ export const ComposePost = ({
|
||||
),
|
||||
})
|
||||
|
||||
let err = cleanError(e.message)
|
||||
let err = e instanceof Error ? cleanError(e.message) : String(e)
|
||||
if (
|
||||
e instanceof apilib.ReplyDeletedError ||
|
||||
err.includes('not locate record')
|
||||
@@ -1417,8 +1407,8 @@ export const ComposePost = ({
|
||||
publishingStage={publishingStage}
|
||||
topBarAnimatedStyle={topBarAnimatedStyle}
|
||||
onCancel={onPressCancel}
|
||||
onPublish={onPressPublish}
|
||||
onSelectDraft={handleSelectDraft}
|
||||
onPublish={() => void onPressPublish()}
|
||||
onSelectDraft={draft => void handleSelectDraft(draft)}
|
||||
onSaveDraft={saveCurrentDraft}
|
||||
onDiscard={handleClearComposer}
|
||||
isEmpty={isComposerEmpty}
|
||||
@@ -1531,7 +1521,7 @@ export const ComposePost = ({
|
||||
{allPostsWithinLimit && (
|
||||
<Prompt.Action
|
||||
cta={composerState.draftId ? l`Save changes` : l`Save draft`}
|
||||
onPress={handleSaveDraft}
|
||||
onPress={() => void handleSaveDraft()}
|
||||
color="primary"
|
||||
/>
|
||||
)}
|
||||
@@ -1694,7 +1684,7 @@ let ComposerPost = memo(function ComposerPost({
|
||||
postId: post.id,
|
||||
})
|
||||
}}
|
||||
onPhotoPasted={onPhotoPasted}
|
||||
onPhotoPasted={uri => void onPhotoPasted(uri)}
|
||||
onNewLink={onNewLink}
|
||||
onError={onError}
|
||||
onPressPublish={onPublish}
|
||||
@@ -2188,7 +2178,7 @@ function ComposerFooter({
|
||||
}),
|
||||
).catch(e => {
|
||||
logger.error(`createComposerImage failed`, {
|
||||
safeMessage: e.message,
|
||||
safeMessage: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2425,24 +2415,31 @@ function useScrollTracker({
|
||||
}
|
||||
|
||||
function useKeyboardVerticalOffset() {
|
||||
const {top, bottom} = useSafeAreaInsets()
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
// Android etc
|
||||
if (!IS_IOS) {
|
||||
// need to account for the edge-to-edge nav bar
|
||||
return bottom * -1
|
||||
// the keyboardavoidingview has bottom padding to avoid being obscured by the safe area when keyboard is closed.
|
||||
// however, this leads to a gap when the keyboard is open. we account for that by subtracting the bottom inset when open.
|
||||
let keyboardVerticalOffset = insets.bottom * -1
|
||||
|
||||
// iOS requires a bit of extra offset to account for the native sheet not being at the top of the screen
|
||||
if (IS_IOS) {
|
||||
// they ditched the gap behaviour on 26
|
||||
if (IS_LIQUID_GLASS) {
|
||||
keyboardVerticalOffset += insets.top
|
||||
}
|
||||
|
||||
// iPhone SE
|
||||
else if (insets.top === 20) {
|
||||
keyboardVerticalOffset += 40
|
||||
}
|
||||
|
||||
// all other iPhones on <26
|
||||
else {
|
||||
keyboardVerticalOffset += insets.top + 10
|
||||
}
|
||||
}
|
||||
|
||||
// they ditched the gap behaviour on 26
|
||||
if (IS_LIQUID_GLASS) {
|
||||
return top
|
||||
}
|
||||
|
||||
// iPhone SE
|
||||
if (top === 20) return 40
|
||||
|
||||
// all other iPhones on <26
|
||||
return top + 10
|
||||
return keyboardVerticalOffset
|
||||
}
|
||||
|
||||
async function whenAppViewReady(
|
||||
|
||||
Reference in New Issue
Block a user