Cull unused files (#11029)

This commit is contained in:
Samuel Newman
2026-06-30 19:41:35 +03:00
committed by GitHub
parent bb20f234f3
commit ade5d2b783
17 changed files with 8 additions and 1495 deletions
-32
View File
@@ -927,11 +927,6 @@
"count": 1
}
},
"src/lib/hooks/useTabFocusEffect.ts": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/lib/hooks/useToggleMutationQueue.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 2
@@ -1073,17 +1068,6 @@
"count": 1
}
},
"src/screens/Bookmarks/index.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
},
"@typescript-eslint/no-misused-promises": {
"count": 3
},
"@typescript-eslint/no-unsafe-member-access": {
"count": 2
}
},
"src/screens/Deactivated.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -2291,14 +2275,6 @@
"count": 3
}
},
"src/view/com/util/ViewSelector.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 6
},
"@typescript-eslint/no-unsafe-member-access": {
"count": 1
}
},
"src/view/com/util/Views.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -2314,14 +2290,6 @@
"count": 1
}
},
"src/view/screens/Debug.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
},
"@typescript-eslint/no-unsafe-member-access": {
"count": 4
}
},
"src/view/screens/Feeds.tsx": {
"@typescript-eslint/no-floating-promises": {
"count": 3
-46
View File
@@ -1,46 +0,0 @@
import {Children, cloneElement, Fragment, isValidElement} from 'react'
import {View} from 'react-native'
import {atoms, useTheme} from '#/alf'
/**
* NOT FINISHED, just here as a reference
*/
export function InputGroup(props: React.PropsWithChildren<{}>) {
const t = useTheme()
const children = Children.toArray(props.children)
const total = children.length
return (
<View style={[atoms.w_full]}>
{children.map((child, i) => {
return isValidElement(child) ? (
<Fragment key={i}>
{i > 0 ? (
<View
style={[atoms.border_b, {borderColor: t.palette.contrast_500}]}
/>
) : null}
{cloneElement(child, {
// @ts-ignore
style: [
// @ts-ignore
...(Array.isArray(child.props?.style)
? // @ts-ignore
child.props.style
: // @ts-ignore
[child.props.style || {}]),
{
borderTopLeftRadius: i > 0 ? 0 : undefined,
borderTopRightRadius: i > 0 ? 0 : undefined,
borderBottomLeftRadius: i < total - 1 ? 0 : undefined,
borderBottomRightRadius: i < total - 1 ? 0 : undefined,
borderBottomWidth: i < total - 1 ? 0 : undefined,
},
],
})}
</Fragment>
) : null
})}
</View>
)
}
@@ -1,170 +0,0 @@
import {View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {useCleanError} from '#/lib/hooks/useCleanError'
import {atoms as a, web} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {Span, Text} from '#/components/Typography'
import {IS_NATIVE} from '#/env'
import {useUpdateLiveEventPreferences} from '#/features/liveEvents/preferences'
import {
type LiveEventFeed,
type LiveEventFeedMetricContext,
} from '#/features/liveEvents/types'
export {useDialogControl} from '#/components/Dialog'
export function LiveEventFeedOptionsMenu({
control,
feed,
metricContext,
}: {
control: Dialog.DialogControlProps
feed: LiveEventFeed
metricContext: LiveEventFeedMetricContext
}) {
const {_} = useLingui()
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(msg`Configure live event banner`)}
style={[web({maxWidth: 400})]}>
<Inner control={control} feed={feed} metricContext={metricContext} />
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
function Inner({
control,
feed,
metricContext,
}: {
control: Dialog.DialogControlProps
feed: LiveEventFeed
metricContext: LiveEventFeedMetricContext
}) {
const {_} = useLingui()
const {
isPending,
mutate: update,
error: rawError,
variables,
} = useUpdateLiveEventPreferences({
feed,
metricContext,
onUpdateSuccess({undoAction}) {
Toast.show(
<Toast.Outer>
<Toast.Icon />
<Toast.Text>
<Trans>Your live event preferences have been updated.</Trans>
</Toast.Text>
{undoAction && (
<Toast.Action
label={_(msg`Undo`)}
onPress={() => {
if (undoAction) {
update(undoAction)
}
}}>
<Trans>Undo</Trans>
</Toast.Action>
)}
</Toast.Outer>,
{type: 'success'},
)
/*
* If there is no `undoAction`, it means that the action was already
* undone, and therefore the menu would have been closed prior to the
* undo happening.
*/
if (undoAction) {
control.close()
}
},
})
const cleanError = useCleanError()
const error = rawError ? cleanError(rawError) : undefined
const isHidingFeed = variables?.type === 'hideFeed' && isPending
const isHidingAllFeeds = variables?.type === 'toggleHideAllFeeds' && isPending
return (
<View style={[a.gap_lg]}>
<View style={[a.gap_sm]}>
<Text style={[a.text_2xl, a.font_semi_bold, a.leading_snug]}>
<Trans>Live event options</Trans>
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
Live events appear occasionally when something exciting is
happening. If you'd like, you can hide this particular event, or all
events for this placement in your app interface.
</Trans>
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
If you choose to hide all events, you can always re-enable them from{' '}
<Span style={[a.font_semi_bold]}>Settings → Content & Media</Span>.
</Trans>
</Text>
</View>
<View style={[a.gap_sm]}>
<Button
label={_(msg`Hide this event`)}
size="large"
color="primary_subtle"
onPress={() => {
update({type: 'hideFeed', id: feed.id})
}}>
<ButtonText>
<Trans>Hide this event</Trans>
</ButtonText>
{isHidingFeed && <ButtonIcon icon={Loader} />}
</Button>
<Button
label={_(msg`Hide all events`)}
size="large"
color="secondary"
onPress={() => {
update({type: 'toggleHideAllFeeds'})
}}>
<ButtonText>
<Trans>Hide all events</Trans>
</ButtonText>
{isHidingAllFeeds && <ButtonIcon icon={Loader} />}
</Button>
{IS_NATIVE && (
<Button
label={_(msg`Cancel`)}
size="large"
color="secondary_inverted"
onPress={() => control.close()}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
)}
</View>
{error && (
<Admonition type="error">
{error.clean || error.raw || _(msg`An unknown error occurred.`)}
</Admonition>
)}
</View>
)
}
@@ -1,44 +0,0 @@
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
import * as Toggle from '#/components/forms/Toggle'
import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live'
import {
useLiveEventPreferences,
useUpdateLiveEventPreferences,
} from '#/features/liveEvents/preferences'
export function LiveEventFeedsSettingsToggle() {
const {_} = useLingui()
const {data: prefs} = useLiveEventPreferences()
const {
isPending,
data: updatedPrefs,
mutate: update,
} = useUpdateLiveEventPreferences({
metricContext: 'settings',
})
const hideAllFeeds = !!(updatedPrefs || prefs)?.hideAllFeeds
return (
<Toggle.Item
name="enable_live_event_banner"
label={_(msg`Show live events in your Discover Feed`)}
value={!hideAllFeeds}
onChange={() => {
if (!isPending) {
update({type: 'toggleHideAllFeeds'})
}
}}>
<SettingsList.Item>
<SettingsList.ItemIcon icon={LiveIcon} />
<SettingsList.ItemText>
<Trans>Show live events in your Discover Feed</Trans>
</SettingsList.ItemText>
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
)
}
-302
View File
@@ -1,302 +0,0 @@
import {useMemo} from 'react'
import {deviceLocales} from '#/locale/deviceLocales'
import {useLanguagePrefs} from '#/state/preferences'
import {useGeolocation} from '#/geolocation'
/**
* From react-native-localize
*
* MIT License
* Copyright (c) 2017-present, Mathieu Acthernoene
*
* @see https://github.com/zoontek/react-native-localize/blob/master/LICENSE
* @see https://github.com/zoontek/react-native-localize/blob/ee5bf25e0bb8f3b8e4f3fd055f67ad46269c81ea/src/constants.ts
*/
export const countryCodeToCurrency: Record<string, string> = {
ad: 'eur',
ae: 'aed',
af: 'afn',
ag: 'xcd',
ai: 'xcd',
al: 'all',
am: 'amd',
an: 'ang',
ao: 'aoa',
ar: 'ars',
as: 'usd',
at: 'eur',
au: 'aud',
aw: 'awg',
ax: 'eur',
az: 'azn',
ba: 'bam',
bb: 'bbd',
bd: 'bdt',
be: 'eur',
bf: 'xof',
bg: 'bgn',
bh: 'bhd',
bi: 'bif',
bj: 'xof',
bl: 'eur',
bm: 'bmd',
bn: 'bnd',
bo: 'bob',
bq: 'usd',
br: 'brl',
bs: 'bsd',
bt: 'btn',
bv: 'nok',
bw: 'bwp',
by: 'byn',
bz: 'bzd',
ca: 'cad',
cc: 'aud',
cd: 'cdf',
cf: 'xaf',
cg: 'xaf',
ch: 'chf',
ci: 'xof',
ck: 'nzd',
cl: 'clp',
cm: 'xaf',
cn: 'cny',
co: 'cop',
cr: 'crc',
cu: 'cup',
cv: 'cve',
cw: 'ang',
cx: 'aud',
cy: 'eur',
cz: 'czk',
de: 'eur',
dj: 'djf',
dk: 'dkk',
dm: 'xcd',
do: 'dop',
dz: 'dzd',
ec: 'usd',
ee: 'eur',
eg: 'egp',
eh: 'mad',
er: 'ern',
es: 'eur',
et: 'etb',
fi: 'eur',
fj: 'fjd',
fk: 'fkp',
fm: 'usd',
fo: 'dkk',
fr: 'eur',
ga: 'xaf',
gb: 'gbp',
gd: 'xcd',
ge: 'gel',
gf: 'eur',
gg: 'gbp',
gh: 'ghs',
gi: 'gip',
gl: 'dkk',
gm: 'gmd',
gn: 'gnf',
gp: 'eur',
gq: 'xaf',
gr: 'eur',
gs: 'gbp',
gt: 'gtq',
gu: 'usd',
gw: 'xof',
gy: 'gyd',
hk: 'hkd',
hm: 'aud',
hn: 'hnl',
hr: 'hrk',
ht: 'htg',
hu: 'huf',
id: 'idr',
ie: 'eur',
il: 'ils',
im: 'gbp',
in: 'inr',
io: 'usd',
iq: 'iqd',
ir: 'irr',
is: 'isk',
it: 'eur',
je: 'gbp',
jm: 'jmd',
jo: 'jod',
jp: 'jpy',
ke: 'kes',
kg: 'kgs',
kh: 'khr',
ki: 'aud',
km: 'kmf',
kn: 'xcd',
kp: 'kpw',
kr: 'krw',
kw: 'kwd',
ky: 'kyd',
kz: 'kzt',
la: 'lak',
lb: 'lbp',
lc: 'xcd',
li: 'chf',
lk: 'lkr',
lr: 'lrd',
ls: 'lsl',
lt: 'eur',
lu: 'eur',
lv: 'eur',
ly: 'lyd',
ma: 'mad',
mc: 'eur',
md: 'mdl',
me: 'eur',
mf: 'eur',
mg: 'mga',
mh: 'usd',
mk: 'mkd',
ml: 'xof',
mm: 'mmk',
mn: 'mnt',
mo: 'mop',
mp: 'usd',
mq: 'eur',
mr: 'mro',
ms: 'xcd',
mt: 'eur',
mu: 'mur',
mv: 'mvr',
mw: 'mwk',
mx: 'mxn',
my: 'myr',
mz: 'mzn',
na: 'nad',
nc: 'xpf',
ne: 'xof',
nf: 'aud',
ng: 'ngn',
ni: 'nio',
nl: 'eur',
no: 'nok',
np: 'npr',
nr: 'aud',
nu: 'nzd',
nz: 'nzd',
om: 'omr',
pa: 'pab',
pe: 'pen',
pf: 'xpf',
pg: 'pgk',
ph: 'php',
pk: 'pkr',
pl: 'pln',
pm: 'eur',
pn: 'nzd',
pr: 'usd',
ps: 'ils',
pt: 'eur',
pw: 'usd',
py: 'pyg',
qa: 'qar',
re: 'eur',
ro: 'ron',
rs: 'rsd',
ru: 'rub',
rw: 'rwf',
sa: 'sar',
sb: 'sbd',
sc: 'scr',
sd: 'sdg',
se: 'sek',
sg: 'sgd',
sh: 'shp',
si: 'eur',
sj: 'nok',
sk: 'eur',
sl: 'sll',
sm: 'eur',
sn: 'xof',
so: 'sos',
sr: 'srd',
ss: 'ssp',
st: 'std',
sv: 'svc',
sx: 'ang',
sy: 'syp',
sz: 'szl',
tc: 'usd',
td: 'xaf',
tf: 'eur',
tg: 'xof',
th: 'thb',
tj: 'tjs',
tk: 'nzd',
tl: 'usd',
tm: 'tmt',
tn: 'tnd',
to: 'top',
tr: 'try',
tt: 'ttd',
tv: 'aud',
tw: 'twd',
tz: 'tzs',
ua: 'uah',
ug: 'ugx',
um: 'usd',
us: 'usd',
uy: 'uyu',
uz: 'uzs',
va: 'eur',
vc: 'xcd',
ve: 'vef',
vg: 'usd',
vi: 'usd',
vn: 'vnd',
vu: 'vuv',
wf: 'xpf',
ws: 'wst',
ye: 'yer',
yt: 'eur',
za: 'zar',
zm: 'zmw',
zw: 'zwl',
}
/**
* Best-guess currency formatting.
*
* Attempts to use `getLocales` from `expo-localization` if available,
* otherwise falls back to the `persisted.appLanguage` setting, and geolocation
* API for region.
*/
export function useFormatCurrency(
options?: Parameters<typeof Intl.NumberFormat>[1],
) {
const geolocation = useGeolocation()
const {appLanguage} = useLanguagePrefs()
return useMemo(() => {
const locale = deviceLocales.at(0)
const languageTag = locale?.languageTag || appLanguage || 'en-US'
const countryCode = (
locale?.regionCode ||
geolocation?.countryCode ||
'us'
).toLowerCase()
const currency = countryCodeToCurrency[countryCode] || 'usd'
const format = new Intl.NumberFormat(languageTag, {
...(options || {}),
style: 'currency',
currency: currency,
}).format
return {
format,
currency,
countryCode,
languageTag,
}
}, [geolocation, appLanguage, options])
}
-27
View File
@@ -1,27 +0,0 @@
import {withDelay, withSequence, withTiming} from 'react-native-reanimated'
export function ShrinkAndPop() {
'worklet'
const animations = {
opacity: withDelay(125, withTiming(0, {duration: 125})),
transform: [
{
scale: withSequence(
withTiming(0.7, {duration: 75}),
withTiming(1.1, {duration: 150}),
),
},
],
}
const initialValues = {
opacity: 1,
transform: [{scale: 1}],
}
return {
animations,
initialValues,
}
}
-28
View File
@@ -1,28 +0,0 @@
import {useEffect, useState} from 'react'
import {useNavigation} from '@react-navigation/native'
import {getTabState, TabState} from '#/lib/routes/helpers'
export function useTabFocusEffect(
tabName: string,
cb: (isInside: boolean) => void,
) {
const [isInside, setIsInside] = useState(false)
// get root navigator state
let nav = useNavigation()
while (nav.getParent()) {
nav = nav.getParent()
}
const state = nav.getState()
useEffect(() => {
// check if inside
let v = getTabState(state, tabName) !== TabState.Outside
if (v !== isInside) {
// fire
setIsInside(v)
cb(v)
}
}, [state, isInside, setIsInside, tabName, cb])
}
-32
View File
@@ -1,32 +0,0 @@
import {useCallback, useEffect, useRef} from 'react'
/**
* Helper hook to run persistent timers on views
*/
export function useTimer(time: number, handler: () => void) {
const timer = useRef<undefined | NodeJS.Timeout>(undefined)
// function to restart the timer
const reset = useCallback(() => {
if (timer.current) {
clearTimeout(timer.current)
}
timer.current = setTimeout(handler, time)
}, [time, timer, handler])
// function to cancel the timer
const cancel = useCallback(() => {
if (timer.current) {
clearTimeout(timer.current)
timer.current = undefined
}
}, [timer])
// start the timer immediately
useEffect(() => {
reset()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return [reset, cancel]
}
View File
@@ -14,13 +14,13 @@ import {
useNavigation,
} from '@react-navigation/native'
import {useCleanError} from '#/lib/hooks/useCleanError'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery'
import {Post} from '#/view/com/post/Post'
@@ -92,7 +92,6 @@ type ListItem =
function BookmarksInner() {
const initialNumToRender = useInitialNumToRender()
const cleanError = useCleanError()
const [isPTRing, setIsPTRing] = useState(false)
const trackPostView = usePostViewTracking('Bookmarks')
const {
@@ -104,10 +103,6 @@ function BookmarksInner() {
error,
refetch,
} = useBookmarksQuery()
const cleanedError = useMemo(() => {
const {raw, clean} = cleanError(error)
return clean || raw
}, [error, cleanError])
const onRefresh = useCallback(async () => {
setIsPTRing(true)
@@ -174,10 +169,10 @@ function BookmarksInner() {
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onRefresh={() => void onRefresh()}
onEndReached={() => void onEndReached()}
onEndReachedThreshold={4}
onItemSeen={item => {
onItemSeen={(item: ListItem) => {
if (item.type === 'bookmark') {
trackPostView(item.bookmark.item)
}
@@ -185,7 +180,7 @@ function BookmarksInner() {
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanedError}
error={cleanError(error)}
onRetry={fetchNextPage}
style={[isEmpty && a.border_t_0]}
/>
@@ -209,7 +204,6 @@ function BookmarkNotFound({
const t = useTheme()
const {_} = useLingui()
const {mutateAsync: bookmark} = useBookmarkMutation()
const cleanError = useCleanError()
const remove = async () => {
try {
@@ -217,9 +211,8 @@ function BookmarkNotFound({
toast.show(_(msg`Removed from saved posts`), {
type: 'info',
})
} catch (e: any) {
const {raw, clean} = cleanError(e)
toast.show(clean || raw || e, {
} catch (err) {
toast.show(cleanError(err), {
type: 'error',
})
}
@@ -259,7 +252,7 @@ function BookmarkNotFound({
label={_(msg`Remove from saved posts`)}
size="tiny"
color="secondary"
onPress={remove}>
onPress={() => void remove()}>
<ButtonIcon icon={BookmarkFilled} />
<ButtonText>
<Trans>Remove</Trans>
@@ -1,60 +0,0 @@
import {View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {atoms as a, useTheme} from '#/alf'
import {ButtonText} from '#/components/Button'
import {BookmarkDeleteLarge} from '#/components/icons/Bookmark'
import {Link} from '#/components/Link'
import {Text} from '#/components/Typography'
export function EmptyState() {
const t = useTheme()
const {_} = useLingui()
return (
<View
style={[
a.align_center,
{
paddingVertical: 64,
},
]}>
<BookmarkDeleteLarge
width={64}
fill={t.atoms.text_contrast_medium.color}
/>
<View style={[a.pt_sm]}>
<Text
style={[
a.text_lg,
a.font_medium,
a.text_center,
t.atoms.text_contrast_medium,
]}>
<Trans>Nothing saved yet</Trans>
</Text>
</View>
<View style={[a.pt_2xl]}>
<Link
to="/"
action="navigate"
label={_(
msg({
message: `Go home`,
context: `Button to go back to the home timeline`,
}),
)}
size="small"
color="secondary">
<ButtonText>
<Trans context="Button to go back to the home timeline">
Go home
</Trans>
</ButtonText>
</Link>
</View>
</View>
)
}
-35
View File
@@ -1,35 +0,0 @@
import {type AppBskyFeedGetSuggestedFeeds} from '@atproto/api'
import {
type InfiniteData,
type QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
const suggestedFeedsQueryKeyRoot = 'suggestedFeeds'
export const suggestedFeedsQueryKey = [suggestedFeedsQueryKeyRoot]
export function useSuggestedFeedsQuery() {
const agent = useAgent()
return useInfiniteQuery<
AppBskyFeedGetSuggestedFeeds.OutputSchema,
Error,
InfiniteData<AppBskyFeedGetSuggestedFeeds.OutputSchema>,
QueryKey,
string | undefined
>({
staleTime: STALE.HOURS.ONE,
queryKey: suggestedFeedsQueryKey,
queryFn: async ({pageParam}) => {
const res = await agent.app.bsky.feed.getSuggestedFeeds({
limit: 10,
cursor: pageParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
})
}
-19
View File
@@ -1,19 +0,0 @@
import {createContext, useContext} from 'react'
interface PostProgressState {
progress: number
status: 'pending' | 'success' | 'error' | 'idle'
error?: string
}
const PostProgressContext = createContext<PostProgressState>({
progress: 0,
status: 'idle',
})
PostProgressContext.displayName = 'PostProgressContext'
export function Provider() {}
export function usePostProgress() {
return useContext(PostProgressContext)
}
-27
View File
@@ -1,27 +0,0 @@
import {createContext, useContext, useState} from 'react'
type StateContext =
| {
uri: string
isClip?: boolean
}
| undefined
type SetContext = (v: StateContext) => void
const stateContext = createContext<StateContext>(undefined)
stateContext.displayName = 'ActiveStarterPackStateContext'
const setContext = createContext<SetContext>((_: StateContext) => {})
setContext.displayName = 'ActiveStarterPackSetContext'
export function Provider({children}: {children: React.ReactNode}) {
const [state, setState] = useState<StateContext>()
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setState}>{children}</setContext.Provider>
</stateContext.Provider>
)
}
export const useActiveStarterPack = () => useContext(stateContext)
export const useSetActiveStarterPack = () => useContext(setContext)
@@ -1,33 +0,0 @@
import {View} from 'react-native'
import {KeyboardStickyView} from 'react-native-keyboard-controller'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {atoms as a, useTheme} from '#/alf'
import {IS_WEB} from '#/env'
export function KeyboardAccessory({children}: {children: React.ReactNode}) {
const t = useTheme()
const {bottom} = useSafeAreaInsets()
const style = [
a.flex_row,
a.py_xs,
a.pl_sm,
a.pr_xl,
a.align_center,
a.border_t,
t.atoms.border_contrast_medium,
t.atoms.bg,
]
// todo: when iPad support is added, it should also not use the KeyboardStickyView
if (IS_WEB) {
return <View style={style}>{children}</View>
}
return (
<KeyboardStickyView offset={{closed: -bottom}} style={style}>
{children}
</KeyboardStickyView>
)
}
-238
View File
@@ -1,238 +0,0 @@
import {
forwardRef,
type JSX,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import {
type NativeScrollEvent,
type NativeSyntheticEvent,
Pressable,
RefreshControl,
ScrollView,
StyleSheet,
View,
} from 'react-native'
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
import {usePalette} from '#/lib/hooks/usePalette'
import {clamp} from '#/lib/numbers'
import {colors, s} from '#/lib/styles'
import {IS_ANDROID} from '#/env'
import {Text} from './text/Text'
import {FlatList_INTERNAL} from './Views'
const HEADER_ITEM = {_reactKey: '__header__'}
const SELECTOR_ITEM = {_reactKey: '__selector__'}
const STICKY_HEADER_INDICES = [1]
export type ViewSelectorHandle = {
scrollToTop: () => void
}
export const ViewSelector = forwardRef<
ViewSelectorHandle,
{
sections: string[]
items: any[]
refreshing?: boolean
swipeEnabled?: boolean
renderHeader?: () => JSX.Element
renderItem: (item: any) => JSX.Element
ListFooterComponent?:
| React.ComponentType<any>
| React.ReactElement<any>
| null
| undefined
onSelectView?: (viewIndex: number) => void
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
onRefresh?: () => void
onEndReached?: (info: {distanceFromEnd: number}) => void
}
>(function ViewSelectorImpl(
{
sections,
items,
refreshing,
renderHeader,
renderItem,
ListFooterComponent,
onSelectView,
onScroll,
onRefresh,
onEndReached,
},
ref,
) {
const pal = usePalette('default')
const [selectedIndex, setSelectedIndex] = useState<number>(0)
const flatListRef = useRef<FlatList_INTERNAL>(null)
// events
// =
const keyExtractor = useCallback((item: any) => item._reactKey, [])
const onPressSelection = useCallback(
(index: number) => setSelectedIndex(clamp(index, 0, sections.length)),
[setSelectedIndex, sections],
)
useEffect(() => {
onSelectView?.(selectedIndex)
}, [selectedIndex, onSelectView])
useImperativeHandle(ref, () => ({
scrollToTop: () => {
flatListRef.current?.scrollToOffset({offset: 0})
},
}))
// rendering
// =
const renderItemInternal = useCallback(
({item}: {item: any}) => {
if (item === HEADER_ITEM) {
if (renderHeader) {
return renderHeader()
}
return <View />
} else if (item === SELECTOR_ITEM) {
return (
<Selector
items={sections}
selectedIndex={selectedIndex}
onSelect={onPressSelection}
/>
)
} else {
return renderItem(item)
}
},
[sections, selectedIndex, onPressSelection, renderHeader, renderItem],
)
const data = useMemo(() => [HEADER_ITEM, SELECTOR_ITEM, ...items], [items])
return (
<FlatList_INTERNAL
// @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn
ref={flatListRef}
data={data}
keyExtractor={keyExtractor}
renderItem={renderItemInternal}
ListFooterComponent={ListFooterComponent}
// NOTE sticky header disabled on android due to major performance issues -prf
stickyHeaderIndices={IS_ANDROID ? undefined : STICKY_HEADER_INDICES}
onScroll={onScroll}
onEndReached={onEndReached}
refreshControl={
<RefreshControl
refreshing={refreshing!}
onRefresh={onRefresh}
tintColor={pal.colors.text}
/>
}
onEndReachedThreshold={0.6}
contentContainerStyle={s.contentContainer}
removeClippedSubviews={true}
scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464
/>
)
})
export function Selector({
selectedIndex,
items,
onSelect,
}: {
selectedIndex: number
items: string[]
onSelect?: (index: number) => void
}) {
const pal = usePalette('default')
const borderColor = useColorSchemeStyle(
{borderColor: colors.black},
{borderColor: colors.white},
)
const onPressItem = (index: number) => {
onSelect?.(index)
}
return (
<View
style={{
width: '100%',
backgroundColor: pal.colors.background,
}}>
<ScrollView
testID="selector"
horizontal
showsHorizontalScrollIndicator={false}>
<View style={[pal.view, styles.outer]}>
{items.map((item, i) => {
const selected = i === selectedIndex
return (
<Pressable
testID={`selector-${i}`}
key={item}
onPress={() => onPressItem(i)}
accessibilityLabel={item}
accessibilityHint={`Selects ${item}`}
// TODO: Modify the component API such that lint fails
// at the invocation site as well
>
<View
style={[
styles.item,
selected && styles.itemSelected,
borderColor,
]}>
<Text
style={
selected
? [styles.labelSelected, pal.text]
: [styles.label, pal.textLight]
}>
{item}
</Text>
</View>
</Pressable>
)
})}
</View>
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
outer: {
flexDirection: 'row',
paddingHorizontal: 14,
},
item: {
marginRight: 14,
paddingHorizontal: 10,
paddingTop: 8,
paddingBottom: 12,
},
itemSelected: {
borderBottomWidth: 3,
},
label: {
fontWeight: '600',
},
labelSelected: {
fontWeight: '600',
},
underline: {
position: 'absolute',
height: 4,
bottom: 0,
},
})
-387
View File
@@ -1,387 +0,0 @@
import {useState} from 'react'
import {ScrollView, View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {s} from '#/lib/styles'
import {type PaletteColorName, ThemeProvider} from '#/lib/ThemeContext'
import {EmptyState} from '#/view/com/util/EmptyState'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {Button} from '#/view/com/util/forms/Button'
import * as LoadingPlaceholder from '#/view/com/util/LoadingPlaceholder'
import {Text} from '#/view/com/util/text/Text'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {ViewSelector} from '#/view/com/util/ViewSelector'
import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
import * as Layout from '#/components/Layout'
import * as Toast from '#/components/Toast'
const MAIN_VIEWS = ['Base', 'Controls', 'Error', 'Notifs']
export const DebugScreen = ({}: NativeStackScreenProps<
CommonNavigatorParams,
'Debug'
>) => {
const [colorScheme, setColorScheme] = useState<'light' | 'dark'>('light')
const onToggleColorScheme = () => {
setColorScheme(colorScheme === 'light' ? 'dark' : 'light')
}
return (
<ThemeProvider theme={colorScheme}>
<Layout.Screen>
<DebugInner
colorScheme={colorScheme}
onToggleColorScheme={onToggleColorScheme}
/>
</Layout.Screen>
</ThemeProvider>
)
}
function DebugInner({}: {
colorScheme: 'light' | 'dark'
onToggleColorScheme: () => void
}) {
const [currentView, setCurrentView] = useState<number>(0)
const pal = usePalette('default')
const {_} = useLingui()
const renderItem = (item: any) => {
return (
<View key={`view-${item.currentView}`}>
{item.currentView === 3 ? (
<NotifsView />
) : item.currentView === 2 ? (
<ErrorView />
) : item.currentView === 1 ? (
<ControlsView />
) : (
<BaseView />
)}
</View>
)
}
const items = [{currentView}]
return (
<View style={[s.hContentRegion, pal.view]}>
<ViewHeader title={_(msg`Debug panel`)} />
<ViewSelector
swipeEnabled
sections={MAIN_VIEWS}
items={items}
renderItem={renderItem}
onSelectView={setCurrentView}
/>
</View>
)
}
function Heading({label}: {label: string}) {
const pal = usePalette('default')
return (
<View style={[s.pt10, s.pb5]}>
<Text type="title-lg" style={pal.text}>
{label}
</Text>
</View>
)
}
function BaseView() {
return (
<View style={[s.pl10, s.pr10]}>
<Heading label="Typography" />
<TypographyView />
<Heading label="Palettes" />
<PaletteView palette="default" />
<PaletteView palette="primary" />
<PaletteView palette="secondary" />
<PaletteView palette="inverted" />
<PaletteView palette="error" />
<Heading label="Empty state" />
<EmptyStateView />
<Heading label="Loading placeholders" />
<LoadingPlaceholderView />
<View style={s.footerSpacer} />
</View>
)
}
function ControlsView() {
return (
<ScrollView style={[s.pl10, s.pr10]}>
<Heading label="Buttons" />
<ButtonsView />
<View style={s.footerSpacer} />
</ScrollView>
)
}
function ErrorView() {
return (
<View style={s.p10}>
<View style={s.mb5}>
<ErrorScreen
title="Error screen"
message="A major error occurred that led the entire screen to fail"
details="Here are some details"
onPressTryAgain={() => {}}
/>
</View>
<View style={s.mb5}>
<ErrorMessage message="This is an error that occurred while things were being done" />
</View>
<View style={s.mb5}>
<ErrorMessage
message="This is an error that occurred while things were being done"
numberOfLines={1}
/>
</View>
<View style={s.mb5}>
<ErrorMessage
message="This is an error that occurred while things were being done"
onPressTryAgain={() => {}}
/>
</View>
<View style={s.mb5}>
<ErrorMessage
message="This is an error that occurred while things were being done"
onPressTryAgain={() => {}}
numberOfLines={1}
/>
</View>
</View>
)
}
function NotifsView() {
const triggerPush = () => {
// TODO: implement local notification for testing
}
const triggerToast = () => {
Toast.show('The task has been completed')
}
const triggerToast2 = () => {
Toast.show('The task has been completed successfully and with no problems')
}
return (
<View style={s.p10}>
<View style={{flexDirection: 'row'}}>
<Button onPress={triggerPush} label="Trigger Push" />
<Button onPress={triggerToast} label="Trigger Toast" />
<Button onPress={triggerToast2} label="Trigger Toast 2" />
</View>
</View>
)
}
function PaletteView({palette}: {palette: PaletteColorName}) {
const defaultPal = usePalette('default')
const pal = usePalette(palette)
return (
<View style={[pal.view, pal.border, s.p10, s.mb5, {borderWidth: 1}]}>
<Text style={[pal.text]}>{palette} colors</Text>
<Text style={[pal.textLight]}>Light text</Text>
<Text style={[pal.link]}>Link text</Text>
{palette !== 'default' && (
<View style={[defaultPal.view]}>
<Text style={[pal.textInverted]}>Inverted text</Text>
</View>
)}
</View>
)
}
function TypographyView() {
const pal = usePalette('default')
return (
<View style={[pal.view]}>
<Text type="2xl-thin" style={[pal.text]}>
'2xl-thin' lorem ipsum dolor
</Text>
<Text type="2xl" style={[pal.text]}>
'2xl' lorem ipsum dolor
</Text>
<Text type="2xl-medium" style={[pal.text]}>
'2xl-medium' lorem ipsum dolor
</Text>
<Text type="2xl-bold" style={[pal.text]}>
'2xl-bold' lorem ipsum dolor
</Text>
<Text type="2xl-heavy" style={[pal.text]}>
'2xl-heavy' lorem ipsum dolor
</Text>
<Text type="xl-thin" style={[pal.text]}>
'xl-thin' lorem ipsum dolor
</Text>
<Text type="xl" style={[pal.text]}>
'xl' lorem ipsum dolor
</Text>
<Text type="xl-medium" style={[pal.text]}>
'xl-medium' lorem ipsum dolor
</Text>
<Text type="xl-bold" style={[pal.text]}>
'xl-bold' lorem ipsum dolor
</Text>
<Text type="xl-heavy" style={[pal.text]}>
'xl-heavy' lorem ipsum dolor
</Text>
<Text type="lg-thin" style={[pal.text]}>
'lg-thin' lorem ipsum dolor
</Text>
<Text type="lg" style={[pal.text]}>
'lg' lorem ipsum dolor
</Text>
<Text type="lg-medium" style={[pal.text]}>
'lg-medium' lorem ipsum dolor
</Text>
<Text type="lg-bold" style={[pal.text]}>
'lg-bold' lorem ipsum dolor
</Text>
<Text type="lg-heavy" style={[pal.text]}>
'lg-heavy' lorem ipsum dolor
</Text>
<Text type="md-thin" style={[pal.text]}>
'md-thin' lorem ipsum dolor
</Text>
<Text type="md" style={[pal.text]}>
'md' lorem ipsum dolor
</Text>
<Text type="md-medium" style={[pal.text]}>
'md-medium' lorem ipsum dolor
</Text>
<Text type="md-bold" style={[pal.text]}>
'md-bold' lorem ipsum dolor
</Text>
<Text type="md-heavy" style={[pal.text]}>
'md-heavy' lorem ipsum dolor
</Text>
<Text type="sm-thin" style={[pal.text]}>
'sm-thin' lorem ipsum dolor
</Text>
<Text type="sm" style={[pal.text]}>
'sm' lorem ipsum dolor
</Text>
<Text type="sm-medium" style={[pal.text]}>
'sm-medium' lorem ipsum dolor
</Text>
<Text type="sm-bold" style={[pal.text]}>
'sm-bold' lorem ipsum dolor
</Text>
<Text type="sm-heavy" style={[pal.text]}>
'sm-heavy' lorem ipsum dolor
</Text>
<Text type="xs-thin" style={[pal.text]}>
'xs-thin' lorem ipsum dolor
</Text>
<Text type="xs" style={[pal.text]}>
'xs' lorem ipsum dolor
</Text>
<Text type="xs-medium" style={[pal.text]}>
'xs-medium' lorem ipsum dolor
</Text>
<Text type="xs-bold" style={[pal.text]}>
'xs-bold' lorem ipsum dolor
</Text>
<Text type="xs-heavy" style={[pal.text]}>
'xs-heavy' lorem ipsum dolor
</Text>
<Text type="title-2xl" style={[pal.text]}>
'title-2xl' lorem ipsum dolor
</Text>
<Text type="title-xl" style={[pal.text]}>
'title-xl' lorem ipsum dolor
</Text>
<Text type="title-lg" style={[pal.text]}>
'title-lg' lorem ipsum dolor
</Text>
<Text type="title" style={[pal.text]}>
'title' lorem ipsum dolor
</Text>
<Text type="button" style={[pal.text]}>
Button
</Text>
<Text type="button-lg" style={[pal.text]}>
Button-lg
</Text>
</View>
)
}
function EmptyStateView() {
const {_} = useLingui()
return (
<EmptyState
icon={HashtagWideIcon}
iconSize="2xl"
message={_(msg`This is an empty state`)}
/>
)
}
function LoadingPlaceholderView() {
return (
<>
<LoadingPlaceholder.PostLoadingPlaceholder />
<LoadingPlaceholder.NotificationLoadingPlaceholder />
</>
)
}
function ButtonsView() {
const defaultPal = usePalette('default')
const buttonStyles = {marginRight: 5}
return (
<View style={[defaultPal.view]}>
<View style={[{flexDirection: 'row'}, s.mb5]}>
<Button type="primary" label="Primary solid" style={buttonStyles} />
<Button type="secondary" label="Secondary solid" style={buttonStyles} />
</View>
<View style={[{flexDirection: 'row'}, s.mb5]}>
<Button type="default" label="Default solid" style={buttonStyles} />
<Button type="inverted" label="Inverted solid" style={buttonStyles} />
</View>
<View style={{flexDirection: 'row'}}>
<Button
type="primary-outline"
label="Primary outline"
style={buttonStyles}
/>
<Button
type="secondary-outline"
label="Secondary outline"
style={buttonStyles}
/>
</View>
<View style={{flexDirection: 'row'}}>
<Button
type="primary-light"
label="Primary light"
style={buttonStyles}
/>
<Button
type="secondary-light"
label="Secondary light"
style={buttonStyles}
/>
</View>
<View style={{flexDirection: 'row'}}>
<Button
type="default-light"
label="Default light"
style={buttonStyles}
/>
</View>
</View>
)
}