Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9b6421aa1 | |||
| f6b8854a9b | |||
| 3d0fbc5030 | |||
| 0a5ae17738 | |||
| 02b849996c | |||
| f231b67b76 | |||
| d2519a4f67 | |||
| a049a6538c | |||
| 3c21fee6c2 | |||
| e74b57ab78 | |||
| a169bd862f | |||
| 8bd6d9d135 | |||
| 5868804d3b | |||
| bdce8e8ecd | |||
| 99257f2816 | |||
| 011d8d2f7c | |||
| c0d3010f3e | |||
| 3685439ffb | |||
| 165dd5a779 | |||
| 84b026efb7 | |||
| be56066ee9 | |||
| cca3326b21 | |||
| e0ea778e58 | |||
| 9fe808f8a8 | |||
| b9f3d04d65 |
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "26.0"
|
||||
xcode-version: "26.4"
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: "26.0"
|
||||
xcode-version: "26.4"
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
|
||||
@@ -431,16 +431,30 @@ yarn intl:compile # Compile translations for runtime
|
||||
// src/state/queries/profile.ts
|
||||
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
// Query key pattern
|
||||
const RQKEY_ROOT = 'profile'
|
||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||
import {createQueryKey} from '#/state/queries/util'
|
||||
|
||||
// Query hook
|
||||
/*
|
||||
* Query key name should match the query hook name for consistency
|
||||
*/
|
||||
const profileQueryKeyRoot = 'profile'
|
||||
|
||||
/*
|
||||
* Use object params and createQueryKey helper for better readability and to
|
||||
* avoid bugs with parameter order or types.
|
||||
*/
|
||||
export const createProfileQueryKey = (args: {did: string}) =>
|
||||
createQueryKey(profileQueryKeyRoot, args)
|
||||
|
||||
/*
|
||||
* Query hook should be named use[Name]Query, where [Name] describes the data
|
||||
* being fetched. This is not a strict requirement, but it's a helpful
|
||||
* convention for discoverability
|
||||
*/
|
||||
export function useProfileQuery({did}: {did: string}) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery({
|
||||
queryKey: RQKEY(did),
|
||||
queryKey: createProfileQueryKey({did}),
|
||||
queryFn: async () => {
|
||||
const res = await agent.getProfile({actor: did})
|
||||
return res.data
|
||||
@@ -450,8 +464,12 @@ export function useProfileQuery({did}: {did: string}) {
|
||||
})
|
||||
}
|
||||
|
||||
// Mutation hook
|
||||
export function useUpdateProfile() {
|
||||
/*
|
||||
* Mutation hook should match the name of the query hook, but with "Mutation"
|
||||
* suffix. This is not a strict requirement, but it's a helpful convention for
|
||||
* discoverability and consistency.
|
||||
*/
|
||||
export function useProfileMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
@@ -459,7 +477,9 @@ export function useUpdateProfile() {
|
||||
// Update logic
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({queryKey: RQKEY(variables.did)})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: createProfileQueryKey({did: variables.did}),
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isNetworkError(error)) {
|
||||
@@ -473,6 +493,24 @@ export function useUpdateProfile() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* If cache mutation is needed, include specific interfaces for the specific
|
||||
* mutations you require adjacent to the source queries. Naming should be
|
||||
* descriptive of the mutation's purpose, e.g. use[Name]CacheMutation. This is
|
||||
* not a strict requirement, but it's a helpful convention for discoverability
|
||||
* and consistency.
|
||||
*/
|
||||
export function useProfileCacheMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return (data: Partial<Profile>) => {
|
||||
queryClient.setQueryData(createProfileQueryKey({did: data.did}), oldData => {
|
||||
if (!oldData) return oldData
|
||||
return {...oldData, ...data}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Stale Time Constants** (from `src/state/queries/index.ts`):
|
||||
@@ -491,7 +529,7 @@ export function useDraftsQuery() {
|
||||
const agent = useAgent()
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: ['drafts'],
|
||||
queryKey: createQueryKey('drafts'),
|
||||
queryFn: async ({pageParam}) => {
|
||||
const res = await agent.app.bsky.draft.getDrafts({cursor: pageParam})
|
||||
return res.data
|
||||
@@ -504,6 +542,19 @@ export function useDraftsQuery() {
|
||||
|
||||
To get all items from pages: `data?.pages.flatMap(page => page.items) ?? []`
|
||||
|
||||
**Persisted Queries**
|
||||
|
||||
To persist query data across app restarts, `createQueryKey` supports a third
|
||||
parameter called `options`, which has a `persistedVersion` property. When this
|
||||
property is set to a number, the query will be persisted.
|
||||
|
||||
When this property is updated (e.g. incremented), the persisted data will be cleared and replaced with the new data from the query function. This is useful for cases where the shape of the data has changed and old persisted data would no longer be valid.
|
||||
|
||||
```tsx
|
||||
export const createProfileQueryKey = (args: {did: string}) =>
|
||||
createQueryKey(profileQueryKeyRoot, args, {persistedVersion: 1})
|
||||
```
|
||||
|
||||
### Preferences (React Context)
|
||||
|
||||
```tsx
|
||||
|
||||
+2
-2
@@ -54,7 +54,7 @@ module.exports = function (_config) {
|
||||
},
|
||||
icon: './assets/app-icons/ios_icon_default_next.png',
|
||||
userInterfaceStyle: 'automatic',
|
||||
primaryColor: '#1083fe',
|
||||
primaryColor: '#006AFF',
|
||||
newArchEnabled: false,
|
||||
ios: {
|
||||
supportsTablet: false,
|
||||
@@ -64,6 +64,7 @@ module.exports = function (_config) {
|
||||
},
|
||||
icon: IOS_ICON_FILE,
|
||||
infoPlist: {
|
||||
CADisableMinimumFrameDurationOnPhone: true,
|
||||
UIBackgroundModes: ['remote-notification'],
|
||||
NSCameraUsageDescription:
|
||||
'Used for profile pictures, posts, and other kinds of content.',
|
||||
@@ -296,7 +297,6 @@ module.exports = function (_config) {
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidManifestIntentQueriesPlugin.js',
|
||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||
'./plugins/withAndroidDayNightThemePlugin.js',
|
||||
'./plugins/withAndroidNoJitpackPlugin.js',
|
||||
'./plugins/shareExtension/withShareExtensions.js',
|
||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#fff" d="M6.514 2.143A1 1 0 0 0 5 3v18a1 1 0 0 0 1.514.858l15-9a1 1 0 0 0 0-1.716l-15-9Z"/></svg>
|
||||
|
After Width: | Height: | Size: 182 B |
@@ -14,7 +14,7 @@ import {ComponentChildren, h} from 'preact'
|
||||
import {useMemo} from 'preact/hooks'
|
||||
|
||||
import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg'
|
||||
import playIcon from '../../assets/play_filled_corner2_rounded.svg'
|
||||
import playIcon from '../../assets/play_filled_corner0_rounded.svg'
|
||||
import starterPackIcon from '../../assets/starterPack.svg'
|
||||
import {CONTENT_LABELS, labelsToInfo} from '../labels'
|
||||
import * as bsky from '../types/bsky'
|
||||
@@ -389,7 +389,6 @@ function GenericWithImageEmbed({
|
||||
)
|
||||
}
|
||||
|
||||
// just the thumbnail and a play button
|
||||
function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) {
|
||||
let aspectRatio = 1
|
||||
|
||||
@@ -398,6 +397,28 @@ function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) {
|
||||
aspectRatio = clamp(width / height, 1 / 1, 3 / 1)
|
||||
}
|
||||
|
||||
const supportsHls = useMemo(() => {
|
||||
const video = document.createElement('video')
|
||||
return video.canPlayType('application/vnd.apple.mpegurl') !== ''
|
||||
}, [])
|
||||
|
||||
if (supportsHls) {
|
||||
return (
|
||||
<video
|
||||
src={content.playlist}
|
||||
poster={content.thumbnail}
|
||||
controls
|
||||
playsinline
|
||||
preload="metadata"
|
||||
loading="lazy"
|
||||
aria-label={content.alt || undefined}
|
||||
onClickCapture={evt => evt.stopPropagation()}
|
||||
className="w-full rounded-xl bg-black"
|
||||
style={{aspectRatio: `${aspectRatio} / 1`}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full overflow-hidden rounded-xl aspect-square relative"
|
||||
|
||||
@@ -127,6 +127,7 @@ export default defineConfig(
|
||||
*/
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
'react/hook-use-state': 'warn',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react-native/no-inline-styles': 'off',
|
||||
|
||||
+9
-5
@@ -81,13 +81,15 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.19.5",
|
||||
"@atproto/api": "^0.19.6",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@bsky.app/alf": "^0.1.7",
|
||||
"@bsky.app/expo-image-crop-tool": "^0.5.0",
|
||||
"@bsky.app/expo-translate-text": "^0.2.9",
|
||||
"@bsky.app/react-native-mmkv": "2.12.5",
|
||||
"@bsky.app/sift": "^0.3.1",
|
||||
"@bsky.app/tapper": "^0.5.0",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
@@ -116,9 +118,9 @@
|
||||
"@react-navigation/native": "^7.1.33",
|
||||
"@react-navigation/native-stack": "^7.14.4",
|
||||
"@sentry/react-native": "~6.20.0",
|
||||
"@tanstack/query-async-storage-persister": "^5.95.2",
|
||||
"@tanstack/react-query": "^5.95.2",
|
||||
"@tanstack/react-query-persist-client": "^5.95.2",
|
||||
"@tanstack/query-async-storage-persister": "^5.96.2",
|
||||
"@tanstack/react-query": "^5.96.2",
|
||||
"@tanstack/react-query-persist-client": "^5.96.2",
|
||||
"@tiptap/core": "^2.9.1",
|
||||
"@tiptap/extension-document": "^2.9.1",
|
||||
"@tiptap/extension-hard-break": "^2.9.1",
|
||||
@@ -166,7 +168,7 @@
|
||||
"expo-location": "~19.0.8",
|
||||
"expo-media-library": "~18.2.1",
|
||||
"expo-notifications": "~0.32.16",
|
||||
"expo-paste-input": "^0.1.10",
|
||||
"expo-paste-input": "^0.1.12",
|
||||
"expo-privacy-sensitive": "^0.1.0",
|
||||
"expo-screen-orientation": "~9.0.8",
|
||||
"expo-sharing": "~14.0.8",
|
||||
@@ -179,6 +181,7 @@
|
||||
"expo-web-browser": "~15.0.10",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"fuse.js": "^7.1.0",
|
||||
"hls.js": "^1.6.2",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"js-sha256": "^0.9.0",
|
||||
@@ -199,6 +202,7 @@
|
||||
"react": "19.1.0",
|
||||
"react-compiler-runtime": "^19.1.0-rc.1",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hotkeys-hook": "5.2.4",
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-is": "19",
|
||||
"react-keyed-flatten-children": "^5.0.0",
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// Based on https://github.com/expo/expo/pull/33957
|
||||
// Could be removed once the app has been updated to Expo 53
|
||||
const {withAndroidStyles} = require('@expo/config-plugins')
|
||||
|
||||
module.exports = function withAndroidDayNightThemePlugin(appConfig) {
|
||||
const cleanupList = new Set([
|
||||
'colorPrimary',
|
||||
'android:editTextBackground',
|
||||
'android:textColor',
|
||||
'android:editTextStyle',
|
||||
])
|
||||
|
||||
return withAndroidStyles(appConfig, config => {
|
||||
config.modResults.resources.style = config.modResults.resources.style
|
||||
?.map(style => {
|
||||
if (style.$.name === 'AppTheme' && style.item != null) {
|
||||
style.item = style.item.filter(item => !cleanupList.has(item.$.name))
|
||||
}
|
||||
return style
|
||||
})
|
||||
.filter(style => {
|
||||
return style.$.name !== 'ResetEditText'
|
||||
})
|
||||
|
||||
return config
|
||||
})
|
||||
}
|
||||
+15
-17
@@ -11,13 +11,11 @@ import {
|
||||
import * as ScreenOrientation from 'expo-screen-orientation'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
|
||||
import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {s} from '#/lib/styles'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import {Provider as TranslateOnDeviceProvider} from '#/lib/translation'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
@@ -59,7 +57,7 @@ import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||
import {Shell} from '#/view/shell'
|
||||
import {ThemeProvider as Alf} from '#/alf'
|
||||
import {atoms as a, ThemeProvider as Alf} from '#/alf'
|
||||
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||
import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
@@ -89,9 +87,9 @@ import {Splash} from '#/Splash'
|
||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
void SplashScreen.preventAutoHideAsync()
|
||||
if (IS_IOS) {
|
||||
SystemUI.setBackgroundColorAsync('black')
|
||||
void SystemUI.setBackgroundColorAsync('black')
|
||||
}
|
||||
if (IS_ANDROID) {
|
||||
// iOS is handled by the config plugin -sfn
|
||||
@@ -105,17 +103,17 @@ if (IS_ANDROID) {
|
||||
/**
|
||||
* Begin geolocation ASAP
|
||||
*/
|
||||
Geo.resolve()
|
||||
prefetchAgeAssuranceConfig()
|
||||
prefetchLiveEvents()
|
||||
prefetchAppConfig()
|
||||
void Geo.resolve()
|
||||
void prefetchAgeAssuranceConfig()
|
||||
void prefetchLiveEvents()
|
||||
void prefetchAppConfig()
|
||||
|
||||
function InnerApp() {
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const {currentAccount} = useSession()
|
||||
const {resumeSession} = useSessionApi()
|
||||
const theme = useColorModeTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const hasCheckedReferrer = useStarterPackEntry()
|
||||
|
||||
// init
|
||||
@@ -134,16 +132,16 @@ function InnerApp() {
|
||||
}
|
||||
}
|
||||
const account = readLastActiveAccount()
|
||||
onLaunch(account)
|
||||
void onLaunch(account)
|
||||
}, [resumeSession])
|
||||
|
||||
useEffect(() => {
|
||||
return listenSessionDropped(() => {
|
||||
Toast.show(_(msg`Sorry! Your session expired. Please sign in again.`), {
|
||||
Toast.show(l`Sorry! Your session expired. Please sign in again.`, {
|
||||
type: 'info',
|
||||
})
|
||||
})
|
||||
}, [_])
|
||||
}, [l])
|
||||
|
||||
return (
|
||||
<Alf theme={theme}>
|
||||
@@ -176,7 +174,7 @@ function InnerApp() {
|
||||
<EmailVerificationProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
style={a.h_full}>
|
||||
<GlobalGestureEventsProvider>
|
||||
<IntentDialogProvider>
|
||||
<TranslateOnDeviceProvider>
|
||||
@@ -220,8 +218,8 @@ function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() =>
|
||||
setReady(true),
|
||||
void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(
|
||||
() => setReady(true),
|
||||
)
|
||||
}, [])
|
||||
|
||||
|
||||
+17
-15
@@ -5,10 +5,10 @@ import './style.css'
|
||||
import {Fragment, useEffect, useState} from 'react'
|
||||
import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller'
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
|
||||
import {Provider as HotkeysProvider} from '#/lib/hotkeys'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import {Provider as TranslateOnDeviceProvider} from '#/lib/translation'
|
||||
@@ -82,17 +82,17 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom
|
||||
/**
|
||||
* Begin geolocation ASAP
|
||||
*/
|
||||
Geo.resolve()
|
||||
prefetchAgeAssuranceConfig()
|
||||
prefetchLiveEvents()
|
||||
prefetchAppConfig()
|
||||
void Geo.resolve()
|
||||
void prefetchAgeAssuranceConfig()
|
||||
void prefetchLiveEvents()
|
||||
void prefetchAppConfig()
|
||||
|
||||
function InnerApp() {
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const {currentAccount} = useSession()
|
||||
const {resumeSession} = useSessionApi()
|
||||
const theme = useColorModeTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const hasCheckedReferrer = useStarterPackEntry()
|
||||
|
||||
// init
|
||||
@@ -105,22 +105,22 @@ function InnerApp() {
|
||||
await features.init
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`session: resumeSession failed`, {message: e})
|
||||
logger.error('session: resumeSession failed', {message: e})
|
||||
} finally {
|
||||
setIsReady(true)
|
||||
}
|
||||
}
|
||||
const account = readLastActiveAccount()
|
||||
onLaunch(account)
|
||||
void onLaunch(account)
|
||||
}, [resumeSession])
|
||||
|
||||
useEffect(() => {
|
||||
return listenSessionDropped(() => {
|
||||
Toast.show(_(msg`Sorry! Your session expired. Please sign in again.`), {
|
||||
Toast.show(l`Sorry! Your session expired. Please sign in again.`, {
|
||||
type: 'info',
|
||||
})
|
||||
})
|
||||
}, [_])
|
||||
}, [l])
|
||||
|
||||
return (
|
||||
<Alf theme={theme}>
|
||||
@@ -156,8 +156,10 @@ function InnerApp() {
|
||||
<HideBottomBarBorderProvider>
|
||||
<IntentDialogProvider>
|
||||
<TranslateOnDeviceProvider>
|
||||
<Shell />
|
||||
<ToastOutlet />
|
||||
<HotkeysProvider>
|
||||
<Shell />
|
||||
<ToastOutlet />
|
||||
</HotkeysProvider>
|
||||
</TranslateOnDeviceProvider>
|
||||
</IntentDialogProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
@@ -195,8 +197,8 @@ function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() =>
|
||||
setReady(true),
|
||||
void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(
|
||||
() => setReady(true),
|
||||
)
|
||||
}, [])
|
||||
|
||||
|
||||
+37
-1
@@ -1,3 +1,39 @@
|
||||
import {StyleSheet} from 'react-native'
|
||||
import {type DimensionValue, StyleSheet} from 'react-native'
|
||||
|
||||
export const flatten = StyleSheet.flatten
|
||||
|
||||
/**
|
||||
* Coerce a style value to a number. Padding values are typed as
|
||||
* `DimensionValue` (numbers, percentages, "auto", etc.) but our ALF atoms
|
||||
* are always plain numbers. Non-numeric values are treated as 0.
|
||||
*/
|
||||
function num(v: unknown): number {
|
||||
return typeof v === 'number' ? v : 0
|
||||
}
|
||||
|
||||
interface PaddingStyle {
|
||||
padding?: DimensionValue
|
||||
paddingHorizontal?: DimensionValue
|
||||
paddingVertical?: DimensionValue
|
||||
paddingTop?: DimensionValue
|
||||
paddingBottom?: DimensionValue
|
||||
paddingLeft?: DimensionValue
|
||||
paddingRight?: DimensionValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract resolved padding values from a style object. Returns numbers for
|
||||
* each side, resolving shorthand properties (padding → paddingVertical →
|
||||
* paddingTop/paddingBottom, etc.). Values are expected to be numbers — any
|
||||
* non-numeric `DimensionValue` (e.g. percentages) is treated as 0.
|
||||
*/
|
||||
export function extractPadding(style: PaddingStyle | PaddingStyle[]) {
|
||||
const s = flatten(style as any) ?? {}
|
||||
const base = num(s.padding)
|
||||
return {
|
||||
paddingTop: num(s.paddingTop) || num(s.paddingVertical) || base,
|
||||
paddingBottom: num(s.paddingBottom) || num(s.paddingVertical) || base,
|
||||
paddingLeft: num(s.paddingLeft) || num(s.paddingHorizontal) || base,
|
||||
paddingRight: num(s.paddingRight) || num(s.paddingHorizontal) || base,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export enum Features {
|
||||
LiveNowBetaDisable = 'live_now_beta:disable',
|
||||
ImageUploadsHighResolution = 'image_uploads:high_resolution',
|
||||
GroupChatsEnable = 'group_chats:enable',
|
||||
DmsNewMessageComposerEnable = 'dms:new_message_composer:enable',
|
||||
|
||||
AATest = 'aa-test',
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ const Context = createContext<AnalyticsBaseContextType>({
|
||||
},
|
||||
},
|
||||
})
|
||||
Context.displayName = 'AnalyticsContext'
|
||||
|
||||
/**
|
||||
* Ensures that deviceId is set and migrated from legacy storage. Handled on
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Sift, type UseSiftReturn} from '@bsky.app/sift'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {type AutocompleteItem} from '#/components/Autocomplete/types'
|
||||
import {useOnKeyboard} from '#/components/hooks/useOnKeyboard'
|
||||
import {Portal} from '#/components/Portal'
|
||||
import {IS_WEB} from '#/env'
|
||||
import {AutocompleteItemEmoji} from './AutocompleteItemEmoji'
|
||||
import {AutocompleteItemProfile} from './AutocompleteItemProfile'
|
||||
import {AutocompleteItemSearch} from './AutocompleteItemSearch'
|
||||
|
||||
function renderItem(
|
||||
item: Parameters<Parameters<typeof Sift<AutocompleteItem>>[0]['render']>[0],
|
||||
) {
|
||||
switch (item.item.type) {
|
||||
case 'profile':
|
||||
return <AutocompleteItemProfile {...item} />
|
||||
case 'emoji':
|
||||
return <AutocompleteItemEmoji {...item} />
|
||||
case 'search':
|
||||
return <AutocompleteItemSearch {...item} />
|
||||
default:
|
||||
return <View />
|
||||
}
|
||||
}
|
||||
|
||||
export function Autocomplete({
|
||||
inverted,
|
||||
sift,
|
||||
data,
|
||||
render = renderItem,
|
||||
onSelect,
|
||||
onDismiss,
|
||||
}: {
|
||||
inverted?: boolean
|
||||
sift: UseSiftReturn
|
||||
data: AutocompleteItem[]
|
||||
render?: Parameters<typeof Sift<AutocompleteItem>>[0]['render']
|
||||
onSelect: (item: AutocompleteItem) => void
|
||||
onDismiss: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
const updatePosition = useCallback(() => {
|
||||
sift.updatePosition()
|
||||
}, [sift])
|
||||
|
||||
useOnKeyboard('keyboardDidShow', updatePosition)
|
||||
useOnKeyboard('keyboardDidHide', updatePosition)
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Sift
|
||||
inverted={inverted}
|
||||
sift={sift}
|
||||
data={data}
|
||||
onSelect={onSelect}
|
||||
onDismiss={onDismiss}
|
||||
style={[
|
||||
a.overflow_hidden,
|
||||
a.rounded_md,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
a.w_full,
|
||||
IS_WEB
|
||||
? {
|
||||
maxWidth: 300,
|
||||
}
|
||||
: {},
|
||||
]}
|
||||
render={render}
|
||||
/>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {SiftItem} from '@bsky.app/sift'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {type AutocompleteItemProps} from './types'
|
||||
|
||||
export function AutocompleteItemEmoji({
|
||||
active,
|
||||
props,
|
||||
item,
|
||||
}: AutocompleteItemProps) {
|
||||
const t = useTheme()
|
||||
|
||||
if (item.type !== 'emoji') return null
|
||||
|
||||
return (
|
||||
<SiftItem
|
||||
{...props}
|
||||
style={s => [
|
||||
{paddingVertical: 6, paddingHorizontal: 10},
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
active || s.hovered || s.pressed ? [t.atoms.bg_contrast_25] : [],
|
||||
]}>
|
||||
<Text style={[a.text_xl, a.leading_tight]}>{item.value}</Text>
|
||||
<Text style={[a.text_md, a.leading_tight]}>:{item.emoji.id}:</Text>
|
||||
</SiftItem>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {SiftItem} from '@bsky.app/sift'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {type AutocompleteItemProps} from './types'
|
||||
|
||||
export function AutocompleteItemProfile({
|
||||
active,
|
||||
isFirst,
|
||||
isLast,
|
||||
props,
|
||||
item,
|
||||
}: AutocompleteItemProps) {
|
||||
const t = useTheme()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
if (item.type !== 'profile' || !moderationOpts) return null
|
||||
|
||||
return (
|
||||
<SiftItem
|
||||
{...props}
|
||||
style={s => [
|
||||
a.py_sm,
|
||||
a.px_md,
|
||||
active || s.hovered || s.pressed ? [t.atoms.bg_contrast_25] : [],
|
||||
isFirst && {
|
||||
paddingTop: a.py_sm.paddingTop * 1.2,
|
||||
},
|
||||
isLast && {
|
||||
paddingBottom: a.py_sm.paddingTop * 1.2,
|
||||
},
|
||||
]}>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
disabledPreview
|
||||
profile={item.profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.NameAndHandle
|
||||
profile={item.profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
</ProfileCard.Header>
|
||||
</SiftItem>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import {View} from 'react-native'
|
||||
import {SiftItem} from '@bsky.app/sift'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/components/icons/MagnifyingGlass'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {type AutocompleteItemProps} from './types'
|
||||
|
||||
export function AutocompleteItemSearch({
|
||||
active,
|
||||
isFirst,
|
||||
isLast,
|
||||
props,
|
||||
item,
|
||||
}: AutocompleteItemProps) {
|
||||
const t = useTheme()
|
||||
|
||||
if (item.type !== 'search') return null
|
||||
|
||||
return (
|
||||
<SiftItem
|
||||
{...props}
|
||||
style={s => [
|
||||
a.py_sm,
|
||||
a.px_md,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
active || s.hovered || s.pressed ? [t.atoms.bg_contrast_25] : [],
|
||||
isFirst && {
|
||||
paddingTop: a.py_sm.paddingTop * 1.2,
|
||||
},
|
||||
isLast && {
|
||||
paddingBottom: a.py_sm.paddingTop * 1.2,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.align_center,
|
||||
{
|
||||
width: 40,
|
||||
},
|
||||
]}>
|
||||
<MagnifyingGlassIcon fill={t.atoms.text_contrast_low.color} size="xl" />
|
||||
</View>
|
||||
<Text style={[a.text_md, a.leading_snug]}>{item.value}</Text>
|
||||
</SiftItem>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './Autocomplete'
|
||||
export * from './AutocompleteItemEmoji'
|
||||
export * from './AutocompleteItemProfile'
|
||||
export * from './types'
|
||||
export * from './useAutocomplete'
|
||||
export * from './util'
|
||||
@@ -0,0 +1,48 @@
|
||||
import {type Sift} from '@bsky.app/sift'
|
||||
import {type Emoji} from '@emoji-mart/data'
|
||||
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export type AutocompleteProfile = {
|
||||
key: string
|
||||
type: 'profile'
|
||||
value: string
|
||||
profile: bsky.profile.AnyProfileView
|
||||
}
|
||||
|
||||
export type AutocompleteTag = {
|
||||
key: string
|
||||
type: 'tag'
|
||||
value: string
|
||||
tag: string
|
||||
}
|
||||
|
||||
export type AutocompleteEmoji = {
|
||||
key: string
|
||||
type: 'emoji'
|
||||
value: string
|
||||
emoji: Emoji
|
||||
}
|
||||
|
||||
export type AutocompleteSearch = {
|
||||
key: string
|
||||
type: 'search'
|
||||
value: string
|
||||
}
|
||||
|
||||
export type AutocompleteItem =
|
||||
| AutocompleteProfile
|
||||
| AutocompleteTag
|
||||
| AutocompleteEmoji
|
||||
| AutocompleteSearch
|
||||
|
||||
export type AutocompleteItemType = AutocompleteItem['type']
|
||||
|
||||
export type AutocompleteItemProps = Parameters<
|
||||
Parameters<typeof Sift<AutocompleteItem>>[0]['render']
|
||||
>[0]
|
||||
|
||||
export type AutocompleteApi = {
|
||||
query: string
|
||||
items: AutocompleteItem[]
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import {useCallback} from 'react'
|
||||
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
||||
import {keepPreviousData, useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
type AutocompleteApi,
|
||||
type AutocompleteItem,
|
||||
type AutocompleteItemType,
|
||||
type AutocompleteProfile,
|
||||
} from '#/components/Autocomplete/types'
|
||||
import {useEmojiSearch} from './useEmojiSearch'
|
||||
|
||||
const DEFAULT_MOD_OPTS = {
|
||||
userDid: undefined,
|
||||
prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
|
||||
}
|
||||
|
||||
export function useAutocomplete({
|
||||
type,
|
||||
query: q,
|
||||
limit,
|
||||
showSearchFallback = false,
|
||||
}: {
|
||||
type: AutocompleteItemType
|
||||
query: string
|
||||
limit?: number
|
||||
showSearchFallback?: boolean
|
||||
}): AutocompleteApi {
|
||||
const agent = useAgent()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const emojiSearch = useEmojiSearch()
|
||||
|
||||
const query = useQuery({
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryKey: [
|
||||
'autocomplete',
|
||||
{
|
||||
type,
|
||||
query: q,
|
||||
},
|
||||
],
|
||||
async queryFn() {
|
||||
if (type === 'profile') {
|
||||
// TODO return recents
|
||||
if (!q) return []
|
||||
|
||||
// Going from "foo" to "foo." should not clear matches.
|
||||
q = q.toLowerCase().trim().replace(/\.$/, '')
|
||||
|
||||
const res = await agent.searchActorsTypeahead({
|
||||
q,
|
||||
limit: limit || 8,
|
||||
})
|
||||
|
||||
return (res?.data.actors || []).map(profile => ({
|
||||
key: profile.did,
|
||||
type: 'profile' as const,
|
||||
value: '@' + profile.handle,
|
||||
profile,
|
||||
}))
|
||||
} else if (type === 'emoji') {
|
||||
return emojiSearch(q, limit || 8)
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
select: useCallback(
|
||||
(items: AutocompleteItem[]) => {
|
||||
const seen = new Set<string>()
|
||||
let results: AutocompleteItem[] = []
|
||||
|
||||
for (const item of items) {
|
||||
if (seen.has(item.key)) continue
|
||||
seen.add(item.key)
|
||||
|
||||
if (item.type === 'profile') {
|
||||
const moderated = moderateProfileItem({
|
||||
query: q,
|
||||
item,
|
||||
moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
|
||||
})
|
||||
if (moderated) results.push(moderated)
|
||||
} else {
|
||||
results.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSearchFallback && q) {
|
||||
results.unshift({
|
||||
key: `search-${q}`,
|
||||
type: 'search' as const,
|
||||
value: q,
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
},
|
||||
[q, showSearchFallback, moderationOpts],
|
||||
),
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
return {
|
||||
query: q,
|
||||
items: query.data || [],
|
||||
}
|
||||
}
|
||||
|
||||
function moderateProfileItem({
|
||||
query,
|
||||
item,
|
||||
moderationOpts,
|
||||
}: {
|
||||
query: string
|
||||
item: AutocompleteProfile
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const modui = moderateProfile(item.profile, moderationOpts).ui('profileList')
|
||||
const isExactMatch = query && item.profile.handle.toLowerCase() === query
|
||||
|
||||
if (
|
||||
(isExactMatch && !moduiContainsHideableOffense(modui)) ||
|
||||
!modui.filter ||
|
||||
isJustAMute(modui)
|
||||
) {
|
||||
return item
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {useCallback} from 'react'
|
||||
import {type Emoji} from '@emoji-mart/data'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
import {useGetEmojis} from '#/lib/useGetEmojis'
|
||||
import {type AutocompleteEmoji} from '#/components/Autocomplete/types'
|
||||
|
||||
/*
|
||||
* Lazily loaded Fuse instance for emoji search. Built once on first search,
|
||||
* then reused for all subsequent searches.
|
||||
*/
|
||||
let emojiFuseInstance: Fuse<Emoji> | null = null
|
||||
|
||||
export function useEmojiSearch(): (
|
||||
query: string,
|
||||
limit?: number,
|
||||
) => Promise<AutocompleteEmoji[]> {
|
||||
const getEmojis = useGetEmojis()
|
||||
|
||||
return useCallback(
|
||||
async (query: string, limit: number = 8) => {
|
||||
if (!emojiFuseInstance) {
|
||||
const data = await getEmojis()
|
||||
emojiFuseInstance = new Fuse(Object.values(data.emojis), {
|
||||
keys: ['search'],
|
||||
threshold: 0.3,
|
||||
})
|
||||
}
|
||||
|
||||
const results = emojiFuseInstance.search(query, {limit})
|
||||
return results.map(result => ({
|
||||
key: result.item.id,
|
||||
type: 'emoji' as const,
|
||||
value: result.item.skins[0].native,
|
||||
emoji: result.item,
|
||||
}))
|
||||
},
|
||||
[getEmojis],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export function parseAutocompleteItemType(type: string) {
|
||||
switch (type) {
|
||||
case 'mention':
|
||||
return 'profile'
|
||||
case 'tag':
|
||||
return 'tag'
|
||||
case 'emoji':
|
||||
return 'emoji'
|
||||
default:
|
||||
throw new Error(`Unknown autocomplete item type: ${type}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
import {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'
|
||||
import {
|
||||
type TextInput,
|
||||
type TextInputSubmitEditingEvent,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {useSift, type UseSiftReturn} from '@bsky.app/sift'
|
||||
import {
|
||||
facets,
|
||||
type TapperActiveFacet,
|
||||
type TapperFacet,
|
||||
useTapper,
|
||||
} from '@bsky.app/tapper'
|
||||
|
||||
import {mergeRefs} from '#/lib/merge-refs'
|
||||
import {
|
||||
atoms as a,
|
||||
type TextStyleProp,
|
||||
useAlf,
|
||||
type ViewStyleProp,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {normalizeTextStyles} from '#/alf/typography'
|
||||
import {
|
||||
Autocomplete as AutocompleteBase,
|
||||
AutocompleteItemEmoji,
|
||||
AutocompleteItemProfile,
|
||||
parseAutocompleteItemType,
|
||||
useAutocomplete,
|
||||
} from '#/components/Autocomplete'
|
||||
import {
|
||||
AutosizedTextarea,
|
||||
type AutosizedTextareaProps,
|
||||
} from '#/components/forms/AutosizedTextarea'
|
||||
import {Span, Text} from '#/components/Typography'
|
||||
import {IS_IOS, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
|
||||
|
||||
export type SubmitRequest =
|
||||
| {
|
||||
platform: 'web'
|
||||
shiftKey: boolean
|
||||
metaKey: boolean
|
||||
nativeEvent: KeyboardEvent
|
||||
}
|
||||
| {
|
||||
platform: 'native'
|
||||
nativeEvent: TextInputSubmitEditingEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Imperative API exposed via `internalApiRef` prop for parent components that
|
||||
* need to control the composer programmatically, e.g. to clear the input or
|
||||
* insert text at the current cursor position.
|
||||
*/
|
||||
export type ComposerInternalApi = {
|
||||
input?: ReturnType<typeof useTapper>['input']
|
||||
clear: () => void
|
||||
insert(text: string): void
|
||||
setAutocompleteAnchor: (node: View | null) => void
|
||||
}
|
||||
|
||||
export function useComposerInternalApiRef() {
|
||||
return useRef<ComposerInternalApi>(null)
|
||||
}
|
||||
|
||||
/*
|
||||
* ─── Composer ─────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
export type ComposerProps = Omit<
|
||||
AutosizedTextareaProps,
|
||||
| 'value'
|
||||
| 'onChange'
|
||||
| 'onChangeText'
|
||||
| 'onSelectionChange'
|
||||
| 'selection'
|
||||
| 'style'
|
||||
| 'onSubmitEditing'
|
||||
> & {
|
||||
label: string
|
||||
ref?: React.RefObject<TextInput>
|
||||
internalApiRef?: React.Ref<ComposerInternalApi>
|
||||
outerStyle?: ViewStyleProp['style']
|
||||
contentTextStyle?: TextStyleProp['style']
|
||||
contentPaddingStyle?: {
|
||||
paddingTop?: number
|
||||
paddingBottom?: number
|
||||
paddingLeft?: number
|
||||
paddingRight?: number
|
||||
}
|
||||
onChange?: (text: string) => void
|
||||
onActiveFacet?: (activeFacet: TapperActiveFacet | null) => void
|
||||
onFacetCommitted?: (facet: TapperFacet) => void
|
||||
onRequestSubmit?: (request: SubmitRequest) => void
|
||||
autocompletePlacement?: Exclude<
|
||||
Parameters<typeof useSift>[0],
|
||||
undefined
|
||||
>['placement']
|
||||
disableEmojiFacets?: boolean
|
||||
}
|
||||
|
||||
export function Composer({
|
||||
label,
|
||||
ref,
|
||||
internalApiRef,
|
||||
outerStyle,
|
||||
contentTextStyle,
|
||||
contentPaddingStyle,
|
||||
onChange: onChangeOuter,
|
||||
onActiveFacet: onActiveFacetOuter,
|
||||
onFacetCommitted: onFacetCommittedOuter,
|
||||
onRequestSubmit,
|
||||
autocompletePlacement,
|
||||
defaultValue,
|
||||
disableEmojiFacets = !IS_WEB,
|
||||
...rest
|
||||
}: ComposerProps) {
|
||||
const {theme: t, fonts} = useAlf()
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
/*
|
||||
* Meat and potatoes
|
||||
*/
|
||||
const tapper = useTapper({
|
||||
initialText: defaultValue ?? '',
|
||||
facets: disableEmojiFacets
|
||||
? {
|
||||
mention: facets.mention,
|
||||
tag: facets.tag,
|
||||
url: facets.url,
|
||||
}
|
||||
: facets,
|
||||
})
|
||||
const sift = useSift({
|
||||
offset: a.p_sm.padding,
|
||||
placement: autocompletePlacement,
|
||||
dynamicWidth: IS_WEB,
|
||||
insets,
|
||||
})
|
||||
|
||||
/*
|
||||
* Active facet state for controlling the visibility of the Autocomplete.
|
||||
*/
|
||||
const [activeFacet, setActiveFacet] = useState<TapperActiveFacet | null>(null)
|
||||
|
||||
/*
|
||||
* Reanimated shared value for syncing scroll on all platforms.
|
||||
*/
|
||||
const inputScrollSharedValue = useSharedValue(0)
|
||||
|
||||
/*
|
||||
* Expose imperative internal API
|
||||
*/
|
||||
useImperativeHandle(
|
||||
internalApiRef,
|
||||
() => ({
|
||||
input: tapper.input,
|
||||
clear: () => {
|
||||
tapper.inputProps.onChangeText('')
|
||||
inputScrollSharedValue.value = 0
|
||||
},
|
||||
insert: tapper.insert,
|
||||
setAutocompleteAnchor: sift.refs.setAnchor,
|
||||
}),
|
||||
[tapper.input, tapper.insert, inputScrollSharedValue, sift.refs.setAnchor],
|
||||
)
|
||||
|
||||
/*
|
||||
* Skip the initial mount to avoid an unnecessary re-render — the parent
|
||||
* already knows the initial value since it passed `initialText`.
|
||||
*/
|
||||
const isFirstRender = useRef(true)
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false
|
||||
return
|
||||
}
|
||||
onChangeOuter?.(tapper.state.text)
|
||||
}, [tapper.state.text, onChangeOuter])
|
||||
|
||||
/*
|
||||
* Tapper callbacks
|
||||
*/
|
||||
const callbackRefs = useRef({
|
||||
onActiveFacetOuter,
|
||||
onFacetCommittedOuter,
|
||||
})
|
||||
callbackRefs.current = {
|
||||
onActiveFacetOuter,
|
||||
onFacetCommittedOuter,
|
||||
}
|
||||
useEffect(() => {
|
||||
const offActiveFacet = tapper.on('activeFacet', facet => {
|
||||
setActiveFacet(facet)
|
||||
callbackRefs.current.onActiveFacetOuter?.(facet)
|
||||
})
|
||||
const offFacetCommitted = tapper.on('facetCommitted', facet => {
|
||||
callbackRefs.current.onFacetCommittedOuter?.(facet)
|
||||
})
|
||||
const offAfterInsert = tapper.on('afterInsert', () => {
|
||||
tapper.input.focus()
|
||||
})
|
||||
return () => {
|
||||
offActiveFacet()
|
||||
offFacetCommitted()
|
||||
offAfterInsert()
|
||||
}
|
||||
}, [tapper.on, tapper.input])
|
||||
|
||||
/*
|
||||
* Styles
|
||||
*/
|
||||
const previewScrollStyle = useAnimatedStyle(() => ({
|
||||
transform: [{translateY: -inputScrollSharedValue.value}],
|
||||
}))
|
||||
const textStyle = useMemo(() => {
|
||||
const ts = normalizeTextStyles(
|
||||
[a.leading_snug, t.atoms.text, contentTextStyle],
|
||||
{
|
||||
fontScale: fonts.scaleMultiplier,
|
||||
fontFamily: fonts.family,
|
||||
flags: {},
|
||||
},
|
||||
)
|
||||
/**
|
||||
* On iOS, having a lineHeight on the Text component causes the text to be
|
||||
* vertically misaligned with the TextInput.
|
||||
*
|
||||
* This only seems to be an issue on iOS, and not on Android or web. It's
|
||||
* possible that this is a bug in React Native's Text component on iOS,
|
||||
* but in the meantime, we'll just remove the lineHeight on iOS to ensure
|
||||
* the text is properly aligned.
|
||||
*/
|
||||
if (IS_IOS) {
|
||||
delete ts.lineHeight
|
||||
}
|
||||
return ts
|
||||
}, [contentTextStyle, fonts])
|
||||
|
||||
/*
|
||||
* Web keyboard handling
|
||||
*/
|
||||
const isComposing = useRef(false)
|
||||
const onKeyPressWeb = (e: React.KeyboardEvent | any) => {
|
||||
if (IS_WEB_TOUCH_DEVICE) return
|
||||
if (isComposing.current) return
|
||||
|
||||
/*
|
||||
* On Safari, the final keydown to dismiss an IME is also "Enter" with
|
||||
* keyCode 229. Chrome/Firefox don't have this problem.
|
||||
*
|
||||
* @see https://github.com/bluesky-social/social-app/issues/4178
|
||||
*/
|
||||
if (e.key === 'Enter' && e.keyCode === 229) return
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
onRequestSubmit?.({
|
||||
platform: 'web',
|
||||
shiftKey: e.shiftKey,
|
||||
metaKey: e.metaKey,
|
||||
nativeEvent: e.nativeEvent,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Sift popover positioning
|
||||
*/
|
||||
const updateAutocompletePosition = () => {
|
||||
sift.updatePosition()
|
||||
}
|
||||
|
||||
const textContent = (
|
||||
<Text style={[textStyle, web({whiteSpace: 'pre-wrap'})]}>
|
||||
{tapper.state.nodes.map((node, i) => {
|
||||
switch (node.type) {
|
||||
case 'text':
|
||||
return <Span key={i}>{node.value}</Span>
|
||||
case 'trigger':
|
||||
case 'facet':
|
||||
return (
|
||||
<Span
|
||||
key={i}
|
||||
ref={IS_WEB ? sift.refs.setAnchor : undefined}
|
||||
style={
|
||||
node.type === 'facet' && {
|
||||
color: t.palette.primary_500,
|
||||
}
|
||||
}>
|
||||
{node.raw}
|
||||
</Span>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</Text>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={[a.relative, outerStyle]}>
|
||||
{IS_WEB && (
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={[a.absolute, a.inset_0, a.z_10, {overflow: 'hidden'}]}>
|
||||
<Animated.View
|
||||
style={[
|
||||
contentPaddingStyle,
|
||||
{position: 'absolute', left: 0, right: 0},
|
||||
previewScrollStyle,
|
||||
]}>
|
||||
{textContent}
|
||||
</Animated.View>
|
||||
</View>
|
||||
)}
|
||||
<AutosizedTextarea
|
||||
placeholderTextColor={t.palette.contrast_500}
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint={label}
|
||||
onSubmitEditing={e => {
|
||||
onRequestSubmit?.({platform: 'native', nativeEvent: e})
|
||||
}}
|
||||
style={[
|
||||
textStyle,
|
||||
contentPaddingStyle,
|
||||
a.z_20,
|
||||
{
|
||||
color: 'transparent',
|
||||
background: 'transparent',
|
||||
},
|
||||
web({
|
||||
caretColor: textStyle.color ?? 'black',
|
||||
overscrollBehavior: 'none',
|
||||
}),
|
||||
]}
|
||||
{...rest}
|
||||
{...tapper.inputProps}
|
||||
{...sift.targetProps}
|
||||
ref={mergeRefs([ref, tapper.inputProps.ref, sift.targetProps.ref])}
|
||||
onBlur={e => {
|
||||
rest.onBlur?.(e)
|
||||
setActiveFacet(null)
|
||||
}}
|
||||
onKeyPress={IS_WEB ? onKeyPressWeb : undefined}
|
||||
onScroll={e => {
|
||||
if (IS_WEB) {
|
||||
inputScrollSharedValue.value = (e.target as any).scrollTop
|
||||
} else {
|
||||
inputScrollSharedValue.value = e.nativeEvent.contentOffset.y
|
||||
}
|
||||
}}
|
||||
// @ts-ignore web only
|
||||
onCompositionStart={() => {
|
||||
isComposing.current = true
|
||||
}}
|
||||
// @ts-ignore web only
|
||||
onCompositionEnd={() => {
|
||||
isComposing.current = false
|
||||
}}
|
||||
onUpdateHeight={updateAutocompletePosition}>
|
||||
{IS_WEB ? null : textContent}
|
||||
</AutosizedTextarea>
|
||||
</View>
|
||||
|
||||
{activeFacet && activeFacet.type !== 'url' && (
|
||||
<AutocompleteInner
|
||||
sift={sift}
|
||||
activeFacet={activeFacet}
|
||||
onDismiss={() => setActiveFacet(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* ─── Autocomplete (private) ───────────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
function AutocompleteInner({
|
||||
sift,
|
||||
activeFacet,
|
||||
onDismiss,
|
||||
}: {
|
||||
sift: UseSiftReturn
|
||||
activeFacet: TapperActiveFacet
|
||||
onDismiss: () => void
|
||||
}) {
|
||||
const {items} = useAutocomplete({
|
||||
type: parseAutocompleteItemType(activeFacet.type),
|
||||
query: activeFacet.value,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
activeFacet?.type === 'emoji' &&
|
||||
!!activeFacet.value.length &&
|
||||
activeFacet.raw.endsWith(':')
|
||||
) {
|
||||
if (items?.[0]) {
|
||||
activeFacet.replace(items[0].value, {noTrailingSpace: true})
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}, [items, activeFacet])
|
||||
|
||||
return items && items.length ? (
|
||||
<AutocompleteBase
|
||||
inverted={!IS_WEB}
|
||||
sift={sift}
|
||||
data={items}
|
||||
render={props => {
|
||||
if (props.item.type === 'profile') {
|
||||
return <AutocompleteItemProfile {...props} />
|
||||
}
|
||||
if (props.item.type === 'emoji') {
|
||||
return <AutocompleteItemEmoji {...props} />
|
||||
}
|
||||
return <View />
|
||||
}}
|
||||
onSelect={item => {
|
||||
activeFacet.replace(item.value)
|
||||
onDismiss()
|
||||
}}
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import Animated, {
|
||||
LayoutAnimationConfig,
|
||||
LinearTransition,
|
||||
} from 'react-native-reanimated'
|
||||
import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
@@ -15,11 +15,9 @@ import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSuggestedFollowsByActorWithDismiss} from '#/state/queries/suggested-follows'
|
||||
import {useGetSuggestedUsersForDiscoverQuery} from '#/state/queries/trending/useGetSuggestedUsersForDiscoverQuery'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {type SeenPost} from '#/state/userActionHistory'
|
||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||
import {
|
||||
atoms as a,
|
||||
@@ -37,12 +35,12 @@ import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Has
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {ProgressGuideList} from '#/components/ProgressGuide/List'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {type Metrics, useAnalytics} from '#/analytics'
|
||||
import {IS_IOS} from '#/env'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {FollowDialogWithoutGuide} from './ProgressGuide/FollowDialog'
|
||||
import {ProgressGuideList} from './ProgressGuide/List'
|
||||
|
||||
const DISMISS_ANIMATION_DURATION = 200
|
||||
|
||||
@@ -109,95 +107,6 @@ export function SuggestedFeedsCardPlaceholder() {
|
||||
)
|
||||
}
|
||||
|
||||
function getRank(seenPost: SeenPost): string {
|
||||
let tier: string
|
||||
if (seenPost.feedContext === 'popfriends') {
|
||||
tier = 'a'
|
||||
} else if (seenPost.feedContext?.startsWith('cluster')) {
|
||||
tier = 'b'
|
||||
} else if (seenPost.feedContext === 'popcluster') {
|
||||
tier = 'c'
|
||||
} else if (seenPost.feedContext?.startsWith('ntpc')) {
|
||||
tier = 'd'
|
||||
} else if (seenPost.feedContext?.startsWith('t-')) {
|
||||
tier = 'e'
|
||||
} else if (seenPost.feedContext === 'nettop') {
|
||||
tier = 'f'
|
||||
} else {
|
||||
tier = 'g'
|
||||
}
|
||||
let score = Math.round(
|
||||
Math.log(
|
||||
1 + seenPost.likeCount + seenPost.repostCount + seenPost.replyCount,
|
||||
),
|
||||
)
|
||||
if (seenPost.isFollowedBy || Math.random() > 0.9) {
|
||||
score *= 2
|
||||
}
|
||||
const rank = 100 - score
|
||||
return `${tier}-${rank}`
|
||||
}
|
||||
|
||||
function sortSeenPosts(postA: SeenPost, postB: SeenPost): 0 | 1 | -1 {
|
||||
const rankA = getRank(postA)
|
||||
const rankB = getRank(postB)
|
||||
// Yes, we're comparing strings here.
|
||||
// The "larger" string means a worse rank.
|
||||
if (rankA > rankB) {
|
||||
return 1
|
||||
} else if (rankA < rankB) {
|
||||
return -1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
function useExperimentalSuggestedUsersQuery() {
|
||||
const {currentAccount} = useSession()
|
||||
const userActionSnapshot = userActionHistory.useActionHistorySnapshot()
|
||||
const dids = useMemo(() => {
|
||||
const {likes, follows, followSuggestions, seen} = userActionSnapshot
|
||||
const likeDids = likes
|
||||
.map(l => new AtUri(l))
|
||||
.map(uri => uri.host)
|
||||
.filter(did => !follows.includes(did))
|
||||
let suggestedDids: string[] = []
|
||||
if (followSuggestions.length > 0) {
|
||||
suggestedDids = [
|
||||
// It's ok if these will pick the same item (weighed by its frequency)
|
||||
/* eslint-disable react-hooks/purity */
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
/* eslint-enable react-hooks/purity */
|
||||
]
|
||||
}
|
||||
const seenDids = seen
|
||||
.sort(sortSeenPosts)
|
||||
.map(l => new AtUri(l.uri))
|
||||
.map(uri => uri.host)
|
||||
return [...new Set([...suggestedDids, ...likeDids, ...seenDids])].filter(
|
||||
did => did !== currentAccount?.did,
|
||||
)
|
||||
}, [userActionSnapshot, currentAccount])
|
||||
const {data, isLoading, error} = useProfilesQuery({
|
||||
handles: dids.slice(0, 16),
|
||||
})
|
||||
|
||||
const profiles = data
|
||||
? data.profiles.filter(profile => {
|
||||
return !profile.viewer?.following
|
||||
})
|
||||
: []
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
profiles: profiles.slice(0, 6),
|
||||
}
|
||||
}
|
||||
|
||||
export function SuggestedFollows({feed}: {feed: FeedDescriptor}) {
|
||||
const {currentAccount} = useSession()
|
||||
const [feedType, feedUriOrDid] = feed.split('|')
|
||||
@@ -229,11 +138,9 @@ export function SuggestedFollowsProfile({did}: {did: string}) {
|
||||
}
|
||||
|
||||
export function SuggestedFollowsHome() {
|
||||
const {
|
||||
isLoading: isSuggestionsLoading,
|
||||
profiles: experimentalProfiles,
|
||||
error: experimentalError,
|
||||
} = useExperimentalSuggestedUsersQuery()
|
||||
const {isLoading, data, error} = useGetSuggestedUsersForDiscoverQuery()
|
||||
|
||||
const profiles = data?.actors
|
||||
|
||||
const [dismissedDids, setDismissedDids] = useState<Set<string>>(new Set())
|
||||
|
||||
@@ -247,12 +154,12 @@ export function SuggestedFollowsHome() {
|
||||
recId?: string
|
||||
}> = []
|
||||
|
||||
for (const profile of experimentalProfiles) {
|
||||
result.push({actor: profile, recId: undefined})
|
||||
for (const profile of profiles ?? []) {
|
||||
result.push({actor: profile, recId: data?.recId})
|
||||
}
|
||||
|
||||
return result
|
||||
}, [experimentalProfiles])
|
||||
}, [data?.recId, profiles])
|
||||
|
||||
const filteredProfiles = useMemo(() => {
|
||||
return allProfiles.filter(p => !dismissedDids.has(p.actor.did))
|
||||
@@ -260,10 +167,10 @@ export function SuggestedFollowsHome() {
|
||||
|
||||
return (
|
||||
<ProfileGrid
|
||||
isSuggestionsLoading={isSuggestionsLoading}
|
||||
isSuggestionsLoading={isLoading}
|
||||
profiles={filteredProfiles}
|
||||
totalProfileCount={allProfiles.length}
|
||||
error={experimentalError}
|
||||
error={error}
|
||||
viewContext="feed"
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
|
||||
@@ -136,6 +136,8 @@ export const BookmarkButton = memo(function BookmarkButton({
|
||||
<PostControlButton
|
||||
testID="postBookmarkBtn"
|
||||
big={big}
|
||||
active={isBookmarked}
|
||||
activeColor={t.palette.primary_500}
|
||||
label={
|
||||
isBookmarked
|
||||
? _(msg`Remove from saved posts`)
|
||||
@@ -143,10 +145,7 @@ export const BookmarkButton = memo(function BookmarkButton({
|
||||
}
|
||||
onPress={onHandlePress}
|
||||
hitSlop={hitSlop}>
|
||||
<PostControlButtonIcon
|
||||
fill={isBookmarked ? t.palette.primary_500 : undefined}
|
||||
icon={isBookmarked ? BookmarkFilled : Bookmark}
|
||||
/>
|
||||
<PostControlButtonIcon icon={isBookmarked ? BookmarkFilled : Bookmark} />
|
||||
</PostControlButton>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -130,8 +130,11 @@ export function PostControlButtonText({style, ...props}: TextProps) {
|
||||
<Text
|
||||
style={[
|
||||
color,
|
||||
a.user_select_none,
|
||||
big ? a.text_md : a.text_sm,
|
||||
active && a.font_semi_bold,
|
||||
// prevent layout shift on android
|
||||
{includeFontPadding: false, textAlignVertical: 'center'},
|
||||
style,
|
||||
]}
|
||||
{...props}
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
ProgressGuideAction,
|
||||
useProgressGuideControls,
|
||||
} from '#/state/shell/progress-guide'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Reply as Bubble} from '#/components/icons/Reply'
|
||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||
import * as Skele from '#/components/Skeleton'
|
||||
@@ -74,6 +74,7 @@ let PostControls = ({
|
||||
forceGoogleTranslate?: boolean
|
||||
}): React.ReactNode => {
|
||||
const ax = useAnalytics()
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const {openComposer} = useOpenComposer()
|
||||
const {feedDescriptor} = useFeedFeedbackContext()
|
||||
@@ -270,6 +271,8 @@ let PostControls = ({
|
||||
<PostControlButton
|
||||
testID="likeBtn"
|
||||
big={big}
|
||||
active={Boolean(post.viewer?.like)}
|
||||
activeColor={t.palette.pink}
|
||||
onPress={() => requireAuth(() => onPressToggleLike())}
|
||||
label={
|
||||
post.viewer?.like
|
||||
@@ -296,10 +299,14 @@ let PostControls = ({
|
||||
hasBeenToggled={hasLikeIconBeenToggled}
|
||||
/>
|
||||
<CountWheel
|
||||
likeCount={post.likeCount ?? 0}
|
||||
big={big}
|
||||
isLiked={Boolean(post.viewer?.like)}
|
||||
count={post.likeCount ?? 0}
|
||||
isToggled={Boolean(post.viewer?.like)}
|
||||
hasBeenToggled={hasLikeIconBeenToggled}
|
||||
renderCount={({count}) => (
|
||||
<PostControlButtonText>
|
||||
{formatPostStatCount(count)}
|
||||
</PostControlButtonText>
|
||||
)}
|
||||
/>
|
||||
</PostControlButton>
|
||||
</View>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {View} from 'react-native'
|
||||
import {useWindowDimensions, View} from 'react-native'
|
||||
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {atoms as a, type ViewStyleProp} from '#/alf'
|
||||
import {atoms as a, useAlf, type ViewStyleProp} from '#/alf'
|
||||
import {BotBadge, BotBadgeButton, isBotAccount} from '#/components/BotBadge'
|
||||
import {useSimpleVerificationState} from '#/components/verification'
|
||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||
@@ -38,12 +38,21 @@ export function ProfileBadges({
|
||||
}) {
|
||||
const shadowed = useProfileShadow(profile)
|
||||
const verification = useSimpleVerificationState({profile})
|
||||
const {fontScale: nativeScaleMultiplier} = useWindowDimensions()
|
||||
const {
|
||||
fonts: {scaleMultiplier: alfScaleMultiplier},
|
||||
} = useAlf()
|
||||
|
||||
// if nothing to show, don't render the container at all
|
||||
if (!verification.showBadge && !isBotAccount(shadowed)) return null
|
||||
|
||||
const isOnTheSmallSide = size === 'xs' || size === 'sm'
|
||||
|
||||
const verificationIconWidth =
|
||||
verificationIconSizes[size] * nativeScaleMultiplier * alfScaleMultiplier
|
||||
const botIconWidth =
|
||||
botIconSizes[size] * nativeScaleMultiplier * alfScaleMultiplier
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -56,19 +65,19 @@ export function ProfileBadges({
|
||||
<>
|
||||
<VerificationCheckButton
|
||||
profile={shadowed}
|
||||
width={verificationIconSizes[size]}
|
||||
width={verificationIconWidth}
|
||||
/>
|
||||
<BotBadgeButton profile={shadowed} width={botIconSizes[size]} />
|
||||
<BotBadgeButton profile={shadowed} width={botIconWidth} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{verification.showBadge && (
|
||||
<VerificationCheck
|
||||
verifier={verification.role === 'verifier'}
|
||||
width={verificationIconSizes[size]}
|
||||
width={verificationIconWidth}
|
||||
/>
|
||||
)}
|
||||
<BotBadge profile={shadowed} width={botIconSizes[size]} />
|
||||
<BotBadge profile={shadowed} width={botIconWidth} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useActorSearch} from '#/state/queries/actor-search'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||
import {useGetSuggestedUsersForSeeMoreQuery} from '#/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery'
|
||||
import {useSession} from '#/state/session'
|
||||
import {type Follow10ProgressGuide} from '#/state/shell/progress-guide'
|
||||
import {type ListMethods} from '#/view/com/util/List'
|
||||
@@ -141,7 +141,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
data: suggestions,
|
||||
isFetching: isFetchingSuggestions,
|
||||
error: suggestionsError,
|
||||
} = useGetSuggestedUsersQuery({
|
||||
} = useGetSuggestedUsersForSeeMoreQuery({
|
||||
category: selectedInterest,
|
||||
limit: 50,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import {useMemo, useRef, useState} from 'react'
|
||||
import {
|
||||
TextInput,
|
||||
type TextInputContentSizeChangeEvent,
|
||||
type TextInputProps,
|
||||
} from 'react-native'
|
||||
|
||||
import {mergeRefs} from '#/lib/merge-refs'
|
||||
import {atoms as a, extractPadding, useAlf, web} from '#/alf'
|
||||
import {normalizeTextStyles} from '#/alf/typography'
|
||||
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
|
||||
|
||||
export type AutosizedTextareaProps = Omit<TextInputProps, 'multiline'> & {
|
||||
ref?: React.Ref<TextInput>
|
||||
label: string
|
||||
minRows?: number
|
||||
maxRows?: number
|
||||
onUpdateHeight?: (height: number) => void
|
||||
}
|
||||
|
||||
export function AutosizedTextarea({
|
||||
ref,
|
||||
label,
|
||||
minRows = 1,
|
||||
maxRows,
|
||||
onUpdateHeight,
|
||||
|
||||
onChangeText: onChangeTextOuter,
|
||||
onContentSizeChange: onContentSizeChangeOuter,
|
||||
style: outerStyle,
|
||||
...rest
|
||||
}: AutosizedTextareaProps) {
|
||||
const {theme: t, fonts} = useAlf()
|
||||
const internalRef = useRef<TextInput>(null)
|
||||
const {style, minInputHeight, maxInputHeight, verticalContentPadding} =
|
||||
useMemo(() => {
|
||||
const normalizedStyles = normalizeTextStyles(
|
||||
[a.text_md, a.leading_snug, t.atoms.text, outerStyle],
|
||||
{
|
||||
fontScale: fonts.scaleMultiplier,
|
||||
fontFamily: fonts.family,
|
||||
flags: {},
|
||||
},
|
||||
)
|
||||
const lineHeight = normalizedStyles.lineHeight || 20
|
||||
const {paddingTop, paddingBottom} = extractPadding(normalizedStyles ?? {})
|
||||
const verticalContentPadding = paddingTop + paddingBottom
|
||||
const minInputHeight = lineHeight * minRows + verticalContentPadding
|
||||
const maxInputHeight = maxRows
|
||||
? lineHeight * maxRows + verticalContentPadding
|
||||
: Infinity
|
||||
|
||||
/*
|
||||
* iOS: minHeight/maxHeight works fine natively.
|
||||
* Web + Android: we set an explicit initial height and resize dynamically
|
||||
* (web via DOM measurement, Android via onContentSizeChange state).
|
||||
*
|
||||
* iOS also seems to need 1px headroom to actually expand to the correct
|
||||
* maxHeight
|
||||
*/
|
||||
const heightConstraints = IS_IOS
|
||||
? {minHeight: minInputHeight, maxHeight: maxInputHeight + 1}
|
||||
: {height: minInputHeight}
|
||||
|
||||
return {
|
||||
style: {
|
||||
...normalizedStyles,
|
||||
...heightConstraints,
|
||||
},
|
||||
minInputHeight,
|
||||
maxInputHeight,
|
||||
verticalContentPadding,
|
||||
}
|
||||
}, [t, fonts, outerStyle, minRows, maxRows])
|
||||
|
||||
/*
|
||||
* Web handling
|
||||
*/
|
||||
const prevWebHeight = useRef(0)
|
||||
const handleResizeWeb = () => {
|
||||
const el = internalRef.current as unknown as HTMLTextAreaElement
|
||||
if (!el) return
|
||||
// collapse to get natural scroll height
|
||||
el.style.height = '0px'
|
||||
const scrollHeight = Math.ceil(el.scrollHeight)
|
||||
const nextHeight = Math.min(
|
||||
Math.max(scrollHeight, minInputHeight),
|
||||
maxInputHeight,
|
||||
)
|
||||
// immediately update height to prevent flicker
|
||||
el.style.height = `${nextHeight}px`
|
||||
el.style.overflowY = scrollHeight > maxInputHeight ? 'auto' : 'hidden'
|
||||
if (nextHeight !== prevWebHeight.current) {
|
||||
prevWebHeight.current = nextHeight
|
||||
onUpdateHeight?.(nextHeight)
|
||||
}
|
||||
}
|
||||
const onChangeText = (text: string) => {
|
||||
if (IS_WEB) handleResizeWeb()
|
||||
onChangeTextOuter?.(text)
|
||||
}
|
||||
|
||||
/*
|
||||
* Native handling
|
||||
*
|
||||
* We track the height as state on native, and on Android, we use this to
|
||||
* directly drive the `height`.
|
||||
*/
|
||||
const [nativeHeight, setNativeHeight] = useState(minInputHeight)
|
||||
const onContentSizeChange = (e: TextInputContentSizeChangeEvent) => {
|
||||
const contentSize = Math.ceil(e.nativeEvent.contentSize.height)
|
||||
// ios reports the content size without padding
|
||||
const height = IS_IOS ? contentSize + verticalContentPadding : contentSize
|
||||
const nextHeight = Math.min(
|
||||
Math.max(height, minInputHeight),
|
||||
maxInputHeight,
|
||||
)
|
||||
|
||||
if (nextHeight !== nativeHeight) {
|
||||
setNativeHeight(nextHeight)
|
||||
onUpdateHeight?.(nextHeight)
|
||||
}
|
||||
|
||||
onContentSizeChangeOuter?.(e)
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
multiline
|
||||
placeholderTextColor={t.palette.contrast_500}
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint={label}
|
||||
placeholder={label}
|
||||
keyboardAppearance={t.scheme}
|
||||
submitBehavior="newline"
|
||||
scrollEnabled={nativeHeight >= maxInputHeight}
|
||||
style={[
|
||||
a.relative,
|
||||
a.border_0,
|
||||
{
|
||||
textAlignVertical: 'top',
|
||||
includeFontPadding: false,
|
||||
},
|
||||
web({
|
||||
resize: 'none',
|
||||
outline: 'none',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}),
|
||||
style,
|
||||
IS_ANDROID ? {height: nativeHeight} : {},
|
||||
]}
|
||||
{...rest}
|
||||
ref={mergeRefs([
|
||||
(node: TextInput | null) => {
|
||||
internalRef.current = node
|
||||
// bop resize on first render
|
||||
if (IS_WEB && node) handleResizeWeb()
|
||||
},
|
||||
ref,
|
||||
])}
|
||||
onChangeText={onChangeText}
|
||||
onContentSizeChange={onContentSizeChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import {forwardRef} from 'react'
|
||||
import {useEffect, useRef} from 'react'
|
||||
import {type TextInput, View} from 'react-native'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {mergeRefs} from '#/lib/merge-refs'
|
||||
import {listenFocusSearch} from '#/state/events'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
@@ -10,73 +12,88 @@ import {MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
|
||||
type SearchInputProps = Omit<TextField.InputProps, 'label'> & {
|
||||
type Props = Omit<TextField.InputProps, 'label'> & {
|
||||
label?: TextField.InputProps['label']
|
||||
/**
|
||||
* Called when the user presses the (X) button
|
||||
*/
|
||||
onClearText?: () => void
|
||||
hotkey?: boolean
|
||||
ref?: React.Ref<TextInput>
|
||||
}
|
||||
|
||||
export const SearchInput = forwardRef<TextInput, SearchInputProps>(
|
||||
function SearchInput({value, label, onClearText, ...rest}, ref) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const showClear = value && value.length > 0
|
||||
export function SearchInput({
|
||||
value,
|
||||
label,
|
||||
onClearText,
|
||||
hotkey,
|
||||
ref,
|
||||
...rest
|
||||
}: Props) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const showClear = value && value.length > 0
|
||||
const internalRef = useRef<TextInput>(null)
|
||||
|
||||
return (
|
||||
<View style={[a.w_full, a.relative]}>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={MagnifyingGlassIcon} />
|
||||
<TextField.Input
|
||||
inputRef={ref}
|
||||
label={label || l`Search`}
|
||||
value={value}
|
||||
placeholder={l`Search`}
|
||||
returnKeyType="search"
|
||||
keyboardAppearance={t.scheme}
|
||||
selectTextOnFocus={IS_NATIVE}
|
||||
autoFocus={false}
|
||||
accessibilityRole="search"
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
style={[
|
||||
showClear
|
||||
? {
|
||||
paddingRight: 24,
|
||||
}
|
||||
: {},
|
||||
]}
|
||||
{...rest}
|
||||
/>
|
||||
</TextField.Root>
|
||||
useEffect(() => {
|
||||
if (!hotkey) return
|
||||
return listenFocusSearch(() => {
|
||||
internalRef.current?.focus()
|
||||
})
|
||||
}, [hotkey])
|
||||
|
||||
{showClear && (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_20,
|
||||
a.my_auto,
|
||||
a.inset_0,
|
||||
a.justify_center,
|
||||
a.pr_sm,
|
||||
{left: 'auto'},
|
||||
]}>
|
||||
<Button
|
||||
testID="searchTextInputClearBtn"
|
||||
onPress={onClearText}
|
||||
label={l`Clear search query`}
|
||||
hitSlop={HITSLOP_10}
|
||||
size="tiny"
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary">
|
||||
<ButtonIcon icon={X} size="xs" />
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<View style={[a.w_full, a.relative]}>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={MagnifyingGlassIcon} />
|
||||
<TextField.Input
|
||||
inputRef={mergeRefs([internalRef, ref])}
|
||||
label={label || l`Search`}
|
||||
value={value}
|
||||
placeholder={l`Search`}
|
||||
returnKeyType="search"
|
||||
keyboardAppearance={t.scheme}
|
||||
selectTextOnFocus={IS_NATIVE}
|
||||
autoFocus={false}
|
||||
accessibilityRole="search"
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
style={[
|
||||
showClear
|
||||
? {
|
||||
paddingRight: 24,
|
||||
}
|
||||
: {},
|
||||
]}
|
||||
{...rest}
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
{showClear && (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_20,
|
||||
a.my_auto,
|
||||
a.inset_0,
|
||||
a.justify_center,
|
||||
a.pr_sm,
|
||||
{left: 'auto'},
|
||||
]}>
|
||||
<Button
|
||||
testID="searchTextInputClearBtn"
|
||||
onPress={onClearText}
|
||||
label={l`Clear search query`}
|
||||
hitSlop={HITSLOP_10}
|
||||
size="tiny"
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary">
|
||||
<ButtonIcon icon={X} size="xs" />
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ function Inner(props: ReportDialogProps) {
|
||||
)
|
||||
})
|
||||
}, [
|
||||
props,
|
||||
props.subject,
|
||||
allLabelers,
|
||||
state.selectedOption,
|
||||
isBskyOnlyReason,
|
||||
@@ -241,7 +241,17 @@ function Inner(props: ReportDialogProps) {
|
||||
} finally {
|
||||
setPending(false)
|
||||
}
|
||||
}, [_, submitReport, state, dispatch, props, setPending, setSuccess])
|
||||
}, [
|
||||
_,
|
||||
submitReport,
|
||||
state,
|
||||
dispatch,
|
||||
props.subject,
|
||||
props.control,
|
||||
props.onAfterSubmit,
|
||||
setPending,
|
||||
setSuccess,
|
||||
])
|
||||
|
||||
useCallOnce(() => {
|
||||
ax.metric('reportDialog:open', {
|
||||
|
||||
@@ -8,10 +8,7 @@ import Animated, {
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
import {decideShouldRoll} from '#/lib/custom-animations/util'
|
||||
import {s} from '#/lib/styles'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
const animationConfig = {
|
||||
duration: 400,
|
||||
@@ -87,89 +84,66 @@ function ExitingDown() {
|
||||
}
|
||||
|
||||
export function CountWheel({
|
||||
likeCount,
|
||||
big,
|
||||
isLiked,
|
||||
count,
|
||||
isToggled,
|
||||
hasBeenToggled,
|
||||
renderCount,
|
||||
}: {
|
||||
likeCount: number
|
||||
big?: boolean
|
||||
isLiked: boolean
|
||||
count: number
|
||||
isToggled: boolean
|
||||
hasBeenToggled: boolean
|
||||
renderCount: (props: {count: number}) => React.ReactNode
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const shouldAnimate = !useReducedMotion() && hasBeenToggled
|
||||
const shouldRoll = decideShouldRoll(isLiked, likeCount)
|
||||
const shouldRoll = decideShouldRoll(isToggled, count)
|
||||
|
||||
// Incrementing the key will cause the `Animated.View` to re-render, with the newly selected entering/exiting
|
||||
// animation
|
||||
// The initial entering/exiting animations will get skipped, since these will happen on screen mounts and would
|
||||
// be unnecessary
|
||||
const [key, setKey] = useState(0)
|
||||
const [prevCount, setPrevCount] = useState(likeCount)
|
||||
const prevIsLiked = useRef(isLiked)
|
||||
const formatPostStatCount = useFormatPostStatCount()
|
||||
const formattedCount = formatPostStatCount(likeCount)
|
||||
const formattedPrevCount = formatPostStatCount(prevCount)
|
||||
const [prevCount, setPrevCount] = useState(count)
|
||||
const prevIsToggled = useRef(isToggled)
|
||||
|
||||
useEffect(() => {
|
||||
if (isLiked === prevIsLiked.current) {
|
||||
if (isToggled === prevIsToggled.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const newPrevCount = isLiked ? likeCount - 1 : likeCount + 1
|
||||
const newPrevCount = isToggled ? count - 1 : count + 1
|
||||
setKey(prev => prev + 1)
|
||||
setPrevCount(newPrevCount)
|
||||
prevIsLiked.current = isLiked
|
||||
}, [isLiked, likeCount])
|
||||
prevIsToggled.current = isToggled
|
||||
}, [isToggled, count])
|
||||
|
||||
const enteringAnimation =
|
||||
shouldAnimate && shouldRoll
|
||||
? isLiked
|
||||
? isToggled
|
||||
? EnteringUp
|
||||
: EnteringDown
|
||||
: undefined
|
||||
const exitingAnimation =
|
||||
shouldAnimate && shouldRoll
|
||||
? isLiked
|
||||
? isToggled
|
||||
? ExitingUp
|
||||
: ExitingDown
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<LayoutAnimationConfig skipEntering skipExiting>
|
||||
{likeCount > 0 ? (
|
||||
{count > 0 ? (
|
||||
<View style={[a.justify_center]}>
|
||||
<Animated.View entering={enteringAnimation} key={key}>
|
||||
<Text
|
||||
testID="likeCount"
|
||||
style={[
|
||||
big ? a.text_md : a.text_sm,
|
||||
a.user_select_none,
|
||||
isLiked
|
||||
? [a.font_semi_bold, s.likeColor]
|
||||
: {color: t.palette.contrast_500},
|
||||
]}>
|
||||
{formattedCount}
|
||||
</Text>
|
||||
{renderCount({count})}
|
||||
</Animated.View>
|
||||
{shouldAnimate && (likeCount > 1 || !isLiked) ? (
|
||||
{shouldAnimate && (count > 1 || !isToggled) ? (
|
||||
<Animated.View
|
||||
entering={exitingAnimation}
|
||||
// Add 2 to the key so there are never duplicates
|
||||
key={key + 2}
|
||||
style={[a.absolute, {width: 50, opacity: 0}]}
|
||||
aria-disabled={true}>
|
||||
<Text
|
||||
style={[
|
||||
big ? a.text_md : a.text_sm,
|
||||
a.user_select_none,
|
||||
isLiked
|
||||
? [a.font_semi_bold, s.likeColor]
|
||||
: {color: t.palette.contrast_500},
|
||||
]}>
|
||||
{formattedPrevCount}
|
||||
</Text>
|
||||
{renderCount({count: prevCount})}
|
||||
</Animated.View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -3,10 +3,6 @@ import {View} from 'react-native'
|
||||
import {useReducedMotion} from 'react-native-reanimated'
|
||||
|
||||
import {decideShouldRoll} from '#/lib/custom-animations/util'
|
||||
import {s} from '#/lib/styles'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useFormatPostStatCount} from '#/components/PostControls/util'
|
||||
|
||||
const animationConfig = {
|
||||
duration: 400,
|
||||
@@ -35,50 +31,46 @@ const exitingDownKeyframe = [
|
||||
]
|
||||
|
||||
export function CountWheel({
|
||||
likeCount,
|
||||
big,
|
||||
isLiked,
|
||||
count,
|
||||
isToggled,
|
||||
hasBeenToggled,
|
||||
renderCount,
|
||||
}: {
|
||||
likeCount: number
|
||||
big?: boolean
|
||||
isLiked: boolean
|
||||
count: number
|
||||
isToggled: boolean
|
||||
hasBeenToggled: boolean
|
||||
renderCount: (props: {count: number}) => React.ReactNode
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const shouldAnimate = !useReducedMotion() && hasBeenToggled
|
||||
const shouldRoll = decideShouldRoll(isLiked, likeCount)
|
||||
const shouldRoll = decideShouldRoll(isToggled, count)
|
||||
|
||||
const countView = useRef<HTMLDivElement>(null)
|
||||
const prevCountView = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [prevCount, setPrevCount] = useState(likeCount)
|
||||
const prevIsLiked = useRef(isLiked)
|
||||
const formatPostStatCount = useFormatPostStatCount()
|
||||
const formattedCount = formatPostStatCount(likeCount)
|
||||
const formattedPrevCount = formatPostStatCount(prevCount)
|
||||
const [prevCount, setPrevCount] = useState(count)
|
||||
const prevIsToggled = useRef(isToggled)
|
||||
|
||||
useEffect(() => {
|
||||
if (isLiked === prevIsLiked.current) {
|
||||
if (isToggled === prevIsToggled.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const newPrevCount = isLiked ? likeCount - 1 : likeCount + 1
|
||||
const newPrevCount = isToggled ? count - 1 : count + 1
|
||||
if (shouldAnimate && shouldRoll) {
|
||||
countView.current?.animate?.(
|
||||
isLiked ? enteringUpKeyframe : enteringDownKeyframe,
|
||||
isToggled ? enteringUpKeyframe : enteringDownKeyframe,
|
||||
animationConfig,
|
||||
)
|
||||
prevCountView.current?.animate?.(
|
||||
isLiked ? exitingUpKeyframe : exitingDownKeyframe,
|
||||
isToggled ? exitingUpKeyframe : exitingDownKeyframe,
|
||||
animationConfig,
|
||||
)
|
||||
setPrevCount(newPrevCount)
|
||||
}
|
||||
prevIsLiked.current = isLiked
|
||||
}, [isLiked, likeCount, shouldAnimate, shouldRoll])
|
||||
prevIsToggled.current = isToggled
|
||||
}, [isToggled, count, shouldAnimate, shouldRoll])
|
||||
|
||||
if (likeCount < 1) {
|
||||
if (count < 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -87,34 +79,15 @@ export function CountWheel({
|
||||
<View
|
||||
// @ts-expect-error is div
|
||||
ref={countView}>
|
||||
<Text
|
||||
testID="likeCount"
|
||||
style={[
|
||||
big ? a.text_md : a.text_sm,
|
||||
a.user_select_none,
|
||||
isLiked
|
||||
? [a.font_semi_bold, s.likeColor]
|
||||
: {color: t.palette.contrast_500},
|
||||
]}>
|
||||
{formattedCount}
|
||||
</Text>
|
||||
{renderCount({count})}
|
||||
</View>
|
||||
{shouldAnimate && (likeCount > 1 || !isLiked) ? (
|
||||
{shouldAnimate && (count > 1 || !isToggled) ? (
|
||||
<View
|
||||
style={{position: 'absolute', opacity: 0}}
|
||||
aria-disabled={true}
|
||||
// @ts-expect-error is div
|
||||
ref={prevCountView}>
|
||||
<Text
|
||||
style={[
|
||||
big ? a.text_md : a.text_sm,
|
||||
a.user_select_none,
|
||||
isLiked
|
||||
? [a.font_semi_bold, s.likeColor]
|
||||
: {color: t.palette.contrast_500},
|
||||
]}>
|
||||
{formattedPrevCount}
|
||||
</Text>
|
||||
{renderCount({count: prevCount})}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -5,7 +5,6 @@ import Animated, {
|
||||
useReducedMotion,
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
import {s} from '#/lib/styles'
|
||||
import {useTheme} from '#/alf'
|
||||
import {
|
||||
Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled,
|
||||
@@ -86,7 +85,7 @@ export function AnimatedLikeIcon({
|
||||
{isLiked ? (
|
||||
<Animated.View
|
||||
entering={shouldAnimate ? keyframe.duration(300) : undefined}>
|
||||
<HeartIconFilled style={s.likeColor} width={size} />
|
||||
<HeartIconFilled style={{color: t.palette.pink}} width={size} />
|
||||
</Animated.View>
|
||||
) : (
|
||||
<HeartIconOutline
|
||||
@@ -100,7 +99,7 @@ export function AnimatedLikeIcon({
|
||||
entering={circle1Keyframe.duration(300)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: s.likeColor.color,
|
||||
backgroundColor: t.palette.pink,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: size,
|
||||
|
||||
@@ -2,7 +2,6 @@ import {useEffect, useRef} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useReducedMotion} from 'react-native-reanimated'
|
||||
|
||||
import {s} from '#/lib/styles'
|
||||
import {useTheme} from '#/alf'
|
||||
import {
|
||||
Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled,
|
||||
@@ -74,7 +73,7 @@ export function AnimatedLikeIcon({
|
||||
{isLiked ? (
|
||||
// @ts-expect-error is div
|
||||
<View ref={likeIconRef}>
|
||||
<HeartIconFilled style={s.likeColor} width={size} />
|
||||
<HeartIconFilled style={{color: t.palette.pink}} width={size} />
|
||||
</View>
|
||||
) : (
|
||||
<HeartIconOutline
|
||||
@@ -87,7 +86,7 @@ export function AnimatedLikeIcon({
|
||||
ref={circle1Ref}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: s.likeColor.color,
|
||||
backgroundColor: t.palette.pink,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: size,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
return children
|
||||
}
|
||||
|
||||
export function useHotkeysContext() {
|
||||
return {
|
||||
enableScope: () => {},
|
||||
disableScope: () => {},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import {
|
||||
HotkeysProvider,
|
||||
useHotkeys,
|
||||
useHotkeysContext,
|
||||
} from 'react-hotkeys-hook'
|
||||
|
||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||
import {emitFocusSearch} from '#/state/events'
|
||||
import {useSession} from '#/state/session'
|
||||
|
||||
enum Hotkeys {
|
||||
OPEN_COMPOSER = 'n',
|
||||
FOCUS_SEARCH = 'slash',
|
||||
}
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<unknown>) {
|
||||
return (
|
||||
<HotkeysProvider initiallyActiveScopes={['global']}>
|
||||
<KeyboardShortcuts>{children}</KeyboardShortcuts>
|
||||
</HotkeysProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export {useHotkeysContext}
|
||||
|
||||
function KeyboardShortcuts({children}: React.PropsWithChildren<unknown>) {
|
||||
useKeyboardShortcuts()
|
||||
return children
|
||||
}
|
||||
|
||||
function useKeyboardShortcuts() {
|
||||
const {openComposer} = useOpenComposer()
|
||||
const {hasSession} = useSession()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
const shouldIgnore = (requiresSession: boolean = false) => {
|
||||
if (requiresSession && !hasSession) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const handleKey = (
|
||||
callback: () => void,
|
||||
options?: {requiresSession?: boolean},
|
||||
) => {
|
||||
if (shouldIgnore(options?.requiresSession)) {
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
||||
useHotkeys(
|
||||
Hotkeys.OPEN_COMPOSER,
|
||||
() =>
|
||||
handleKey(
|
||||
() => {
|
||||
openComposer({logContext: 'Other'})
|
||||
},
|
||||
{
|
||||
requiresSession: true,
|
||||
},
|
||||
),
|
||||
{scopes: ['global'], description: l`Compose new post`},
|
||||
[openComposer],
|
||||
)
|
||||
|
||||
useHotkeys(Hotkeys.FOCUS_SEARCH, () => handleKey(emitFocusSearch), {
|
||||
scopes: ['global'],
|
||||
preventDefault: true,
|
||||
description: l`Focus the search field`,
|
||||
})
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
* returns a ref callback function that can be used to merge multiple refs into a single ref.
|
||||
*/
|
||||
export function mergeRefs<T = any>(
|
||||
refs: Array<React.MutableRefObject<T> | React.Ref<T>>,
|
||||
refs: Array<React.MutableRefObject<T> | React.Ref<T> | undefined>,
|
||||
): React.RefCallback<T> {
|
||||
return value => {
|
||||
refs.forEach(ref => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
import {createPersistedQueryStorage} from '#/lib/persisted-query-storage'
|
||||
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
|
||||
import {PERSISTED_QUERY_ROOT} from '#/state/queries'
|
||||
import {isQueryPersisted} from '#/state/queries/util'
|
||||
import * as env from '#/env'
|
||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||
|
||||
@@ -137,8 +137,7 @@ const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehyd
|
||||
{
|
||||
shouldDehydrateMutation: (_: any) => false,
|
||||
shouldDehydrateQuery: query => {
|
||||
const root = String(query.queryKey[0])
|
||||
return root === PERSISTED_QUERY_ROOT
|
||||
return isQueryPersisted(query.queryKey)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -190,7 +189,10 @@ function QueryProviderInner({
|
||||
})
|
||||
useEffect(() => {
|
||||
if (IS_WEB) {
|
||||
window.__TANSTACK_QUERY_CLIENT__ = queryClient
|
||||
// WARNING, BROKEN
|
||||
// something since v5.32.0 causes OOMs. not important
|
||||
// so disable for now
|
||||
// window.__TANSTACK_QUERY_CLIENT__ = queryClient
|
||||
}
|
||||
}, [queryClient])
|
||||
return (
|
||||
|
||||
+1
-124
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
Dimensions,
|
||||
type StyleProp,
|
||||
StyleSheet,
|
||||
type TextStyle,
|
||||
} from 'react-native'
|
||||
import {type StyleProp, StyleSheet, type TextStyle} from 'react-native'
|
||||
|
||||
import {IS_WEB} from '#/env'
|
||||
import {type Theme, type TypographyVariant} from './ThemeContext'
|
||||
@@ -61,14 +56,6 @@ export const colors = {
|
||||
green5: '#082b03',
|
||||
|
||||
unreadNotifBg: '#ebf6ff',
|
||||
brandBlue: '#0066FF',
|
||||
like: '#ec4899',
|
||||
}
|
||||
|
||||
export const gradients = {
|
||||
blueLight: {start: '#5A71FA', end: colors.blue3}, // buttons
|
||||
blue: {start: '#5E55FB', end: colors.blue3}, // fab
|
||||
blueDark: {start: '#5F45E0', end: colors.blue3}, // avis, banner
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,57 +65,6 @@ export const s = StyleSheet.create({
|
||||
// helpers
|
||||
footerSpacer: {height: 100},
|
||||
contentContainer: {paddingBottom: 200},
|
||||
contentContainerExtra: {paddingBottom: 300},
|
||||
border0: {borderWidth: 0},
|
||||
border1: {borderWidth: 1},
|
||||
borderTop1: {borderTopWidth: 1},
|
||||
borderRight1: {borderRightWidth: 1},
|
||||
borderBottom1: {borderBottomWidth: 1},
|
||||
borderLeft1: {borderLeftWidth: 1},
|
||||
hidden: {display: 'none'},
|
||||
dimmed: {opacity: 0.5},
|
||||
|
||||
// font weights
|
||||
fw600: {fontWeight: '600'},
|
||||
bold: {fontWeight: '600'},
|
||||
fw500: {fontWeight: '600'},
|
||||
semiBold: {fontWeight: '600'},
|
||||
fw400: {fontWeight: '400'},
|
||||
normal: {fontWeight: '400'},
|
||||
fw300: {fontWeight: '400'},
|
||||
light: {fontWeight: '400'},
|
||||
|
||||
// text decoration
|
||||
underline: {textDecorationLine: 'underline'},
|
||||
|
||||
// font variants
|
||||
tabularNum: {fontVariant: ['tabular-nums']},
|
||||
|
||||
// font sizes
|
||||
f9: {fontSize: 9},
|
||||
f10: {fontSize: 10},
|
||||
f11: {fontSize: 11},
|
||||
f12: {fontSize: 12},
|
||||
f13: {fontSize: 13},
|
||||
f14: {fontSize: 14},
|
||||
f15: {fontSize: 15},
|
||||
f16: {fontSize: 16},
|
||||
f17: {fontSize: 17},
|
||||
f18: {fontSize: 18},
|
||||
|
||||
// line heights
|
||||
['lh13-1']: {lineHeight: 13},
|
||||
['lh13-1.3']: {lineHeight: 16.9}, // 1.3 of 13px
|
||||
['lh14-1']: {lineHeight: 14},
|
||||
['lh14-1.3']: {lineHeight: 18.2}, // 1.3 of 14px
|
||||
['lh15-1']: {lineHeight: 15},
|
||||
['lh15-1.3']: {lineHeight: 19.5}, // 1.3 of 15px
|
||||
['lh16-1']: {lineHeight: 16},
|
||||
['lh16-1.3']: {lineHeight: 20.8}, // 1.3 of 16px
|
||||
['lh17-1']: {lineHeight: 17},
|
||||
['lh17-1.3']: {lineHeight: 22.1}, // 1.3 of 17px
|
||||
['lh18-1']: {lineHeight: 18},
|
||||
['lh18-1.3']: {lineHeight: 23.4}, // 1.3 of 18px
|
||||
|
||||
// margins
|
||||
mr2: {marginRight: 2},
|
||||
@@ -171,74 +107,15 @@ export const s = StyleSheet.create({
|
||||
pb20: {paddingBottom: 20},
|
||||
px5: {paddingHorizontal: 5},
|
||||
|
||||
// flex
|
||||
flexRow: {flexDirection: 'row'},
|
||||
flexCol: {flexDirection: 'column'},
|
||||
flex1: {flex: 1},
|
||||
flexGrow1: {flexGrow: 1},
|
||||
alignCenter: {alignItems: 'center'},
|
||||
alignBaseline: {alignItems: 'baseline'},
|
||||
justifyCenter: {justifyContent: 'center'},
|
||||
|
||||
// position
|
||||
absolute: {position: 'absolute'},
|
||||
|
||||
// dimensions
|
||||
w100pct: {width: '100%'},
|
||||
h100pct: {height: '100%'},
|
||||
hContentRegion: IS_WEB ? {minHeight: '100%'} : {height: '100%'},
|
||||
window: {
|
||||
width: Dimensions.get('window').width,
|
||||
height: Dimensions.get('window').height,
|
||||
},
|
||||
|
||||
// text align
|
||||
textLeft: {textAlign: 'left'},
|
||||
textCenter: {textAlign: 'center'},
|
||||
textRight: {textAlign: 'right'},
|
||||
|
||||
// colors
|
||||
white: {color: colors.white},
|
||||
black: {color: colors.black},
|
||||
|
||||
gray1: {color: colors.gray1},
|
||||
gray2: {color: colors.gray2},
|
||||
gray3: {color: colors.gray3},
|
||||
gray4: {color: colors.gray4},
|
||||
gray5: {color: colors.gray5},
|
||||
|
||||
blue1: {color: colors.blue1},
|
||||
blue2: {color: colors.blue2},
|
||||
blue3: {color: colors.blue3},
|
||||
blue4: {color: colors.blue4},
|
||||
blue5: {color: colors.blue5},
|
||||
|
||||
red1: {color: colors.red1},
|
||||
red2: {color: colors.red2},
|
||||
red3: {color: colors.red3},
|
||||
red4: {color: colors.red4},
|
||||
red5: {color: colors.red5},
|
||||
|
||||
pink1: {color: colors.pink1},
|
||||
pink2: {color: colors.pink2},
|
||||
pink3: {color: colors.pink3},
|
||||
pink4: {color: colors.pink4},
|
||||
pink5: {color: colors.pink5},
|
||||
|
||||
purple1: {color: colors.purple1},
|
||||
purple2: {color: colors.purple2},
|
||||
purple3: {color: colors.purple3},
|
||||
purple4: {color: colors.purple4},
|
||||
purple5: {color: colors.purple5},
|
||||
|
||||
green1: {color: colors.green1},
|
||||
green2: {color: colors.green2},
|
||||
green3: {color: colors.green3},
|
||||
green4: {color: colors.green4},
|
||||
green5: {color: colors.green5},
|
||||
|
||||
brandBlue: {color: colors.brandBlue},
|
||||
likeColor: {color: colors.like},
|
||||
})
|
||||
|
||||
export function lh(
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import Emojis, {type EmojiMartData} from '@emoji-mart/data'
|
||||
|
||||
export async function getEmojis(): Promise<EmojiMartData> {
|
||||
return Emojis as EmojiMartData
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import {type EmojiMartData} from '@emoji-mart/data'
|
||||
|
||||
export async function getEmojis(): Promise<EmojiMartData> {
|
||||
return (await import('@emoji-mart/data')).default as EmojiMartData
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {useCallback} from 'react'
|
||||
|
||||
import {getEmojis} from './getEmojis'
|
||||
|
||||
let emojis: Awaited<ReturnType<typeof getEmojis>> | null = null
|
||||
|
||||
export function useGetEmojis() {
|
||||
return useCallback(async () => {
|
||||
emojis ??= await getEmojis()
|
||||
return emojis
|
||||
}, [])
|
||||
}
|
||||
+230
-147
File diff suppressed because it is too large
Load Diff
+257
-175
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
+257
-175
File diff suppressed because it is too large
Load Diff
+257
-175
File diff suppressed because it is too large
Load Diff
+234
-151
File diff suppressed because it is too large
Load Diff
+232
-149
File diff suppressed because it is too large
Load Diff
+238
-155
File diff suppressed because it is too large
Load Diff
+234
-151
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+102
-100
@@ -910,8 +910,8 @@ msgstr ""
|
||||
msgid "Add media to post"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:522
|
||||
#: src/components/moderation/ReportDialog/index.tsx:526
|
||||
#: src/components/moderation/ReportDialog/index.tsx:532
|
||||
#: src/components/moderation/ReportDialog/index.tsx:536
|
||||
msgid "Add more details (optional)"
|
||||
msgstr ""
|
||||
|
||||
@@ -966,7 +966,7 @@ msgstr ""
|
||||
msgid "Add to lists"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PostControls/BookmarkButton.tsx:142
|
||||
#: src/components/PostControls/BookmarkButton.tsx:144
|
||||
msgid "Add to saved posts"
|
||||
msgstr ""
|
||||
|
||||
@@ -1002,7 +1002,7 @@ msgstr ""
|
||||
msgid "Additional details (limit 1000 characters)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:540
|
||||
#: src/components/moderation/ReportDialog/index.tsx:550
|
||||
msgid "Additional details (limit 300 characters)"
|
||||
msgstr ""
|
||||
|
||||
@@ -1832,20 +1832,20 @@ msgstr ""
|
||||
msgid "Browse custom feeds"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FeedInterstitials.tsx:627
|
||||
#: src/components/FeedInterstitials.tsx:534
|
||||
msgid "Browse more accounts"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FeedInterstitials.tsx:757
|
||||
#: src/components/FeedInterstitials.tsx:664
|
||||
msgid "Browse more feeds on the Explore page"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FeedInterstitials.tsx:738
|
||||
#: src/components/FeedInterstitials.tsx:741
|
||||
#: src/components/FeedInterstitials.tsx:645
|
||||
#: src/components/FeedInterstitials.tsx:648
|
||||
msgid "Browse more suggestions"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FeedInterstitials.tsx:766
|
||||
#: src/components/FeedInterstitials.tsx:673
|
||||
msgid "Browse more suggestions on the Explore page"
|
||||
msgstr ""
|
||||
|
||||
@@ -1887,9 +1887,9 @@ msgstr ""
|
||||
#. placeholder {0}: sanitizeHandle(item.feed.creator.handle, '@')
|
||||
#. placeholder {0}: sanitizeHandle(labeler.creator.handle, '@')
|
||||
#: src/components/LabelingServiceCard/index.tsx:62
|
||||
#: src/components/moderation/ReportDialog/index.tsx:843
|
||||
#: src/components/moderation/ReportDialog/index.tsx:853
|
||||
#: src/screens/Search/components/StarterPackCard.tsx:107
|
||||
#: src/screens/Search/Explore.tsx:969
|
||||
#: src/screens/Search/Explore.tsx:970
|
||||
msgid "By {0}"
|
||||
msgstr ""
|
||||
|
||||
@@ -1953,7 +1953,7 @@ msgstr ""
|
||||
#: src/screens/Deactivated.tsx:150
|
||||
#: src/screens/Profile/Header/EditProfileDialog.tsx:215
|
||||
#: src/screens/Profile/Header/EditProfileDialog.tsx:223
|
||||
#: src/screens/Search/Shell.tsx:396
|
||||
#: src/screens/Search/Shell.tsx:397
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:42
|
||||
#: src/screens/Settings/AppIconSettings/index.tsx:228
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:80
|
||||
@@ -1979,13 +1979,13 @@ msgstr ""
|
||||
msgid "Cancel reactivation and sign out"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Shell.tsx:387
|
||||
#: src/screens/Search/Shell.tsx:388
|
||||
msgid "Cancel search"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PostControls/index.tsx:109
|
||||
#: src/components/PostControls/index.tsx:140
|
||||
#: src/components/PostControls/index.tsx:168
|
||||
#: src/components/PostControls/index.tsx:110
|
||||
#: src/components/PostControls/index.tsx:141
|
||||
#: src/components/PostControls/index.tsx:169
|
||||
#: src/state/shell/composer/index.tsx:107
|
||||
msgid "Cannot interact with a blocked user"
|
||||
msgstr ""
|
||||
@@ -2028,7 +2028,7 @@ msgstr ""
|
||||
msgid "Change Handle"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:439
|
||||
#: src/components/moderation/ReportDialog/index.tsx:449
|
||||
msgid "Change moderation service"
|
||||
msgstr ""
|
||||
|
||||
@@ -2041,11 +2041,11 @@ msgstr ""
|
||||
msgid "Change password dialog"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:304
|
||||
#: src/components/moderation/ReportDialog/index.tsx:314
|
||||
msgid "Change report category"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:384
|
||||
#: src/components/moderation/ReportDialog/index.tsx:394
|
||||
msgid "Change report reason"
|
||||
msgstr ""
|
||||
|
||||
@@ -2218,7 +2218,7 @@ msgstr ""
|
||||
msgid "Clear image cache"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/forms/SearchInput.tsx:69
|
||||
#: src/components/forms/SearchInput.tsx:87
|
||||
msgid "Clear search query"
|
||||
msgstr ""
|
||||
|
||||
@@ -2338,7 +2338,7 @@ msgstr ""
|
||||
msgid "Close dialog"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/shell/index.web.tsx:131
|
||||
#: src/view/shell/index.web.tsx:129
|
||||
msgid "Close drawer menu"
|
||||
msgstr ""
|
||||
|
||||
@@ -2430,6 +2430,7 @@ msgstr ""
|
||||
msgid "Complete the challenge"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/hotkeys/index.tsx:66
|
||||
#: src/view/com/feeds/ComposerPrompt.tsx:147
|
||||
#: src/view/shell/desktop/LeftNav.tsx:575
|
||||
msgid "Compose new post"
|
||||
@@ -2918,8 +2919,8 @@ msgstr ""
|
||||
|
||||
#. Accessibility label for button to create a moderation report for the selected option
|
||||
#. placeholder {0}: option.title
|
||||
#: src/components/moderation/ReportDialog/index.tsx:703
|
||||
#: src/components/moderation/ReportDialog/index.tsx:749
|
||||
#: src/components/moderation/ReportDialog/index.tsx:713
|
||||
#: src/components/moderation/ReportDialog/index.tsx:759
|
||||
msgid "Create report for {0}"
|
||||
msgstr ""
|
||||
|
||||
@@ -3256,7 +3257,7 @@ msgstr ""
|
||||
msgid "Discover new custom feeds"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Explore.tsx:451
|
||||
#: src/screens/Search/Explore.tsx:452
|
||||
msgid "Discover new feeds"
|
||||
msgstr ""
|
||||
|
||||
@@ -3293,7 +3294,7 @@ msgstr ""
|
||||
msgid "Dismiss this section"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FeedInterstitials.tsx:441
|
||||
#: src/components/FeedInterstitials.tsx:348
|
||||
msgid "Dismiss this suggestion"
|
||||
msgstr ""
|
||||
|
||||
@@ -4018,16 +4019,16 @@ msgstr ""
|
||||
msgid "Failed to load conversations"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Explore.tsx:528
|
||||
#: src/screens/Search/Explore.tsx:573
|
||||
#: src/screens/Search/Explore.tsx:619
|
||||
#: src/screens/Search/Explore.tsx:529
|
||||
#: src/screens/Search/Explore.tsx:574
|
||||
#: src/screens/Search/Explore.tsx:620
|
||||
msgid "Failed to load feeds"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Explore.tsx:487
|
||||
#: src/screens/Search/Explore.tsx:542
|
||||
#: src/screens/Search/Explore.tsx:587
|
||||
#: src/screens/Search/Explore.tsx:633
|
||||
#: src/screens/Search/Explore.tsx:488
|
||||
#: src/screens/Search/Explore.tsx:543
|
||||
#: src/screens/Search/Explore.tsx:588
|
||||
#: src/screens/Search/Explore.tsx:634
|
||||
msgid "Failed to load feeds preferences"
|
||||
msgstr ""
|
||||
|
||||
@@ -4057,14 +4058,14 @@ msgstr ""
|
||||
msgid "Failed to load preference."
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Explore.tsx:480
|
||||
#: src/screens/Search/Explore.tsx:535
|
||||
#: src/screens/Search/Explore.tsx:580
|
||||
#: src/screens/Search/Explore.tsx:626
|
||||
#: src/screens/Search/Explore.tsx:481
|
||||
#: src/screens/Search/Explore.tsx:536
|
||||
#: src/screens/Search/Explore.tsx:581
|
||||
#: src/screens/Search/Explore.tsx:627
|
||||
msgid "Failed to load suggested feeds"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Explore.tsx:390
|
||||
#: src/screens/Search/Explore.tsx:391
|
||||
msgid "Failed to load suggested follows"
|
||||
msgstr ""
|
||||
|
||||
@@ -4126,7 +4127,7 @@ msgstr ""
|
||||
msgid "Failed to save settings. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/InterestsSettings.tsx:137
|
||||
#: src/screens/Settings/InterestsSettings.tsx:147
|
||||
msgctxt "toast"
|
||||
msgid "Failed to save your interests."
|
||||
msgstr ""
|
||||
@@ -4206,7 +4207,7 @@ msgstr ""
|
||||
#. placeholder {0}: sanitizeHandle(feed.creatorHandle, '@')
|
||||
#. placeholder {0}: sanitizeHandle(view.creator.handle, '@')
|
||||
#: src/components/FeedCard.tsx:170
|
||||
#: src/state/queries/feed.ts:121
|
||||
#: src/state/queries/feed.ts:118
|
||||
#: src/view/com/feeds/FeedSourceCard.tsx:151
|
||||
msgid "Feed by {0}"
|
||||
msgstr ""
|
||||
@@ -4349,7 +4350,7 @@ msgstr ""
|
||||
msgid "Find people to follow"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Shell.tsx:529
|
||||
#: src/screens/Search/Shell.tsx:530
|
||||
msgid "Find posts, users, and feeds on Bluesky"
|
||||
msgstr ""
|
||||
|
||||
@@ -4393,6 +4394,10 @@ msgstr ""
|
||||
msgid "Focus code input"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/hotkeys/index.tsx:73
|
||||
msgid "Focus the search field"
|
||||
msgstr "Focus the search field"
|
||||
|
||||
#. User is not following this account, click to follow
|
||||
#: src/components/ProfileCard.tsx:546
|
||||
#: src/components/ProfileHoverCard/index.web.tsx:495
|
||||
@@ -5706,7 +5711,7 @@ msgstr ""
|
||||
|
||||
#. Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form
|
||||
#. placeholder {0}: post.likeCount || 0
|
||||
#: src/components/PostControls/index.tsx:284
|
||||
#: src/components/PostControls/index.tsx:287
|
||||
msgid "Like ({0, plural, one {# like} other {# likes}})"
|
||||
msgstr ""
|
||||
|
||||
@@ -5925,12 +5930,12 @@ msgstr ""
|
||||
msgid "Live link"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Explore.tsx:88
|
||||
#: src/screens/Search/Explore.tsx:90
|
||||
msgid "Load more"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Explore.tsx:517
|
||||
#: src/screens/Search/Explore.tsx:608
|
||||
#: src/screens/Search/Explore.tsx:518
|
||||
#: src/screens/Search/Explore.tsx:609
|
||||
msgid "Load more suggested feeds"
|
||||
msgstr ""
|
||||
|
||||
@@ -5938,7 +5943,7 @@ msgstr ""
|
||||
msgid "Load new notifications"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Profile/ProfileFeed/index.tsx:203
|
||||
#: src/screens/Profile/ProfileFeed/index.tsx:204
|
||||
#: src/screens/Profile/Sections/Feed.tsx:117
|
||||
#: src/screens/ProfileList/FeedSection.tsx:113
|
||||
#: src/view/com/feeds/FeedPage.tsx:170
|
||||
@@ -6107,10 +6112,12 @@ msgstr ""
|
||||
msgid "Message from server: {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:166
|
||||
#: src/screens/Messages/components/MessageInput.tsx:154
|
||||
msgid "Message input field"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:69
|
||||
#: src/screens/Messages/components/MessageInput.tsx:79
|
||||
#: src/screens/Messages/components/MessageInput.web.tsx:60
|
||||
msgid "Message is too long"
|
||||
@@ -6388,8 +6395,8 @@ msgstr ""
|
||||
msgid "Navigates to your profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:335
|
||||
#: src/components/moderation/ReportDialog/index.tsx:352
|
||||
#: src/components/moderation/ReportDialog/index.tsx:345
|
||||
#: src/components/moderation/ReportDialog/index.tsx:362
|
||||
msgid "Need to report a copyright violation, legal request, or regulatory compliance issue?"
|
||||
msgstr ""
|
||||
|
||||
@@ -6475,7 +6482,7 @@ msgstr ""
|
||||
msgid "New password"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Profile/ProfileFeed/index.tsx:220
|
||||
#: src/screens/Profile/ProfileFeed/index.tsx:221
|
||||
#: src/screens/ProfileList/index.tsx:251
|
||||
#: src/screens/ProfileList/index.tsx:300
|
||||
#: src/view/screens/Feeds.tsx:553
|
||||
@@ -6485,13 +6492,9 @@ msgid "New post"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/feeds/FeedPage.tsx:181
|
||||
msgctxt "action"
|
||||
msgid "New post"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/shell/desktop/LeftNav.tsx:583
|
||||
msgctxt "action"
|
||||
msgid "New Post"
|
||||
msgid "New post"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:545
|
||||
@@ -6683,7 +6686,7 @@ msgid "No results"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: interestsDisplayNames[selectedInterest]
|
||||
#: src/screens/Search/Explore.tsx:826
|
||||
#: src/screens/Search/Explore.tsx:827
|
||||
msgid "No results for \"{0}\"."
|
||||
msgstr ""
|
||||
|
||||
@@ -6700,7 +6703,7 @@ msgstr ""
|
||||
msgid "No results found for “<0>{query}</0>”."
|
||||
msgstr "No results found for “<0>{query}</0>”."
|
||||
|
||||
#: src/screens/Search/Explore.tsx:830
|
||||
#: src/screens/Search/Explore.tsx:831
|
||||
msgid "No results."
|
||||
msgstr ""
|
||||
|
||||
@@ -6979,6 +6982,7 @@ msgstr ""
|
||||
msgid "Open drawer menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:143
|
||||
#: src/screens/Messages/components/MessageInput.web.tsx:180
|
||||
#: src/view/com/composer/Composer.tsx:1979
|
||||
msgid "Open emoji picker"
|
||||
@@ -7594,7 +7598,7 @@ msgctxt "action"
|
||||
msgid "Post"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/PostThread/index.tsx:552
|
||||
#: src/screens/PostThread/index.tsx:553
|
||||
msgctxt "description"
|
||||
msgid "Post"
|
||||
msgstr ""
|
||||
@@ -8121,7 +8125,7 @@ msgstr ""
|
||||
msgid "Remove from saved feeds"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PostControls/BookmarkButton.tsx:141
|
||||
#: src/components/PostControls/BookmarkButton.tsx:143
|
||||
#: src/screens/Bookmarks/index.tsx:262
|
||||
msgid "Remove from saved posts"
|
||||
msgstr ""
|
||||
@@ -8282,7 +8286,7 @@ msgstr ""
|
||||
|
||||
#. Accessibility label for the reply button, verb form followed by number of replies and noun form
|
||||
#. placeholder {0}: post.replyCount || 0
|
||||
#: src/components/PostControls/index.tsx:242
|
||||
#: src/components/PostControls/index.tsx:243
|
||||
msgid "Reply ({0, plural, one {# reply} other {# replies}})"
|
||||
msgstr ""
|
||||
|
||||
@@ -8338,7 +8342,7 @@ msgid "Report conversation"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:98
|
||||
#: src/components/moderation/ReportDialog/index.tsx:255
|
||||
#: src/components/moderation/ReportDialog/index.tsx:265
|
||||
msgid "Report dialog"
|
||||
msgstr ""
|
||||
|
||||
@@ -8553,7 +8557,7 @@ msgstr ""
|
||||
#: src/components/dms/MessageItem.tsx:322
|
||||
#: src/components/Error.tsx:66
|
||||
#: src/components/Lists.tsx:115
|
||||
#: src/components/moderation/ReportDialog/index.tsx:289
|
||||
#: src/components/moderation/ReportDialog/index.tsx:299
|
||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:56
|
||||
#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:59
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:377
|
||||
@@ -8575,7 +8579,7 @@ msgstr ""
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:286
|
||||
#: src/components/moderation/ReportDialog/index.tsx:296
|
||||
#: src/view/screens/Storybook/Admonitions.tsx:61
|
||||
msgid "Retry loading report options"
|
||||
msgstr ""
|
||||
@@ -8731,10 +8735,10 @@ msgid "Scroll to top"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dialogs/SearchablePeopleList.tsx:515
|
||||
#: src/components/forms/SearchInput.tsx:33
|
||||
#: src/components/forms/SearchInput.tsx:35
|
||||
#: src/components/forms/SearchInput.tsx:51
|
||||
#: src/components/forms/SearchInput.tsx:53
|
||||
#: src/screens/Search/Shell.tsx:354
|
||||
#: src/screens/Search/Shell.tsx:517
|
||||
#: src/screens/Search/Shell.tsx:518
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:199
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
@@ -8768,23 +8772,19 @@ msgstr ""
|
||||
msgid "Search for \"{interestsDisplayName}\" (active)"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/components/AutocompleteResults.tsx:47
|
||||
#: src/screens/Search/components/AutocompleteResults.tsx:54
|
||||
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 ""
|
||||
|
||||
#: src/screens/Search/Explore.tsx:377
|
||||
#: src/screens/Search/Explore.tsx:378
|
||||
msgid "Search for more accounts"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Explore.tsx:454
|
||||
#: src/screens/Search/Explore.tsx:455
|
||||
msgid "Search for more feeds"
|
||||
msgstr ""
|
||||
|
||||
@@ -8873,12 +8873,12 @@ msgstr ""
|
||||
msgid "See jobs at Bluesky"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FeedInterstitials.tsx:583
|
||||
#: src/components/FeedInterstitials.tsx:641
|
||||
#: src/components/FeedInterstitials.tsx:490
|
||||
#: src/components/FeedInterstitials.tsx:548
|
||||
msgid "See more"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FeedInterstitials.tsx:564
|
||||
#: src/components/FeedInterstitials.tsx:471
|
||||
msgid "See more suggested profiles"
|
||||
msgstr ""
|
||||
|
||||
@@ -8915,7 +8915,7 @@ msgstr ""
|
||||
msgid "Select a color"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:374
|
||||
#: src/components/moderation/ReportDialog/index.tsx:384
|
||||
msgid "Select a reason"
|
||||
msgstr ""
|
||||
|
||||
@@ -9000,7 +9000,7 @@ msgstr ""
|
||||
msgid "Select languages"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:424
|
||||
#: src/components/moderation/ReportDialog/index.tsx:434
|
||||
msgid "Select moderation service"
|
||||
msgstr ""
|
||||
|
||||
@@ -9046,7 +9046,7 @@ msgid "Select your date of birth"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Onboarding/StepInterests/index.tsx:69
|
||||
#: src/screens/Settings/InterestsSettings.tsx:168
|
||||
#: src/screens/Settings/InterestsSettings.tsx:178
|
||||
msgid "Select your interests from the options below"
|
||||
msgstr ""
|
||||
|
||||
@@ -9086,6 +9086,7 @@ msgstr ""
|
||||
msgid "Send feedback"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageComposer.tsx:216
|
||||
#: src/screens/Messages/components/MessageInput.tsx:194
|
||||
#: src/screens/Messages/components/MessageInput.web.tsx:234
|
||||
msgid "Send message"
|
||||
@@ -9099,7 +9100,7 @@ msgstr ""
|
||||
msgid "Send post to..."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:814
|
||||
#: src/components/moderation/ReportDialog/index.tsx:824
|
||||
msgid "Send report to {title}"
|
||||
msgstr ""
|
||||
|
||||
@@ -9574,7 +9575,7 @@ msgstr ""
|
||||
msgid "Some of your verifications are invalid."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/FeedInterstitials.tsx:720
|
||||
#: src/components/FeedInterstitials.tsx:627
|
||||
msgid "Some other feeds you might like"
|
||||
msgstr ""
|
||||
|
||||
@@ -9602,7 +9603,7 @@ msgid "Something went wrong"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:139
|
||||
#: src/components/moderation/ReportDialog/index.tsx:281
|
||||
#: src/components/moderation/ReportDialog/index.tsx:291
|
||||
#: src/screens/Deactivated.tsx:86
|
||||
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:59
|
||||
#: src/view/screens/Storybook/Admonitions.tsx:56
|
||||
@@ -9634,7 +9635,7 @@ msgstr ""
|
||||
msgid "Sorry, we're unable to load account suggestions at this time."
|
||||
msgstr ""
|
||||
|
||||
#: src/App.native.tsx:142
|
||||
#: src/App.native.tsx:140
|
||||
#: src/App.web.tsx:119
|
||||
msgid "Sorry! Your session expired. Please sign in again."
|
||||
msgstr ""
|
||||
@@ -9720,7 +9721,7 @@ msgstr ""
|
||||
msgid "Starter pack is invalid"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Explore.tsx:663
|
||||
#: src/screens/Search/Explore.tsx:664
|
||||
#: src/view/screens/Profile.tsx:241
|
||||
msgid "Starter Packs"
|
||||
msgstr ""
|
||||
@@ -9787,9 +9788,9 @@ msgstr ""
|
||||
msgid "Submit Appeal"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:502
|
||||
#: src/components/moderation/ReportDialog/index.tsx:563
|
||||
#: src/components/moderation/ReportDialog/index.tsx:570
|
||||
#: src/components/moderation/ReportDialog/index.tsx:512
|
||||
#: src/components/moderation/ReportDialog/index.tsx:573
|
||||
#: src/components/moderation/ReportDialog/index.tsx:580
|
||||
msgid "Submit report"
|
||||
msgstr ""
|
||||
|
||||
@@ -9836,12 +9837,12 @@ msgstr ""
|
||||
msgid "Suggested"
|
||||
msgstr "Suggested"
|
||||
|
||||
#: src/screens/Search/Explore.tsx:374
|
||||
#: src/screens/Search/Explore.tsx:375
|
||||
msgid "Suggested accounts"
|
||||
msgstr ""
|
||||
|
||||
#. Accounts suggested to the user for them to follow
|
||||
#: src/components/FeedInterstitials.tsx:561
|
||||
#: src/components/FeedInterstitials.tsx:468
|
||||
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:180
|
||||
msgid "Suggested for you"
|
||||
msgstr ""
|
||||
@@ -10183,8 +10184,8 @@ msgstr ""
|
||||
msgid "There was an issue fetching notifications. Tap here to try again."
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Search/Explore.tsx:1025
|
||||
#: src/view/com/posts/PostFeed.tsx:763
|
||||
#: src/screens/Search/Explore.tsx:1026
|
||||
#: src/view/com/posts/PostFeed.tsx:773
|
||||
msgid "There was an issue fetching posts. Tap here to try again."
|
||||
msgstr ""
|
||||
|
||||
@@ -10856,7 +10857,7 @@ msgstr ""
|
||||
msgid "Unfollows the user"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/moderation/ReportDialog/index.tsx:486
|
||||
#: src/components/moderation/ReportDialog/index.tsx:496
|
||||
msgid "Unfortunately, none of your subscribed labelers supports this report type."
|
||||
msgstr ""
|
||||
|
||||
@@ -10886,7 +10887,7 @@ msgstr ""
|
||||
|
||||
#. Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun
|
||||
#. placeholder {0}: post.likeCount || 0
|
||||
#: src/components/PostControls/index.tsx:276
|
||||
#: src/components/PostControls/index.tsx:279
|
||||
msgid "Unlike ({0, plural, one {# like} other {# likes}})"
|
||||
msgstr ""
|
||||
|
||||
@@ -11153,7 +11154,7 @@ msgid "User list by {0}"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: sanitizeHandle(view.creator.handle, '@')
|
||||
#: src/state/queries/feed.ts:162
|
||||
#: src/state/queries/feed.ts:159
|
||||
msgid "User List by {0}"
|
||||
msgstr ""
|
||||
|
||||
@@ -11485,7 +11486,7 @@ msgid "View your default post interaction settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/home/HomeHeaderLayout.web.tsx:57
|
||||
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:75
|
||||
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:82
|
||||
msgid "View your feeds and explore more"
|
||||
msgstr ""
|
||||
|
||||
@@ -11555,8 +11556,8 @@ msgid "We apply the highest privacy standards, and never share or sell your cont
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/feeds/MissingFeed.tsx:141
|
||||
msgid "We could not connect to the service that provides this custom feed. It may be temporarily unavailable and experiencing issues, or permanently unavailable."
|
||||
msgstr ""
|
||||
msgid "We could not connect to the service that provides this custom feed. It may be temporarily experiencing issues, or permanently unavailable."
|
||||
msgstr "We could not connect to the service that provides this custom feed. It may be temporarily experiencing issues, or permanently unavailable."
|
||||
|
||||
#: src/view/com/feeds/MissingFeed.tsx:147
|
||||
msgid "We could not find this list. It was probably deleted."
|
||||
@@ -11611,7 +11612,7 @@ msgstr ""
|
||||
msgid "We ran out of posts from your follows. Here's the latest from <0/>."
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/InterestsSettings.tsx:161
|
||||
#: src/screens/Settings/InterestsSettings.tsx:171
|
||||
msgid "We recommend selecting at least two interests."
|
||||
msgstr ""
|
||||
|
||||
@@ -11846,6 +11847,7 @@ 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"
|
||||
@@ -12476,11 +12478,11 @@ msgstr ""
|
||||
#: src/screens/Search/modules/ExploreInterestsCard.tsx:68
|
||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:94
|
||||
#: src/screens/Settings/ContentAndMediaSettings.tsx:97
|
||||
#: src/screens/Settings/InterestsSettings.tsx:47
|
||||
#: src/screens/Settings/InterestsSettings.tsx:49
|
||||
msgid "Your interests"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/InterestsSettings.tsx:128
|
||||
#: src/screens/Settings/InterestsSettings.tsx:138
|
||||
msgctxt "toast"
|
||||
msgid "Your interests have been updated!"
|
||||
msgstr ""
|
||||
@@ -12539,11 +12541,11 @@ msgid "Your reply was sent"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: state.selectedLabeler?.creator.displayName
|
||||
#: src/components/moderation/ReportDialog/index.tsx:513
|
||||
#: src/components/moderation/ReportDialog/index.tsx:523
|
||||
msgid "Your report will be sent to <0>{0}</0>."
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/InterestsSettings.tsx:61
|
||||
#: src/screens/Settings/InterestsSettings.tsx:63
|
||||
msgid "Your selected interests help us serve you content you care about."
|
||||
msgstr ""
|
||||
|
||||
|
||||
+234
-151
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
+231
-148
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
+231
-148
File diff suppressed because it is too large
Load Diff
+231
-148
File diff suppressed because it is too large
Load Diff
+263
-180
File diff suppressed because it is too large
Load Diff
+231
-148
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
+237
-154
File diff suppressed because it is too large
Load Diff
+231
-148
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
+231
-148
File diff suppressed because it is too large
Load Diff
+231
-148
File diff suppressed because it is too large
Load Diff
+257
-175
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
+235
-152
File diff suppressed because it is too large
Load Diff
+257
-175
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+232
-149
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
+245
-162
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
+231
-148
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
+230
-147
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,251 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import {countGraphemes} from 'unicode-segmenter/grapheme'
|
||||
|
||||
import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {isBskyPostUrl} from '#/lib/strings/url-helpers'
|
||||
import {useEmail} from '#/state/email-verification'
|
||||
import {
|
||||
useMessageDraft,
|
||||
useSaveMessageDraft,
|
||||
} from '#/state/messages/message-drafts'
|
||||
import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
|
||||
import {
|
||||
type Emoji,
|
||||
EmojiPicker,
|
||||
type EmojiPickerState,
|
||||
} from '#/view/com/composer/text-input/web/EmojiPicker'
|
||||
import {atoms as a, useTheme} 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 * as Toast from '#/components/Toast'
|
||||
import {IS_WEB} from '#/env'
|
||||
|
||||
export function MessageComposer({
|
||||
onSendMessage,
|
||||
hasEmbed,
|
||||
setEmbed,
|
||||
children,
|
||||
}: {
|
||||
onSendMessage: (message: string) => void
|
||||
hasEmbed: boolean
|
||||
setEmbed: (embedUrl: string | undefined) => void
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const playHaptic = useHaptics()
|
||||
const {needsEmailVerification} = useEmail()
|
||||
const editable = !needsEmailVerification
|
||||
const {getDraft, clearDraft} = useMessageDraft()
|
||||
const [emojiPickerState, setEmojiPickerState] = useState<EmojiPickerState>({
|
||||
isOpen: false,
|
||||
pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null},
|
||||
})
|
||||
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)
|
||||
|
||||
const openEmojiPicker = (pos: any) => {
|
||||
setEmojiPickerState({isOpen: true, pos})
|
||||
}
|
||||
|
||||
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',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
clearDraft()
|
||||
onSendMessage(text)
|
||||
playHaptic()
|
||||
setEmbed(undefined)
|
||||
composerInternalApiRef.current?.clear()
|
||||
|
||||
if (IS_WEB) {
|
||||
composerInternalApiRef.current?.input?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function onEmojiInserted(emoji: Emoji) {
|
||||
composerInternalApiRef.current?.insert(emoji.native)
|
||||
}
|
||||
textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted)
|
||||
return () => {
|
||||
textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={[a.px_md, a.pb_sm, a.pt_xs]}>
|
||||
{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,
|
||||
},
|
||||
})
|
||||
})
|
||||
}}
|
||||
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>
|
||||
</View>
|
||||
|
||||
{IS_WEB && (
|
||||
<EmojiPicker
|
||||
pinToTop
|
||||
state={emojiPickerState}
|
||||
close={() => setEmojiPickerState(prev => ({...prev, isOpen: false}))}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
} from '#/view/com/composer/text-input/web/EmojiPicker'
|
||||
import {List, type ListMethods} from '#/view/com/util/List'
|
||||
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 {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
|
||||
@@ -50,8 +51,8 @@ import {MessageItem} from '#/components/dms/MessageItem'
|
||||
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import {IS_WEB} from '#/env'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||
import {ChatStatusInfo} from './ChatStatusInfo'
|
||||
import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
|
||||
|
||||
@@ -102,6 +103,7 @@ export function MessagesList({
|
||||
footer?: React.ReactNode
|
||||
hasAcceptOverride?: boolean
|
||||
}) {
|
||||
const ax = useAnalytics()
|
||||
const convoState = useConvoActive()
|
||||
const agent = useAgent()
|
||||
const getPost = useGetPost()
|
||||
@@ -457,13 +459,22 @@ export function MessagesList({
|
||||
<ConversationFooter
|
||||
convoState={convoState}
|
||||
hasAcceptOverride={hasAcceptOverride}>
|
||||
<MessageInput
|
||||
onSendMessage={onSendMessage}
|
||||
hasEmbed={!!embedUri}
|
||||
setEmbed={setEmbed}
|
||||
openEmojiPicker={onOpenEmojiPicker}>
|
||||
<MessageInputEmbed embedUri={embedUri} setEmbed={setEmbed} />
|
||||
</MessageInput>
|
||||
{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>
|
||||
)}
|
||||
</ConversationFooter>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
@@ -52,8 +52,9 @@ import {atoms as a, native, platform, useBreakpoints, web} from '#/alf'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {ListFooter} from '#/components/Lists'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
|
||||
const PARENT_CHUNK_SIZE = 20
|
||||
const PARENT_CHUNK_SIZE = IS_NATIVE ? 5 : 20
|
||||
const CHILDREN_CHUNK_SIZE = 50
|
||||
|
||||
export function PostThread({uri}: {uri: string}) {
|
||||
|
||||
@@ -28,7 +28,10 @@ import {
|
||||
createGetSuggestedFeedsQueryKey,
|
||||
useGetSuggestedFeedsQuery,
|
||||
} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
|
||||
import {getSuggestedUsersQueryKeyRoot} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||
import {
|
||||
getSuggestedUsersForExploreQueryKeyRoot,
|
||||
useGetSuggestedUsersForExploreQuery,
|
||||
} from '#/state/queries/trending/useGetSuggestedUsersForExploreQuery'
|
||||
import {createGetTrendsQueryKey} from '#/state/queries/trending/useGetTrendsQuery'
|
||||
import {
|
||||
createSuggestedStarterPacksQueryKey,
|
||||
@@ -48,7 +51,6 @@ import {ExploreInterestsCard} from '#/screens/Search/modules/ExploreInterestsCar
|
||||
import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations'
|
||||
import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
|
||||
import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
|
||||
import {useSuggestedUsers} from '#/screens/Search/util/useSuggestedUsers'
|
||||
import {atoms as a, native, platform, useTheme} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button} from '#/components/Button'
|
||||
@@ -242,9 +244,8 @@ export function Explore({
|
||||
isLoading: suggestedUsersIsLoading,
|
||||
error: suggestedUsersError,
|
||||
isRefetching: suggestedUsersIsRefetching,
|
||||
} = useSuggestedUsers({
|
||||
} = useGetSuggestedUsersForExploreQuery({
|
||||
category: selectedInterest || (useFullExperience ? null : interests[0]),
|
||||
search: !useFullExperience,
|
||||
})
|
||||
/* End special language handling */
|
||||
|
||||
@@ -316,7 +317,7 @@ export function Explore({
|
||||
queryKey: createSuggestedStarterPacksQueryKey(),
|
||||
}),
|
||||
qc.resetQueries({
|
||||
queryKey: [getSuggestedUsersQueryKeyRoot],
|
||||
queryKey: [getSuggestedUsersForExploreQueryKeyRoot],
|
||||
}),
|
||||
qc.resetQueries({
|
||||
queryKey: [useActorSearchQueryKeyRoot],
|
||||
|
||||
@@ -380,6 +380,7 @@ export function SearchScreenShell({
|
||||
inputPlaceholder ?? l`Search for posts, users, or feeds`
|
||||
}
|
||||
hitSlop={{...HITSLOP_20, top: 0}}
|
||||
hotkey={true}
|
||||
/>
|
||||
</View>
|
||||
{showAutocomplete && (
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import {memo} from 'react'
|
||||
import {ActivityIndicator, View} from 'react-native'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {SearchLinkCard} from '#/view/shell/desktop/Search'
|
||||
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, native} from '#/alf'
|
||||
import * as Layout from '#/components/Layout'
|
||||
@@ -76,3 +83,52 @@ let AutocompleteResults = ({
|
||||
}
|
||||
AutocompleteResults = memo(AutocompleteResults)
|
||||
export {AutocompleteResults}
|
||||
|
||||
let SearchLinkCard = ({
|
||||
label,
|
||||
to,
|
||||
onPress,
|
||||
style,
|
||||
}: {
|
||||
label: string
|
||||
to?: string
|
||||
onPress?: () => void
|
||||
style?: ViewStyle
|
||||
}): React.ReactNode => {
|
||||
const pal = usePalette('default')
|
||||
|
||||
const inner = (
|
||||
<View
|
||||
style={[pal.border, {paddingVertical: 16, paddingHorizontal: 12}, style]}>
|
||||
<Text type="md" style={[pal.text]}>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
||||
if (onPress) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint="">
|
||||
{inner}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import {useMemo} from 'react'
|
||||
|
||||
import {useInterestsDisplayNames} from '#/lib/interests'
|
||||
import {useActorSearch} from '#/state/queries/actor-search'
|
||||
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||
|
||||
/**
|
||||
* Conditional hook, used in case a user is a non-english speaker, in which
|
||||
* case we fall back to searching for users instead of our more curated set.
|
||||
*/
|
||||
export function useSuggestedUsers({
|
||||
category = null,
|
||||
search = false,
|
||||
}: {
|
||||
category?: string | null
|
||||
/**
|
||||
* If true, we'll search for users using the translated value of `category`,
|
||||
* based on the user's app language setting
|
||||
*/
|
||||
search?: boolean
|
||||
}) {
|
||||
const interestsDisplayNames = useInterestsDisplayNames()
|
||||
const curated = useGetSuggestedUsersQuery({
|
||||
enabled: !search,
|
||||
category,
|
||||
})
|
||||
const searched = useActorSearch({
|
||||
enabled: !!search,
|
||||
// use user's app language translation for this value
|
||||
query: category ? interestsDisplayNames[category] : '',
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
return useMemo(() => {
|
||||
if (search) {
|
||||
return {
|
||||
// we're not paginating right now
|
||||
data: searched?.data
|
||||
? {
|
||||
actors: searched.data.pages.flatMap(p => p.actors) ?? [],
|
||||
recId: undefined,
|
||||
}
|
||||
: undefined,
|
||||
isLoading: searched.isLoading,
|
||||
error: searched.error,
|
||||
isRefetching: searched.isRefetching,
|
||||
refetch: searched.refetch,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
data: curated.data,
|
||||
isLoading: curated.isLoading,
|
||||
error: curated.error,
|
||||
isRefetching: curated.isRefetching,
|
||||
refetch: curated.refetch,
|
||||
}
|
||||
}
|
||||
}, [curated, searched, search])
|
||||
}
|
||||
@@ -19,7 +19,9 @@ import {
|
||||
} from '#/state/queries/preferences'
|
||||
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||
import {createGetSuggestedFeedsQueryKey} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
|
||||
import {createGetSuggestedUsersQueryKey} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||
import {createGetSuggestedUsersForDiscoverQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForDiscoverQuery'
|
||||
import {createGetSuggestedUsersForExploreQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForExploreQuery'
|
||||
import {createGetSuggestedUsersForSeeMoreQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery'
|
||||
import {createSuggestedStarterPacksQueryKey} from '#/state/queries/useSuggestedStarterPacksQuery'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||
@@ -120,7 +122,15 @@ function Inner({
|
||||
await Promise.all([
|
||||
qc.resetQueries({queryKey: createSuggestedStarterPacksQueryKey()}),
|
||||
qc.resetQueries({queryKey: createGetSuggestedFeedsQueryKey()}),
|
||||
qc.resetQueries({queryKey: createGetSuggestedUsersQueryKey({})}),
|
||||
qc.resetQueries({
|
||||
queryKey: createGetSuggestedUsersForDiscoverQueryKey({}),
|
||||
}),
|
||||
qc.resetQueries({
|
||||
queryKey: createGetSuggestedUsersForExploreQueryKey({}),
|
||||
}),
|
||||
qc.resetQueries({
|
||||
queryKey: createGetSuggestedUsersForSeeMoreQueryKey({}),
|
||||
}),
|
||||
])
|
||||
|
||||
Toast.show(
|
||||
|
||||
Vendored
+6
-2
@@ -26,7 +26,9 @@ import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData
|
||||
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInSuggestedOnboardingUsersQueryData} from '#/state/queries/trending/useGetSuggestedOnboardingUsersQuery'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersQueryData} from '#/state/queries/trending/useGetSuggestedUsersQuery'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersForDiscoverQueryData} from '#/state/queries/trending/useGetSuggestedUsersForDiscoverQuery'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersForExploreQueryData} from '#/state/queries/trending/useGetSuggestedUsersForExploreQuery'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersForSeeMoreQueryData} from '#/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInPostThreadV2QueryData} from '#/state/queries/usePostThread/queryCache'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {castAsShadow, type Shadow} from './types'
|
||||
@@ -249,7 +251,9 @@ function* findProfilesInCache(
|
||||
yield* findAllProfilesInProfileFollowersQueryData(queryClient, did)
|
||||
yield* findAllProfilesInProfileFollowsQueryData(queryClient, did)
|
||||
yield* findAllProfilesInSuggestedOnboardingUsersQueryData(queryClient, did)
|
||||
yield* findAllProfilesInSuggestedUsersQueryData(queryClient, did)
|
||||
yield* findAllProfilesInSuggestedUsersForDiscoverQueryData(queryClient, did)
|
||||
yield* findAllProfilesInSuggestedUsersForExploreQueryData(queryClient, did)
|
||||
yield* findAllProfilesInSuggestedUsersForSeeMoreQueryData(queryClient, did)
|
||||
yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
|
||||
yield* findAllProfilesInActorSearchQueryData(queryClient, did)
|
||||
yield* findAllProfilesInListConvosQueryData(queryClient, did)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user