Merge branch 'main' into app-1934

This commit is contained in:
vineyardbovines
2026-04-02 11:03:08 -04:00
18 changed files with 320 additions and 551 deletions
+32 -36
View File
@@ -560,32 +560,30 @@ export function ProfileGrid({
<Text style={[a.text_sm, a.font_semi_bold, t.atoms.text]}>
<Trans>Suggested for you</Trans>
</Text>
{!isProfileHeaderContext && (
<Button
label={l`See more suggested profiles`}
onPress={() => {
followDialogControl.open()
ax.metric('suggestedUser:seeMore', {
logContext,
recId,
})
}}>
{({hovered}) => (
<Text
style={[
a.text_sm,
{color: t.palette.primary_500},
hovered &&
web({
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary_500,
}),
]}>
<Trans>See more</Trans>
</Text>
)}
</Button>
)}
<Button
label={l`See more suggested profiles`}
onPress={() => {
followDialogControl.open()
ax.metric('suggestedUser:seeMore', {
logContext,
recId,
})
}}>
{({hovered}) => (
<Text
style={[
a.text_sm,
{color: t.palette.primary_500},
hovered &&
web({
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary_500,
}),
]}>
<Trans>See more</Trans>
</Text>
)}
</Button>
</View>
<FollowDialogWithoutGuide control={followDialogControl} />
<LayoutAnimationConfig skipExiting skipEntering>
@@ -605,16 +603,14 @@ export function ProfileGrid({
decelerationRate="fast">
{content}
{!isProfileHeaderContext && (
<SeeMoreSuggestedProfilesCard
onPress={() => {
followDialogControl.open()
ax.metric('suggestedUser:seeMore', {
logContext,
})
}}
/>
)}
<SeeMoreSuggestedProfilesCard
onPress={() => {
followDialogControl.open()
ax.metric('suggestedUser:seeMore', {
logContext,
})
}}
/>
</ScrollView>
</BlockDrawerGesture>
)}
@@ -13,6 +13,7 @@ import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause'
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
import {KeepAwake} from '#/components/KeepAwake'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {GifPresentationControls} from '../GifPresentationControls'
@@ -112,6 +113,7 @@ export function VideoEmbedInnerNative({
/>
)}
<MediaInsetBorder />
<KeepAwake enabled={isPlaying} />
</View>
)
}
+2 -3
View File
@@ -3,8 +3,7 @@ import {View} from 'react-native'
import {Select as RadixSelect} from 'radix-ui'
import {useA11y} from '#/state/a11y'
import {flatten, useTheme, web} from '#/alf'
import {atoms as a} from '#/alf'
import {atoms as a, flatten, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {
@@ -109,7 +108,7 @@ export function Trigger({children, label}: TriggerProps) {
borderRadius: 10,
maxWidth: 400,
outline: 0,
borderWidth: 2,
borderWidth: 1,
borderStyle: 'solid',
borderColor: focused
? t.palette.primary_500
+5 -1
View File
@@ -1,6 +1,6 @@
import {View} from 'react-native'
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
import {atoms as a, useTheme, type ViewStyleProp, web as webOnly} from '#/alf'
import {IS_NATIVE, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
export function SubtleHover({
@@ -33,6 +33,10 @@ export function SubtleHover({
a.transition_opacity,
t.atoms.bg_contrast_50,
style,
// Force Safari to composite the overlay on its own GPU layer.
// This fixes a layout shift that happens due to different subpixel
// rounding when the overlay is composited on hover.
webOnly({willChange: 'opacity'}),
{opacity: hover ? opacity : 0},
]}
/>
@@ -63,7 +63,7 @@ export function DateFieldButton({
paddingLeft: 14,
paddingRight: 14,
borderColor: 'transparent',
borderWidth: 2,
borderWidth: 1,
},
native({
paddingTop: 10,
+8
View File
@@ -10,6 +10,10 @@ const iOSMajorVersion =
Platform.OS === 'ios' && typeof Platform.Version === 'string'
? parseInt(Platform.Version.split('.')[0], 10)
: 0
const androidPlatformVersion =
Platform.OS === 'android' && typeof Platform.Version === 'number'
? Platform.Version
: 0
/**
* The semver version of the app, specified in our `package.json`.file. On
@@ -49,3 +53,7 @@ export const IS_WEB_FIREFOX: boolean = false
export const IS_HIGH_DPI: boolean = true
// ideally we'd use isLiquidGlassAvailable() from expo-glass-effect but checking iOS version is good enough for now
export const IS_LIQUID_GLASS: boolean = iOSMajorVersion >= 26
// So we can avoid attempting on-device translation when we know it's unsupported.
export const IS_TRANSLATION_SUPPORTED: boolean =
(IS_IOS && iOSMajorVersion >= 18) ||
(IS_ANDROID && androidPlatformVersion > 22)
+1
View File
@@ -48,3 +48,4 @@ export const IS_HIGH_DPI: boolean = window.matchMedia(
'(min-resolution: 2dppx)',
).matches
export const IS_LIQUID_GLASS: boolean = false
export const IS_TRANSLATION_SUPPORTED: boolean = false
+31 -10
View File
@@ -1,18 +1,17 @@
import {useCallback, useContext, useEffect, useMemo, useState} from 'react'
import {LayoutAnimation, Platform} from 'react-native'
import {getLocales} from 'expo-localization'
import {
isTranslationSupported,
onTranslateTask,
} from '@bsky.app/expo-translate-text'
import {onTranslateTask} from '@bsky.app/expo-translate-text'
import {type TranslationTaskResult} from '@bsky.app/expo-translate-text/build/ExpoTranslateText.types'
import {useLingui} from '@lingui/react/macro'
import {useFocusEffect} from '@react-navigation/native'
import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
import {codeToLanguageName} from '#/locale/helpers'
import {logger} from '#/logger'
import {useLanguagePrefs} from '#/state/preferences'
import {useAnalytics} from '#/analytics'
import {IS_ANDROID, IS_IOS} from '#/env'
import {IS_ANDROID, IS_IOS, IS_TRANSLATION_SUPPORTED} from '#/env'
import {Context} from './context'
import {
type ContextType,
@@ -25,6 +24,11 @@ import {guessLanguage} from './utils'
export * from './types'
export * from './utils'
const E_SAME_AS_SOURCE_LANGUAGE =
'Translation result is the same as the source text.'
const E_EMPTY_RESULT = 'Translation result is empty.'
const E_INVALID_SOURCE_LANGUAGE = 'Invalid source language'
/**
* Attempts on-device translation via @bsky.app/expo-translate-text.
* Uses a lazy import to avoid crashing if the native module isn't linked into
@@ -80,11 +84,11 @@ async function attemptTranslation(
typeof result.translatedTexts === 'string' ? result.translatedTexts : ''
if (translatedText === input) {
throw new Error('Translation result is the same as the source text.')
throw new Error(E_SAME_AS_SOURCE_LANGUAGE)
}
if (translatedText === '') {
throw new Error('Translation result is empty.')
throw new Error(E_EMPTY_RESULT)
}
return {
@@ -159,6 +163,7 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
>({})
const [refCounts, setRefCounts] = useState<Record<string, number>>({})
const ax = useAnalytics()
const langPrefs = useLanguagePrefs()
const {t: l} = useLingui()
const googleTranslate = useGoogleTranslate()
@@ -235,7 +240,7 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
googleTranslate: shouldForceGoogleTranslate,
})
if (shouldForceGoogleTranslate || !isTranslationSupported()) {
if (shouldForceGoogleTranslate || !IS_TRANSLATION_SUPPORTED) {
await googleTranslate(
text,
expectedTargetLanguage,
@@ -280,7 +285,8 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
postLanguages: possibleSourceLanguages,
},
}))
} catch (e) {
} catch (err) {
const e = err as Error
logger.error('Failed to translate text on device', {safeMessage: e})
// On-device translation failed (language pack missing or user
// dismissed the download prompt).
@@ -295,6 +301,21 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
textLength: text.length,
})
let errorMessage = l`Device failed to translate :(`
if (e.message === E_SAME_AS_SOURCE_LANGUAGE) {
errorMessage = l`Translation to the same language is unavailable on your device.`
}
if (e.message === E_EMPTY_RESULT) {
errorMessage = l`No translation received from your device.`
}
if (
expectedSourceLanguage &&
e.message.includes(E_INVALID_SOURCE_LANGUAGE)
) {
errorMessage = l`${codeToLanguageName(
expectedSourceLanguage,
langPrefs.appLanguage,
)} is not supported by your device.`
}
if (!IS_ANDROID) {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
}
@@ -304,7 +325,7 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
}))
}
},
[ax, googleTranslate, l],
[ax, googleTranslate, l, langPrefs.appLanguage],
)
const ctx = useMemo(
+47 -30
View File
@@ -182,6 +182,11 @@ msgstr ""
msgid "{0} is not available"
msgstr ""
#. placeholder {0}: codeToLanguageName( expectedSourceLanguage, langPrefs.appLanguage, )
#: src/lib/translation/index.tsx:314
msgid "{0} is not supported by your device."
msgstr "{0} is not supported by your device."
#. placeholder {0}: formatCount(i18n, JOINED_THIS_WEEK)
#: src/screens/StarterPack/StarterPackLandingScreen.tsx:232
msgid "{0} joined this week"
@@ -852,8 +857,8 @@ msgstr ""
#: src/view/com/composer/GifAltText.tsx:76
#: src/view/com/composer/GifAltText.tsx:150
#: src/view/com/composer/GifAltText.tsx:217
#: src/view/com/composer/photos/Gallery.tsx:194
#: src/view/com/composer/photos/Gallery.tsx:241
#: src/view/com/composer/photos/Gallery.tsx:196
#: src/view/com/composer/photos/Gallery.tsx:243
#: src/view/com/composer/photos/ImageAltTextDialog.tsx:95
#: src/view/com/composer/photos/ImageAltTextDialog.tsx:103
msgid "Add alt text"
@@ -1157,7 +1162,7 @@ msgstr ""
#: src/components/images/Gallery.tsx:120
#: src/components/Post/Embed/VideoEmbed/GifPresentationControls.tsx:94
#: src/view/com/composer/GifAltText.tsx:100
#: src/view/com/composer/photos/Gallery.tsx:212
#: src/view/com/composer/photos/Gallery.tsx:214
msgid "ALT"
msgstr ""
@@ -1176,7 +1181,7 @@ msgid "Alt Text"
msgstr ""
#: src/view/com/composer/GifAltText.tsx:105
#: src/view/com/composer/photos/Gallery.tsx:124
#: src/view/com/composer/photos/Gallery.tsx:125
msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone."
msgstr ""
@@ -1827,20 +1832,20 @@ msgstr ""
msgid "Browse custom feeds"
msgstr ""
#: src/components/FeedInterstitials.tsx:631
#: src/components/FeedInterstitials.tsx:627
msgid "Browse more accounts"
msgstr ""
#: src/components/FeedInterstitials.tsx:761
#: src/components/FeedInterstitials.tsx:757
msgid "Browse more feeds on the Explore page"
msgstr ""
#: src/components/FeedInterstitials.tsx:742
#: src/components/FeedInterstitials.tsx:745
#: src/components/FeedInterstitials.tsx:738
#: src/components/FeedInterstitials.tsx:741
msgid "Browse more suggestions"
msgstr ""
#: src/components/FeedInterstitials.tsx:770
#: src/components/FeedInterstitials.tsx:766
msgid "Browse more suggestions on the Explore page"
msgstr ""
@@ -3159,7 +3164,7 @@ msgstr ""
msgid "Developer options"
msgstr ""
#: src/lib/translation/index.tsx:297
#: src/lib/translation/index.tsx:303
msgid "Device failed to translate :("
msgstr "Device failed to translate :("
@@ -3489,7 +3494,7 @@ msgstr ""
#: src/view/com/composer/photos/EditImageDialog.web.tsx:86
#: src/view/com/composer/photos/EditImageDialog.web.tsx:90
#: src/view/com/composer/photos/Gallery.tsx:219
#: src/view/com/composer/photos/Gallery.tsx:221
msgid "Edit image"
msgstr ""
@@ -3760,7 +3765,7 @@ msgstr ""
msgid "Enter your username and password"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:148
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:150
msgid "Enters full screen"
msgstr ""
@@ -6236,7 +6241,7 @@ msgstr ""
msgid "Music"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:169
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:171
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:97
msgctxt "video"
msgid "Mute"
@@ -6712,6 +6717,10 @@ msgstr ""
msgid "No thanks"
msgstr ""
#: src/lib/translation/index.tsx:308
msgid "No translation received from your device."
msgstr "No translation received from your device."
#: src/view/screens/Profile.tsx:500
msgid "No video posts yet"
msgstr ""
@@ -7262,7 +7271,7 @@ msgstr ""
msgid "Password updated!"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:153
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:155
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:395
msgid "Pause"
msgstr ""
@@ -7377,7 +7386,7 @@ msgstr ""
msgid "Pinned to your feeds"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:153
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:155
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:396
msgid "Play"
msgstr ""
@@ -7404,7 +7413,7 @@ msgstr ""
msgid "Plays or pauses the GIF"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:154
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:156
msgid "Plays or pauses the video"
msgstr ""
@@ -8121,7 +8130,7 @@ msgstr ""
msgid "Remove from your feeds?"
msgstr ""
#: src/view/com/composer/photos/Gallery.tsx:228
#: src/view/com/composer/photos/Gallery.tsx:230
msgid "Remove image"
msgstr ""
@@ -8759,14 +8768,14 @@ msgstr ""
msgid "Search for \"{interestsDisplayName}\" (active)"
msgstr ""
#: src/view/shell/desktop/Search.tsx:130
msgid "Search for \"{query}\""
msgstr ""
#: src/screens/Search/components/AutocompleteResults.tsx:47
msgid "Search for \"{searchText}\""
msgstr ""
#: src/view/shell/desktop/Search.tsx:128
msgid "Search for “{tQuery}”"
msgstr "Search for “{tQuery}”"
#: src/screens/StarterPack/Wizard/index.tsx:552
msgid "Search for feeds that you want to suggest to others."
msgstr ""
@@ -8864,12 +8873,12 @@ msgstr ""
msgid "See jobs at Bluesky"
msgstr ""
#: src/components/FeedInterstitials.tsx:584
#: src/components/FeedInterstitials.tsx:645
#: src/components/FeedInterstitials.tsx:583
#: src/components/FeedInterstitials.tsx:641
msgid "See more"
msgstr ""
#: src/components/FeedInterstitials.tsx:565
#: src/components/FeedInterstitials.tsx:564
msgid "See more suggested profiles"
msgstr ""
@@ -9565,7 +9574,7 @@ msgstr ""
msgid "Some of your verifications are invalid."
msgstr ""
#: src/components/FeedInterstitials.tsx:724
#: src/components/FeedInterstitials.tsx:720
msgid "Some other feeds you might like"
msgstr ""
@@ -10609,7 +10618,7 @@ msgstr ""
msgid "Toggle to enable or disable adult content"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:171
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:173
msgid "Toggles the sound"
msgstr ""
@@ -10664,6 +10673,10 @@ msgstr "Translating"
msgid "Translating…"
msgstr "Translating…"
#: src/lib/translation/index.tsx:305
msgid "Translation to the same language is unavailable on your device."
msgstr "Translation to the same language is unavailable on your device."
#: src/screens/Settings/ThreadPreferences.tsx:87
#: src/screens/Settings/ThreadPreferences.tsx:92
msgid "Tree view"
@@ -10882,7 +10895,7 @@ msgstr ""
msgid "Unmute"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:170
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:96
msgctxt "video"
msgid "Unmute"
@@ -10981,6 +10994,10 @@ msgstr ""
msgid "Unsubscribed from list"
msgstr ""
#: src/view/com/composer/text-input/TextInput.tsx:131
msgid "Unsupported clipboard content"
msgstr "Unsupported clipboard content"
#: src/view/com/composer/Composer.tsx:1369
msgid "Unsupported video type: {mimeType}"
msgstr ""
@@ -11295,8 +11312,8 @@ msgstr ""
msgid "Version {0}"
msgstr ""
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:86
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:147
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:87
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:149
msgid "Video"
msgstr ""
@@ -11344,7 +11361,7 @@ msgid "Video uploaded"
msgstr ""
#. placeholder {0}: embed.alt
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:86
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:87
msgid "Video: {0}"
msgstr ""
+32 -26
View File
@@ -1,9 +1,5 @@
import {AtUri} from '@atproto/api'
import {
type QueryClient,
useQuery,
type UseQueryResult,
} from '@tanstack/react-query'
import {AtUri, type BskyAgent} from '@atproto/api'
import {type QueryClient, queryOptions, useQuery} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
@@ -12,26 +8,12 @@ import {useUnstableProfileViewCache} from './profile'
const RQKEY_ROOT = 'resolved-did'
export const RQKEY = (didOrHandle: string) => [RQKEY_ROOT, didOrHandle]
type UriUseQueryResult = UseQueryResult<{did: string; uri: string}, Error>
export function useResolveUriQuery(uri: string | undefined): UriUseQueryResult {
const urip = new AtUri(uri || '')
const res = useResolveDidQuery(urip.host)
if (res.data) {
// @ts-expect-error TODO new-sdk-migration
urip.host = res.data
return {
...res,
data: {did: urip.host, uri: urip.toString()},
} as UriUseQueryResult
}
return res as UriUseQueryResult
}
export function useResolveDidQuery(didOrHandle: string | undefined) {
const agent = useAgent()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery<string, Error>({
const resolvedDidQueryOptions = (
agent: BskyAgent,
getUnstableProfile: (did: string) => {did: string} | undefined,
didOrHandle: string | undefined,
) =>
queryOptions({
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY(didOrHandle ?? ''),
queryFn: async () => {
@@ -50,6 +32,30 @@ export function useResolveDidQuery(didOrHandle: string | undefined) {
},
enabled: !!didOrHandle,
})
export function useResolveUriQuery(uri: string | undefined) {
const urip = new AtUri(uri || '')
const host = urip.host
const agent = useAgent()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery({
...resolvedDidQueryOptions(agent, getUnstableProfile, host),
select: did => ({
did,
uri: AtUri.make(did, urip.collection, urip.rkey).toString(),
}),
})
}
export function useResolveDidQuery(didOrHandle: string | undefined) {
const agent = useAgent()
const {getUnstableProfile} = useUnstableProfileViewCache()
return useQuery(
resolvedDidQueryOptions(agent, getUnstableProfile, didOrHandle),
)
}
export function precacheResolvedUri(
+1 -1
View File
@@ -101,7 +101,7 @@ export function GifAltTextDialogLoaded({
</Text>
</TouchableOpacity>
<Admonition type="tip" style={[a.mt_sm]}>
<Admonition type="info" style={[a.mt_sm]}>
<Trans>
Alt text describes images for blind and low-vision users, and helps
give context to everyone.
+8 -6
View File
@@ -99,12 +99,14 @@ const GalleryInner = ({images, dispatch}: GalleryInnerProps) => {
)
})}
</ScrollView>
<Admonition type="tip" style={[a.mt_sm]}>
<Trans>
Alt text describes images for blind and low-vision users, and helps
give context to everyone.
</Trans>
</Admonition>
{images.some(image => !image.alt) && (
<Admonition type="info" style={[a.mt_sm]}>
<Trans>
Alt text describes images for blind and low-vision users, and helps
give context to everyone.
</Trans>
</Admonition>
)}
</>
) : null
}
+52 -52
View File
@@ -8,21 +8,17 @@ import {
import {
type NativeSyntheticEvent,
Text as RNText,
TextInput as RNTextInput,
type TextInputSelectionChangeEventData,
View,
} from 'react-native'
import {type PasteEventPayload, TextInputWrapper} from 'expo-paste-input'
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
import PasteInput, {
type PastedFile,
type PasteInputRef,
// @ts-expect-error no types when installing from github
// eslint-disable-next-line import-x/no-unresolved
} from '@mattermost/react-native-paste-input'
import {useLingui} from '@lingui/react/macro'
import {POST_IMG_MAX} from '#/lib/constants'
import {downloadAndResize} from '#/lib/media/manip'
import {isUriImage} from '#/lib/media/util'
import {cleanError} from '#/lib/strings/errors'
import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip'
import {useTheme} from '#/lib/ThemeContext'
import {
@@ -51,8 +47,9 @@ export function TextInput({
onError,
...props
}: TextInputProps) {
const {t: l} = useLingui()
const {theme: t, fonts} = useAlf()
const textInput = useRef<PasteInputRef>(null)
const textInput = useRef<RNTextInput>(null)
const textInputSelection = useRef<Selection>({start: 0, end: 0})
const theme = useTheme()
const [autocompletePrefix, setAutocompletePrefix] = useState('')
@@ -129,19 +126,21 @@ export function TextInput({
)
const onPaste = useCallback(
async (err: string | undefined, files: PastedFile[]) => {
if (err) {
return onError(cleanError(err))
(payload: PasteEventPayload) => {
if (payload.type === 'unsupported') {
onError(l`Unsupported clipboard content`)
return
}
const uris = files.map(f => f.uri)
const uri = uris.find(isUriImage)
if (uri) {
onPhotoPasted(uri)
if (payload.type === 'images') {
for (const uri of payload.uris) {
if (isUriImage(uri)) {
onPhotoPasted(uri)
}
}
}
},
[onError, onPhotoPasted],
[l, onError, onPhotoPasted],
)
const onSelectionChange = useCallback(
@@ -217,41 +216,42 @@ export function TextInput({
return (
<View style={[a.flex_1, a.pl_md, hasRightPadding && a.pr_4xl]}>
<PasteInput
testID="composerTextInput"
ref={textInput}
onChangeText={onChangeText}
onPaste={onPaste}
onSelectionChange={onSelectionChange}
placeholder={placeholder}
placeholderTextColor={t.atoms.text_contrast_low.color}
keyboardAppearance={theme.colorScheme}
autoFocus={props.autoFocus !== undefined ? props.autoFocus : true}
allowFontScaling
multiline
scrollEnabled={false}
numberOfLines={2}
// Note: should be the default value, but as of v1.104
// it switched to "none" on Android
autoCapitalize="sentences"
{...props}
style={[
inputTextStyle,
a.w_full,
!autocompletePrefix && a.h_full,
{
textAlignVertical: 'top',
minHeight: 60,
includeFontPadding: false,
},
{
borderWidth: 1,
borderColor: 'transparent',
},
props.style,
]}>
{textDecorated}
</PasteInput>
<TextInputWrapper onPaste={onPaste}>
<RNTextInput
testID="composerTextInput"
ref={textInput}
onChangeText={onChangeText}
onSelectionChange={onSelectionChange}
placeholder={placeholder}
placeholderTextColor={t.atoms.text_contrast_low.color}
keyboardAppearance={theme.colorScheme}
autoFocus={props.autoFocus !== undefined ? props.autoFocus : true}
allowFontScaling
multiline
scrollEnabled={false}
numberOfLines={2}
// Note: should be the default value, but as of v1.104
// it switched to "none" on Android
autoCapitalize="sentences"
{...props}
style={[
inputTextStyle,
a.w_full,
!autocompletePrefix && a.h_full,
{
textAlignVertical: 'top',
minHeight: 60,
includeFontPadding: false,
},
{
borderWidth: 1,
borderColor: 'transparent',
},
props.style,
]}>
{textDecorated}
</RNTextInput>
</TextInputWrapper>
<Autocomplete
prefix={autocompletePrefix}
onSelect={onSelectAutocompleteItem}
+59 -69
View File
@@ -1,7 +1,6 @@
import {memo, useCallback, useState} from 'react'
import {
ActivityIndicator,
StyleSheet,
type StyleProp,
TouchableOpacity,
View,
type ViewStyle,
@@ -9,15 +8,17 @@ import {
import {useLingui} from '@lingui/react/macro'
import {StackActions, useNavigation} from '@react-navigation/native'
import {usePalette} from '#/lib/hooks/usePalette'
import {type NavigationProp} from '#/lib/routes/types'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {Link} from '#/view/com/util/Link'
import {Text} from '#/view/com/util/text/Text'
import {SearchProfileCard} from '#/screens/Search/components/SearchProfileCard'
import {atoms as a} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {SearchInput} from '#/components/forms/SearchInput'
import {Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
const WHITESPACE_RE = /\s+/gu
let SearchLinkCard = ({
label,
@@ -28,20 +29,17 @@ let SearchLinkCard = ({
label: string
to?: string
onPress?: () => void
style?: ViewStyle
style?: StyleProp<ViewStyle>
}): React.ReactNode => {
const pal = usePalette('default')
const t = useTheme()
const inner = (
<View
style={[pal.border, {paddingVertical: 16, paddingHorizontal: 12}, style]}>
<Text type="md" style={[pal.text]}>
{label}
</Text>
<View style={[a.py_lg, a.px_md, t.atoms.border_contrast_low, style]}>
<Text style={[a.text_md, t.atoms.text]}>{label}</Text>
</View>
)
if (onPress) {
if (onPress || !to) {
return (
<TouchableOpacity
onPress={onPress}
@@ -53,17 +51,12 @@ let SearchLinkCard = ({
}
return (
<Link href={to} asAnchor anchorNoUnderline>
<View
style={[
pal.border,
{paddingVertical: 16, paddingHorizontal: 12},
style,
]}>
<Text type="md" style={[pal.text]}>
{label}
</Text>
</View>
<Link
label={label}
to={to}
style={[a.py_lg, a.px_md, t.atoms.border_contrast_low, style]}
hoverStyle={[t.atoms.bg_contrast_25]}>
<Text style={[a.text_md, t.atoms.text]}>{label}</Text>
</Link>
)
}
@@ -71,8 +64,8 @@ SearchLinkCard = memo(SearchLinkCard)
export {SearchLinkCard}
export function DesktopSearch() {
const t = useTheme()
const {t: l} = useLingui()
const pal = usePalette('default')
const navigation = useNavigation<NavigationProp>()
const [isActive, setIsActive] = useState<boolean>(false)
const [query, setQuery] = useState<string>('')
@@ -80,6 +73,7 @@ export function DesktopSearch() {
query,
true,
)
const tQuery = query.replace(WHITESPACE_RE, ' ').trim()
const moderationOpts = useModerationOpts()
@@ -95,9 +89,9 @@ export function DesktopSearch() {
const onSubmit = useCallback(() => {
setIsActive(false)
if (!query.length) return
navigation.dispatch(StackActions.push('Search', {q: query}))
}, [query, navigation])
if (!tQuery.length) return
navigation.dispatch(StackActions.push('Search', {q: tQuery}))
}, [tQuery, navigation])
const onSearchProfileCardPress = useCallback(() => {
setQuery('')
@@ -105,62 +99,58 @@ export function DesktopSearch() {
}, [])
return (
<View style={[styles.container, pal.view]}>
<View style={[a.relative, a.w_full, a.z_10, t.atoms.bg]}>
<SearchInput
value={query}
onChangeText={onChangeText}
onClearText={onPressCancelSearch}
onSubmitEditing={onSubmit}
/>
{query !== '' && isActive && moderationOpts && (
{tQuery !== '' && isActive && moderationOpts && (
<View
style={[
pal.view,
pal.borderDark,
styles.resultsContainer,
a.overflow_hidden,
a.mt_sm,
a.flex_col,
a.w_full,
a.border,
a.rounded_sm,
a.zoom_fade_in,
t.atoms.bg,
t.atoms.shadow_sm,
t.atoms.border_contrast_low,
{
overflow: 'hidden',
position: 'absolute',
top: '100%',
},
]}>
<SearchLinkCard
label={l`Search for “${tQuery}`}
to={`/search?q=${encodeURIComponent(tQuery)}`}
style={(autocompleteData?.length ?? 0) > 0 ? a.border_b : undefined}
/>
{isFetching && !autocompleteData?.length ? (
<View style={{padding: 8}}>
<ActivityIndicator />
<View
style={[
a.py_lg,
a.align_center,
a.border_t,
t.atoms.border_contrast_low,
]}>
<Loader size="lg" />
</View>
) : (
<>
<SearchLinkCard
label={l`Search for "${query}"`}
to={`/search?q=${encodeURIComponent(query)}`}
style={
(autocompleteData?.length ?? 0) > 0
? {borderBottomWidth: 1}
: undefined
}
autocompleteData?.map(item => (
<SearchProfileCard
key={item.did}
profile={item}
moderationOpts={moderationOpts}
onPress={onSearchProfileCardPress}
/>
{autocompleteData?.map(item => (
<SearchProfileCard
key={item.did}
profile={item}
moderationOpts={moderationOpts}
onPress={onSearchProfileCardPress}
/>
))}
</>
))
)}
</View>
)}
</View>
)
}
const styles = StyleSheet.create({
container: {
position: 'relative',
width: '100%',
},
resultsContainer: {
marginTop: 10,
flexDirection: 'column',
width: '100%',
borderWidth: 1,
borderRadius: 6,
},
})