From daf7a5fdf3db42d464ed76dc3967981d48103ab0 Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Sun, 28 Dec 2025 10:59:01 +0100 Subject: [PATCH 1/3] Added two new user settings to filter out replies and boosts from timeline views, allowing users to see only original posts when desired. Users may want to focus on original content in their timelines without the noise of replies and boosted posts. --- src/pages/account-statuses.jsx | 9 +++++--- src/pages/bookmarks.jsx | 14 +++++++++++- src/pages/favourites.jsx | 14 +++++++++++- src/pages/following.jsx | 3 +++ src/pages/hashtag.jsx | 5 +++++ src/pages/list.jsx | 3 +++ src/pages/mentions.jsx | 21 ++++++++++++++++- src/pages/public.jsx | 3 +++ src/pages/settings.jsx | 41 ++++++++++++++++++++++++++++++++++ src/utils/states.js | 12 ++++++++++ src/utils/timeline-utils.js | 18 +++++++++++++++ 11 files changed, 137 insertions(+), 6 deletions(-) diff --git a/src/pages/account-statuses.jsx b/src/pages/account-statuses.jsx index 7166ac9423..c0c5def648 100644 --- a/src/pages/account-statuses.jsx +++ b/src/pages/account-statuses.jsx @@ -28,6 +28,7 @@ import { isMediaFirstInstance, } from '../utils/store-utils'; import supports from '../utils/supports'; +import { applyTimelineFilters } from '../utils/timeline-utils'; import useTitle from '../utils/useTitle'; const LIMIT = 20; @@ -160,13 +161,14 @@ function AccountStatuses() { .values() .next(); if (value?.length && !tagged && !media) { - const pinnedStatuses = value.map((status) => { + let pinnedStatuses = value.map((status) => { saveStatus(status, instance); return { ...status, _pinned: true, }; }); + pinnedStatuses = applyTimelineFilters(pinnedStatuses, snapStates.settings); if (pinnedStatuses.length >= 3) { const pinnedStatusesIds = pinnedStatuses.map((status) => status.id); results.push({ @@ -216,9 +218,10 @@ function AccountStatuses() { } } - results.push(...value); + const filteredValue = applyTimelineFilters(value, snapStates.settings); + results.push(...filteredValue); - value.forEach((item) => { + filteredValue.forEach((item) => { saveStatus(item, instance); }); } diff --git a/src/pages/bookmarks.jsx b/src/pages/bookmarks.jsx index 4bd5b4d806..ff2a0cf856 100644 --- a/src/pages/bookmarks.jsx +++ b/src/pages/bookmarks.jsx @@ -1,14 +1,18 @@ import { useLingui } from '@lingui/react/macro'; import { useRef } from 'preact/hooks'; +import { useSnapshot } from 'valtio'; import Timeline from '../components/timeline'; import { api } from '../utils/api'; +import states from '../utils/states'; +import { applyTimelineFilters } from '../utils/timeline-utils'; import useTitle from '../utils/useTitle'; const LIMIT = 20; function Bookmarks() { const { t } = useLingui(); + const snapStates = useSnapshot(states); useTitle(t`Bookmarks`, '/b'); const { masto, instance } = api(); const bookmarksIterator = useRef(); @@ -18,7 +22,15 @@ function Bookmarks() { .list({ limit: LIMIT }) .values(); } - return await bookmarksIterator.current.next(); + const results = await bookmarksIterator.current.next(); + let { value } = results; + if (value?.length) { + value = applyTimelineFilters(value, snapStates.settings); + } + return { + ...results, + value, + }; } return ( diff --git a/src/pages/favourites.jsx b/src/pages/favourites.jsx index 4f274d7a03..d4d70c6d94 100644 --- a/src/pages/favourites.jsx +++ b/src/pages/favourites.jsx @@ -1,14 +1,18 @@ import { Trans, useLingui } from '@lingui/react/macro'; import { useRef } from 'preact/hooks'; +import { useSnapshot } from 'valtio'; import Timeline from '../components/timeline'; import { api } from '../utils/api'; +import states from '../utils/states'; +import { applyTimelineFilters } from '../utils/timeline-utils'; import useTitle from '../utils/useTitle'; const LIMIT = 20; function Favourites() { const { t } = useLingui(); + const snapStates = useSnapshot(states); useTitle(t`Likes`, '/favourites'); const { masto, instance } = api(); const favouritesIterator = useRef(); @@ -18,7 +22,15 @@ function Favourites() { .list({ limit: LIMIT }) .values(); } - return await favouritesIterator.current.next(); + const results = await favouritesIterator.current.next(); + let { value } = results; + if (value?.length) { + value = applyTimelineFilters(value, snapStates.settings); + } + return { + ...results, + value, + }; } return ( diff --git a/src/pages/following.jsx b/src/pages/following.jsx index 3d0bdd411d..d833d54601 100644 --- a/src/pages/following.jsx +++ b/src/pages/following.jsx @@ -8,6 +8,7 @@ import { filteredItems } from '../utils/filters'; import states, { getStatus, saveStatus } from '../utils/states'; import supports from '../utils/supports'; import { + applyTimelineFilters, assignFollowedTags, clearFollowedTagsState, dedupeBoosts, @@ -77,6 +78,7 @@ function Following({ title, path, id, ...props }) { saveStatus(item, instance); }); value = dedupeBoosts(value, instance); + value = applyTimelineFilters(value, snapStates.settings); if (firstLoad && latestItemChanged) clearFollowedTagsState(); setTimeout(() => { assignFollowedTags(value, instance); @@ -110,6 +112,7 @@ function Following({ title, path, id, ...props }) { if (value?.length && !valueContainsLatestItem) { latestItem.current = value[0].id; value = dedupeBoosts(value, instance); + value = applyTimelineFilters(value, snapStates.settings); value = filteredItems(value, 'home'); if (value.some((item) => !item.reblog)) { return true; diff --git a/src/pages/hashtag.jsx b/src/pages/hashtag.jsx index d19a36905e..bfde05b0c1 100644 --- a/src/pages/hashtag.jsx +++ b/src/pages/hashtag.jsx @@ -9,6 +9,7 @@ import { } from '@szhsin/react-menu'; import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { useSnapshot } from 'valtio'; import Icon from '../components/icon'; import MenuConfirm from '../components/menu-confirm'; @@ -20,6 +21,7 @@ import { filteredItems } from '../utils/filters'; import showToast from '../utils/show-toast'; import states, { saveStatus } from '../utils/states'; import { isMediaFirstInstance } from '../utils/store-utils'; +import { applyTimelineFilters } from '../utils/timeline-utils'; import useTitle from '../utils/useTitle'; const LIMIT = 20; @@ -32,6 +34,7 @@ const TOTAL_TAGS_LIMIT = TAGS_LIMIT_PER_MODE + 1; function Hashtags({ media: mediaView, columnMode, ...props }) { const { t } = useLingui(); + const snapStates = useSnapshot(states); // const navigate = useNavigate(); let { hashtag, ...params } = columnMode ? {} : useParams(); if (props.hashtag) hashtag = props.hashtag; @@ -97,6 +100,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) { skipThreading: media || mediaFirst, // If media view, no need to form threads }); }); + value = applyTimelineFilters(value, snapStates.settings); maxID.current = value[value.length - 1].id; } @@ -121,6 +125,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) { let { value } = results; const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported if (value?.length && !valueContainsLatestItem) { + value = applyTimelineFilters(value, snapStates.settings); value = filteredItems(value, 'public'); return true; } diff --git a/src/pages/list.jsx b/src/pages/list.jsx index c72dd1cc03..6246e8fe3f 100644 --- a/src/pages/list.jsx +++ b/src/pages/list.jsx @@ -20,6 +20,7 @@ import { api } from '../utils/api'; import { filteredItems } from '../utils/filters'; import { getList, getLists } from '../utils/lists'; import states, { saveStatus } from '../utils/states'; +import { applyTimelineFilters } from '../utils/timeline-utils'; import useTitle from '../utils/useTitle'; const LIMIT = 20; @@ -54,6 +55,7 @@ function List(props) { value.forEach((item) => { saveStatus(item, instance); }); + value = applyTimelineFilters(value, snapStates.settings); } return { ...results, @@ -70,6 +72,7 @@ function List(props) { let { value } = results; const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported if (value?.length && !valueContainsLatestItem) { + value = applyTimelineFilters(value, snapStates.settings); value = filteredItems(value, 'home'); return true; } diff --git a/src/pages/mentions.jsx b/src/pages/mentions.jsx index cde15d9885..a02a3d82dc 100644 --- a/src/pages/mentions.jsx +++ b/src/pages/mentions.jsx @@ -1,12 +1,14 @@ import { Trans, useLingui } from '@lingui/react/macro'; import { useMemo, useRef, useState } from 'preact/hooks'; import { useSearchParams } from 'react-router-dom'; +import { useSnapshot } from 'valtio'; import Link from '../components/link'; import Timeline from '../components/timeline'; import { api } from '../utils/api'; import { fixNotifications } from '../utils/group-notifications'; -import { saveStatus } from '../utils/states'; +import states, { saveStatus } from '../utils/states'; +import { applyTimelineFilters } from '../utils/timeline-utils'; import useTitle from '../utils/useTitle'; const LIMIT = 20; @@ -14,6 +16,7 @@ const emptySearchParams = new URLSearchParams(); function Mentions({ columnMode, ...props }) { const { t } = useLingui(); + const snapStates = useSnapshot(states); const { masto, instance } = api(); const [searchParams] = columnMode ? [emptySearchParams] : useSearchParams(); const [stateType, setStateType] = useState(null); @@ -45,6 +48,14 @@ function Mentions({ columnMode, ...props }) { value.forEach(({ status: item }) => { saveStatus(item, instance); }); + + const statuses = value?.map((item) => item.status); + const filteredStatuses = applyTimelineFilters(statuses, snapStates.settings); + + return { + ...results, + value: filteredStatuses, + }; } return { ...results, @@ -74,6 +85,14 @@ function Mentions({ columnMode, ...props }) { value.forEach(({ lastStatus: item }) => { saveStatus(item, instance); }); + + const statuses = value?.map((item) => item.lastStatus); + const filteredStatuses = applyTimelineFilters(statuses, snapStates.settings); + + return { + ...results, + value: filteredStatuses, + }; } console.log('results', results); return { diff --git a/src/pages/public.jsx b/src/pages/public.jsx index ddac68a7f2..606642c851 100644 --- a/src/pages/public.jsx +++ b/src/pages/public.jsx @@ -11,6 +11,7 @@ import { api } from '../utils/api'; import { filteredItems } from '../utils/filters'; import states, { saveStatus } from '../utils/states'; import supports from '../utils/supports'; +import { applyTimelineFilters } from '../utils/timeline-utils'; import useTitle from '../utils/useTitle'; const LIMIT = 20; @@ -54,6 +55,7 @@ function Public({ local, columnMode, ...props }) { value.forEach((item) => { saveStatus(item, instance); }); + value = applyTimelineFilters(value, snapStates.settings); } return { ...results, @@ -74,6 +76,7 @@ function Public({ local, columnMode, ...props }) { let { value } = results; const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported if (value?.length && !valueContainsLatestItem) { + value = applyTimelineFilters(value, snapStates.settings); value = filteredItems(value, 'public'); return true; } diff --git a/src/pages/settings.jsx b/src/pages/settings.jsx index 66202d7ca3..00f0033ffa 100644 --- a/src/pages/settings.jsx +++ b/src/pages/settings.jsx @@ -437,6 +437,47 @@ function Settings({ onClose }) { Boosts carousel +
  • + +
    + + + Hides all boosted posts from timelines. Individual status + pages still show boosts in threads. + + +
    +
  • +
  • + +
    + + + Hides all reply posts from timelines, including + self-replies. Individual status pages still show replies in + threads. + + +
    +
  • {!!TRANSLANG_INSTANCES && (