Migrate SavedFeeds screen
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native'
|
import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native'
|
||||||
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -7,13 +8,11 @@ import {useFocusEffect} from '@react-navigation/native'
|
|||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
import {track} from '#/lib/analytics/analytics'
|
import {track} from '#/lib/analytics/analytics'
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {
|
import {
|
||||||
usePinFeedMutation,
|
|
||||||
usePreferencesQuery,
|
usePreferencesQuery,
|
||||||
useSetSaveFeedsMutation,
|
useSetSaveFeedsMutation,
|
||||||
useUnpinFeedMutation,
|
useUpdateSavedFeedMutation,
|
||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||||
import {useSetMinimalShellMode} from '#/state/shell'
|
import {useSetMinimalShellMode} from '#/state/shell'
|
||||||
@@ -65,10 +64,9 @@ export function SavedFeeds({}: Props) {
|
|||||||
const currentFeeds =
|
const currentFeeds =
|
||||||
optimisticSavedFeedsResponse && !setSavedFeedsError
|
optimisticSavedFeedsResponse && !setSavedFeedsError
|
||||||
? optimisticSavedFeedsResponse
|
? optimisticSavedFeedsResponse
|
||||||
: preferences?.feeds || {saved: [], pinned: []}
|
: preferences?.savedFeeds || []
|
||||||
const unpinned = currentFeeds.saved.filter(f => {
|
const pinnedFeeds = currentFeeds.filter(f => f.pinned)
|
||||||
return !currentFeeds.pinned?.includes(f)
|
const unpinnedFeeds = currentFeeds.filter(f => !f.pinned)
|
||||||
})
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
@@ -93,7 +91,7 @@ export function SavedFeeds({}: Props) {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{preferences?.feeds ? (
|
{preferences?.feeds ? (
|
||||||
!currentFeeds.pinned.length ? (
|
!pinnedFeeds.length ? (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
pal.border,
|
pal.border,
|
||||||
@@ -106,10 +104,10 @@ export function SavedFeeds({}: Props) {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
currentFeeds.pinned.map(uri => (
|
pinnedFeeds.map(f => (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={uri}
|
key={f.id}
|
||||||
feedUri={uri}
|
feed={f}
|
||||||
isPinned
|
isPinned
|
||||||
setSavedFeeds={setSavedFeeds}
|
setSavedFeeds={setSavedFeeds}
|
||||||
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
|
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
|
||||||
@@ -127,7 +125,7 @@ export function SavedFeeds({}: Props) {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{preferences?.feeds ? (
|
{preferences?.feeds ? (
|
||||||
!unpinned.length ? (
|
!unpinnedFeeds.length ? (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
pal.border,
|
pal.border,
|
||||||
@@ -140,10 +138,10 @@ export function SavedFeeds({}: Props) {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
unpinned.map(uri => (
|
unpinnedFeeds.map(f => (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={uri}
|
key={f.id}
|
||||||
feedUri={uri}
|
feed={f}
|
||||||
isPinned={false}
|
isPinned={false}
|
||||||
setSavedFeeds={setSavedFeeds}
|
setSavedFeeds={setSavedFeeds}
|
||||||
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
|
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
|
||||||
@@ -178,16 +176,15 @@ export function SavedFeeds({}: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ListItem({
|
function ListItem({
|
||||||
feedUri,
|
feed,
|
||||||
isPinned,
|
isPinned,
|
||||||
currentFeeds,
|
currentFeeds,
|
||||||
setSavedFeeds,
|
setSavedFeeds,
|
||||||
resetSaveFeedsMutationState,
|
resetSaveFeedsMutationState,
|
||||||
preferences,
|
|
||||||
}: {
|
}: {
|
||||||
feedUri: string // uri
|
feed: AppBskyActorDefs.SavedFeed
|
||||||
isPinned: boolean
|
isPinned: boolean
|
||||||
currentFeeds: {saved: string[]; pinned: string[]}
|
currentFeeds: AppBskyActorDefs.SavedFeed[]
|
||||||
setSavedFeeds: ReturnType<typeof useSetSaveFeedsMutation>['mutateAsync']
|
setSavedFeeds: ReturnType<typeof useSetSaveFeedsMutation>['mutateAsync']
|
||||||
resetSaveFeedsMutationState: ReturnType<
|
resetSaveFeedsMutationState: ReturnType<
|
||||||
typeof useSetSaveFeedsMutation
|
typeof useSetSaveFeedsMutation
|
||||||
@@ -197,17 +194,9 @@ function ListItem({
|
|||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
|
const {isPending: isUpdatePending, mutateAsync: updateFeed} =
|
||||||
const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
|
useUpdateSavedFeedMutation()
|
||||||
useUnpinFeedMutation()
|
const feedUri = feed.value
|
||||||
const isPending = isPinPending || isUnpinPending
|
|
||||||
const gate = useGate()
|
|
||||||
const primaryAlgo = preferences.primaryAlgorithm
|
|
||||||
const isPrimaryAlgoExperimentEnabled = gate(
|
|
||||||
'reduced_onboarding_and_home_algo',
|
|
||||||
)
|
|
||||||
const isPrimaryAlgo = primaryAlgo?.enabled && primaryAlgo?.uri === feedUri
|
|
||||||
const showPinButton = !(isPrimaryAlgoExperimentEnabled && isPrimaryAlgo)
|
|
||||||
|
|
||||||
const onTogglePinned = React.useCallback(async () => {
|
const onTogglePinned = React.useCallback(async () => {
|
||||||
playHaptic()
|
playHaptic()
|
||||||
@@ -215,67 +204,74 @@ function ListItem({
|
|||||||
try {
|
try {
|
||||||
resetSaveFeedsMutationState()
|
resetSaveFeedsMutationState()
|
||||||
|
|
||||||
if (isPinned) {
|
if (feed.pinned) {
|
||||||
await unpinFeed({uri: feedUri})
|
await updateFeed({
|
||||||
|
...feed,
|
||||||
|
pinned: false,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
await pinFeed({uri: feedUri})
|
await updateFeed({
|
||||||
|
...feed,
|
||||||
|
pinned: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.show(_(msg`There was an issue contacting the server`))
|
Toast.show(_(msg`There was an issue contacting the server`))
|
||||||
logger.error('Failed to toggle pinned feed', {message: e})
|
logger.error('Failed to toggle pinned feed', {message: e})
|
||||||
}
|
}
|
||||||
}, [
|
}, [_, playHaptic, feed, updateFeed, resetSaveFeedsMutationState])
|
||||||
playHaptic,
|
|
||||||
resetSaveFeedsMutationState,
|
|
||||||
isPinned,
|
|
||||||
unpinFeed,
|
|
||||||
feedUri,
|
|
||||||
pinFeed,
|
|
||||||
_,
|
|
||||||
])
|
|
||||||
|
|
||||||
const onPressUp = React.useCallback(async () => {
|
const onPressUp = React.useCallback(async () => {
|
||||||
if (!isPinned) return
|
if (!isPinned) return
|
||||||
|
|
||||||
// create new array, do not mutate
|
const nextFeeds = currentFeeds.slice()
|
||||||
const pinned = [...currentFeeds.pinned]
|
const ids = currentFeeds.map(f => f.id)
|
||||||
const index = pinned.indexOf(feedUri)
|
const index = ids.indexOf(feed.id)
|
||||||
|
const nextIndex = index - 1
|
||||||
|
|
||||||
if (index === -1 || index === 0) return
|
if (index === -1 || index === 0) return
|
||||||
;[pinned[index], pinned[index - 1]] = [pinned[index - 1], pinned[index]]
|
;[nextFeeds[index], nextFeeds[nextIndex]] = [
|
||||||
|
nextFeeds[nextIndex],
|
||||||
|
nextFeeds[index],
|
||||||
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setSavedFeeds({saved: currentFeeds.saved, pinned})
|
await setSavedFeeds(nextFeeds)
|
||||||
track('CustomFeed:Reorder', {
|
track('CustomFeed:Reorder', {
|
||||||
uri: feedUri,
|
uri: feed.value,
|
||||||
index: pinned.indexOf(feedUri),
|
index: nextIndex,
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.show(_(msg`There was an issue contacting the server`))
|
Toast.show(_(msg`There was an issue contacting the server`))
|
||||||
logger.error('Failed to set pinned feed order', {message: e})
|
logger.error('Failed to set pinned feed order', {message: e})
|
||||||
}
|
}
|
||||||
}, [feedUri, isPinned, setSavedFeeds, currentFeeds, _])
|
}, [feed, isPinned, setSavedFeeds, currentFeeds, _])
|
||||||
|
|
||||||
const onPressDown = React.useCallback(async () => {
|
const onPressDown = React.useCallback(async () => {
|
||||||
if (!isPinned) return
|
if (!isPinned) return
|
||||||
|
|
||||||
const pinned = [...currentFeeds.pinned]
|
const nextFeeds = currentFeeds.slice()
|
||||||
const index = pinned.indexOf(feedUri)
|
const ids = currentFeeds.map(f => f.id)
|
||||||
|
const index = ids.indexOf(feed.id)
|
||||||
|
const nextIndex = index + 1
|
||||||
|
|
||||||
if (index === -1 || index >= pinned.length - 1) return
|
if (index === -1 || index >= nextFeeds.length - 1) return
|
||||||
;[pinned[index], pinned[index + 1]] = [pinned[index + 1], pinned[index]]
|
;[nextFeeds[index], nextFeeds[nextIndex]] = [
|
||||||
|
nextFeeds[nextIndex],
|
||||||
|
nextFeeds[index],
|
||||||
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setSavedFeeds({saved: currentFeeds.saved, pinned})
|
await setSavedFeeds(nextFeeds)
|
||||||
track('CustomFeed:Reorder', {
|
track('CustomFeed:Reorder', {
|
||||||
uri: feedUri,
|
uri: feed.value,
|
||||||
index: pinned.indexOf(feedUri),
|
index: nextIndex,
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.show(_(msg`There was an issue contacting the server`))
|
Toast.show(_(msg`There was an issue contacting the server`))
|
||||||
logger.error('Failed to set pinned feed order', {message: e})
|
logger.error('Failed to set pinned feed order', {message: e})
|
||||||
}
|
}
|
||||||
}, [feedUri, isPinned, setSavedFeeds, currentFeeds, _])
|
}, [feed, isPinned, setSavedFeeds, currentFeeds, _])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -284,12 +280,13 @@ function ListItem({
|
|||||||
{isPinned ? (
|
{isPinned ? (
|
||||||
<View style={styles.webArrowButtonsContainer}>
|
<View style={styles.webArrowButtonsContainer}>
|
||||||
<Pressable
|
<Pressable
|
||||||
disabled={isPending}
|
disabled={isUpdatePending}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={onPressUp}
|
onPress={onPressUp}
|
||||||
hitSlop={HITSLOP_TOP}
|
hitSlop={HITSLOP_TOP}
|
||||||
style={state => ({
|
style={state => ({
|
||||||
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
|
opacity:
|
||||||
|
state.hovered || state.focused || isUpdatePending ? 0.5 : 1,
|
||||||
})}>
|
})}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="arrow-up"
|
icon="arrow-up"
|
||||||
@@ -298,12 +295,13 @@ function ListItem({
|
|||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
disabled={isPending}
|
disabled={isUpdatePending}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={onPressDown}
|
onPress={onPressDown}
|
||||||
hitSlop={HITSLOP_BOTTOM}
|
hitSlop={HITSLOP_BOTTOM}
|
||||||
style={state => ({
|
style={state => ({
|
||||||
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
|
opacity:
|
||||||
|
state.hovered || state.focused || isUpdatePending ? 0.5 : 1,
|
||||||
})}>
|
})}>
|
||||||
<FontAwesomeIcon icon="arrow-down" size={12} style={[pal.text]} />
|
<FontAwesomeIcon icon="arrow-down" size={12} style={[pal.text]} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -316,15 +314,15 @@ function ListItem({
|
|||||||
showSaveBtn
|
showSaveBtn
|
||||||
showMinimalPlaceholder
|
showMinimalPlaceholder
|
||||||
/>
|
/>
|
||||||
{showPinButton && (
|
|
||||||
<View style={{paddingRight: 16}}>
|
<View style={{paddingRight: 16}}>
|
||||||
<Pressable
|
<Pressable
|
||||||
disabled={isPending}
|
disabled={isUpdatePending}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
hitSlop={10}
|
hitSlop={10}
|
||||||
onPress={onTogglePinned}
|
onPress={onTogglePinned}
|
||||||
style={state => ({
|
style={state => ({
|
||||||
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
|
opacity:
|
||||||
|
state.hovered || state.focused || isUpdatePending ? 0.5 : 1,
|
||||||
})}>
|
})}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="thumb-tack"
|
icon="thumb-tack"
|
||||||
@@ -333,7 +331,6 @@ function ListItem({
|
|||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)}
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user