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