Merge remote-tracking branch 'origin/explore-page-trending-and-starterpacks' into hailey/add-topic-screen

This commit is contained in:
Hailey
2024-12-17 19:01:48 -08:00
80 changed files with 1642 additions and 509 deletions
+16
View File
@@ -105,3 +105,19 @@ jest.mock('expo-modules-core', () => ({
return () => null
}),
}))
jest.mock('expo-localization', () => ({
getLocales: () => [],
}))
jest.mock('statsig-react-native-expo', () => ({
Statsig: {
initialize() {},
initializeCalled() {
return false
},
},
}))
jest.mock('../src/lib/bitdrift', () => ({}))
jest.mock('../src/lib/statsig/statsig', () => ({}))
+3 -4
View File
@@ -120,7 +120,7 @@
"emoji-mart": "^5.5.2",
"emoji-regex": "^10.4.0",
"eventemitter3": "^5.0.1",
"expo": "~52.0.17",
"expo": "~52.0.19",
"expo-application": "^6.0.1",
"expo-blur": "^14.0.1",
"expo-build-properties": "^0.13.1",
@@ -128,7 +128,7 @@
"expo-clipboard": "^7.0.0",
"expo-dev-client": "^5.0.4",
"expo-device": "~7.0.1",
"expo-file-system": "^18.0.4",
"expo-file-system": "^18.0.6",
"expo-font": "~13.0.1",
"expo-haptics": "^14.0.0",
"expo-image": "~2.0.3",
@@ -141,7 +141,7 @@
"expo-navigation-bar": "~4.0.4",
"expo-notifications": "~0.29.11",
"expo-sharing": "^13.0.0",
"expo-splash-screen": "~0.29.16",
"expo-splash-screen": "~0.29.18",
"expo-status-bar": "~2.0.0",
"expo-system-ui": "^4.0.4",
"expo-task-manager": "~12.0.3",
@@ -213,7 +213,6 @@
"@babel/runtime": "^7.26.0",
"@did-plc/server": "^0.0.1",
"@expo/config-plugins": "9.0.10",
"@expo/prebuild-config": "8.0.22",
"@lingui/cli": "^4.14.1",
"@lingui/macro": "^4.14.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
-35
View File
@@ -1,35 +0,0 @@
diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
index 47c4d15..afe138d 100644
--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
@@ -125,6 +125,10 @@ internal fun peekResponseBody(
}
internal fun shouldParseBody(response: Response): Boolean {
+ if (response.request.url.encodedPath == "/bitdrift_public.protobuf.client.v1.ApiService/Mux") {
+ return false
+ }
+
// Check for Content-Type
val skipContentTypes = listOf(
"text/event-stream", // Server Sent Events
diff --git a/node_modules/expo-modules-core/src/uuid/uuid.ts b/node_modules/expo-modules-core/src/uuid/uuid.ts
index 148beac..fabdff5 100644
--- a/node_modules/expo-modules-core/src/uuid/uuid.ts
+++ b/node_modules/expo-modules-core/src/uuid/uuid.ts
@@ -5,6 +5,7 @@ const nativeUuidv4 = globalThis?.expo?.uuidv4;
const nativeUuidv5 = globalThis?.expo?.uuidv5;
function uuidv4(): string {
+ const nativeUuidv4 = globalThis?.expo?.uuidv4;
if (!nativeUuidv4) {
throw Error(
"Native UUID version 4 generator implementation wasn't found in `expo-modules-core`"
@@ -23,6 +24,7 @@ function uuidv5(name: string, namespace: string | number[]) {
throw new Error('`namespace` must be a valid UUID string or an Array of 16 byte values');
}
+ const nativeUuidv5 = globalThis?.expo?.uuidv5;
if (!nativeUuidv5) {
throw Error("Native UUID type 5 generator implementation wasn't found in `expo-modules-core`");
}
+15
View File
@@ -0,0 +1,15 @@
diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
index 47c4d15..afe138d 100644
--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/kotlin/devtools/ExpoNetworkInspectOkHttpInterceptors.kt
@@ -125,6 +125,10 @@ internal fun peekResponseBody(
}
internal fun shouldParseBody(response: Response): Boolean {
+ if (response.request.url.encodedPath == "/bitdrift_public.protobuf.client.v1.ApiService/Mux") {
+ return false
+ }
+
// Check for Content-Type
val skipContentTypes = listOf(
"text/event-stream", // Server Sent Events
+36 -1
View File
@@ -222,7 +222,7 @@ index 40aaf9c..1c60164 100644
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
index e9ce48c..ccd9ad6 100644
index e9ce48c..84a6fca 100644
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
@@ -159,26 +159,8 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
@@ -329,3 +329,38 @@ index e9ce48c..ccd9ad6 100644
}
}
@@ -1055,6 +1082,22 @@ -(type)getter \
RCT_SET_AND_PRESERVE_OFFSET(setShowsVerticalScrollIndicator, showsVerticalScrollIndicator, BOOL)
RCT_SET_AND_PRESERVE_OFFSET(setZoomScale, zoomScale, CGFloat);
+- (void)setScrollIndicatorInsets:(UIEdgeInsets)value
+{
+ [_scrollView setScrollIndicatorInsets:value];
+}
+
+- (UIEdgeInsets)scrollIndicatorInsets
+{
+ UIEdgeInsets verticalScrollIndicatorInsets = [_scrollView verticalScrollIndicatorInsets];
+ UIEdgeInsets horizontalScrollIndicatorInsets = [_scrollView horizontalScrollIndicatorInsets];
+ return UIEdgeInsetsMake(
+ verticalScrollIndicatorInsets.top,
+ horizontalScrollIndicatorInsets.left,
+ verticalScrollIndicatorInsets.bottom,
+ horizontalScrollIndicatorInsets.right);
+}
+
- (void)setAutomaticallyAdjustsScrollIndicatorInsets:(BOOL)automaticallyAdjusts API_AVAILABLE(ios(13.0))
{
// `automaticallyAdjustsScrollIndicatorInsets` is available since iOS 13.
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
index cd1e7eb..c1d0172 100644
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
@@ -83,6 +83,7 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval)
RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
+RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets)
RCT_EXPORT_VIEW_PROPERTY(verticalScrollIndicatorInsets, UIEdgeInsets)
RCT_EXPORT_VIEW_PROPERTY(scrollToOverflowEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
+12
View File
@@ -302,6 +302,18 @@ export const atoms = {
border_0: {
borderWidth: 0,
},
border_t_0: {
borderTopWidth: 0,
},
border_b_0: {
borderBottomWidth: 0,
},
border_l_0: {
borderLeftWidth: 0,
},
border_r_0: {
borderRightWidth: 0,
},
border: {
borderWidth: StyleSheet.hairlineWidth,
},
+1 -1
View File
@@ -280,8 +280,8 @@ export function ProfileGrid({
profile={profile}
moderationOpts={moderationOpts}
logContext="FeedInterstitial"
color="secondary_inverted"
shape="round"
colorInverted
/>
</ProfileCard.Header>
<ProfileCard.Description profile={profile} numberOfLines={2} />
+6 -1
View File
@@ -285,6 +285,7 @@ export type FollowButtonProps = {
moderationOpts: ModerationOpts
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext']
colorInverted?: boolean
} & Partial<ButtonProps>
export function FollowButton(props: FollowButtonProps) {
@@ -297,6 +298,8 @@ export function FollowButtonInner({
profile: profileUnshadowed,
moderationOpts,
logContext,
onPress: onPressProp,
colorInverted,
...rest
}: FollowButtonProps) {
const {_} = useLingui()
@@ -321,6 +324,7 @@ export function FollowButtonInner({
)}`,
),
)
onPressProp?.(e)
} catch (err: any) {
if (err?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
@@ -341,6 +345,7 @@ export function FollowButtonInner({
)}`,
),
)
onPressProp?.(e)
} catch (err: any) {
if (err?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
@@ -387,7 +392,7 @@ export function FollowButtonInner({
label={followLabel}
size="small"
variant="solid"
color="primary"
color={colorInverted ? 'secondary_inverted' : 'primary'}
{...rest}
onPress={onPressFollow}>
<ButtonIcon icon={Plus} position={isRound ? undefined : 'left'} />
@@ -0,0 +1,829 @@
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {ScrollView, TextInput, useWindowDimensions, View} from 'react-native'
import Animated, {
LayoutAnimationConfig,
LinearTransition,
ZoomInEasyDown,
} from 'react-native-reanimated'
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorSearchPaginated} from '#/state/queries/actor-search'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
import {useSession} from '#/state/session'
import {Follow10ProgressGuide} from '#/state/shell/progress-guide'
import {ListMethods} from '#/view/com/util/List'
import {
popularInterests,
useInterestsDisplayNames,
} from '#/screens/Onboarding/state'
import {
atoms as a,
native,
tokens,
useBreakpoints,
useTheme,
ViewStyleProp,
web,
} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2'
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import {ListFooter} from '../Lists'
import {ProgressGuideTask} from './Task'
type Item =
| {
type: 'profile'
key: string
profile: AppBskyActorDefs.ProfileView
isSuggestion: boolean
}
| {
type: 'empty'
key: string
message: string
}
| {
type: 'placeholder'
key: string
}
| {
type: 'error'
key: string
}
export function FollowDialog({guide}: {guide: Follow10ProgressGuide}) {
const {_} = useLingui()
const control = Dialog.useDialogControl()
const {gtMobile} = useBreakpoints()
const {height: minHeight} = useWindowDimensions()
return (
<>
<Button
label={_(msg`Find people to follow`)}
onPress={control.open}
size={gtMobile ? 'small' : 'large'}
color="primary"
variant="solid">
<ButtonIcon icon={PersonGroupIcon} />
<ButtonText>
<Trans>Find people to follow</Trans>
</ButtonText>
</Button>
<Dialog.Outer control={control} nativeOptions={{minHeight}}>
<Dialog.Handle />
<DialogInner guide={guide} />
</Dialog.Outer>
</>
)
}
// Fine to keep this top-level.
let lastSelectedInterest = ''
let lastSearchText = ''
function DialogInner({guide}: {guide: Follow10ProgressGuide}) {
const {_} = useLingui()
const interestsDisplayNames = useInterestsDisplayNames()
const {data: preferences} = usePreferencesQuery()
const personalizedInterests = preferences?.interests?.tags
const interests = Object.keys(interestsDisplayNames)
.sort(boostInterests(popularInterests))
.sort(boostInterests(personalizedInterests))
const [selectedInterest, setSelectedInterest] = useState(
() =>
lastSelectedInterest ||
(personalizedInterests && interests.includes(personalizedInterests[0])
? personalizedInterests[0]
: interests[0]),
)
const [searchText, setSearchText] = useState(lastSearchText)
const moderationOpts = useModerationOpts()
const listRef = useRef<ListMethods>(null)
const inputRef = useRef<TextInput>(null)
const [headerHeight, setHeaderHeight] = useState(0)
const {currentAccount} = useSession()
const [suggestedAccounts, setSuggestedAccounts] = useState<
Map<string, AppBskyActorDefs.ProfileView[]>
>(() => new Map())
useEffect(() => {
lastSearchText = searchText
lastSelectedInterest = selectedInterest
}, [searchText, selectedInterest])
const query = searchText || selectedInterest
const {
data: searchResults,
isFetching,
error,
isError,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useActorSearchPaginated({
query,
})
const hasSearchText = !!searchText
const items = useMemo(() => {
const results = searchResults?.pages.flatMap(r => r.actors)
let _items: Item[] = []
const seen = new Set<string>()
if (isError) {
_items.push({
type: 'empty',
key: 'empty',
message: _(msg`We're having network issues, try again`),
})
} else if (results) {
// First pass: search results
for (const profile of results) {
if (profile.did === currentAccount?.did) continue
if (profile.viewer?.following) continue
// my sincere apologies to Jake Gold - your bio is too keyword-filled and
// your page-rank too high, so you're at the top of half the categories -sfn
if (
!hasSearchText &&
profile.did === 'did:plc:tpg43qhh4lw4ksiffs4nbda3' &&
// constrain to 'tech'
selectedInterest !== 'tech'
) {
continue
}
seen.add(profile.did)
_items.push({
type: 'profile',
// Don't share identity across tabs or typing attempts
key: query + ':' + profile.did,
profile,
isSuggestion: false,
})
}
// Second pass: suggestions
_items = _items.flatMap(item => {
if (item.type !== 'profile') {
return item
}
const suggestions = suggestedAccounts.get(item.profile.did)
if (!suggestions) {
return item
}
const itemWithSuggestions = [item]
for (const suggested of suggestions) {
if (seen.has(suggested.did)) {
// Skip search results from previous step or already seen suggestions
continue
}
seen.add(suggested.did)
itemWithSuggestions.push({
type: 'profile',
key: suggested.did,
profile: suggested,
isSuggestion: true,
})
if (itemWithSuggestions.length === 1 + 3) {
break
}
}
return itemWithSuggestions
})
} else {
const placeholders: Item[] = Array(10)
.fill(0)
.map((__, i) => ({
type: 'placeholder',
key: i + '',
}))
_items.push(...placeholders)
}
return _items
}, [
_,
searchResults,
isError,
currentAccount?.did,
hasSearchText,
selectedInterest,
suggestedAccounts,
query,
])
if (searchText && !isFetching && !items.length && !isError) {
items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
}
const renderItems = useCallback(
({item, index}: {item: Item; index: number}) => {
switch (item.type) {
case 'profile': {
return (
<FollowProfileCard
profile={item.profile}
isSuggestion={item.isSuggestion}
moderationOpts={moderationOpts!}
setSuggestedAccounts={setSuggestedAccounts}
noBorder={index === 0}
/>
)
}
case 'placeholder': {
return <ProfileCardSkeleton key={item.key} />
}
case 'empty': {
return <Empty key={item.key} message={item.message} />
}
default:
return null
}
},
[moderationOpts],
)
const onSelectTab = useCallback(
(interest: string) => {
setSelectedInterest(interest)
inputRef.current?.clear()
setSearchText('')
listRef.current?.scrollToOffset({
offset: 0,
animated: false,
})
},
[setSelectedInterest, setSearchText],
)
const listHeader = (
<Header
guide={guide}
inputRef={inputRef}
listRef={listRef}
searchText={searchText}
onSelectTab={onSelectTab}
setHeaderHeight={setHeaderHeight}
setSearchText={setSearchText}
interests={interests}
selectedInterest={selectedInterest}
interestsDisplayNames={interestsDisplayNames}
/>
)
const onEndReached = useCallback(async () => {
if (isFetchingNextPage || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more people to follow', {message: err})
}
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
return (
<Dialog.InnerFlatList
ref={listRef}
data={items}
renderItem={renderItems}
ListHeaderComponent={listHeader}
stickyHeaderIndices={[0]}
keyExtractor={(item: Item) => item.key}
style={[
a.px_0,
web([a.py_0, {height: '100vh', maxHeight: 600}]),
native({height: '100%'}),
]}
webInnerContentContainerStyle={a.py_0}
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
keyboardDismissMode="on-drag"
scrollIndicatorInsets={{top: headerHeight}}
initialNumToRender={8}
maxToRenderPerBatch={8}
onEndReached={onEndReached}
itemLayoutAnimation={LinearTransition}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
}
/>
)
}
let Header = ({
guide,
inputRef,
listRef,
searchText,
onSelectTab,
setHeaderHeight,
setSearchText,
interests,
selectedInterest,
interestsDisplayNames,
}: {
guide: Follow10ProgressGuide
inputRef: React.RefObject<TextInput>
listRef: React.RefObject<ListMethods>
onSelectTab: (v: string) => void
searchText: string
setHeaderHeight: (v: number) => void
setSearchText: (v: string) => void
interests: string[]
selectedInterest: string
interestsDisplayNames: Record<string, string>
}): React.ReactNode => {
const t = useTheme()
const control = Dialog.useDialogContext()
return (
<View
onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}
style={[
a.relative,
web(a.pt_lg),
native(a.pt_4xl),
a.pb_xs,
a.border_b,
t.atoms.border_contrast_low,
t.atoms.bg,
]}>
<HeaderTop guide={guide} />
<View style={[web(a.pt_xs), a.pb_xs]}>
<SearchInput
inputRef={inputRef}
defaultValue={searchText}
onChangeText={text => {
setSearchText(text)
listRef.current?.scrollToOffset({offset: 0, animated: false})
}}
onEscape={control.close}
/>
<Tabs
onSelectTab={onSelectTab}
interests={interests}
selectedInterest={selectedInterest}
hasSearchText={!!searchText}
interestsDisplayNames={interestsDisplayNames}
/>
</View>
</View>
)
}
Header = memo(Header)
function HeaderTop({guide}: {guide: Follow10ProgressGuide}) {
const {_} = useLingui()
const t = useTheme()
const control = Dialog.useDialogContext()
return (
<View
style={[
a.px_lg,
a.relative,
a.flex_row,
a.justify_between,
a.align_center,
]}>
<Text
style={[
a.z_10,
a.text_lg,
a.font_heavy,
a.leading_tight,
t.atoms.text_contrast_high,
]}>
<Trans>Find people to follow</Trans>
</Text>
<View style={isWeb && {paddingRight: 36}}>
<ProgressGuideTask
current={guide.numFollows + 1}
total={10 + 1}
title={`${guide.numFollows} / 10`}
tabularNumsTitle
/>
</View>
{isWeb ? (
<Button
label={_(msg`Close`)}
size="small"
shape="round"
variant={isWeb ? 'ghost' : 'solid'}
color="secondary"
style={[
a.absolute,
a.z_20,
web({right: -4}),
native({right: 0}),
native({height: 32, width: 32, borderRadius: 16}),
]}
onPress={() => control.close()}>
<ButtonIcon icon={X} size="md" />
</Button>
) : null}
</View>
)
}
let Tabs = ({
onSelectTab,
interests,
selectedInterest,
hasSearchText,
interestsDisplayNames,
}: {
onSelectTab: (tab: string) => void
interests: string[]
selectedInterest: string
hasSearchText: boolean
interestsDisplayNames: Record<string, string>
}): React.ReactNode => {
const listRef = useRef<ScrollView>(null)
const [scrollX, setScrollX] = useState(0)
const [totalWidth, setTotalWidth] = useState(0)
const pendingTabOffsets = useRef<{x: number; width: number}[]>([])
const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([])
const onInitialLayout = useNonReactiveCallback(() => {
const index = interests.indexOf(selectedInterest)
scrollIntoViewIfNeeded(index)
})
useEffect(() => {
if (tabOffsets) {
onInitialLayout()
}
}, [tabOffsets, onInitialLayout])
function scrollIntoViewIfNeeded(index: number) {
const btnLayout = tabOffsets[index]
if (!btnLayout) return
const viewportLeftEdge = scrollX
const viewportRightEdge = scrollX + totalWidth
const shouldScrollToLeftEdge = viewportLeftEdge > btnLayout.x
const shouldScrollToRightEdge =
viewportRightEdge < btnLayout.x + btnLayout.width
if (shouldScrollToLeftEdge) {
listRef.current?.scrollTo({
x: btnLayout.x - tokens.space.lg,
animated: true,
})
} else if (shouldScrollToRightEdge) {
listRef.current?.scrollTo({
x: btnLayout.x - totalWidth + btnLayout.width + tokens.space.lg,
animated: true,
})
}
}
function handleSelectTab(index: number) {
const tab = interests[index]
onSelectTab(tab)
scrollIntoViewIfNeeded(index)
}
function handleTabLayout(index: number, x: number, width: number) {
if (!tabOffsets.length) {
pendingTabOffsets.current[index] = {x, width}
if (pendingTabOffsets.current.length === interests.length) {
setTabOffsets(pendingTabOffsets.current)
}
}
}
return (
<ScrollView
ref={listRef}
horizontal
contentContainerStyle={[a.gap_sm, a.px_lg]}
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToOffsets={
tabOffsets.length === interests.length
? tabOffsets.map(o => o.x - tokens.space.xl)
: undefined
}
onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
scrollEventThrottle={200} // big throttle
onScroll={evt => setScrollX(evt.nativeEvent.contentOffset.x)}>
{interests.map((interest, i) => {
const active = interest === selectedInterest && !hasSearchText
return (
<Tab
key={interest}
onSelectTab={handleSelectTab}
active={active}
index={i}
interest={interest}
interestsDisplayName={interestsDisplayNames[interest]}
onLayout={handleTabLayout}
/>
)
})}
</ScrollView>
)
}
Tabs = memo(Tabs)
let Tab = ({
onSelectTab,
interest,
active,
index,
interestsDisplayName,
onLayout,
}: {
onSelectTab: (index: number) => void
interest: string
active: boolean
index: number
interestsDisplayName: string
onLayout: (index: number, x: number, width: number) => void
}): React.ReactNode => {
const {_} = useLingui()
const activeText = active ? _(msg` (active)`) : ''
return (
<View
key={interest}
onLayout={e =>
onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width)
}>
<Button
label={_(msg`Search for "${interestsDisplayName}"${activeText}`)}
variant={active ? 'solid' : 'outline'}
color={active ? 'primary' : 'secondary'}
size="small"
onPress={() => onSelectTab(index)}>
<ButtonIcon icon={SearchIcon} />
<ButtonText>{interestsDisplayName}</ButtonText>
</Button>
</View>
)
}
Tab = memo(Tab)
let FollowProfileCard = ({
profile,
moderationOpts,
isSuggestion,
setSuggestedAccounts,
noBorder,
}: {
profile: AppBskyActorDefs.ProfileView
moderationOpts: ModerationOpts
isSuggestion: boolean
setSuggestedAccounts: (
updater: (
v: Map<string, AppBskyActorDefs.ProfileView[]>,
) => Map<string, AppBskyActorDefs.ProfileView[]>,
) => void
noBorder?: boolean
}): React.ReactNode => {
const [hasFollowed, setHasFollowed] = useState(false)
const followupSuggestion = useSuggestedFollowsByActorQuery({
did: profile.did,
enabled: hasFollowed,
})
const candidates = followupSuggestion.data?.suggestions
useEffect(() => {
// TODO: Move out of effect.
if (hasFollowed && candidates && candidates.length > 0) {
setSuggestedAccounts(suggestions => {
const newSuggestions = new Map(suggestions)
newSuggestions.set(profile.did, candidates)
return newSuggestions
})
}
}, [hasFollowed, profile.did, candidates, setSuggestedAccounts])
return (
<LayoutAnimationConfig skipEntering={!isSuggestion}>
<Animated.View entering={native(ZoomInEasyDown)}>
<FollowProfileCardInner
profile={profile}
moderationOpts={moderationOpts}
onFollow={() => setHasFollowed(true)}
noBorder={noBorder}
/>
</Animated.View>
</LayoutAnimationConfig>
)
}
FollowProfileCard = memo(FollowProfileCard)
function FollowProfileCardInner({
profile,
moderationOpts,
onFollow,
noBorder,
}: {
profile: AppBskyActorDefs.ProfileView
moderationOpts: ModerationOpts
onFollow?: () => void
noBorder?: boolean
}) {
const control = Dialog.useDialogContext()
const t = useTheme()
return (
<ProfileCard.Link
profile={profile}
style={[a.flex_1]}
onPress={() => control.close()}>
{({hovered, pressed}) => (
<CardOuter
style={[
a.flex_1,
noBorder && a.border_t_0,
(hovered || pressed) && t.atoms.border_contrast_high,
]}>
<ProfileCard.Outer>
<ProfileCard.Header>
<ProfileCard.Avatar
profile={profile}
moderationOpts={moderationOpts}
/>
<ProfileCard.NameAndHandle
profile={profile}
moderationOpts={moderationOpts}
/>
<ProfileCard.FollowButton
profile={profile}
moderationOpts={moderationOpts}
logContext="PostOnboardingFindFollows"
shape="round"
onPress={onFollow}
colorInverted
/>
</ProfileCard.Header>
<ProfileCard.Description profile={profile} numberOfLines={2} />
</ProfileCard.Outer>
</CardOuter>
)}
</ProfileCard.Link>
)
}
function CardOuter({
children,
style,
}: {children: React.ReactNode | React.ReactNode[]} & ViewStyleProp) {
const t = useTheme()
return (
<View
style={[
a.w_full,
a.py_md,
a.px_lg,
a.border_t,
t.atoms.border_contrast_low,
style,
]}>
{children}
</View>
)
}
function SearchInput({
onChangeText,
onEscape,
inputRef,
defaultValue,
}: {
onChangeText: (text: string) => void
onEscape: () => void
inputRef: React.RefObject<TextInput>
defaultValue: string
}) {
const t = useTheme()
const {_} = useLingui()
const {
state: hovered,
onIn: onMouseEnter,
onOut: onMouseLeave,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const interacted = hovered || focused
return (
<View
{...web({
onMouseEnter,
onMouseLeave,
})}
style={[a.flex_row, a.align_center, a.gap_sm, a.px_lg, a.py_xs]}>
<SearchIcon
size="md"
fill={interacted ? t.palette.primary_500 : t.palette.contrast_300}
/>
<TextInput
ref={inputRef}
placeholder={_(msg`Search`)}
defaultValue={defaultValue}
onChangeText={onChangeText}
onFocus={onFocus}
onBlur={onBlur}
style={[a.flex_1, a.py_md, a.text_md, t.atoms.text]}
placeholderTextColor={t.palette.contrast_500}
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
returnKeyType="search"
clearButtonMode="while-editing"
maxLength={50}
onKeyPress={({nativeEvent}) => {
if (nativeEvent.key === 'Escape') {
onEscape()
}
}}
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
accessibilityLabel={_(msg`Search profiles`)}
accessibilityHint={_(msg`Search profiles`)}
/>
</View>
)
}
function ProfileCardSkeleton() {
const t = useTheme()
return (
<View
style={[
a.flex_1,
a.py_md,
a.px_lg,
a.gap_md,
a.align_center,
a.flex_row,
]}>
<View
style={[
a.rounded_full,
{width: 42, height: 42},
t.atoms.bg_contrast_25,
]}
/>
<View style={[a.flex_1, a.gap_sm]}>
<View
style={[
a.rounded_xs,
{width: 80, height: 14},
t.atoms.bg_contrast_25,
]}
/>
<View
style={[
a.rounded_xs,
{width: 120, height: 10},
t.atoms.bg_contrast_25,
]}
/>
</View>
</View>
)
}
function Empty({message}: {message: string}) {
const t = useTheme()
return (
<View style={[a.p_lg, a.py_xl, a.align_center, a.gap_md]}>
<Text style={[a.text_sm, a.italic, t.atoms.text_contrast_high]}>
{message}
</Text>
<Text style={[a.text_xs, t.atoms.text_contrast_low]}>(°°) </Text>
</View>
)
}
function boostInterests(boosts?: string[]) {
return (_a: string, _b: string) => {
const indexA = boosts?.indexOf(_a) ?? -1
const indexB = boosts?.indexOf(_b) ?? -1
const rankA = indexA === -1 ? Infinity : indexA
const rankB = indexB === -1 ? Infinity : indexB
return rankA - rankB
}
}
+31 -13
View File
@@ -10,12 +10,15 @@ import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
import {Text} from '#/components/Typography'
import {FollowDialog} from './FollowDialog'
import {ProgressGuideTask} from './Task'
export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
const t = useTheme()
const {_} = useLingui()
const guide = useProgressGuide('like-10-and-follow-7')
const followProgressGuide = useProgressGuide('follow-10')
const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7')
const guide = followProgressGuide || followAndLikeProgressGuide
const {endProgressGuide} = useProgressGuideControls()
if (guide) {
@@ -41,18 +44,33 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
<ButtonIcon icon={Times} size="sm" />
</Button>
</View>
<ProgressGuideTask
current={guide.numLikes + 1}
total={10 + 1}
title={_(msg`Like 10 posts`)}
subtitle={_(msg`Teach our algorithm what you like`)}
/>
<ProgressGuideTask
current={guide.numFollows + 1}
total={7 + 1}
title={_(msg`Follow 7 accounts`)}
subtitle={_(msg`Bluesky is better with friends!`)}
/>
{guide.guide === 'follow-10' && (
<>
<ProgressGuideTask
current={guide.numFollows + 1}
total={10 + 1}
title={_(msg`Follow 10 accounts`)}
subtitle={_(msg`Bluesky is better with friends!`)}
/>
<FollowDialog guide={guide} />
</>
)}
{guide.guide === 'like-10-and-follow-7' && (
<>
<ProgressGuideTask
current={guide.numLikes + 1}
total={10 + 1}
title={_(msg`Like 10 posts`)}
subtitle={_(msg`Teach our algorithm what you like`)}
/>
<ProgressGuideTask
current={guide.numFollows + 1}
total={7 + 1}
title={_(msg`Follow 7 accounts`)}
subtitle={_(msg`Bluesky is better with friends!`)}
/>
</>
)}
</View>
)
}
+12 -2
View File
@@ -10,11 +10,13 @@ export function ProgressGuideTask({
total,
title,
subtitle,
tabularNumsTitle,
}: {
current: number
total: number
title: string
subtitle?: string
tabularNumsTitle?: boolean
}) {
const t = useTheme()
@@ -33,8 +35,16 @@ export function ProgressGuideTask({
/>
)}
<View style={[a.flex_col, a.gap_2xs, {marginTop: -2}]}>
<Text style={[a.text_sm, a.font_bold, a.leading_tight]}>{title}</Text>
<View style={[a.flex_col, a.gap_2xs, subtitle && {marginTop: -2}]}>
<Text
style={[
a.text_sm,
a.font_bold,
a.leading_tight,
tabularNumsTitle && {fontVariant: ['tabular-nums']},
]}>
{title}
</Text>
{subtitle && (
<Text
style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_tight]}>
@@ -63,6 +63,7 @@ export function SearchablePeopleList({
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const control = Dialog.useDialogContext()
const [headerHeight, setHeaderHeight] = useState(0)
const listRef = useRef<ListMethods>(null)
const {currentAccount} = useSession()
const inputRef = useRef<TextInput>(null)
@@ -237,6 +238,7 @@ export function SearchablePeopleList({
const listHeader = useMemo(() => {
return (
<View
onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}
style={[
a.relative,
web(a.pt_lg),
@@ -315,6 +317,7 @@ export function SearchablePeopleList({
]}
webInnerContentContainerStyle={a.py_0}
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
scrollIndicatorInsets={{top: headerHeight}}
keyboardDismissMode="on-drag"
/>
)
+9 -11
View File
@@ -26,16 +26,16 @@ import {Text} from '#/components/Typography'
export function TrendingInterstitial() {
const {enabled} = useTrendingConfig()
const {trendingDiscoverHidden} = useTrendingSettings()
return !enabled ? null : trendingDiscoverHidden ? null : <Inner />
const {trendingDisabled} = useTrendingSettings()
return enabled && !trendingDisabled ? <Inner /> : null
}
export function Inner() {
const t = useTheme()
const {_} = useLingui()
const gutters = useGutters(['base'])
const gutters = useGutters(['wide', 'base'])
const trendingPrompt = Prompt.usePromptControl()
const {setTrendingDiscoverHidden} = useTrendingSettingsApi()
const {setTrendingDisabled} = useTrendingSettingsApi()
const {data: trending, error, isLoading} = useTrendingTopics()
const noTopics = !isLoading && !error && !trending?.topics?.length
@@ -63,7 +63,7 @@ export function Inner() {
</View>
<Button
label={_(msg`Hide trending topics from your feed`)}
label={_(msg`Hide trending topics`)}
size="tiny"
variant="outline"
color="secondary"
@@ -73,7 +73,7 @@ export function Inner() {
</Button>
</View>
<View style={[a.flex_row, a.flex_wrap, a.gap_sm]}>
<View style={[a.flex_row, a.flex_wrap, {rowGap: 8, columnGap: 6}]}>
{isLoading ? (
Array(TRENDING_TOPICS_COUNT)
.fill(0)
@@ -101,12 +101,10 @@ export function Inner() {
<Prompt.Basic
control={trendingPrompt}
title={_(msg`Hide trending topics in your feed?`)}
description={_(
msg`This is a device setting, and will apply to all accounts on this device. You can update this later from your settings.`,
)}
title={_(msg`Hide trending topics?`)}
description={_(msg`You can update this later from your settings.`)}
confirmButtonCta={_(msg`Hide`)}
onConfirm={() => setTrendingDiscoverHidden(true)}
onConfirm={() => setTrendingDisabled(true)}
/>
</View>
)
+1
View File
@@ -1,5 +1,6 @@
import {init} from '@bitdrift/react-native'
import {Statsig} from 'statsig-react-native-expo'
export {debug, error, info, warn} from '@bitdrift/react-native'
import {initPromise} from './statsig/statsig'
+4
View File
@@ -0,0 +1,4 @@
export function debug() {}
export function error() {}
export function info() {}
export function warn() {}
-10
View File
@@ -23,16 +23,6 @@ export const STARTER_PACK_MAX_SIZE = 150
// -prf
export const JOINED_THIS_WEEK = 2880000 // estimate as of 11/26/24
export const DISCOVER_DEBUG_DIDS: Record<string, true> = {
'did:plc:oisofpd7lj26yvgiivf3lxsi': true, // hailey.at
'did:plc:fpruhuo22xkm5o7ttr2ktxdo': true, // danabra.mov
'did:plc:p2cp5gopk7mgjegy6wadk3ep': true, // samuel.bsky.team
'did:plc:ragtjsm2j2vknwkz3zp4oxrd': true, // pfrazee.com
'did:plc:vpkhqolt662uhesyj6nxm7ys': true, // why.bsky.team
'did:plc:3jpt2mvvsumj2r7eqk4gzzjz': true, // esb.lol
'did:plc:vjug55kidv6sye7ykr5faxxn': true, // emilyliu.me
}
const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new`
export function FEEDBACK_FORM_URL({
email,
+2
View File
@@ -162,6 +162,7 @@ export type LogEvents = {
| 'StarterPackProfilesList'
| 'FeedInterstitial'
| 'ProfileHeaderSuggestedFollows'
| 'PostOnboardingFindFollows'
}
'profile:unfollow': {
logContext:
@@ -177,6 +178,7 @@ export type LogEvents = {
| 'StarterPackProfilesList'
| 'FeedInterstitial'
| 'ProfileHeaderSuggestedFollows'
| 'PostOnboardingFindFollows'
}
'chat:create': {
logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
+3 -2
View File
@@ -1,6 +1,7 @@
export type Gate =
// Keep this alphabetic please.
| 'debug_show_feedcontext' // DISABLED DUE TO EME
| 'post_feed_lang_window' // DISABLED DUE TO EME
| 'debug_show_feedcontext'
| 'debug_subscriptions'
| 'new_postonboarding'
| 'remove_show_latest_button'
| 'trending_topics_beta'
+23 -3
View File
@@ -5,6 +5,7 @@ import {sha256} from 'js-sha256'
import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
import * as bitdrift from '#/lib/bitdrift'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted'
@@ -97,19 +98,38 @@ export function logEvent<E extends keyof LogEvents>(
rawMetadata: LogEvents[E] & FlatJSONRecord,
) {
try {
const fullMetadata = {
...rawMetadata,
} as Record<string, string> // Statsig typings are unnecessarily strict here.
const fullMetadata = toStringRecord(rawMetadata)
fullMetadata.routeName = getCurrentRouteName() ?? '(Uninitialized)'
if (Statsig.initializeCalled()) {
Statsig.logEvent(eventName, null, fullMetadata)
}
// Intentionally bypass the logger abstraction to log rich objects.
console.groupCollapsed(eventName)
console.log(fullMetadata)
console.groupEnd()
bitdrift.info(eventName, fullMetadata)
} catch (e) {
// A log should never interrupt the calling code, whatever happens.
logger.error('Failed to log an event', {message: e})
}
}
function toStringRecord<E extends keyof LogEvents>(
metadata: LogEvents[E] & FlatJSONRecord,
): Record<string, string> {
const record: Record<string, string> = {}
for (let key in metadata) {
if (metadata.hasOwnProperty(key)) {
if (typeof metadata[key] === 'string') {
record[key] = metadata[key]
} else {
record[key] = JSON.stringify(metadata[key])
}
}
}
return record
}
// We roll our own cache in front of Statsig because it is a singleton
// and it's been difficult to get it to behave in a predictable way.
// Our own cache ensures consistent evaluation within a single session.
+3 -3
View File
@@ -3613,7 +3613,7 @@ msgstr "Escribe la tuya clau"
#~ msgstr "Escribe lo tuyo furnidor d'aloch preferiu"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Escribe lo tuyo identificador"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8877,7 +8877,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "La tuya canal de Seguindo ye vueda! Sigue a mas usuarios pa veyer las suyas publicacions aquí."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Lo tuyo identificador completo será"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8921,5 +8921,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "Lo tuyo reporte s'ha ninviau a lo servicio de moderación de Bluesky"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Lo tuyo identificador"
+3 -3
View File
@@ -3509,7 +3509,7 @@ msgstr ""
#~ msgstr ""
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr ""
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8543,7 +8543,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "¡El feed siguiente ta baleru! Sigui a más usuarios pa ver qué pasó."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "L'identificador completu va ser"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8587,5 +8587,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "L'informe unvióse al serviciu de moderación de Bluesky"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr ""
+3 -3
View File
@@ -4268,7 +4268,7 @@ msgstr "Introdueix la teva contrasenya"
#~ msgstr "Introdueix el teu proveïdor d'allotjament preferit"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Introdueix el teu identificador d'usuari"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -10404,7 +10404,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "El teu canal de seguint està buit! Segueix a més usuaris per a saber què està passant."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "El teu identificador complet serà"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -10458,5 +10458,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "El teu informe s'enviarà al servei de moderació de Bluesky"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "El teu identificador d'usuari"
+3 -3
View File
@@ -3742,7 +3742,7 @@ msgstr "Gib dein Passwort ein"
#~ msgstr ""
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Gib deinen Handle ein"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -9663,7 +9663,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Dein Following-Feed ist leer! Folge mehr Benutzern, um auf dem Laufenden zu bleiben."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Dein vollständiger Handle lautet"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -9707,5 +9707,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr ""
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Dein Benutzerhandle"
+3 -3
View File
@@ -3509,7 +3509,7 @@ msgstr ""
#~ msgstr ""
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr ""
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8543,7 +8543,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr ""
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr ""
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8587,5 +8587,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr ""
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr ""
+3 -3
View File
@@ -3509,7 +3509,7 @@ msgstr ""
#~ msgstr ""
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr ""
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8543,7 +8543,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr ""
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr ""
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8587,5 +8587,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr ""
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr ""
+3 -3
View File
@@ -3896,7 +3896,7 @@ msgstr "Introduce tu contraseña"
#~ msgstr "Introduce tu proveedor de alojamiento preferido"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Introduce tu nombre de usuario"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -9518,7 +9518,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "¡Tu feed de Siguiendo esta vacío! Sigue a más usuarios para ver sus posts aquí."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Tu nombre de usuario completo será"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -9562,5 +9562,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "Tu reporte ha sido enviado al servicio de moderación de Bluesky"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Tu nombre de usuario"
+3 -3
View File
@@ -3970,7 +3970,7 @@ msgstr "Syötä salasanasi"
#~ msgstr "Syötä haluamasi palveluntarjoaja"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Syötä käyttäjätunnuksesi"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -9627,7 +9627,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Seuraamiesi syöte on tyhjä! Seuraa lisää käyttäjiä nähdäksesi, mitä tapahtuu."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Käyttäjätunnuksesi tulee olemaan"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -9671,5 +9671,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr ""
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Käyttäjätunnuksesi"
+3 -3
View File
@@ -3501,7 +3501,7 @@ msgstr "Entrez votre mot de passe"
#~ msgstr "Entrez votre hébergeur préféré"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Entrez votre pseudo"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8519,7 +8519,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Votre fil dactu des comptes suivis est vide ! Suivez plus de comptes pour voir ce qui se passe."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Votre nom complet sera"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8563,5 +8563,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "Votre rapport sera envoyé au Service de Modération de Bluesky"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Votre pseudo"
+3 -3
View File
@@ -3508,7 +3508,7 @@ msgstr "Cuir isteach do phasfhocal"
#~ msgstr "Cuir isteach an soláthraí óstála is fearr leat"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Cuir isteach do leasainm"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8542,7 +8542,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Tá an fotha de na daoine a leanann tú folamh! Lean tuilleadh úsáideoirí le feiceáil céard atá ar siúl."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Do leasainm iomlán anseo:"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8586,5 +8586,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "Seolfar do thuairisc go dtí Seirbhís Modhnóireachta Bluesky"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Do leasainm"
+3 -3
View File
@@ -3896,7 +3896,7 @@ msgstr "Introduce o teu contrasinal"
#~ msgstr "Introduce o teu proveedor de aloxamento preferido"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Introduce o teu alcume"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -9523,7 +9523,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "A seguinte canle está baleira! Sigue a máis persoas para ver o que está a acontecer."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "O teu alcume completo será"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -9567,5 +9567,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "O teu informe enviarase ao Servizo de moderación de Bluesky"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "O teu alcume"
+3 -3
View File
@@ -3780,7 +3780,7 @@ msgstr "अपना पासवर्ड दर्ज करें"
#~ msgstr "अपना पसंदीदा होस्टिंग प्रदाता दर्ज करें"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "अपना उपयोगकर्ता हैंडल दर्ज करें"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -9026,7 +9026,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "आपका फ़ॉलोइंग फ़ीड खाली है! क्या चल रहा है जानने के लिए और उपयोगकर्ताओं को फ़ॉलो करें।"
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "आपका पूरा हैंडल होगा"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -9070,5 +9070,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "आपके शिकायत को Bluesky मॉडरेशन सेवा को भेजा जाएगा।"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "आपका उपयोगकर्ता हैंडल"
+3 -3
View File
@@ -3465,7 +3465,7 @@ msgstr "Jelszó megadása"
#~ msgstr "Kívánt tárhelyszolgáltató megadása"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Felhasználónév megadása"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8439,7 +8439,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "A Követett hírfolyamod üres. Kövess több felhasználót, hogy lásd, mi történik!"
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "A teljes felhasználóneved:"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8482,5 +8482,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "Ez a jelentés a Bluesky moderálási szolgáltatásának lesz elküldve"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Saját felhasználónév"
+3 -3
View File
@@ -4007,7 +4007,7 @@ msgstr "Masukkan kata sandi Anda"
#~ msgstr "Masukkan penyedia hosting pilihan Anda"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Masukkan panggilan Anda"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -9678,7 +9678,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Feed mengikuti Anda kosong! Ikuti lebih banyak pengguna untuk melihat apa yang terjadi."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Panggilan lengkap Anda akan menjadi"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -9722,5 +9722,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "Laporan Anda akan dikirim ke Layanan Moderasi Bluesky"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Panggilan Anda"
+3 -3
View File
@@ -3318,7 +3318,7 @@ msgid "Input your password"
msgstr "Inserisci la tua password"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Inserisci il tuo nome utente"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8016,7 +8016,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Il tuo feed seguente è vuoto! Segui più utenti per vedere cosa sta succedendo."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Il tuo nome utente completo sarà"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8056,5 +8056,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "La tua segnalazione verrà inviata al Servizio Moderazione di Bluesky"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Il tuo nome utente"
+3 -3
View File
@@ -3317,7 +3317,7 @@ msgid "Input your password"
msgstr "あなたのパスワードを入力"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "あなたのユーザーハンドルを入力"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8047,7 +8047,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Followingフィードは空です!もっと多くのユーザーをフォローして、近況を確認しましょう。"
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "フルハンドルは"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8087,5 +8087,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "あなたの報告はBluesky Moderation Serviceに送られます"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "あなたのユーザーハンドル"
+3 -3
View File
@@ -3317,7 +3317,7 @@ msgid "Input your password"
msgstr "비밀번호를 입력합니다"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "사용자 핸들을 입력합니다"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8019,7 +8019,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "팔로우 중 피드가 비어 있습니다. 더 많은 사용자를 팔로우하여 무슨 일이 일어나고 있는지 확인하세요."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "내 전체 핸들:"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8059,5 +8059,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "신고가 Bluesky Moderation Service로 보내집니다."
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "내 사용자 핸들"
+3 -3
View File
@@ -3496,7 +3496,7 @@ msgstr "Vul je wachtwoord in"
#~ msgstr "Vul je voorkeurshostingprovider in"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Vul je gebruikershandle in."
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8506,7 +8506,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Je volgende feed is leeg! Volg meer gebruikers om te zien wat er gebeurt."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Je volledige handle wordt"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8550,5 +8550,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "Je melding wordt verzonden naar de Bluesky-moderatieservice"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Je gebruikershandle"
+3 -3
View File
@@ -3317,7 +3317,7 @@ msgid "Input your password"
msgstr "Wpisz hasło"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Wpisz nazwę użytkownika"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8031,7 +8031,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Twój kanał osób obserwowanych jest pusty! Zaobserwuj trochę więcej osób."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Twoją pełna nazwa będzie"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8071,5 +8071,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "Zgłoszenie będzie wysłane do usługi moderacji Bluesky"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Twoja nazwa użytkownika"
+3 -3
View File
@@ -4003,7 +4003,7 @@ msgstr "Insira sua senha"
#~ msgstr "Insira seu provedor de hospedagem"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Insira o usuário"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -9667,7 +9667,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Seu feed inicial está vazio! Siga mais usuários para acompanhar o que está acontecendo."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Seu identificador completo será"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -9711,5 +9711,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "Sua denúncia será enviada para o serviço de moderação do Bluesky"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Seu identificador de usuário"
+3 -3
View File
@@ -3578,7 +3578,7 @@ msgstr "Введите ваш пароль"
#~ msgstr "Введите желаемого хостинг-провайдера"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Введите ваш псевдоним"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8768,7 +8768,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Ваша домашняя лента пуста! Подпишитесь на больше пользователей чтобы получать больше постов."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Ваш полный псевдоним будет"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8812,5 +8812,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "Ваш отчет будет отправлен в Службу Модерации Bluesky."
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Ваш псевдоним"
+3 -3
View File
@@ -3994,7 +3994,7 @@ msgstr "ป้อนรหัสผ่านของคุณ"
#~ msgstr "กรอกผู้ให้บริการโฮสติ้งที่คุณต้องการ"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "กรอกชื่อผู้ใช้ของคุณ"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -9665,7 +9665,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "ฟีดผู้ติดตามของคุณว่างเปล่า! ติดตามผู้ใช้เพิ่มเติมเพื่อดูความเคลื่อนไหว"
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "ชื่อผู้ใช้เต็มของคุณจะเป็น"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -9709,5 +9709,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "รายงานของคุณจะถูกส่งไปยัง Bluesky Moderation Service"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "แฮนด์เดิลผู้ใช้ของคุณ"
+3 -3
View File
@@ -4211,7 +4211,7 @@ msgstr "Şifrenizi girin"
#~ msgstr ""
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Kullanıcı adınızı girin"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -10212,7 +10212,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Takip ettiğiniz besleme boş! Neler olduğunu görmek için daha fazla kullanıcı takip edin."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Tam kullanıcı adınız"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -10261,5 +10261,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr ""
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Kullanıcı adınız"
+3 -3
View File
@@ -4007,7 +4007,7 @@ msgstr "Введіть ваш пароль"
#~ msgstr "Введіть бажаного хостинг-провайдера"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Введіть ваш псевдонім"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -9678,7 +9678,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Ваша домашня стрічка порожня! Підпишіться на більше користувачів щоб отримувати більше постів."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Ваш повний псевдонім буде"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -9722,5 +9722,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr ""
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Ваш псевдонім"
+3 -3
View File
@@ -3509,7 +3509,7 @@ msgstr "Nhập mật khẩu của bạn"
#~ msgstr "Nhập nhà cung cấp lưu trữ theo lựa chọn của bạn"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "Nhập tên người dùng của bạn"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8543,7 +8543,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "Bảng tin theo dõi còn trống! Hãy theo dõi thêm người dùng để biết có gì đang diễn ra."
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "Tên người dùng đầy đủ của bạn sẽ là"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8587,5 +8587,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "Báo cáo của bạn sẽ được gởi đến dịch vụ kiểm duyệt của Bluesky"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "Tên người dùng của bạn"
+3 -3
View File
@@ -3314,7 +3314,7 @@ msgid "Input your password"
msgstr "输入你的密码"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "输入你的账户代码"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8004,7 +8004,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "你的“Following”动态源是空的!关注更多用户去看看他们发了什么。"
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "你的完整账户代码将修改为"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8044,5 +8044,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "你的举报将发送至 Bluesky 内容审核服务"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "你的账户代码"
+3 -3
View File
@@ -3314,7 +3314,7 @@ msgid "Input your password"
msgstr "輸入你嘅密碼"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "輸入你嘅帳號頭銜"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8004,7 +8004,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "你嘅「Following」動態源得個吉!跟多啲用戶睇吓發生緊啲咩事。"
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "你嘅完整帳號頭銜會係"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8044,5 +8044,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "你嘅上報會傳送去 Bluesky 審核服務"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "你嘅帳號頭銜"
+3 -3
View File
@@ -3314,7 +3314,7 @@ msgid "Input your password"
msgstr "輸入您的密碼"
#: src/screens/Signup/StepHandle.tsx:114
msgid "Input your user handle"
msgid "Type your desired username"
msgstr "輸入您的帳號代碼"
#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:49
@@ -8004,7 +8004,7 @@ msgid "Your following feed is empty! Follow more users to see what's happening."
msgstr "您的「Following」動態源是空的!跟隨更多用戶來看看發生了什麼事情。"
#: src/screens/Signup/StepHandle.tsx:125
msgid "Your full handle will be"
msgid "Your full username will be"
msgstr "您的完整帳號代碼將修改為"
#: src/screens/Settings/components/ChangeHandleDialog.tsx:219
@@ -8044,5 +8044,5 @@ msgid "Your report will be sent to the Bluesky Moderation Service"
msgstr "您的檢舉將傳送至 Bluesky 內容管理服務"
#: src/screens/Signup/index.tsx:142
msgid "Your user handle"
msgid "Choose your username"
msgstr "您的帳號代碼"
+2 -3
View File
@@ -3,8 +3,7 @@ import {
error as bdError,
info as bdInfo,
warn as bdWarn,
} from '@bitdrift/react-native'
} from '../lib/bitdrift'
import {LogLevel, Transport} from './types'
export function createBitdriftTransport(): Transport {
@@ -18,6 +17,6 @@ export function createBitdriftTransport(): Transport {
return (level, message) => {
const log = logFunctions[level]
log(message.toString())
log('' + message)
}
}
-7
View File
@@ -1,7 +0,0 @@
import {Transport} from './index'
export function createBitdriftTransport(): Transport {
return (_level, _message) => {
// noop
}
}
+6 -2
View File
@@ -14,7 +14,7 @@ import {
TIMELINE_SAVED_FEED,
} from '#/lib/constants'
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {logEvent} from '#/lib/statsig/statsig'
import {logEvent, useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
import {getAllListMembers} from '#/state/queries/list-members'
@@ -57,6 +57,7 @@ export function StepFinished() {
const setActiveStarterPack = useSetActiveStarterPack()
const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
const {startProgressGuide} = useProgressGuideControls()
const gate = useGate()
const finishOnboarding = React.useCallback(async () => {
setSaving(true)
@@ -190,7 +191,9 @@ export function StepFinished() {
setSaving(false)
setActiveStarterPack(undefined)
setHasCheckedForStarterPack(true)
startProgressGuide('like-10-and-follow-7')
startProgressGuide(
gate('new_postonboarding') ? 'follow-10' : 'like-10-and-follow-7',
)
dispatch({type: 'finish'})
onboardDispatch({type: 'finish'})
logEvent('onboarding:finished:nextPressed', {
@@ -221,6 +224,7 @@ export function StepFinished() {
setActiveStarterPack,
setHasCheckedForStarterPack,
startProgressGuide,
gate,
])
return (
+13
View File
@@ -72,6 +72,19 @@ export type ApiResponseMap = {
}
}
// most popular selected interests
export const popularInterests = [
'art',
'gaming',
'sports',
'comics',
'music',
'politics',
'photography',
'science',
'news',
]
export function useInterestsDisplayNames() {
const {_} = useLingui()
@@ -0,0 +1,95 @@
import {View} from 'react-native'
import {Trans} from '@lingui/macro'
import {isWeb} from '#/platform/detection'
import {useTrendingSettings} from '#/state/preferences/trending'
import {
DEFAULT_LIMIT as RECOMMENDATIONS_COUNT,
useTrendingTopics,
} from '#/state/queries/trending/useTrendingTopics'
import {useTrendingConfig} from '#/state/trending-config'
import {atoms as a, useGutters, useTheme} from '#/alf'
import {Hashtag_Stroke2_Corner0_Rounded} from '#/components/icons/Hashtag'
import {
TrendingTopic,
TrendingTopicLink,
TrendingTopicSkeleton,
} from '#/components/TrendingTopics'
import {Text} from '#/components/Typography'
export function ExploreRecommendations() {
const {enabled} = useTrendingConfig()
const {trendingDisabled} = useTrendingSettings()
return enabled && !trendingDisabled ? <Inner /> : null
}
function Inner() {
const t = useTheme()
const gutters = useGutters([0, 'compact'])
const {data: trending, error, isLoading} = useTrendingTopics()
const noRecs = !isLoading && !error && !trending?.suggested?.length
return error || noRecs ? null : (
<>
<View
style={[
isWeb
? [a.flex_row, a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
: [{flexDirection: 'row-reverse'}, a.p_lg, a.pt_2xl, a.gap_md],
a.border_b,
t.atoms.border_contrast_low,
]}>
<View style={[a.flex_1, a.gap_sm]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Hashtag_Stroke2_Corner0_Rounded
size="lg"
fill={t.palette.primary_500}
style={{marginLeft: -2}}
/>
<Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}>
<Trans>Recommended</Trans>
</Text>
</View>
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
<Trans>Feeds we think you might like.</Trans>
</Text>
</View>
</View>
<View style={[a.pt_md, a.pb_lg]}>
<View
style={[
a.flex_row,
a.justify_start,
a.flex_wrap,
{rowGap: 8, columnGap: 6},
gutters,
]}>
{isLoading ? (
Array(RECOMMENDATIONS_COUNT)
.fill(0)
.map((_, i) => <TrendingTopicSkeleton key={i} index={i} />)
) : !trending?.suggested ? null : (
<>
{trending.suggested.map(topic => (
<TrendingTopicLink key={topic.link} topic={topic}>
{({hovered}) => (
<TrendingTopic
topic={topic}
style={[
hovered && [
t.atoms.border_contrast_high,
t.atoms.bg_contrast_25,
],
]}
/>
)}
</TrendingTopicLink>
))}
</>
)}
</View>
</View>
</>
)
}
@@ -2,6 +2,7 @@ import {View} from 'react-native'
import {Trans} from '@lingui/macro'
import {isWeb} from '#/platform/detection'
import {useTrendingSettings} from '#/state/preferences/trending'
import {
DEFAULT_LIMIT as TRENDING_TOPICS_COUNT,
useTrendingTopics,
@@ -19,7 +20,8 @@ import {Text} from '#/components/Typography'
export function ExploreTrendingTopics() {
const {enabled} = useTrendingConfig()
return enabled ? <Inner /> : null
const {trendingDisabled} = useTrendingSettings()
return enabled && !trendingDisabled ? <Inner /> : null
}
function Inner() {
@@ -56,14 +58,20 @@ function Inner() {
</View>
</View>
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
<Trans>What people are posting about now</Trans>
<Trans>What people are posting about.</Trans>
</Text>
</View>
</View>
<View style={[a.pt_md, a.pb_lg]}>
<View
style={[a.flex_row, a.justify_start, a.flex_wrap, a.gap_sm, gutters]}>
style={[
a.flex_row,
a.justify_start,
a.flex_wrap,
{rowGap: 8, columnGap: 6},
gutters,
]}>
{isLoading ? (
Array(TRENDING_TOPICS_COUNT)
.fill(0)
@@ -5,11 +5,11 @@ import {useLingui} from '@lingui/react'
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {IS_INTERNAL} from '#/lib/app-info'
import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {isAndroid} from '#/platform/detection'
import {useSession} from '#/state/session'
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
@@ -23,7 +23,7 @@ export function AppIconSettingsScreen({}: Props) {
const t = useTheme()
const {_} = useLingui()
const sets = useAppIconSets()
const {currentAccount} = useSession()
const gate = useGate()
const [currentAppIcon, setCurrentAppIcon] = useState(() =>
getAppIconName(DynamicAppIcon.getAppIcon()),
)
@@ -86,7 +86,7 @@ export function AppIconSettingsScreen({}: Props) {
))}
</Group>
{DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
{IS_INTERNAL && gate('debug_subscriptions') && (
<>
<Text
style={[
+4 -5
View File
@@ -8,10 +8,10 @@ import Animated, {
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {IS_INTERNAL} from '#/lib/app-info'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {isNative} from '#/platform/detection'
import {useSession} from '#/state/session'
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
import {atoms as a, native, useAlf, useTheme} from '#/alf'
@@ -29,6 +29,7 @@ type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
export function AppearanceSettingsScreen({}: Props) {
const {_} = useLingui()
const {fonts} = useAlf()
const gate = useGate()
const {colorMode, darkTheme} = useThemePrefs()
const {setColorMode, setDarkTheme} = useSetThemePrefs()
@@ -74,8 +75,6 @@ export function AppearanceSettingsScreen({}: Props) {
[fonts],
)
const {currentAccount} = useSession()
return (
<LayoutAnimationConfig skipExiting skipEntering>
<Layout.Screen testID="preferencesThreadsScreen">
@@ -178,7 +177,7 @@ export function AppearanceSettingsScreen({}: Props) {
onChange={onChangeFontScale}
/>
{isNative && DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
{isNative && IS_INTERNAL && gate('debug_subscriptions') && (
<>
<SettingsList.Divider />
<AppIconSettingsListItem />
@@ -36,9 +36,8 @@ export function ContentAndMediaSettingsScreen({}: Props) {
const inAppBrowserPref = useInAppBrowser()
const setUseInAppBrowser = useSetInAppBrowser()
const {enabled: trendingEnabled} = useTrendingConfig()
const {trendingSidebarHidden, trendingDiscoverHidden} = useTrendingSettings()
const {setTrendingSidebarHidden, setTrendingDiscoverHidden} =
useTrendingSettingsApi()
const {trendingDisabled} = useTrendingSettings()
const {setTrendingDisabled} = useTrendingSettingsApi()
return (
<Layout.Screen>
@@ -117,30 +116,15 @@ export function ContentAndMediaSettingsScreen({}: Props) {
{trendingEnabled && (
<>
<SettingsList.Divider />
{!isNative && (
<Toggle.Item
name="show_trending_topics_sidebar"
label={_(msg`Show trending topics in your sidebar`)}
value={!trendingSidebarHidden}
onChange={value => setTrendingSidebarHidden(!value)}>
<SettingsList.Item>
<SettingsList.ItemIcon icon={Graph} />
<SettingsList.ItemText>
<Trans>Show trending topics in your sidebar</Trans>
</SettingsList.ItemText>
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
)}
<Toggle.Item
name="show_trending_topics_discover"
label={_(msg`Show trending topics in your feed`)}
value={!trendingDiscoverHidden}
onChange={value => setTrendingDiscoverHidden(!value)}>
name="show_trending_topics"
label={_(msg`Enable trending topics`)}
value={!trendingDisabled}
onChange={value => setTrendingDisabled(!value)}>
<SettingsList.Item>
<SettingsList.ItemIcon icon={Graph} />
<SettingsList.ItemText>
<Trans>Show trending topics in your feed</Trans>
<Trans>Enable trending topics</Trans>
</SettingsList.ItemText>
<Toggle.Platform />
</SettingsList.Item>
+2 -2
View File
@@ -111,7 +111,7 @@ export function StepHandle() {
handleValueRef.current = val
setDraftValue(val)
}}
label={_(msg`Input your user handle`)}
label={_(msg`Type your desired username`)}
defaultValue={draftValue}
autoCapitalize="none"
autoCorrect={false}
@@ -122,7 +122,7 @@ export function StepHandle() {
</View>
{draftValue !== '' && (
<Text style={[a.text_md]}>
<Trans>Your full handle will be</Trans>{' '}
<Trans>Your full username will be</Trans>{' '}
<Text style={[a.text_md, a.font_bold]}>
@{createFullHandle(draftValue, state.userDomain)}
</Text>
+1 -1
View File
@@ -139,7 +139,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
{state.activeStep === SignupStep.INFO ? (
<Trans>Your account</Trans>
) : state.activeStep === SignupStep.HANDLE ? (
<Trans>Your user handle</Trans>
<Trans>Choose your username</Trans>
) : (
<Trans>Complete the challenge</Trans>
)}
+12
View File
@@ -24,6 +24,7 @@ const _emitter = new EventEmitter()
export async function init() {
broadcast.onmessage = onBroadcastMessage
window.onstorage = onStorage
const stored = readFromStorage()
if (stored) {
_state = stored
@@ -90,6 +91,17 @@ export async function clearStorage() {
}
clearStorage satisfies PersistedApi['clearStorage']
function onStorage() {
const next = readFromStorage()
if (next === _state) {
return
}
if (next) {
_state = next
_emitter.emit('update')
}
}
async function onBroadcastMessage({data}: MessageEvent) {
if (
typeof data === 'object' &&
+2 -12
View File
@@ -125,12 +125,7 @@ const schema = z.object({
subtitlesEnabled: z.boolean().optional(),
/** @deprecated */
mutedThreads: z.array(z.string()),
/*
* Trending
*/
trendingSidebarHidden: z.boolean().optional(),
trendingDiscoverHidden: z.boolean().optional(),
trendingDisabled: z.boolean().optional(),
})
export type Schema = z.infer<typeof schema>
@@ -176,12 +171,7 @@ export const defaults: Schema = {
kawaii: false,
hasCheckedForStarterPack: false,
subtitlesEnabled: true,
/*
* Trending
*/
trendingSidebarHidden: false,
trendingDiscoverHidden: false,
trendingDisabled: false,
}
export function tryParse(rawData: string): Schema | undefined {
+10 -27
View File
@@ -3,31 +3,19 @@ import React from 'react'
import * as persisted from '#/state/persisted'
type StateContext = {
trendingSidebarHidden: Exclude<
persisted.Schema['trendingSidebarHidden'],
undefined
>
trendingDiscoverHidden: Exclude<
persisted.Schema['trendingDiscoverHidden'],
undefined
>
trendingDisabled: Exclude<persisted.Schema['trendingDisabled'], undefined>
}
type ApiContext = {
setTrendingSidebarHidden(
hidden: Exclude<persisted.Schema['trendingSidebarHidden'], undefined>,
): void
setTrendingDiscoverHidden(
hidden: Exclude<persisted.Schema['trendingDiscoverHidden'], undefined>,
setTrendingDisabled(
hidden: Exclude<persisted.Schema['trendingDisabled'], undefined>,
): void
}
const StateContext = React.createContext<StateContext>({
trendingSidebarHidden: Boolean(persisted.defaults.trendingSidebarHidden),
trendingDiscoverHidden: Boolean(persisted.defaults.trendingDiscoverHidden),
trendingDisabled: Boolean(persisted.defaults.trendingDisabled),
})
const ApiContext = React.createContext<ApiContext>({
setTrendingSidebarHidden() {},
setTrendingDiscoverHidden() {},
setTrendingDisabled() {},
})
function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) {
@@ -53,21 +41,16 @@ function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) {
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const [trendingSidebarHidden, setTrendingSidebarHidden] =
usePersistedBooleanValue('trendingSidebarHidden')
const [trendingDiscoverHidden, setTrendingDiscoverHidden] =
usePersistedBooleanValue('trendingDiscoverHidden')
const [trendingDisabled, setTrendingDisabled] =
usePersistedBooleanValue('trendingDisabled')
/*
* Context
*/
const state = React.useMemo(
() => ({trendingSidebarHidden, trendingDiscoverHidden}),
[trendingSidebarHidden, trendingDiscoverHidden],
)
const state = React.useMemo(() => ({trendingDisabled}), [trendingDisabled])
const api = React.useMemo(
() => ({setTrendingSidebarHidden, setTrendingDiscoverHidden}),
[setTrendingSidebarHidden, setTrendingDiscoverHidden],
() => ({setTrendingDisabled}),
[setTrendingDisabled],
)
return (
+22 -4
View File
@@ -1,6 +1,7 @@
import {AppBskyActorDefs, AppBskyActorSearchActors} from '@atproto/api'
import {
InfiniteData,
keepPreviousData,
QueryClient,
QueryKey,
useInfiniteQuery,
@@ -13,10 +14,8 @@ import {useAgent} from '#/state/session'
const RQKEY_ROOT = 'actor-search'
export const RQKEY = (query: string) => [RQKEY_ROOT, query]
export const RQKEY_PAGINATED = (query: string) => [
`${RQKEY_ROOT}_paginated`,
query,
]
const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated`
export const RQKEY_PAGINATED = (query: string) => [RQKEY_ROOT_PAGINATED, query]
export function useActorSearch({
query,
@@ -42,9 +41,11 @@ export function useActorSearch({
export function useActorSearchPaginated({
query,
enabled,
maintainData,
}: {
query: string
enabled?: boolean
maintainData?: boolean
}) {
const agent = useAgent()
return useInfiniteQuery<
@@ -67,6 +68,7 @@ export function useActorSearchPaginated({
enabled: enabled && !!query,
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
placeholderData: maintainData ? keepPreviousData : undefined,
})
}
@@ -89,4 +91,20 @@ export function* findAllProfilesInQueryData(
}
}
}
const queryDatasPaginated = queryClient.getQueriesData<
InfiniteData<AppBskyActorSearchActors.OutputSchema>
>({
queryKey: [RQKEY_ROOT_PAGINATED],
})
for (const [_queryKey, queryData] of queryDatasPaginated) {
if (!queryData) {
continue
}
for (const actor of queryData.pages.flatMap(page => page.actors)) {
if (actor.did === did) {
yield actor
}
}
}
}
+1 -4
View File
@@ -144,11 +144,8 @@ export function usePostFeedQuery(
/**
* The number of posts to fetch in a single request. Because we filter
* unwanted content, we may over-fetch here to try and fill pages by
* `MIN_POSTS`.
* `MIN_POSTS`. But if you're doing this, ask @why if it's ok first.
*/
// TEMPORARILY DISABLING GATE TO PREVENT EVENT CONSUMPTION @TODO EME-GATE
// const fetchLimit = gate('post_feed_lang_window') ? 100 : MIN_POSTS
const fetchLimit = MIN_POSTS
// Make sure this doesn't invalidate unless really needed.
+8 -1
View File
@@ -103,7 +103,13 @@ export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) {
})
}
export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
export function useSuggestedFollowsByActorQuery({
did,
enabled,
}: {
did: string
enabled?: boolean
}) {
const agent = useAgent()
return useQuery({
queryKey: suggestedFollowsByActorQueryKey(did),
@@ -116,6 +122,7 @@ export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
: res.data.suggestions.filter(profile => !profile.viewer?.following)
return {suggestions}
},
enabled,
})
}
@@ -4,25 +4,12 @@ import {describe, expect, it, jest} from '@jest/globals'
import {agentToSessionAccountOrThrow} from '../agent'
import {Action, getInitialState, reducer, State} from '../reducer'
jest.mock('statsig-react-native-expo', () => ({
Statsig: {
initialize() {},
initializeCalled() {
return false
},
},
}))
jest.mock('jwt-decode', () => ({
jwtDecode(_token: string) {
return {}
},
}))
jest.mock('expo-localization', () => ({
getLocales: () => [],
}))
describe('session', () => {
it('can log in and out', () => {
let state = getInitialState([])
+78 -18
View File
@@ -1,4 +1,4 @@
import React from 'react'
import React, {useMemo} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -16,20 +16,32 @@ export enum ProgressGuideAction {
Follow = 'follow',
}
type ProgressGuideName = 'like-10-and-follow-7'
type ProgressGuideName = 'like-10-and-follow-7' | 'follow-10'
/**
* Progress Guides that extend this interface must specify their name in the `guide` field, so it can be used as a discriminated union
*/
interface BaseProgressGuide {
guide: string
guide: ProgressGuideName
isComplete: boolean
[key: string]: any
}
interface Like10AndFollow7ProgressGuide extends BaseProgressGuide {
export interface Like10AndFollow7ProgressGuide extends BaseProgressGuide {
guide: 'like-10-and-follow-7'
numLikes: number
numFollows: number
}
type ProgressGuide = Like10AndFollow7ProgressGuide | undefined
export interface Follow10ProgressGuide extends BaseProgressGuide {
guide: 'follow-10'
numFollows: number
}
export type ProgressGuide =
| Like10AndFollow7ProgressGuide
| Follow10ProgressGuide
| undefined
const ProgressGuideContext = React.createContext<ProgressGuide>(undefined)
@@ -61,15 +73,28 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const {mutateAsync, variables, isPending} =
useSetActiveProgressGuideMutation()
const activeProgressGuide = (
isPending ? variables : preferences?.bskyAppState?.activeProgressGuide
) as ProgressGuide
const activeProgressGuide = useMemo(() => {
const rawProgressGuide = (
isPending ? variables : preferences?.bskyAppState?.activeProgressGuide
) as ProgressGuide
// ensure the unspecced attributes have the correct types
if (activeProgressGuide?.guide === 'like-10-and-follow-7') {
activeProgressGuide.numLikes = Number(activeProgressGuide.numLikes) || 0
activeProgressGuide.numFollows = Number(activeProgressGuide.numFollows) || 0
}
if (!rawProgressGuide) return undefined
// ensure the unspecced attributes have the correct types
// clone then mutate
const {...maybeWronglyTypedProgressGuide} = rawProgressGuide
if (maybeWronglyTypedProgressGuide?.guide === 'like-10-and-follow-7') {
maybeWronglyTypedProgressGuide.numLikes =
Number(maybeWronglyTypedProgressGuide.numLikes) || 0
maybeWronglyTypedProgressGuide.numFollows =
Number(maybeWronglyTypedProgressGuide.numFollows) || 0
} else if (maybeWronglyTypedProgressGuide?.guide === 'follow-10') {
maybeWronglyTypedProgressGuide.numFollows =
Number(maybeWronglyTypedProgressGuide.numFollows) || 0
}
return maybeWronglyTypedProgressGuide
}, [isPending, variables, preferences])
const [localGuideState, setLocalGuideState] =
React.useState<ProgressGuide>(undefined)
@@ -82,7 +107,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const firstLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
const fifthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
const tenthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
const guideCompleteToastRef = React.useRef<ProgressGuideToastRef | null>(null)
const fifthFollowToastRef = React.useRef<ProgressGuideToastRef | null>(null)
const tenthFollowToastRef = React.useRef<ProgressGuideToastRef | null>(null)
const controls = React.useMemo(() => {
return {
@@ -93,7 +120,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
numLikes: 0,
numFollows: 0,
isComplete: false,
}
} satisfies ProgressGuide
setLocalGuideState(guideObj)
mutateAsync(guideObj)
} else if (guide === 'follow-10') {
const guideObj = {
guide: 'follow-10',
numFollows: 0,
isComplete: false,
} satisfies ProgressGuide
setLocalGuideState(guideObj)
mutateAsync(guideObj)
}
@@ -137,6 +172,26 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
isComplete: true,
}
}
} else if (guide?.guide === 'follow-10') {
if (action === ProgressGuideAction.Follow) {
guide = {
...guide,
numFollows: (Number(guide.numFollows) || 0) + count,
}
if (guide.numFollows === 5) {
fifthFollowToastRef.current?.open()
}
if (guide.numFollows === 10) {
tenthFollowToastRef.current?.open()
}
}
if (Number(guide.numFollows) >= 10) {
guide = {
...guide,
isComplete: true,
}
}
}
setLocalGuideState(guide)
@@ -167,9 +222,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
subtitle={_(msg`The Discover feed now knows what you like`)}
/>
<ProgressGuideToast
ref={guideCompleteToastRef}
title={_(msg`Algorithm training complete!`)}
subtitle={_(msg`The Discover feed now knows what you like`)}
ref={fifthFollowToastRef}
title={_(msg`Half way there!`)}
subtitle={_(msg`Follow 10 accounts`)}
/>
<ProgressGuideToast
ref={tenthFollowToastRef}
title={_(msg`Task complete - 10 follows!`)}
subtitle={_(msg`You've found some people to follow`)}
/>
</>
)}
@@ -134,7 +134,7 @@ export function NotificationFeed({
highlightUnread={filter === 'all'}
item={item}
moderationOpts={moderationOpts!}
hideTopBorder={index === 0 && item.notification.isRead}
hideTopBorder={index === 0}
/>
)
},
+1 -1
View File
@@ -145,7 +145,7 @@ export function TabBar({
const desktopStyles = StyleSheet.create({
outer: {
flexDirection: 'row',
width: 598,
width: 600,
},
contentContainer: {
flexGrow: 1,
+23 -15
View File
@@ -35,6 +35,7 @@ import {
} from '#/state/queries/post-feed'
import {useSession} from '#/state/session'
import {useProgressGuide} from '#/state/shell/progress-guide'
import {useBreakpoints} from '#/alf'
import {ProgressGuide, SuggestedFollows} from '#/components/FeedInterstitials'
import {TrendingInterstitial} from '#/components/interstitials/Trending'
import {List, ListRef} from '../util/List'
@@ -162,6 +163,7 @@ let PostFeed = ({
const checkForNewRef = React.useRef<(() => void) | null>(null)
const lastFetchRef = React.useRef<number>(Date.now())
const [feedType, feedUri, feedTab] = feed.split('|')
const {gtTablet} = useBreakpoints()
const opts = React.useMemo(
() => ({enabled, ignoreFilterFor}),
@@ -193,12 +195,16 @@ let PostFeed = ({
}
try {
if (await pollLatest(data.pages[0])) {
onHasNew(true)
if (isEmpty) {
refetch()
} else {
onHasNew(true)
}
}
} catch (e) {
logger.error('Poll latest failed', {feed, message: String(e)})
}
}, [feed, data, isFetching, onHasNew, enabled, disablePoll])
}, [feed, data, isFetching, isEmpty, onHasNew, enabled, disablePoll, refetch])
const myDid = currentAccount?.did || ''
const onPostCreated = React.useCallback(() => {
@@ -226,20 +232,15 @@ let PostFeed = ({
React.useEffect(() => {
if (enabled && !disablePoll) {
const timeSinceFirstLoad = Date.now() - lastFetchRef.current
// DISABLED need to check if this is causing random feed refreshes -prf
/*if (timeSinceFirstLoad > REFRESH_AFTER) {
// do a full refresh
scrollElRef?.current?.scrollToOffset({offset: 0, animated: false})
queryClient.resetQueries({queryKey: RQKEY(feed)})
} else*/ if (
timeSinceFirstLoad > CHECK_LATEST_AFTER &&
if (
(isEmpty || timeSinceFirstLoad > CHECK_LATEST_AFTER) &&
checkForNewRef.current
) {
// check for new on enable (aka on focus)
checkForNewRef.current()
}
}
}, [enabled, disablePoll, feed, queryClient, scrollElRef])
}, [enabled, disablePoll, feed, queryClient, scrollElRef, isEmpty])
React.useEffect(() => {
let cleanup1: () => void | undefined, cleanup2: () => void | undefined
const subscription = AppState.addEventListener('change', nextAppState => {
@@ -260,11 +261,13 @@ let PostFeed = ({
}
}, [pollInterval])
const progressGuide = useProgressGuide('like-10-and-follow-7')
const followProgressGuide = useProgressGuide('follow-10')
const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7')
const {isDesktop} = useWebMediaQueries()
const showProgressIntersitial = progressGuide && !isDesktop
const showProgressIntersitial =
(followProgressGuide || followAndLikeProgressGuide) && !isDesktop
const {trendingDiscoverHidden} = useTrendingSettings()
const {trendingDisabled} = useTrendingSettings()
const feedItems: FeedRow[] = React.useMemo(() => {
let feedKind: 'following' | 'discover' | 'profile' | undefined
@@ -311,7 +314,11 @@ let PostFeed = ({
type: 'interstitialProgressGuide',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
})
} else if (sliceIndex === 15 && !trendingDiscoverHidden) {
} else if (
sliceIndex === 15 &&
!gtTablet &&
!trendingDisabled
) {
arr.push({
type: 'interstitialTrending',
key: 'interstitial-' + sliceIndex + '-' + lastFetchedAt,
@@ -402,7 +409,8 @@ let PostFeed = ({
feedTab,
hasSession,
showProgressIntersitial,
trendingDiscoverHidden,
trendingDisabled,
gtTablet,
])
// events
+9 -1
View File
@@ -53,6 +53,7 @@ let List = React.forwardRef<ListMethods, ListProps>(
headerOffset,
style,
progressViewOffset,
automaticallyAdjustsScrollIndicatorInsets = false,
...props
},
ref,
@@ -151,7 +152,14 @@ let List = React.forwardRef<ListMethods, ListProps>(
return (
<FlatList_INTERNAL
{...props}
scrollIndicatorInsets={{top: headerOffset, right: 1}}
automaticallyAdjustsScrollIndicatorInsets={
automaticallyAdjustsScrollIndicatorInsets
}
scrollIndicatorInsets={{
top: headerOffset,
right: 1,
...props.scrollIndicatorInsets,
}}
contentOffset={contentOffset}
refreshControl={refreshControl}
onScroll={scrollHandler}
+3 -1
View File
@@ -196,7 +196,9 @@ function Toast({
/>
</View>
<View style={[a.h_full, a.justify_center, a.flex_1]}>
<Text style={a.text_md}>{message}</Text>
<Text style={a.text_md} emoji>
{message}
</Text>
</View>
</View>
</GestureDetector>
+55 -82
View File
@@ -1,4 +1,4 @@
import {StyleSheet, View} from 'react-native'
import {View} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@@ -7,12 +7,11 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {useTheme} from '#/lib/ThemeContext'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {Button} from '../forms/Button'
import {Text} from '../text/Text'
import {CenteredView} from '../Views'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon} from '#/components/icons/ArrowRotateCounterClockwise'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
export function ErrorScreen({
title,
@@ -29,22 +28,32 @@ export function ErrorScreen({
testID?: string
showHeader?: boolean
}) {
const theme = useTheme()
const {isMobile} = useWebMediaQueries()
const t = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
return (
<>
{showHeader && isMobile && (
<ViewHeader title={_(msg`Error`)} showBorder />
<Layout.Center testID={testID}>
{showHeader && (
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Error</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
)}
<CenteredView testID={testID} style={[styles.outer, pal.view]}>
<View style={styles.errorIconContainer}>
<View style={[a.px_xl, a.py_2xl]}>
<View style={[a.mb_md, a.align_center]}>
<View
style={[
styles.errorIcon,
{backgroundColor: theme.palette.inverted.background},
a.rounded_full,
{width: 50, height: 50},
a.align_center,
a.justify_center,
{backgroundColor: t.palette.contrast_950},
]}>
<FontAwesomeIcon
icon="exclamation"
@@ -53,86 +62,50 @@ export function ErrorScreen({
/>
</View>
</View>
<Text type="title-lg" style={[styles.title, pal.text]}>
<Text style={[a.text_center, a.font_heavy, a.text_2xl, a.mb_md]}>
{title}
</Text>
<Text style={[styles.message, pal.text]}>{message}</Text>
<Text style={[a.text_center, a.text_md, a.mb_xl]}>{message}</Text>
{details && (
<Text
testID={`${testID}-details`}
style={[styles.details, pal.text, pal.viewLight]}>
{details}
</Text>
<View
style={[
a.w_full,
a.border,
t.atoms.border_contrast_medium,
t.atoms.bg_contrast_25,
a.mb_xl,
a.py_sm,
a.px_lg,
a.rounded_xs,
a.overflow_hidden,
]}>
<Text
testID={`${testID}-details`}
style={[a.text_center, a.text_md, t.atoms.text_contrast_high]}>
{details}
</Text>
</View>
)}
{onPressTryAgain && (
<View style={styles.btnContainer}>
<View style={[a.align_center]}>
<Button
testID="errorScreenTryAgainButton"
type="default"
style={[styles.btn]}
onPress={onPressTryAgain}
accessibilityLabel={_(msg`Retry`)}
variant="solid"
color="secondary_inverted"
size="small"
label={_(msg`Retry`)}
accessibilityHint={_(
msg`Retries the last action, which errored out`,
)}>
<FontAwesomeIcon
icon="arrows-rotate"
style={pal.link as FontAwesomeIconStyle}
size={16}
/>
<Text type="button" style={[styles.btnText, pal.link]}>
<ButtonIcon icon={ArrowRotateCounterClockwiseIcon} />
<ButtonText>
<Trans context="action">Try again</Trans>
</Text>
</ButtonText>
</Button>
</View>
)}
</CenteredView>
</>
</View>
</Layout.Center>
)
}
const styles = StyleSheet.create({
outer: {
flex: 1,
paddingVertical: 30,
paddingHorizontal: 14,
},
title: {
textAlign: 'center',
marginBottom: 10,
},
message: {
textAlign: 'center',
marginBottom: 20,
},
details: {
textAlign: 'center',
paddingVertical: 10,
paddingHorizontal: 14,
overflow: 'hidden',
marginBottom: 20,
},
btnContainer: {
alignItems: 'center',
},
btn: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 10,
},
btnText: {
marginLeft: 5,
},
errorIconContainer: {
alignItems: 'center',
marginBottom: 10,
},
errorIcon: {
borderRadius: 25,
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center',
},
})
+4 -3
View File
@@ -18,12 +18,13 @@ import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {IS_INTERNAL} from '#/lib/app-info'
import {DISCOVER_DEBUG_DIDS, POST_CTRL_HITSLOP} from '#/lib/constants'
import {POST_CTRL_HITSLOP} from '#/lib/constants'
import {CountWheel} from '#/lib/custom-animations/CountWheel'
import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
import {useHaptics} from '#/lib/haptics'
import {makeProfileLink} from '#/lib/routes/links'
import {shareUrl} from '#/lib/sharing'
import {useGate} from '#/lib/statsig/statsig'
import {toShareUrl} from '#/lib/strings/url-helpers'
import {Shadow} from '#/state/cache/types'
import {useFeedFeedbackContext} from '#/state/feed-feedback'
@@ -85,8 +86,8 @@ let PostCtrls = ({
const {sendInteraction} = useFeedFeedbackContext()
const {captureAction} = useProgressGuideControls()
const playHaptic = useHaptics()
const isDiscoverDebugUser =
IS_INTERNAL || DISCOVER_DEBUG_DIDS[currentAccount?.did ?? '']
const gate = useGate()
const isDiscoverDebugUser = IS_INTERNAL || gate('debug_show_feedcontext')
const isBlocked = Boolean(
post.author.viewer?.blocking ||
post.author.viewer?.blockedBy ||
+19 -14
View File
@@ -1,5 +1,6 @@
import React, {useCallback, useMemo} from 'react'
import {StyleSheet} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context'
import {
AppBskyActorDefs,
AppBskyGraphGetActorStarterPacks,
@@ -122,13 +123,15 @@ function ProfileScreenInner({route}: Props) {
}
if (resolveError || profileError) {
return (
<ErrorScreen
testID="profileErrorScreen"
title={profileError ? _(msg`Not Found`) : _(msg`Oops!`)}
message={cleanError(resolveError || profileError)}
onPressTryAgain={onPressTryAgain}
showHeader
/>
<SafeAreaView style={[a.flex_1]}>
<ErrorScreen
testID="profileErrorScreen"
title={profileError ? _(msg`Not Found`) : _(msg`Oops!`)}
message={cleanError(resolveError || profileError)}
onPressTryAgain={onPressTryAgain}
showHeader
/>
</SafeAreaView>
)
}
if (profile && moderationOpts) {
@@ -144,13 +147,15 @@ function ProfileScreenInner({route}: Props) {
}
// should never happen
return (
<ErrorScreen
testID="profileErrorScreen"
title="Oops!"
message="Something went wrong and we're not sure what."
onPressTryAgain={onPressTryAgain}
showHeader
/>
<SafeAreaView style={[a.flex_1]}>
<ErrorScreen
testID="profileErrorScreen"
title="Oops!"
message="Something went wrong and we're not sure what."
onPressTryAgain={onPressTryAgain}
showHeader
/>
</SafeAreaView>
)
}
+13
View File
@@ -24,6 +24,7 @@ import {
ProfileCardFeedLoadingPlaceholder,
} from '#/view/com/util/LoadingPlaceholder'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {ExploreRecommendations} from '#/screens/Search/components/ExploreRecommendations'
import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {Button} from '#/components/Button'
@@ -244,6 +245,10 @@ type ExploreScreenItems =
type: 'trendingTopics'
key: string
}
| {
type: 'recommendations'
key: string
}
| {
type: 'profile'
key: string
@@ -337,6 +342,11 @@ export function Explore() {
key: `trending-topics`,
})
i.push({
type: 'recommendations',
key: `recommendations`,
})
i.push({
type: 'header',
key: 'suggested-follows-header',
@@ -503,6 +513,9 @@ export function Explore() {
case 'trendingTopics': {
return <ExploreTrendingTopics />
}
case 'recommendations': {
return <ExploreRecommendations />
}
case 'profile': {
return (
<View style={[a.border_b, t.atoms.border_contrast_low]}>
+29 -2
View File
@@ -14,7 +14,7 @@ import {createStaticClick, InlineLinkText} from '#/components/Link'
export function DesktopFeeds() {
const t = useTheme()
const {_} = useLingui()
const {data: pinnedFeedInfos} = usePinnedFeedsInfos()
const {data: pinnedFeedInfos, error, isLoading} = usePinnedFeedsInfos()
const selectedFeed = useSelectedFeed()
const setSelectedFeed = useSetSelectedFeed()
const navigation = useNavigation<NavigationProp>()
@@ -25,7 +25,34 @@ export function DesktopFeeds() {
return getCurrentRoute(state)
})
if (!pinnedFeedInfos) {
if (isLoading) {
return (
<View
style={[
{
gap: 12,
},
]}>
{Array(5)
.fill(0)
.map((_, i) => (
<View
key={i}
style={[
a.rounded_sm,
t.atoms.bg_contrast_25,
{
height: 16,
width: i % 2 === 0 ? '60%' : '80%',
},
]}
/>
))}
</View>
)
}
if (error || !pinnedFeedInfos) {
return null
}
+23 -1
View File
@@ -1,6 +1,8 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/core'
import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
@@ -15,6 +17,24 @@ import {InlineLinkText} from '#/components/Link'
import {ProgressGuideList} from '#/components/ProgressGuide/List'
import {Text} from '#/components/Typography'
function useWebQueryParams() {
const navigation = useNavigation()
const [params, setParams] = React.useState<Record<string, string>>({})
React.useEffect(() => {
return navigation.addListener('state', e => {
try {
const {state} = e.data
const lastRoute = state.routes[state.routes.length - 1]
const {params} = lastRoute
setParams(params)
} catch (e) {}
})
}, [navigation, setParams])
return params
}
export function DesktopRightNav({routeName}: {routeName: string}) {
const t = useTheme()
const {_} = useLingui()
@@ -22,6 +42,8 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
const kawaii = useKawaiiMode()
const gutters = useGutters(['base', 0, 'base', 'wide'])
const isSearchScreen = routeName === 'Search'
const {q: searchQuery} = useWebQueryParams()
const showTrending = !isSearchScreen || (isSearchScreen && !!searchQuery)
const {isTablet} = useWebMediaQueries()
if (isTablet) {
@@ -57,7 +79,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
</>
)}
{!isSearchScreen && <SidebarTrendingTopics />}
{showTrending && <SidebarTrendingTopics />}
<Text style={[a.leading_snug, t.atoms.text_contrast_low]}>
{hasSession && (
@@ -6,10 +6,7 @@ import {
useTrendingSettings,
useTrendingSettingsApi,
} from '#/state/preferences/trending'
import {
DEFAULT_LIMIT as TRENDING_TOPICS_COUNT,
useTrendingTopics,
} from '#/state/queries/trending/useTrendingTopics'
import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics'
import {useTrendingConfig} from '#/state/trending-config'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
@@ -24,23 +21,25 @@ import {
} from '#/components/TrendingTopics'
import {Text} from '#/components/Typography'
const TRENDING_LIMIT = 6
export function SidebarTrendingTopics() {
const {enabled} = useTrendingConfig()
const {trendingSidebarHidden} = useTrendingSettings()
return !enabled ? null : trendingSidebarHidden ? null : <Inner />
const {trendingDisabled} = useTrendingSettings()
return !enabled ? null : trendingDisabled ? null : <Inner />
}
function Inner() {
const t = useTheme()
const {_} = useLingui()
const trendingPrompt = Prompt.usePromptControl()
const {setTrendingSidebarHidden} = useTrendingSettingsApi()
const {setTrendingDisabled} = useTrendingSettingsApi()
const {data: trending, error, isLoading} = useTrendingTopics()
const noTopics = !isLoading && !error && !trending?.topics?.length
return error || noTopics ? null : (
<>
<View style={[a.gap_md]}>
<View style={[a.gap_sm, {paddingBottom: 2}]}>
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Graph size="sm" />
<Text
@@ -53,7 +52,7 @@ function Inner() {
<Trans>Trending</Trans>
</Text>
<Button
label={_(msg`Hide trending topics from your sidebar`)}
label={_(msg`Hide trending topics`)}
size="tiny"
variant="ghost"
color="secondary"
@@ -63,16 +62,16 @@ function Inner() {
</Button>
</View>
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
<View style={[a.flex_row, a.flex_wrap, {gap: '6px 4px'}]}>
{isLoading ? (
Array(TRENDING_TOPICS_COUNT)
Array(TRENDING_LIMIT)
.fill(0)
.map((_n, i) => (
<TrendingTopicSkeleton key={i} size="small" index={i} />
))
) : !trending?.topics ? null : (
<>
{trending.topics.map(topic => (
{trending.topics.slice(0, TRENDING_LIMIT).map(topic => (
<TrendingTopicLink key={topic.link} topic={topic}>
{({hovered}) => (
<TrendingTopic
@@ -95,11 +94,9 @@ function Inner() {
<Prompt.Basic
control={trendingPrompt}
title={_(msg`Hide trending topics?`)}
description={_(
msg`This is a device setting, and will apply to all accounts on this device. You can update this later from your settings.`,
)}
description={_(msg`You can update this later from your settings.`)}
confirmButtonCta={_(msg`Hide`)}
onConfirm={() => setTrendingSidebarHidden(true)}
onConfirm={() => setTrendingDisabled(true)}
/>
<Divider />
</>
+81 -76
View File
@@ -3696,10 +3696,10 @@
mv "~2"
safe-json-stringify "~1"
"@expo/cli@0.22.3":
version "0.22.3"
resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.22.3.tgz#8dbcc9396abf01b2dd91fe7f34fc23fdd2d8cc7f"
integrity sha512-1HBtqInFDFHUJWzTJ1CJj5MR3JwvOiozmRUWF2kVQAeq/bKzSYM6We6B3XoZBM5XP6z6WtnrG87C7BjeW5E/cA==
"@expo/cli@0.22.6":
version "0.22.6"
resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.22.6.tgz#dc27b685d2252027549d839437c5285c7814ec3f"
integrity sha512-eDjCnc3uHl2+SJ6aZ5seK0FkMp0W12oAdSI4A/yV8ecYtXzG8X87sfKAISEWt44B4DqJ0a1LEqCD6Vtvc783Mg==
dependencies:
"@0no-co/graphql.web" "^1.0.8"
"@babel/runtime" "^7.20.0"
@@ -3710,15 +3710,15 @@
"@expo/env" "~0.4.0"
"@expo/image-utils" "^0.6.0"
"@expo/json-file" "^9.0.0"
"@expo/metro-config" "~0.19.0"
"@expo/metro-config" "~0.19.8"
"@expo/osascript" "^2.0.31"
"@expo/package-manager" "^1.5.0"
"@expo/plist" "^0.2.0"
"@expo/prebuild-config" "^8.0.22"
"@expo/prebuild-config" "^8.0.23"
"@expo/rudder-sdk-node" "^1.1.1"
"@expo/spawn-async" "^1.7.2"
"@expo/xcpretty" "^4.3.0"
"@react-native/dev-middleware" "0.76.3"
"@react-native/dev-middleware" "0.76.5"
"@urql/core" "^5.0.6"
"@urql/exchange-retry" "^1.3.0"
accepts "^1.3.8"
@@ -3894,10 +3894,10 @@
dotenv-expand "~11.0.6"
getenv "^1.0.0"
"@expo/fingerprint@0.11.3":
version "0.11.3"
resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.11.3.tgz#e370ae8f83e0642f752b058e2102e984a0a5bc98"
integrity sha512-9lgXmcIePvZ7Wef63XtvuN3HfCUevF4E4tQPdEbH9/dUWwpOvvwQ3KT4OJ9jdh8JJ3nTdO9eDQ/8k8xr1aQ5Kg==
"@expo/fingerprint@0.11.4":
version "0.11.4"
resolved "https://registry.yarnpkg.com/@expo/fingerprint/-/fingerprint-0.11.4.tgz#f40bbc784e10a065b783091e0d060a7428d41a7c"
integrity sha512-FfcvHjrWjOJ17wiMfr1iQ1YDyjlj8qfxG+GDce0khrjNSkzRjVdCOIFsMvfVSBPnOPX5NuZlgMRvMkcPUtGClA==
dependencies:
"@expo/spawn-async" "^1.7.2"
arg "^5.0.2"
@@ -3949,34 +3949,10 @@
json5 "^2.2.3"
write-file-atomic "^2.3.0"
"@expo/metro-config@0.19.6":
version "0.19.6"
resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.19.6.tgz#81bbe70c77a322d6c688738fd7b736a3cbb7c5bd"
integrity sha512-pRwZyOstsQa1+Ecss3wOqC28wjyjq9qxvJaQL3LH4G8Sef9x2PX+ySRApeQ01nl4ZN5nlyez6iVDF51tn/WhOw==
dependencies:
"@babel/core" "^7.20.0"
"@babel/generator" "^7.20.5"
"@babel/parser" "^7.20.0"
"@babel/types" "^7.20.0"
"@expo/config" "~10.0.4"
"@expo/env" "~0.4.0"
"@expo/json-file" "~9.0.0"
"@expo/spawn-async" "^1.7.2"
chalk "^4.1.0"
debug "^4.3.2"
fs-extra "^9.1.0"
getenv "^1.0.0"
glob "^10.4.2"
jsc-safe-url "^0.2.4"
lightningcss "~1.27.0"
minimatch "^3.0.4"
postcss "~8.4.32"
resolve-from "^5.0.0"
"@expo/metro-config@~0.19.0":
version "0.19.4"
resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.19.4.tgz#940b6fad7809a92a8ffdb1bbe87aa805f5822c6b"
integrity sha512-2SWwYN8MZvMIRawWEr+1RBYncitPwu2VMACRYig+wBycJ9fsPb6BMVmBYi+3MHDUlJHNy/Bqfw++jn1eqBFETQ==
"@expo/metro-config@0.19.8", "@expo/metro-config@~0.19.8":
version "0.19.8"
resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.19.8.tgz#f1ea552b6fa5217093fe364ff5ca78a7e261a28b"
integrity sha512-dVAOetouQYuOTEJ2zR0OTLNPOH6zPkeEt5fY53TK0Wxi1QmtsmH6vEWg05U4zkSJ6f1aXmQ0Za77R8QxuukESA==
dependencies:
"@babel/core" "^7.20.0"
"@babel/generator" "^7.20.5"
@@ -4032,17 +4008,17 @@
base64-js "^1.2.3"
xmlbuilder "^14.0.0"
"@expo/prebuild-config@8.0.22", "@expo/prebuild-config@^8.0.22":
version "8.0.22"
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-8.0.22.tgz#6e2762a5f333a0519f83ae05b69da45e3e26a913"
integrity sha512-Kwlf3ymHH37W2nuNA9FzYgZvrImJScLA98939kapnOxfNGAPhmhEw26sfIGmBWAa8ymdL6p+HXQ3+b/xJ74bOg==
"@expo/prebuild-config@^8.0.23":
version "8.0.23"
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-8.0.23.tgz#2ec6d5464f35d308bdb94ba75b7e6aba0ebb507d"
integrity sha512-Zf01kFiN2PISmLb0DhIAJh76v3J2oYUKSjiAtGZLOH0HUz59by/qdyU4mGHWdeyRdCCrLUA21Rct2MBykvRMsg==
dependencies:
"@expo/config" "~10.0.4"
"@expo/config-plugins" "~9.0.10"
"@expo/config-types" "^52.0.0"
"@expo/image-utils" "^0.6.0"
"@expo/json-file" "^9.0.0"
"@react-native/normalize-colors" "0.76.3"
"@react-native/normalize-colors" "0.76.5"
debug "^4.3.1"
fs-extra "^9.0.0"
resolve-from "^5.0.0"
@@ -5617,7 +5593,7 @@
dependencies:
"@react-native/codegen" "0.76.1"
"@react-native/babel-preset@0.76.1", "@react-native/babel-preset@0.76.3":
"@react-native/babel-preset@0.76.1", "@react-native/babel-preset@0.76.3", "@react-native/babel-preset@0.76.5":
version "0.76.1"
resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.76.1.tgz#2b9fd113e7c7889c1e87d6a36b7cb0f36118e7a6"
integrity sha512-b6YRmA13CmVuTQKHRen/Q0glHwmZFZoEDs+MJ1NL0UNHq9V5ytvdwTW1ntkmjtXuTnPMzkwYvumJBN9UTZjkBA==
@@ -5718,6 +5694,11 @@
resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.76.3.tgz#531e616f6dad159a58117efc69cec20422d15b0d"
integrity sha512-pMHQ3NpPB28RxXciSvm2yD+uDx3pkhzfuWkc7VFgOduyzPSIr0zotUiOJzsAtrj8++bPbOsAraCeQhCqoOTWQw==
"@react-native/debugger-frontend@0.76.5":
version "0.76.5"
resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.76.5.tgz#0e89940543fb5029506690b83f12547d0bf42cc4"
integrity sha512-5gtsLfBaSoa9WP8ToDb/8NnDBLZjv4sybQQj7rDKytKOdsXm3Pr2y4D7x7GQQtP1ZQRqzU0X0OZrhRz9xNnOqA==
"@react-native/dev-middleware@0.76.3":
version "0.76.3"
resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.76.3.tgz#52edc76c88e0c2c436eb989551b827bf69f2a56f"
@@ -5735,6 +5716,23 @@
serve-static "^1.13.1"
ws "^6.2.3"
"@react-native/dev-middleware@0.76.5":
version "0.76.5"
resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.76.5.tgz#10d02fcc6c3c9d24f6dc147c2ef95d6fa6bd3787"
integrity sha512-f8eimsxpkvMgJia7POKoUu9uqjGF6KgkxX4zqr/a6eoR1qdEAWUd6PonSAqtag3PAqvEaJpB99gLH2ZJI1nDGg==
dependencies:
"@isaacs/ttlcache" "^1.4.1"
"@react-native/debugger-frontend" "0.76.5"
chrome-launcher "^0.15.2"
chromium-edge-launcher "^0.2.0"
connect "^3.6.5"
debug "^2.2.0"
nullthrows "^1.1.1"
open "^7.0.3"
selfsigned "^2.4.1"
serve-static "^1.13.1"
ws "^6.2.3"
"@react-native/eslint-config@^0.76.2":
version "0.76.2"
resolved "https://registry.yarnpkg.com/@react-native/eslint-config/-/eslint-config-0.76.2.tgz#2741eee69ff194b8adc15281c0cb9695ba015ef0"
@@ -5779,7 +5777,7 @@
hermes-parser "0.23.1"
nullthrows "^1.1.1"
"@react-native/normalize-colors@0.76.1", "@react-native/normalize-colors@0.76.3", "@react-native/normalize-colors@^0.73.0", "@react-native/normalize-colors@^0.74.1":
"@react-native/normalize-colors@0.76.1", "@react-native/normalize-colors@0.76.3", "@react-native/normalize-colors@0.76.5", "@react-native/normalize-colors@^0.73.0", "@react-native/normalize-colors@^0.74.1":
version "0.76.1"
resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.76.1.tgz#df8d54d78917a9f075283382fec834f5ccaecefd"
integrity sha512-/+CUk/wGWIdXbJYVLw/q6Fs8Z0x91zzfXIbNiZUdSW1TNEDmytkF371H8a1/Nx3nWa1RqCMVsaZHCG4zqxeDvg==
@@ -8089,10 +8087,10 @@ babel-preset-expo@^12.0.2:
babel-plugin-react-native-web "~0.19.13"
react-refresh "^0.14.2"
babel-preset-expo@~12.0.3:
version "12.0.3"
resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-12.0.3.tgz#2ad62fe007517704841788cfea38b333e307663f"
integrity sha512-1695e8y3U/HjifKx33vcNnFMSUSXwPWwhFxRlL6NRx2TENN6gySH82gPOWgxcra6gi+EJgXx52xG3PcqTjwW6w==
babel-preset-expo@~12.0.4:
version "12.0.4"
resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-12.0.4.tgz#ec965530d866c8905aac1fa478562cb08ab32a55"
integrity sha512-SAzAwqpyjA+/OFrU95OOioj6oTeCv4+rRfrNmBTy5S/gJswrZKBSPJioFudIaJBy43W+BL7HA5AspBIF6tO/aA==
dependencies:
"@babel/plugin-proposal-decorators" "^7.12.9"
"@babel/plugin-transform-export-namespace-from" "^7.22.11"
@@ -8100,7 +8098,7 @@ babel-preset-expo@~12.0.3:
"@babel/plugin-transform-parameters" "^7.22.15"
"@babel/preset-react" "^7.22.15"
"@babel/preset-typescript" "^7.23.0"
"@react-native/babel-preset" "0.76.3"
"@react-native/babel-preset" "0.76.5"
babel-plugin-react-native-web "~0.19.13"
react-refresh "^0.14.2"
@@ -10456,10 +10454,10 @@ expo-eas-client@~0.13.0:
resolved "https://registry.yarnpkg.com/expo-eas-client/-/expo-eas-client-0.13.1.tgz#ebca627f3f58a54906394eb3f5d22f41a1822618"
integrity sha512-IyeDiM6YSJG0c45kbuEo0qt76z0KTEZtisEFEtle+b+vfn9I3N+r3jbPscaI4yS3P6gpuoDyHv81YDVC6Dmkhw==
expo-file-system@^18.0.4, expo-file-system@~18.0.4:
version "18.0.4"
resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-18.0.4.tgz#eecf8dc0b3b545e9ac5cd00352665afe2d57732f"
integrity sha512-aAWEDwnu0XHOBYvQ9Q0+QIa+483vYJaC4IDsXyWQ73Rtsg273NZh5kYowY+cAocvoSmA99G6htrLBn11ax2bTQ==
expo-file-system@^18.0.6, expo-file-system@~18.0.6:
version "18.0.6"
resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-18.0.6.tgz#43f7718530d0e2aa1f49bca7ccb721007acabf2c"
integrity sha512-gGEwIJCXV3/wpIJ/wRyhmieLOSAY7HeFFjb+wEfHs04aE63JYR+rXXV4b7rBpEh1ZgNV9U91zfet/iQG7J8HBQ==
dependencies:
web-streams-polyfill "^3.3.2"
@@ -10542,10 +10540,10 @@ expo-media-library@~17.0.3:
resolved "https://registry.yarnpkg.com/expo-media-library/-/expo-media-library-17.0.3.tgz#4ee3e6a8a2544887d910a72eaf2a15858b78cc0e"
integrity sha512-vo8AqWxv1C8+U8dA5W43qs8+3dgD3VZDvcCkZBQTBnGr/2Rs7x6nNQD5s7UfYyr6qmW6102JB3+OUKHpkwEssg==
expo-modules-autolinking@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-2.0.3.tgz#c0de0129bedf1b6f9aa36093e435d00509f27fcd"
integrity sha512-Q/ALJ54eS7Cr7cmbP+unEDTkHFQivQerWWrqZxuXOrSFYGCYU22+/xAZXaJOpZwseOVsP74zSkoRY/wBimVs7w==
expo-modules-autolinking@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-2.0.4.tgz#28fcd12fb0d066a2933cca3bf3b597da0f6b2f2a"
integrity sha512-e0p+19NhmD50U7s7BV7kWIypWmTNC9n/VlJKlXS05hM/zX7pe6JKmXyb+BFnXJq3SLBalLCUY0tu2gEUF3XeVg==
dependencies:
"@expo/spawn-async" "^1.7.2"
chalk "^4.1.0"
@@ -10556,7 +10554,14 @@ expo-modules-autolinking@2.0.3:
require-from-string "^2.0.2"
resolve-from "^5.0.0"
expo-modules-core@2.1.1, expo-modules-core@^2.1.1:
expo-modules-core@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-2.1.2.tgz#258be4fbd162b69eb4ad2789131ac2dc7e85fc08"
integrity sha512-0OhMU5S8zf9c/CRh1MwiXfOInI9wzz6yiIh5RuR/9J7N6xHRum68hInsPbaSc1UQpo08ZZLM4MPsbpoNRUoqIg==
dependencies:
invariant "^2.2.4"
expo-modules-core@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-2.1.1.tgz#970af4cfd70c8aa6fc0096dd0a6578aa003a479f"
integrity sha512-yQzYCLR2mre4BNMXuqkeJ0oSNgmGEMI6BcmIzeNZbC2NFEjiaDpKvlV9bclYCtyVhUEVNbJcEPYMr6c1Y4eR4w==
@@ -10599,12 +10604,12 @@ expo-sharing@^13.0.0:
resolved "https://registry.yarnpkg.com/expo-sharing/-/expo-sharing-13.0.0.tgz#fbc46f4afdaa265a2811fe88c2a589aae2d2de0f"
integrity sha512-b23ymicRmYn/Pjj05sl9tFZHN5cH9I1f0yiqY1Yk8Q3oCx0Aznri82DnTYA4T/J6D9vrkraX0wQ4jWVMOffmlg==
expo-splash-screen@~0.29.16:
version "0.29.16"
resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.29.16.tgz#80b49af2605213e661a40022d724caa1ea48ccb3"
integrity sha512-1WnExDA23hEJhz+djUthVUWxUvVtDT9sqRrpCgU4srG2OfBN0NryJ+Fbnoc1V2xw2uYc4Ij3ru0nH9a1TNvW9w==
expo-splash-screen@~0.29.18:
version "0.29.18"
resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.29.18.tgz#96ccce3d5a03389a9061743903b0a77c22a16796"
integrity sha512-bTBY+LF6YtYen2j60yGNh2SX/tG4UXZAyBCMMriOSiZZ7LSCs3ARyEufaSiWk+ckWShTeMqItOnaAN/CAF8MJA==
dependencies:
"@expo/prebuild-config" "^8.0.22"
"@expo/prebuild-config" "^8.0.23"
expo-status-bar@~2.0.0:
version "2.0.0"
@@ -10661,26 +10666,26 @@ expo-web-browser@~14.0.1:
resolved "https://registry.yarnpkg.com/expo-web-browser/-/expo-web-browser-14.0.1.tgz#97f3f141b0897364bc8364d90d6e29df0beec8aa"
integrity sha512-QM9F3ie+UyIOoBvqFmT6CZojb1vMc2H+7ZlMT5dEu1PL2jtYyOeK2hLfbt/EMt7CBm/w+P29H9W9Y9gdebOkuQ==
expo@~52.0.17:
version "52.0.17"
resolved "https://registry.yarnpkg.com/expo/-/expo-52.0.17.tgz#8a3edc20dabdb69a47f7b4b92e1bb96284044c14"
integrity sha512-f0WBD2T6p9r/a8v8MqkoWQq7TmbbAgPUg2zZtOp+kBrSCb3obHeNAsPDAUFzh+jEgug2qDVVkauBJa6ACe9AMg==
expo@~52.0.19:
version "52.0.19"
resolved "https://registry.yarnpkg.com/expo/-/expo-52.0.19.tgz#1b881c96ea595da0c5f3c13f578bc368c5261f4a"
integrity sha512-wOb/wbiQa0xqQRhgVBuOhLRus05TSw6fgThVMrPQgdLo24EPuT/ZAiRVcVRdjrEbwOqCDumgQCB7636B9J+jKg==
dependencies:
"@babel/runtime" "^7.20.0"
"@expo/cli" "0.22.3"
"@expo/cli" "0.22.6"
"@expo/config" "~10.0.6"
"@expo/config-plugins" "~9.0.12"
"@expo/fingerprint" "0.11.3"
"@expo/metro-config" "0.19.6"
"@expo/fingerprint" "0.11.4"
"@expo/metro-config" "0.19.8"
"@expo/vector-icons" "^14.0.0"
babel-preset-expo "~12.0.3"
babel-preset-expo "~12.0.4"
expo-asset "~11.0.1"
expo-constants "~17.0.3"
expo-file-system "~18.0.4"
expo-file-system "~18.0.6"
expo-font "~13.0.1"
expo-keep-awake "~14.0.1"
expo-modules-autolinking "2.0.3"
expo-modules-core "2.1.1"
expo-modules-autolinking "2.0.4"
expo-modules-core "2.1.2"
fbemitter "^3.0.0"
web-streams-polyfill "^3.3.2"
whatwg-url-without-unicode "8.0.0-3"