Eric/preferences (#1873)
* Add initial preferences query, couple mutations * Remove unused * Clean up labels, migrate getModerationOpts * Add birth date handling * Migrate feed prefs * Migrate thread view prefs * Migrate homeFeed to use existing key name * Fix up saved feeds in response, no impl yet * Migrate saved feeds to new hooks * Clean up more of preferences * Fix PreferencesThreads load state * Fix modal dismissal * Small spacing fix --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
parent
c8c308e31e
commit
05b728fffc
22 changed files with 1339 additions and 914 deletions
|
@ -4,7 +4,6 @@ import {observer} from 'mobx-react-lite'
|
|||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Slider} from '@miblanchard/react-native-slider'
|
||||
import {Text} from '../com/util/text/Text'
|
||||
import {useStores} from 'state/index'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
|
@ -16,21 +15,31 @@ import {CenteredView} from 'view/com/util/Views'
|
|||
import debounce from 'lodash.debounce'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
useSetFeedViewPreferencesMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
|
||||
function RepliesThresholdInput({enabled}: {enabled: boolean}) {
|
||||
const store = useStores()
|
||||
function RepliesThresholdInput({
|
||||
enabled,
|
||||
initialValue,
|
||||
}: {
|
||||
enabled: boolean
|
||||
initialValue: number
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const [value, setValue] = useState(
|
||||
store.preferences.homeFeed.hideRepliesByLikeCount,
|
||||
)
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
|
||||
const save = React.useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
threshold =>
|
||||
store.preferences.setHomeFeedHideRepliesByLikeCount(threshold),
|
||||
setFeedViewPref({
|
||||
hideRepliesByLikeCount: threshold,
|
||||
}),
|
||||
500,
|
||||
), // debouce for 500ms
|
||||
[store],
|
||||
[setFeedViewPref],
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -67,9 +76,15 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
navigation,
|
||||
}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {mutate: setFeedViewPref, variables} =
|
||||
useSetFeedViewPreferencesMutation()
|
||||
|
||||
const showReplies = !(
|
||||
variables?.hideReplies ?? preferences?.feedViewPrefs?.hideReplies
|
||||
)
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
|
@ -105,17 +120,20 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
<ToggleButton
|
||||
testID="toggleRepliesBtn"
|
||||
type="default-light"
|
||||
label={store.preferences.homeFeed.hideReplies ? 'No' : 'Yes'}
|
||||
isSelected={!store.preferences.homeFeed.hideReplies}
|
||||
onPress={store.preferences.toggleHomeFeedHideReplies}
|
||||
label={showReplies ? 'Yes' : 'No'}
|
||||
isSelected={showReplies}
|
||||
onPress={() =>
|
||||
setFeedViewPref({
|
||||
hideReplies: !(
|
||||
variables?.hideReplies ??
|
||||
preferences?.feedViewPrefs?.hideReplies
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pal.viewLight,
|
||||
styles.card,
|
||||
store.preferences.homeFeed.hideReplies && styles.dimmed,
|
||||
]}>
|
||||
style={[pal.viewLight, styles.card, !showReplies && styles.dimmed]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
<Trans>Reply Filters</Trans>
|
||||
</Text>
|
||||
|
@ -128,10 +146,19 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
<ToggleButton
|
||||
type="default-light"
|
||||
label="Followed users only"
|
||||
isSelected={store.preferences.homeFeed.hideRepliesByUnfollowed}
|
||||
isSelected={Boolean(
|
||||
variables?.hideRepliesByUnfollowed ??
|
||||
preferences?.feedViewPrefs?.hideRepliesByUnfollowed,
|
||||
)}
|
||||
onPress={
|
||||
!store.preferences.homeFeed.hideReplies
|
||||
? store.preferences.toggleHomeFeedHideRepliesByUnfollowed
|
||||
showReplies
|
||||
? () =>
|
||||
setFeedViewPref({
|
||||
hideRepliesByUnfollowed: !(
|
||||
variables?.hideRepliesByUnfollowed ??
|
||||
preferences?.feedViewPrefs?.hideRepliesByUnfollowed
|
||||
),
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
style={[s.mb10]}
|
||||
|
@ -142,9 +169,12 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
feed.
|
||||
</Trans>
|
||||
</Text>
|
||||
<RepliesThresholdInput
|
||||
enabled={!store.preferences.homeFeed.hideReplies}
|
||||
/>
|
||||
{preferences && (
|
||||
<RepliesThresholdInput
|
||||
enabled={showReplies}
|
||||
initialValue={preferences.feedViewPrefs.hideRepliesByLikeCount}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
|
@ -158,9 +188,26 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
label={store.preferences.homeFeed.hideReposts ? 'No' : 'Yes'}
|
||||
isSelected={!store.preferences.homeFeed.hideReposts}
|
||||
onPress={store.preferences.toggleHomeFeedHideReposts}
|
||||
label={
|
||||
variables?.hideReposts ??
|
||||
preferences?.feedViewPrefs?.hideReposts
|
||||
? 'No'
|
||||
: 'Yes'
|
||||
}
|
||||
isSelected={
|
||||
!(
|
||||
variables?.hideReposts ??
|
||||
preferences?.feedViewPrefs?.hideReposts
|
||||
)
|
||||
}
|
||||
onPress={() =>
|
||||
setFeedViewPref({
|
||||
hideReposts: !(
|
||||
variables?.hideReposts ??
|
||||
preferences?.feedViewPrefs?.hideReposts
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
@ -176,9 +223,26 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
label={store.preferences.homeFeed.hideQuotePosts ? 'No' : 'Yes'}
|
||||
isSelected={!store.preferences.homeFeed.hideQuotePosts}
|
||||
onPress={store.preferences.toggleHomeFeedHideQuotePosts}
|
||||
label={
|
||||
variables?.hideQuotePosts ??
|
||||
preferences?.feedViewPrefs?.hideQuotePosts
|
||||
? 'No'
|
||||
: 'Yes'
|
||||
}
|
||||
isSelected={
|
||||
!(
|
||||
variables?.hideQuotePosts ??
|
||||
preferences?.feedViewPrefs?.hideQuotePosts
|
||||
)
|
||||
}
|
||||
onPress={() =>
|
||||
setFeedViewPref({
|
||||
hideQuotePosts: !(
|
||||
variables?.hideQuotePosts ??
|
||||
preferences?.feedViewPrefs?.hideQuotePosts
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
@ -196,10 +260,25 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
|||
<ToggleButton
|
||||
type="default-light"
|
||||
label={
|
||||
store.preferences.homeFeed.lab_mergeFeedEnabled ? 'Yes' : 'No'
|
||||
variables?.lab_mergeFeedEnabled ??
|
||||
preferences?.feedViewPrefs?.lab_mergeFeedEnabled
|
||||
? 'Yes'
|
||||
: 'No'
|
||||
}
|
||||
isSelected={
|
||||
!!(
|
||||
variables?.lab_mergeFeedEnabled ??
|
||||
preferences?.feedViewPrefs?.lab_mergeFeedEnabled
|
||||
)
|
||||
}
|
||||
onPress={() =>
|
||||
setFeedViewPref({
|
||||
lab_mergeFeedEnabled: !(
|
||||
variables?.lab_mergeFeedEnabled ??
|
||||
preferences?.feedViewPrefs?.lab_mergeFeedEnabled
|
||||
),
|
||||
})
|
||||
}
|
||||
isSelected={!!store.preferences.homeFeed.lab_mergeFeedEnabled}
|
||||
onPress={store.preferences.toggleHomeFeedMergeFeedEnabled}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import React from 'react'
|
||||
import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Text} from '../com/util/text/Text'
|
||||
import {useStores} from 'state/index'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
|
@ -14,15 +19,30 @@ import {ViewHeader} from 'view/com/util/ViewHeader'
|
|||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
useSetThreadViewPreferencesMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
|
||||
export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
||||
navigation,
|
||||
}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {_} = useLingui()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {mutate: setThreadViewPrefs, variables} =
|
||||
useSetThreadViewPreferencesMutation()
|
||||
|
||||
const prioritizeFollowedUsers = Boolean(
|
||||
variables?.prioritizeFollowedUsers ??
|
||||
preferences?.threadViewPrefs?.prioritizeFollowedUsers,
|
||||
)
|
||||
const treeViewEnabled = Boolean(
|
||||
variables?.lab_treeViewEnabled ??
|
||||
preferences?.threadViewPrefs?.lab_treeViewEnabled,
|
||||
)
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
|
@ -44,71 +64,79 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
|
|||
</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView>
|
||||
<View style={styles.cardsContainer}>
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
<Trans>Sort Replies</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>Sort replies to the same post by:</Trans>
|
||||
</Text>
|
||||
<View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
|
||||
<RadioGroup
|
||||
{preferences ? (
|
||||
<ScrollView>
|
||||
<View style={styles.cardsContainer}>
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
<Trans>Sort Replies</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>Sort replies to the same post by:</Trans>
|
||||
</Text>
|
||||
<View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
|
||||
<RadioGroup
|
||||
type="default-light"
|
||||
items={[
|
||||
{key: 'oldest', label: 'Oldest replies first'},
|
||||
{key: 'newest', label: 'Newest replies first'},
|
||||
{key: 'most-likes', label: 'Most-liked replies first'},
|
||||
{key: 'random', label: 'Random (aka "Poster\'s Roulette")'},
|
||||
]}
|
||||
onSelect={key => setThreadViewPrefs({sort: key})}
|
||||
initialSelection={preferences?.threadViewPrefs?.sort}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
<Trans>Prioritize Your Follows</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>
|
||||
Show replies by people you follow before all other replies.
|
||||
</Trans>
|
||||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
items={[
|
||||
{key: 'oldest', label: 'Oldest replies first'},
|
||||
{key: 'newest', label: 'Newest replies first'},
|
||||
{key: 'most-likes', label: 'Most-liked replies first'},
|
||||
{key: 'random', label: 'Random (aka "Poster\'s Roulette")'},
|
||||
]}
|
||||
onSelect={store.preferences.setThreadSort}
|
||||
initialSelection={store.preferences.thread.sort}
|
||||
label={prioritizeFollowedUsers ? 'Yes' : 'No'}
|
||||
isSelected={prioritizeFollowedUsers}
|
||||
onPress={() =>
|
||||
setThreadViewPrefs({
|
||||
prioritizeFollowedUsers: !prioritizeFollowedUsers,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
<FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
|
||||
<Trans>Threaded Mode</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>
|
||||
Set this setting to "Yes" to show replies in a threaded view.
|
||||
This is an experimental feature.
|
||||
</Trans>
|
||||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
label={treeViewEnabled ? 'Yes' : 'No'}
|
||||
isSelected={treeViewEnabled}
|
||||
onPress={() =>
|
||||
setThreadViewPrefs({
|
||||
lab_treeViewEnabled: !treeViewEnabled,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
<Trans>Prioritize Your Follows</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>
|
||||
Show replies by people you follow before all other replies.
|
||||
</Trans>
|
||||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
label={
|
||||
store.preferences.thread.prioritizeFollowedUsers ? 'Yes' : 'No'
|
||||
}
|
||||
isSelected={store.preferences.thread.prioritizeFollowedUsers}
|
||||
onPress={store.preferences.togglePrioritizedFollowedUsers}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[pal.viewLight, styles.card]}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
<FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
|
||||
<Trans>Threaded Mode</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>
|
||||
Set this setting to "Yes" to show replies in a threaded view.
|
||||
This is an experimental feature.
|
||||
</Trans>
|
||||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
label={
|
||||
store.preferences.thread.lab_treeViewEnabled ? 'Yes' : 'No'
|
||||
}
|
||||
isSelected={!!store.preferences.thread.lab_treeViewEnabled}
|
||||
onPress={store.preferences.toggleThreadTreeViewEnabled}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
) : (
|
||||
<ActivityIndicator />
|
||||
)}
|
||||
|
||||
<View
|
||||
style={[
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {useCallback, useMemo} from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
|
@ -8,26 +8,34 @@ import {
|
|||
} 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'
|
||||
import {CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {useStores} from 'state/index'
|
||||
import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {ScrollView, CenteredView} from 'view/com/util/Views'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
|
||||
import {FeedSourceModel} from 'state/models/content/feed-source'
|
||||
import {NewFeedSourceCard} from 'view/com/feeds/FeedSourceCard'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {Haptics} from 'lib/haptics'
|
||||
import {TextLink} from 'view/com/util/Link'
|
||||
import {logger} from '#/logger'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
usePinFeedMutation,
|
||||
useUnpinFeedMutation,
|
||||
useSetSaveFeedsMutation,
|
||||
usePreferencesQueryKey,
|
||||
UsePreferencesQueryResponse,
|
||||
} from '#/state/queries/preferences'
|
||||
|
||||
const HITSLOP_TOP = {
|
||||
top: 20,
|
||||
|
@ -43,150 +51,178 @@ const HITSLOP_BOTTOM = {
|
|||
}
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'>
|
||||
export const SavedFeeds = withAuthRequired(
|
||||
observer(function SavedFeedsImpl({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {screen} = useAnalytics()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
export const SavedFeeds = withAuthRequired(function SavedFeedsImpl({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {screen} = useAnalytics()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
|
||||
const savedFeeds = useMemo(() => {
|
||||
const model = new SavedFeedsModel(store)
|
||||
model.refresh()
|
||||
return model
|
||||
}, [store])
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
screen('SavedFeeds')
|
||||
setMinimalShellMode(false)
|
||||
savedFeeds.refresh()
|
||||
}, [screen, setMinimalShellMode, savedFeeds]),
|
||||
)
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
screen('SavedFeeds')
|
||||
setMinimalShellMode(false)
|
||||
}, [screen, setMinimalShellMode]),
|
||||
)
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
s.hContentRegion,
|
||||
pal.border,
|
||||
isTabletOrDesktop && styles.desktopContainer,
|
||||
]}>
|
||||
<ViewHeader title="Edit My Feeds" showOnDesktop showBorder />
|
||||
<ScrollView style={s.flex1}>
|
||||
<View style={[pal.text, pal.border, styles.title]}>
|
||||
<Text type="title" style={pal.text}>
|
||||
Pinned Feeds
|
||||
</Text>
|
||||
</View>
|
||||
{savedFeeds.hasLoaded ? (
|
||||
!savedFeeds.pinned.length ? (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
isMobile && s.flex1,
|
||||
pal.viewLight,
|
||||
styles.empty,
|
||||
]}>
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
You don't have any pinned feeds.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
savedFeeds.pinned.map(feed => (
|
||||
<ListItem
|
||||
key={feed._reactKey}
|
||||
savedFeeds={savedFeeds}
|
||||
item={feed}
|
||||
/>
|
||||
))
|
||||
)
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
s.hContentRegion,
|
||||
pal.border,
|
||||
isTabletOrDesktop && styles.desktopContainer,
|
||||
]}>
|
||||
<ViewHeader title="Edit My Feeds" showOnDesktop showBorder />
|
||||
<ScrollView style={s.flex1}>
|
||||
<View style={[pal.text, pal.border, styles.title]}>
|
||||
<Text type="title" style={pal.text}>
|
||||
Pinned Feeds
|
||||
</Text>
|
||||
</View>
|
||||
{preferences?.feeds ? (
|
||||
!preferences.feeds.pinned.length ? (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
isMobile && s.flex1,
|
||||
pal.viewLight,
|
||||
styles.empty,
|
||||
]}>
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
You don't have any pinned feeds.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ActivityIndicator style={{marginTop: 20}} />
|
||||
)}
|
||||
<View style={[pal.text, pal.border, styles.title]}>
|
||||
<Text type="title" style={pal.text}>
|
||||
Saved Feeds
|
||||
</Text>
|
||||
</View>
|
||||
{savedFeeds.hasLoaded ? (
|
||||
!savedFeeds.unpinned.length ? (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
isMobile && s.flex1,
|
||||
pal.viewLight,
|
||||
styles.empty,
|
||||
]}>
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
You don't have any saved feeds.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
savedFeeds.unpinned.map(feed => (
|
||||
<ListItem
|
||||
key={feed._reactKey}
|
||||
savedFeeds={savedFeeds}
|
||||
item={feed}
|
||||
/>
|
||||
))
|
||||
)
|
||||
preferences?.feeds?.pinned?.map(uri => (
|
||||
<ListItem key={uri} feedUri={uri} isPinned />
|
||||
))
|
||||
)
|
||||
) : (
|
||||
<ActivityIndicator style={{marginTop: 20}} />
|
||||
)}
|
||||
<View style={[pal.text, pal.border, styles.title]}>
|
||||
<Text type="title" style={pal.text}>
|
||||
Saved Feeds
|
||||
</Text>
|
||||
</View>
|
||||
{preferences?.feeds ? (
|
||||
!preferences.feeds.unpinned.length ? (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
isMobile && s.flex1,
|
||||
pal.viewLight,
|
||||
styles.empty,
|
||||
]}>
|
||||
<Text type="lg" style={[pal.text]}>
|
||||
You don't have any saved feeds.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ActivityIndicator style={{marginTop: 20}} />
|
||||
)}
|
||||
preferences.feeds.unpinned.map(uri => (
|
||||
<ListItem key={uri} feedUri={uri} isPinned={false} />
|
||||
))
|
||||
)
|
||||
) : (
|
||||
<ActivityIndicator style={{marginTop: 20}} />
|
||||
)}
|
||||
|
||||
<View style={styles.footerText}>
|
||||
<Text type="sm" style={pal.textLight}>
|
||||
Feeds are custom algorithms that users build with a little coding
|
||||
expertise.{' '}
|
||||
<TextLink
|
||||
type="sm"
|
||||
style={pal.link}
|
||||
href="https://github.com/bluesky-social/feed-generator"
|
||||
text="See this guide"
|
||||
/>{' '}
|
||||
for more information.
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{height: 100}} />
|
||||
</ScrollView>
|
||||
</CenteredView>
|
||||
)
|
||||
}),
|
||||
)
|
||||
<View style={styles.footerText}>
|
||||
<Text type="sm" style={pal.textLight}>
|
||||
Feeds are custom algorithms that users build with a little coding
|
||||
expertise.{' '}
|
||||
<TextLink
|
||||
type="sm"
|
||||
style={pal.link}
|
||||
href="https://github.com/bluesky-social/feed-generator"
|
||||
text="See this guide"
|
||||
/>{' '}
|
||||
for more information.
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{height: 100}} />
|
||||
</ScrollView>
|
||||
</CenteredView>
|
||||
)
|
||||
})
|
||||
|
||||
const ListItem = observer(function ListItemImpl({
|
||||
savedFeeds,
|
||||
item,
|
||||
feedUri,
|
||||
isPinned,
|
||||
}: {
|
||||
savedFeeds: SavedFeedsModel
|
||||
item: FeedSourceModel
|
||||
feedUri: string // uri
|
||||
isPinned: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const isPinned = item.isPinned
|
||||
const queryClient = useQueryClient()
|
||||
const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
|
||||
const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
|
||||
useUnpinFeedMutation()
|
||||
const {isPending: isMovePending, mutateAsync: setSavedFeeds} =
|
||||
useSetSaveFeedsMutation()
|
||||
|
||||
const onTogglePinned = useCallback(() => {
|
||||
const onTogglePinned = React.useCallback(async () => {
|
||||
Haptics.default()
|
||||
item.togglePin().catch(e => {
|
||||
|
||||
try {
|
||||
if (isPinned) {
|
||||
await unpinFeed({uri: feedUri})
|
||||
} else {
|
||||
await pinFeed({uri: feedUri})
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.show('There was an issue contacting the server')
|
||||
logger.error('Failed to toggle pinned feed', {error: e})
|
||||
})
|
||||
}, [item])
|
||||
const onPressUp = useCallback(
|
||||
() =>
|
||||
savedFeeds.movePinnedFeed(item, 'up').catch(e => {
|
||||
Toast.show('There was an issue contacting the server')
|
||||
logger.error('Failed to set pinned feed order', {error: e})
|
||||
}),
|
||||
[savedFeeds, item],
|
||||
)
|
||||
const onPressDown = useCallback(
|
||||
() =>
|
||||
savedFeeds.movePinnedFeed(item, 'down').catch(e => {
|
||||
Toast.show('There was an issue contacting the server')
|
||||
logger.error('Failed to set pinned feed order', {error: e})
|
||||
}),
|
||||
[savedFeeds, item],
|
||||
)
|
||||
}
|
||||
}, [feedUri, isPinned, pinFeed, unpinFeed])
|
||||
|
||||
const onPressUp = React.useCallback(async () => {
|
||||
if (!isPinned) return
|
||||
|
||||
const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>(
|
||||
usePreferencesQueryKey,
|
||||
)?.feeds
|
||||
const pinned = feeds?.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})
|
||||
track('CustomFeed:Reorder', {
|
||||
uri: feedUri,
|
||||
index: pinned.indexOf(feedUri),
|
||||
})
|
||||
} catch (e) {
|
||||
Toast.show('There was an issue contacting the server')
|
||||
logger.error('Failed to set pinned feed order', {error: e})
|
||||
}
|
||||
}, [feedUri, isPinned, queryClient, setSavedFeeds])
|
||||
|
||||
const onPressDown = React.useCallback(async () => {
|
||||
if (!isPinned) return
|
||||
|
||||
const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>(
|
||||
usePreferencesQueryKey,
|
||||
)?.feeds
|
||||
const pinned = feeds?.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})
|
||||
track('CustomFeed:Reorder', {
|
||||
uri: feedUri,
|
||||
index: pinned.indexOf(feedUri),
|
||||
})
|
||||
} catch (e) {
|
||||
Toast.show('There was an issue contacting the server')
|
||||
logger.error('Failed to set pinned feed order', {error: e})
|
||||
}
|
||||
}, [feedUri, isPinned, queryClient, setSavedFeeds])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
|
@ -195,6 +231,7 @@ const ListItem = observer(function ListItemImpl({
|
|||
{isPinned ? (
|
||||
<View style={styles.webArrowButtonsContainer}>
|
||||
<TouchableOpacity
|
||||
disabled={isMovePending}
|
||||
accessibilityRole="button"
|
||||
onPress={onPressUp}
|
||||
hitSlop={HITSLOP_TOP}>
|
||||
|
@ -205,6 +242,7 @@ const ListItem = observer(function ListItemImpl({
|
|||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
disabled={isMovePending}
|
||||
accessibilityRole="button"
|
||||
onPress={onPressDown}
|
||||
hitSlop={HITSLOP_BOTTOM}>
|
||||
|
@ -212,13 +250,14 @@ const ListItem = observer(function ListItemImpl({
|
|||
</TouchableOpacity>
|
||||
</View>
|
||||
) : null}
|
||||
<FeedSourceCard
|
||||
key={item.uri}
|
||||
item={item}
|
||||
showSaveBtn
|
||||
<NewFeedSourceCard
|
||||
key={feedUri}
|
||||
feedUri={feedUri}
|
||||
style={styles.noBorder}
|
||||
showSaveBtn
|
||||
/>
|
||||
<TouchableOpacity
|
||||
disabled={isPinPending || isUnpinPending}
|
||||
accessibilityRole="button"
|
||||
hitSlop={10}
|
||||
onPress={onTogglePinned}>
|
||||
|
|
|
@ -59,6 +59,7 @@ import {
|
|||
} from '#/state/preferences'
|
||||
import {useSession, useSessionApi, SessionAccount} from '#/state/session'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useClearPreferencesMutation} from '#/state/queries/preferences'
|
||||
|
||||
// TEMPORARY (APP-700)
|
||||
// remove after backend testing finishes
|
||||
|
@ -153,6 +154,7 @@ export const SettingsScreen = withAuthRequired(
|
|||
const {openModal} = useModalControls()
|
||||
const {isSwitchingAccounts, accounts, currentAccount} = useSession()
|
||||
const {clearCurrentAccount} = useSessionApi()
|
||||
const {mutate: clearPreferences} = useClearPreferencesMutation()
|
||||
|
||||
const primaryBg = useCustomPalette<ViewStyle>({
|
||||
light: {backgroundColor: colors.blue0},
|
||||
|
@ -219,9 +221,8 @@ export const SettingsScreen = withAuthRequired(
|
|||
}, [openModal])
|
||||
|
||||
const onPressResetPreferences = React.useCallback(async () => {
|
||||
await store.preferences.reset()
|
||||
Toast.show('Preferences reset')
|
||||
}, [store])
|
||||
clearPreferences()
|
||||
}, [clearPreferences])
|
||||
|
||||
const onPressResetOnboarding = React.useCallback(async () => {
|
||||
onboardingDispatch({type: 'start'})
|
||||
|
@ -300,7 +301,7 @@ export const SettingsScreen = withAuthRequired(
|
|||
</View>
|
||||
<View style={[styles.infoLine]}>
|
||||
<Text type="lg-medium" style={pal.text}>
|
||||
<Trans>Birthday: </Trans>
|
||||
<Trans>Birthday:</Trans>{' '}
|
||||
</Text>
|
||||
<Link onPress={() => openModal({name: 'birth-date-settings'})}>
|
||||
<Text type="lg" style={pal.link}>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue