Merge remote-tracking branch 'upstream/main' into invite-code-warning

This commit is contained in:
Samuel Newman 2023-11-30 17:11:51 +00:00
commit b164f151cc
76 changed files with 1718 additions and 1240 deletions

View file

@ -437,6 +437,7 @@ export function FeedsScreen(_props: Props) {
showSaveBtn={hasSession}
showDescription
showLikes
pinOnSave
/>
)
} else if (item.type === 'popularFeedsNoResults') {

View file

@ -40,6 +40,12 @@ function HomeScreenReady({
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
const [selectedPage, setSelectedPage] = React.useState(0)
/**
* Used to ensure that we re-compute `customFeeds` AND force a re-render of
* the pager with the new order of feeds.
*/
const pinnedFeedOrderKey = JSON.stringify(preferences.feeds.pinned)
const customFeeds = React.useMemo(() => {
const pinned = preferences.feeds.pinned
const feeds: FeedDescriptor[] = []
@ -83,7 +89,6 @@ function HomeScreenReady({
emitSoftReset()
}, [])
// TODO(pwi) may need this in public view
const onPageScrollStateChanged = React.useCallback(
(state: 'idle' | 'dragging' | 'settling') => {
if (state === 'dragging') {
@ -118,6 +123,7 @@ function HomeScreenReady({
return hasSession ? (
<Pager
key={pinnedFeedOrderKey}
testID="homeScreen"
onPageSelected={onPageSelected}
onPageScrollStateChanged={onPageScrollStateChanged}

View file

@ -19,7 +19,10 @@ import {logger} from '#/logger'
import {useSetMinimalShellMode} from '#/state/shell'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {
useUnreadNotifications,
useUnreadNotificationsApi,
} from '#/state/queries/notifications/unread'
import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
import {listenSoftReset, emitSoftReset} from '#/state/events'
@ -35,8 +38,9 @@ export function NotificationsScreen({}: Props) {
const {screen} = useAnalytics()
const pal = usePalette('default')
const {isDesktop} = useWebMediaQueries()
const unreadNotifs = useUnreadNotifications()
const queryClient = useQueryClient()
const unreadNotifs = useUnreadNotifications()
const unreadApi = useUnreadNotificationsApi()
const hasNew = !!unreadNotifs
// event handlers
@ -48,10 +52,16 @@ export function NotificationsScreen({}: Props) {
const onPressLoadLatest = React.useCallback(() => {
scrollToTop()
queryClient.invalidateQueries({
queryKey: NOTIFS_RQKEY(),
})
}, [scrollToTop, queryClient])
if (hasNew) {
// render what we have now
queryClient.resetQueries({
queryKey: NOTIFS_RQKEY(),
})
} else {
// check with the server
unreadApi.checkUnread({invalidate: true})
}
}, [scrollToTop, queryClient, unreadApi, hasNew])
// on-visible setup
// =

View file

@ -267,6 +267,7 @@ function ProfileScreenLoaded({
screenDescription="profile"
moderation={moderation.account}>
<PagerWithHeader
testID="profilePager"
isHeaderReady={true}
items={sectionTitles}
onPageSelected={onPageSelected}
@ -403,7 +404,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
const onScrollToTop = React.useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
React.useImperativeHandle(ref, () => ({

View file

@ -353,6 +353,7 @@ export function ProfileFeedScreenInner({
style={styles.btn}
/>
<Button
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
disabled={isPinPending || isUnpinPending}
type={isPinned ? 'default' : 'inverted'}
label={isPinned ? 'Unpin' : 'Pin to home'}
@ -501,7 +502,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])

View file

@ -127,7 +127,7 @@ function ProfileListScreenLoaded({
list,
onChange() {
if (isCurateList) {
queryClient.invalidateQueries({
queryClient.resetQueries({
// TODO(eric) should construct these strings with a fn too
queryKey: FEED_RQKEY(`list|${list.uri}`),
})
@ -530,7 +530,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
React.useImperativeHandle(ref, () => ({

View file

@ -1,14 +1,7 @@
import React from 'react'
import {
StyleSheet,
View,
ActivityIndicator,
Pressable,
TouchableOpacity,
} from 'react-native'
import {StyleSheet, View, ActivityIndicator, Pressable} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useQueryClient} from '@tanstack/react-query'
import {track} from '#/lib/analytics/analytics'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
@ -32,9 +25,8 @@ import {
usePinFeedMutation,
useUnpinFeedMutation,
useSetSaveFeedsMutation,
preferencesQueryKey,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
const HITSLOP_TOP = {
top: 20,
@ -57,6 +49,24 @@ export function SavedFeeds({}: Props) {
const {screen} = useAnalytics()
const setMinimalShellMode = useSetMinimalShellMode()
const {data: preferences} = usePreferencesQuery()
const {
mutateAsync: setSavedFeeds,
variables: optimisticSavedFeedsResponse,
reset: resetSaveFeedsMutationState,
error: setSavedFeedsError,
} = useSetSaveFeedsMutation()
/*
* Use optimistic data if exists and no error, otherwise fallback to remote
* data
*/
const currentFeeds =
optimisticSavedFeedsResponse && !setSavedFeedsError
? optimisticSavedFeedsResponse
: preferences?.feeds || {saved: [], pinned: []}
const unpinned = currentFeeds.saved.filter(f => {
return !currentFeeds.pinned?.includes(f)
})
useFocusEffect(
React.useCallback(() => {
@ -80,7 +90,7 @@ export function SavedFeeds({}: Props) {
</Text>
</View>
{preferences?.feeds ? (
!preferences.feeds.pinned.length ? (
!currentFeeds.pinned.length ? (
<View
style={[
pal.border,
@ -93,8 +103,15 @@ export function SavedFeeds({}: Props) {
</Text>
</View>
) : (
preferences?.feeds?.pinned?.map(uri => (
<ListItem key={uri} feedUri={uri} isPinned />
currentFeeds.pinned.map(uri => (
<ListItem
key={uri}
feedUri={uri}
isPinned
setSavedFeeds={setSavedFeeds}
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
currentFeeds={currentFeeds}
/>
))
)
) : (
@ -106,7 +123,7 @@ export function SavedFeeds({}: Props) {
</Text>
</View>
{preferences?.feeds ? (
!preferences.feeds.unpinned.length ? (
!unpinned.length ? (
<View
style={[
pal.border,
@ -119,8 +136,15 @@ export function SavedFeeds({}: Props) {
</Text>
</View>
) : (
preferences.feeds.unpinned.map(uri => (
<ListItem key={uri} feedUri={uri} isPinned={false} />
unpinned.map(uri => (
<ListItem
key={uri}
feedUri={uri}
isPinned={false}
setSavedFeeds={setSavedFeeds}
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
currentFeeds={currentFeeds}
/>
))
)
) : (
@ -151,22 +175,30 @@ export function SavedFeeds({}: Props) {
function ListItem({
feedUri,
isPinned,
currentFeeds,
setSavedFeeds,
resetSaveFeedsMutationState,
}: {
feedUri: string // uri
isPinned: boolean
currentFeeds: {saved: string[]; pinned: string[]}
setSavedFeeds: ReturnType<typeof useSetSaveFeedsMutation>['mutateAsync']
resetSaveFeedsMutationState: ReturnType<
typeof useSetSaveFeedsMutation
>['reset']
}) {
const pal = usePalette('default')
const queryClient = useQueryClient()
const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
useUnpinFeedMutation()
const {isPending: isMovePending, mutateAsync: setSavedFeeds} =
useSetSaveFeedsMutation()
const isPending = isPinPending || isUnpinPending
const onTogglePinned = React.useCallback(async () => {
Haptics.default()
try {
resetSaveFeedsMutationState()
if (isPinned) {
await unpinFeed({uri: feedUri})
} else {
@ -176,23 +208,20 @@ function ListItem({
Toast.show('There was an issue contacting the server')
logger.error('Failed to toggle pinned feed', {error: e})
}
}, [feedUri, isPinned, pinFeed, unpinFeed])
}, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState])
const onPressUp = React.useCallback(async () => {
if (!isPinned) return
const feeds =
queryClient.getQueryData<UsePreferencesQueryResponse>(
preferencesQueryKey,
)?.feeds
const pinned = feeds?.pinned ?? []
// create new array, do not mutate
const pinned = [...currentFeeds.pinned]
const index = pinned.indexOf(feedUri)
if (index === -1 || index === 0) return
;[pinned[index], pinned[index - 1]] = [pinned[index - 1], pinned[index]]
try {
await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
await setSavedFeeds({saved: currentFeeds.saved, pinned})
track('CustomFeed:Reorder', {
uri: feedUri,
index: pinned.indexOf(feedUri),
@ -201,23 +230,19 @@ function ListItem({
Toast.show('There was an issue contacting the server')
logger.error('Failed to set pinned feed order', {error: e})
}
}, [feedUri, isPinned, queryClient, setSavedFeeds])
}, [feedUri, isPinned, setSavedFeeds, currentFeeds])
const onPressDown = React.useCallback(async () => {
if (!isPinned) return
const feeds =
queryClient.getQueryData<UsePreferencesQueryResponse>(
preferencesQueryKey,
)?.feeds
const pinned = feeds?.pinned ?? []
const pinned = [...currentFeeds.pinned]
const index = pinned.indexOf(feedUri)
if (index === -1 || index >= pinned.length - 1) return
;[pinned[index], pinned[index + 1]] = [pinned[index + 1], pinned[index]]
try {
await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
await setSavedFeeds({saved: currentFeeds.saved, pinned})
track('CustomFeed:Reorder', {
uri: feedUri,
index: pinned.indexOf(feedUri),
@ -226,7 +251,7 @@ function ListItem({
Toast.show('There was an issue contacting the server')
logger.error('Failed to set pinned feed order', {error: e})
}
}, [feedUri, isPinned, queryClient, setSavedFeeds])
}, [feedUri, isPinned, setSavedFeeds, currentFeeds])
return (
<Pressable
@ -234,24 +259,30 @@ function ListItem({
style={[styles.itemContainer, pal.border]}>
{isPinned ? (
<View style={styles.webArrowButtonsContainer}>
<TouchableOpacity
disabled={isMovePending}
<Pressable
disabled={isPending}
accessibilityRole="button"
onPress={onPressUp}
hitSlop={HITSLOP_TOP}>
hitSlop={HITSLOP_TOP}
style={state => ({
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
})}>
<FontAwesomeIcon
icon="arrow-up"
size={12}
style={[pal.text, styles.webArrowUpButton]}
/>
</TouchableOpacity>
<TouchableOpacity
disabled={isMovePending}
</Pressable>
<Pressable
disabled={isPending}
accessibilityRole="button"
onPress={onPressDown}
hitSlop={HITSLOP_BOTTOM}>
hitSlop={HITSLOP_BOTTOM}
style={state => ({
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
})}>
<FontAwesomeIcon icon="arrow-down" size={12} style={[pal.text]} />
</TouchableOpacity>
</Pressable>
</View>
) : null}
<FeedSourceCard
@ -259,18 +290,28 @@ function ListItem({
feedUri={feedUri}
style={styles.noBorder}
showSaveBtn
LoadingComponent={
<FeedLoadingPlaceholder
style={{flex: 1}}
showLowerPlaceholder={false}
showTopBorder={false}
/>
}
/>
<TouchableOpacity
disabled={isPinPending || isUnpinPending}
<Pressable
disabled={isPending}
accessibilityRole="button"
hitSlop={10}
onPress={onTogglePinned}>
onPress={onTogglePinned}
style={state => ({
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
})}>
<FontAwesomeIcon
icon="thumb-tack"
size={20}
color={isPinned ? colors.blue3 : pal.colors.icon}
/>
</TouchableOpacity>
</Pressable>
</Pressable>
)
}

View file

@ -10,11 +10,7 @@ import {
View,
ViewStyle,
} from 'react-native'
import {
useFocusEffect,
useNavigation,
StackActions,
} from '@react-navigation/native'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@ -73,6 +69,8 @@ import {STATUS_PAGE_URL} from 'lib/constants'
import {Plural, Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
function SettingsAccountCard({account}: {account: SessionAccount}) {
const pal = usePalette('default')
@ -154,13 +152,14 @@ export function SettingsScreen({}: Props) {
const {screen, track} = useAnalytics()
const {openModal} = useModalControls()
const {isSwitchingAccounts, accounts, currentAccount} = useSession()
const {clearCurrentAccount} = useSessionApi()
const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
getAgent(),
)
const {mutate: clearPreferences} = useClearPreferencesMutation()
const {data: invites} = useInviteCodesQuery()
const invitesAvailable = invites?.available?.length ?? 0
const {setShowLoggedOut} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const primaryBg = useCustomPalette<ViewStyle>({
light: {backgroundColor: colors.blue0},
@ -189,10 +188,9 @@ export function SettingsScreen({}: Props) {
const onPressAddAccount = React.useCallback(() => {
track('Settings:AddAccountButtonClicked')
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
clearCurrentAccount()
}, [track, navigation, clearCurrentAccount])
setShowLoggedOut(true)
closeAllActiveElements()
}, [track, setShowLoggedOut, closeAllActiveElements])
const onPressChangeHandle = React.useCallback(() => {
track('Settings:ChangeHandleButtonClicked')