diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts
index 3ff156dd67..d1f516bc0a 100644
--- a/src/lib/api/feed-manip.ts
+++ b/src/lib/api/feed-manip.ts
@@ -1,4 +1,9 @@
-import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
+import {
+ AppBskyFeedDefs,
+ AppBskyFeedPost,
+ AppBskyEmbedRecordWithMedia,
+ AppBskyEmbedRecord,
+} from '@atproto/api'
import lande from 'lande'
import {hasProp} from 'lib/type-guards'
import {LANGUAGES_MAP_CODE2} from '../../locale/languages'
@@ -156,6 +161,38 @@ export class FeedTuner {
return slices
}
+ static removeReplies(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
+ for (let i = slices.length - 1; i >= 0; i--) {
+ if (slices[i].isReply) {
+ slices.splice(i, 1)
+ }
+ }
+ return slices
+ }
+
+ static removeReposts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
+ for (let i = slices.length - 1; i >= 0; i--) {
+ const reason = slices[i].rootItem.reason
+ if (AppBskyFeedDefs.isReasonRepost(reason)) {
+ slices.splice(i, 1)
+ }
+ }
+ return slices
+ }
+
+ static removeQuotePosts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
+ for (let i = slices.length - 1; i >= 0; i--) {
+ const embed = slices[i].rootItem.post.embed
+ if (
+ AppBskyEmbedRecord.isView(embed) ||
+ AppBskyEmbedRecordWithMedia.isView(embed)
+ ) {
+ slices.splice(i, 1)
+ }
+ }
+ return slices
+ }
+
static dedupReposts(
tuner: FeedTuner,
slices: FeedViewPostsSlice[],
@@ -178,23 +215,25 @@ export class FeedTuner {
return slices
}
- static likedRepliesOnly(
- tuner: FeedTuner,
- slices: FeedViewPostsSlice[],
- ): FeedViewPostsSlice[] {
- // remove any replies without at least 2 likes
- for (let i = slices.length - 1; i >= 0; i--) {
- if (slices[i].isFullThread || !slices[i].isReply) {
- continue
- }
+ static likedRepliesOnly({repliesThreshold}: {repliesThreshold: number}) {
+ return (
+ tuner: FeedTuner,
+ slices: FeedViewPostsSlice[],
+ ): FeedViewPostsSlice[] => {
+ // remove any replies without at least repliesThreshold likes
+ for (let i = slices.length - 1; i >= 0; i--) {
+ if (slices[i].isFullThread || !slices[i].isReply) {
+ continue
+ }
- const item = slices[i].rootItem
- const isRepost = Boolean(item.reason)
- if (!isRepost && (item.post.likeCount || 0) < 2) {
- slices.splice(i, 1)
+ const item = slices[i].rootItem
+ const isRepost = Boolean(item.reason)
+ if (!isRepost && (item.post.likeCount || 0) < repliesThreshold) {
+ slices.splice(i, 1)
+ }
}
+ return slices
}
- return slices
}
static preferredLangOnly(langsCode2: string[]) {
diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts
index b7d4def137..594143bf2b 100644
--- a/src/state/models/feeds/posts.ts
+++ b/src/state/models/feeds/posts.ts
@@ -115,6 +115,12 @@ export class PostsFeedModel {
}
get feedTuners() {
+ const areRepliesEnabled = this.rootStore.preferences.homeFeedRepliesEnabled
+ const repliesThreshold = this.rootStore.preferences.homeFeedRepliesThreshold
+ const areRepostsEnabled = this.rootStore.preferences.homeFeedRepostsEnabled
+ const areQuotePostsEnabled =
+ this.rootStore.preferences.homeFeedQuotePostsEnabled
+
if (this.feedType === 'custom') {
return [
FeedTuner.dedupReposts,
@@ -124,7 +130,13 @@ export class PostsFeedModel {
]
}
if (this.feedType === 'home') {
- return [FeedTuner.dedupReposts, FeedTuner.likedRepliesOnly]
+ return [
+ areRepostsEnabled && FeedTuner.dedupReposts,
+ !areRepostsEnabled && FeedTuner.removeReposts,
+ areRepliesEnabled && FeedTuner.likedRepliesOnly({repliesThreshold}),
+ !areRepliesEnabled && FeedTuner.removeReplies,
+ !areQuotePostsEnabled && FeedTuner.removeQuotePosts,
+ ].filter(Boolean)
}
return []
}
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
index a42f0a837e..6c9dc756e5 100644
--- a/src/state/models/ui/preferences.ts
+++ b/src/state/models/ui/preferences.ts
@@ -51,6 +51,10 @@ export class PreferencesModel {
contentLabels = new LabelPreferencesModel()
savedFeeds: string[] = []
pinnedFeeds: string[] = []
+ homeFeedRepliesEnabled: boolean = true
+ homeFeedRepliesThreshold: number = 2
+ homeFeedRepostsEnabled: boolean = true
+ homeFeedQuotePostsEnabled: boolean = true
// used to linearize async modifications to state
lock = new AwaitLock()
@@ -65,6 +69,10 @@ export class PreferencesModel {
contentLabels: this.contentLabels,
savedFeeds: this.savedFeeds,
pinnedFeeds: this.pinnedFeeds,
+ homeFeedRepliesEnabled: this.homeFeedRepliesEnabled,
+ homeFeedRepliesThreshold: this.homeFeedRepliesThreshold,
+ homeFeedRepostsEnabled: this.homeFeedRepostsEnabled,
+ homeFeedQuotePostsEnabled: this.homeFeedQuotePostsEnabled,
}
}
@@ -102,6 +110,30 @@ export class PreferencesModel {
) {
this.pinnedFeeds = v.pinnedFeeds
}
+ if (
+ hasProp(v, 'homeFeedRepliesEnabled') &&
+ typeof v.homeFeedRepliesEnabled === 'boolean'
+ ) {
+ this.homeFeedRepliesEnabled = v.homeFeedRepliesEnabled
+ }
+ if (
+ hasProp(v, 'homeFeedRepliesThreshold') &&
+ typeof v.homeFeedRepliesThreshold === 'number'
+ ) {
+ this.homeFeedRepliesThreshold = v.homeFeedRepliesThreshold
+ }
+ if (
+ hasProp(v, 'homeFeedRepostsEnabled') &&
+ typeof v.homeFeedRepostsEnabled === 'boolean'
+ ) {
+ this.homeFeedRepostsEnabled = v.homeFeedRepostsEnabled
+ }
+ if (
+ hasProp(v, 'homeFeedQuotePostsEnabled') &&
+ typeof v.homeFeedQuotePostsEnabled === 'boolean'
+ ) {
+ this.homeFeedQuotePostsEnabled = v.homeFeedQuotePostsEnabled
+ }
}
}
@@ -380,4 +412,20 @@ export class PreferencesModel {
this.pinnedFeeds.filter(uri => uri !== v),
)
}
+
+ toggleHomeFeedRepliesEnabled() {
+ this.homeFeedRepliesEnabled = !this.homeFeedRepliesEnabled
+ }
+
+ setHomeFeedRepliesThreshold(threshold: number) {
+ this.homeFeedRepliesThreshold = threshold
+ }
+
+ toggleHomeFeedRepostsEnabled() {
+ this.homeFeedRepostsEnabled = !this.homeFeedRepostsEnabled
+ }
+
+ toggleHomeFeedQuotePostsEnabled() {
+ this.homeFeedQuotePostsEnabled = !this.homeFeedQuotePostsEnabled
+ }
}
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index 3853f23956..c7e72e6950 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -111,6 +111,10 @@ export interface ContentLanguagesSettingsModal {
name: 'content-languages-settings'
}
+export interface PreferencesHomeFeed {
+ name: 'preferences-home-feed'
+}
+
export type Modal =
// Account
| AddAppPasswordModal
@@ -121,6 +125,7 @@ export type Modal =
// Curation
| ContentFilteringSettingsModal
| ContentLanguagesSettingsModal
+ | PreferencesHomeFeed
// Moderation
| ReportAccountModal
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 864dcc847e..5989d9ff9b 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -24,6 +24,7 @@ import * as InviteCodesModal from './InviteCodes'
import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './ContentLanguagesSettings'
+import * as PreferencesHomeFeed from './PreferencesHomeFeed'
const DEFAULT_SNAPPOINTS = ['90%']
@@ -105,6 +106,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
} else if (activeModal?.name === 'content-languages-settings') {
snapPoints = ContentLanguagesSettingsModal.snapPoints
element =
+ } else if (activeModal?.name === 'preferences-home-feed') {
+ snapPoints = PreferencesHomeFeed.snapPoints
+ element =
} else {
return null
}
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 27b2641ba5..3895d47ac5 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -24,6 +24,7 @@ import * as InviteCodesModal from './InviteCodes'
import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
import * as ContentLanguagesSettingsModal from './ContentLanguagesSettings'
+import * as PreferencesHomeFeed from './PreferencesHomeFeed'
export const ModalsContainer = observer(function ModalsContainer() {
const store = useStores()
@@ -97,6 +98,8 @@ function Modal({modal}: {modal: ModalIface}) {
element =
} else if (modal.name === 'edit-image') {
element =
+ } else if (modal.name === 'preferences-home-feed') {
+ element =
} else {
return null
}
diff --git a/src/view/com/modals/PreferencesHomeFeed.tsx b/src/view/com/modals/PreferencesHomeFeed.tsx
new file mode 100644
index 0000000000..14bf01e7d4
--- /dev/null
+++ b/src/view/com/modals/PreferencesHomeFeed.tsx
@@ -0,0 +1,176 @@
+import React, {useState} from 'react'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {Slider} from '@miblanchard/react-native-slider'
+import {Text} from '../util/text/Text'
+import {useStores} from 'state/index'
+import {s, colors} from 'lib/styles'
+import {usePalette} from 'lib/hooks/usePalette'
+import {isWeb, isDesktopWeb} from 'platform/detection'
+import {ToggleButton} from 'view/com/util/forms/ToggleButton'
+import {ScrollView} from 'view/com/modals/util'
+
+export const snapPoints = ['90%']
+
+function RepliesThresholdInput({enabled}: {enabled: boolean}) {
+ const store = useStores()
+ const [value, setValue] = useState(store.preferences.homeFeedRepliesThreshold)
+
+ return (
+
+
+ {value === 0
+ ? `Show all replies`
+ : `Show replies with greater than ${value} ${
+ value > 1 ? `likes` : `like`
+ }`}
+
+ {
+ const threshold = Math.floor(Array.isArray(v) ? v[0] : v)
+ setValue(threshold)
+ store.preferences.setHomeFeedRepliesThreshold(threshold)
+ }}
+ minimumValue={0}
+ maximumValue={25}
+ containerStyle={isWeb ? undefined : s.flex1}
+ disabled={!enabled}
+ thumbTintColor={colors.blue3}
+ />
+
+ )
+}
+
+export const Component = observer(function Component() {
+ const pal = usePalette('default')
+ const store = useStores()
+
+ return (
+
+
+
+ Home Feed Preferences
+
+
+ Fine-tune the content you see on your home screen.
+
+
+
+
+
+
+
+ Show Replies
+
+
+ Adjust the number of likes a reply must have to be shown in your
+ feed.
+
+
+
+
+
+
+
+
+ Show Reposts
+
+
+ Disable this setting to hide all reposts from your feed.
+
+
+
+
+
+
+ Show Quote Posts
+
+
+ Disable this setting to hide all quote posts from your feed.
+ Reposts will still be visible.
+
+
+
+
+
+
+
+ {
+ store.shell.closeModal()
+ }}
+ style={[styles.btn]}
+ accessibilityRole="button"
+ accessibilityLabel="Confirm"
+ accessibilityHint="">
+ Done
+
+
+
+ )
+})
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingBottom: isDesktopWeb ? 0 : 60,
+ },
+ titleSection: {
+ padding: 20,
+ paddingBottom: 30,
+ },
+ title: {
+ textAlign: 'center',
+ marginBottom: 5,
+ },
+ description: {
+ textAlign: 'center',
+ paddingHorizontal: 32,
+ },
+ cardsContainer: {
+ paddingHorizontal: 20,
+ },
+ card: {
+ padding: 16,
+ backgroundColor: s.gray1.color,
+ borderRadius: 10,
+ marginBottom: 20,
+ },
+ btn: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 32,
+ padding: 14,
+ backgroundColor: colors.blue3,
+ },
+ btnContainer: {
+ paddingTop: 20,
+ paddingHorizontal: 20,
+ borderTopWidth: isDesktopWeb ? 0 : 1,
+ },
+ dimmed: {
+ opacity: 0.3,
+ },
+})
diff --git a/src/view/index.ts b/src/view/index.ts
index f06bdacccc..118a6de224 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -67,8 +67,9 @@ import {faSatelliteDish} from '@fortawesome/free-solid-svg-icons/faSatelliteDish
import {faShare} from '@fortawesome/free-solid-svg-icons/faShare'
import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare'
import {faShield} from '@fortawesome/free-solid-svg-icons/faShield'
-import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus'
import {faSignal} from '@fortawesome/free-solid-svg-icons/faSignal'
+import {faSliders} from '@fortawesome/free-solid-svg-icons/faSliders'
+import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus'
import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket'
import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan'
import {faUser} from '@fortawesome/free-regular-svg-icons/faUser'
@@ -153,8 +154,9 @@ export function setup() {
faShare,
faShareFromSquare,
faShield,
- faSquarePlus,
faSignal,
+ faSliders,
+ faSquarePlus,
faUser,
faUsers,
faUserCheck,
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 3d057451ab..3f02bbee57 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -166,6 +166,12 @@ export const SettingsScreen = withAuthRequired(
Toast.show('Copied build version to clipboard')
}, [])
+ const openPreferencesModal = React.useCallback(() => {
+ store.shell.openModal({
+ name: 'preferences-home-feed',
+ })
+ }, [store])
+
return (
@@ -346,6 +352,23 @@ export const SettingsScreen = withAuthRequired(
Advanced
+
+
+
+
+
+ Home Feed Preferences
+
+