From 0bc1b598c3795f9d888d3c1e632faa3ef3c8c81f Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 3 Nov 2023 21:45:31 +0800 Subject: [PATCH] Breaking: rewrote filters implementation --- src/components/media-post.jsx | 31 ++- src/components/status.jsx | 33 ++-- src/components/timeline.jsx | 360 ++++++++++++++++++---------------- src/pages/following.jsx | 5 +- src/pages/hashtag.jsx | 5 +- src/pages/list.jsx | 5 +- src/pages/notifications.jsx | 1 + src/pages/public.jsx | 5 +- src/pages/trending.jsx | 5 +- src/utils/filter-context.js | 4 + src/utils/filters.jsx | 34 +++- src/utils/mem.js | 4 +- src/utils/states.js | 2 +- 13 files changed, 287 insertions(+), 207 deletions(-) create mode 100644 src/utils/filter-context.js diff --git a/src/components/media-post.jsx b/src/components/media-post.jsx index c208896..6a5a30a 100644 --- a/src/components/media-post.jsx +++ b/src/components/media-post.jsx @@ -1,9 +1,13 @@ import './media-post.css'; import { memo } from 'preact/compat'; +import { useContext, useMemo } from 'preact/hooks'; import { useSnapshot } from 'valtio'; +import FilterContext from '../utils/filter-context'; +import { isFiltered } from '../utils/filters'; import states, { statusKey } from '../utils/states'; +import store from '../utils/store'; import Media from './media'; @@ -13,7 +17,7 @@ function MediaPost({ status, instance, parent, - allowFilters, + // allowFilters, onMediaClick, }) { let sKey = statusKey(statusID, instance); @@ -68,7 +72,7 @@ function MediaPost({ // Non-API props _deleted, _pinned, - _filtered, + // _filtered, } = status; if (!mediaAttachments?.length) { @@ -83,6 +87,20 @@ function MediaPost({ } }; + const currentAccount = useMemo(() => { + return store.session.get('currentAccount'); + }, []); + const isSelf = useMemo(() => { + return currentAccount && currentAccount === accountId; + }, [accountId, currentAccount]); + + const filterContext = useContext(FilterContext); + const filterInfo = !isSelf && isFiltered(filtered, filterContext); + + if (filterInfo?.action === 'hide') { + return null; + } + console.debug('RENDER Media post', id, status?.account.displayName); // const readingExpandSpoilers = useMemo(() => { @@ -95,6 +113,7 @@ function MediaPost({ return mediaAttachments.map((media, i) => { const mediaKey = `${sKey}-${media.id}`; + const filterTitleStr = filterInfo?.titlesStr; return ( diff --git a/src/components/status.jsx b/src/components/status.jsx index 1208565..105d380 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -13,6 +13,7 @@ import pThrottle from 'p-throttle'; import { memo } from 'preact/compat'; import { useCallback, + useContext, useEffect, useMemo, useRef, @@ -34,6 +35,8 @@ import Poll from '../components/poll'; import { api } from '../utils/api'; import emojifyText from '../utils/emojify-text'; import enhanceContent from '../utils/enhance-content'; +import FilterContext from '../utils/filter-context'; +import { isFiltered } from '../utils/filters'; import getTranslateTargetLanguage from '../utils/get-translate-target-language'; import getHTMLText from '../utils/getHTMLText'; import handleContentLinks from '../utils/handle-content-links'; @@ -90,7 +93,7 @@ function Status({ enableTranslate, forceTranslate: _forceTranslate, previewMode, - allowFilters, + // allowFilters, onMediaClick, quoted, onStatusLinkClick = () => {}, @@ -166,9 +169,24 @@ function Status({ // Non-API props _deleted, _pinned, - _filtered, + // _filtered, } = status; + const currentAccount = useMemo(() => { + return store.session.get('currentAccount'); + }, []); + const isSelf = useMemo(() => { + return currentAccount && currentAccount === accountId; + }, [accountId, currentAccount]); + + const filterContext = useContext(FilterContext); + const filterInfo = + !isSelf && !readOnly && !previewMode && isFiltered(filtered, filterContext); + + if (filterInfo?.action === 'hide') { + return null; + } + console.debug('RENDER Status', id, status?.account.displayName, quoted); const debugHover = (e) => { @@ -179,11 +197,11 @@ function Status({ } }; - if (allowFilters && size !== 'l' && _filtered) { + if (/*allowFilters && */ size !== 'l' && filterInfo) { return ( { - return store.session.get('currentAccount'); - }, []); - const isSelf = useMemo(() => { - return currentAccount && currentAccount === accountId; - }, [accountId, currentAccount]); - let inReplyToAccountRef = mentions?.find( (mention) => mention.id === inReplyToAccountId, ); diff --git a/src/components/timeline.jsx b/src/components/timeline.jsx index a51a1bf..fa24d15 100644 --- a/src/components/timeline.jsx +++ b/src/components/timeline.jsx @@ -4,6 +4,8 @@ import { InView } from 'react-intersection-observer'; import { useDebouncedCallback } from 'use-debounce'; import { useSnapshot } from 'valtio'; +import FilterContext from '../utils/filter-context'; +import { isFiltered } from '../utils/filters'; import states, { statusKey } from '../utils/states'; import statusPeek from '../utils/status-peek'; import { groupBoosts, groupContext } from '../utils/timeline-utils'; @@ -13,7 +15,6 @@ import useScroll from '../utils/useScroll'; import Icon from './icon'; import Link from './link'; -import Media from './media'; import MediaPost from './media-post'; import NavMenu from './nav-menu'; import Status from './status'; @@ -39,9 +40,10 @@ function Timeline({ headerStart, headerEnd, timelineStart, - allowFilters, + // allowFilters, refresh, view, + filterContext, }) { const snapStates = useSnapshot(states); const [items, setItems] = useState([]); @@ -285,172 +287,182 @@ function Timeline({ const hiddenUI = scrollDirection === 'end' && !nearReachStart; return ( -
{ - scrollableRef.current = node; - jRef.current = node; - kRef.current = node; - oRef.current = node; - }} - tabIndex="-1" - > -
- - {!!timelineStart && ( -
+
{ + scrollableRef.current = node; + jRef.current = node; + kRef.current = node; + oRef.current = node; + }} + tabIndex="-1" + > +
+
- )} - {!!items.length ? ( - <> -
    - {items.map((status) => ( - - ))} - {showMore && - uiState === 'loading' && - (view === 'media' ? null : ( - <> -
  • - -
  • -
  • - -
  • - - ))} -
- {uiState === 'default' && - (showMore ? ( - { - if (inView) { - loadItems(); - } +
+
+ + {headerStart !== null && headerStart !== undefined ? ( + headerStart + ) : ( + + + + )} +
+ {title && (titleComponent ? titleComponent :

{title}

)} +
+ {/*
+
+ {items.length > 0 && + uiState !== 'loading' && + !hiddenUI && + showNew && ( + -
- ) : ( -

The end.

- ))} - - ) : uiState === 'loading' ? ( -
    - {Array.from({ length: 5 }).map((_, i) => - view === 'media' ? ( -
    - ) : ( -
  • - -
  • - ), - )} -
- ) : ( - uiState !== 'error' &&

{emptyText}

- )} - {uiState === 'error' && ( -

- {errorText} -
-
- + )} + + {!!timelineStart && ( +

- Try again - -

- )} + {timelineStart} +
+ )} + {!!items.length ? ( + <> +
    + {items.map((status) => ( + + ))} + {showMore && + uiState === 'loading' && + (view === 'media' ? null : ( + <> +
  • + +
  • +
  • + +
  • + + ))} +
+ {uiState === 'default' && + (showMore ? ( + { + if (inView) { + loadItems(); + } + }} + > + + + ) : ( +

The end.

+ ))} + + ) : uiState === 'loading' ? ( +
    + {Array.from({ length: 5 }).map((_, i) => + view === 'media' ? ( +
    + ) : ( +
  • + +
  • + ), + )} +
+ ) : ( + uiState !== 'error' &&

{emptyText}

+ )} + {uiState === 'error' && ( +

+ {errorText} +
+
+ +

+ )} +
-
+ ); } -function TimelineItem({ status, instance, useItemID, allowFilters, view }) { +function TimelineItem({ + status, + instance, + useItemID, + // allowFilters, + filterContext, + view, +}) { const { id: statusID, reblog, items, type, _pinned } = status; const actualStatusID = reblog?.id || statusID; const url = instance @@ -467,10 +479,18 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) { if (isCarousel) { // Here, we don't hide filtered posts, but we sort them last items.sort((a, b) => { - if (a._filtered && !b._filtered) { + // if (a._filtered && !b._filtered) { + // return 1; + // } + // if (!a._filtered && b._filtered) { + // return -1; + // } + const aFiltered = isFiltered(a.filtered, filterContext); + const bFiltered = isFiltered(b.filtered, filterContext); + if (aFiltered && !bFiltered) { return 1; } - if (!a._filtered && b._filtered) { + if (!aFiltered && bFiltered) { return -1; } return 0; @@ -493,7 +513,7 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) { instance={instance} size="s" contentTextWeight - allowFilters={allowFilters} + // allowFilters={allowFilters} /> ) : ( )} @@ -541,13 +561,13 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) { ) : ( )} @@ -566,7 +586,7 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) { key={itemKey} statusID={statusID} instance={instance} - allowFilters={allowFilters} + // allowFilters={allowFilters} /> ) : ( ); } @@ -587,13 +607,13 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) { ) : ( )} diff --git a/src/pages/following.jsx b/src/pages/following.jsx index 3d081f3..c0505fe 100644 --- a/src/pages/following.jsx +++ b/src/pages/following.jsx @@ -32,7 +32,7 @@ function Following({ title, path, id, ...props }) { console.log('First load', latestItem.current); } - value = filteredItems(value, 'home'); + // value = filteredItems(value, 'home'); value.forEach((item) => { saveStatus(item, instance); }); @@ -115,7 +115,8 @@ function Following({ title, path, id, ...props }) { useItemID boostsCarousel={snapStates.settings.boostsCarousel} {...props} - allowFilters + // allowFilters + filterContext="home" /> ); } diff --git a/src/pages/hashtag.jsx b/src/pages/hashtag.jsx index 977a83c..552c93b 100644 --- a/src/pages/hashtag.jsx +++ b/src/pages/hashtag.jsx @@ -78,7 +78,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) { latestItem.current = value[0].id; } - value = filteredItems(value, 'public'); + // value = filteredItems(value, 'public'); value.forEach((item) => { saveStatus(item, instance, { skipThreading: media, // If media view, no need to form threads @@ -153,7 +153,8 @@ function Hashtags({ media: mediaView, columnMode, ...props }) { useItemID view={media ? 'media' : undefined} refresh={media} - allowFilters + // allowFilters + filterContext="public" headerEnd={ { saveStatus(item, instance); }); @@ -102,7 +102,8 @@ function List(props) { checkForUpdates={checkForUpdates} useItemID boostsCarousel={snapStates.settings.boostsCarousel} - allowFilters + // allowFilters + filterContext="home" // refresh={reloadCount} headerStart={ diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx index 94222dd..b2b9a17 100644 --- a/src/pages/notifications.jsx +++ b/src/pages/notifications.jsx @@ -195,6 +195,7 @@ function Notifications({ columnMode }) { snapStates.notificationsShowNew && uiState !== 'loading' ) { + setShowNew(false); loadNotifications(true); } else { setShowNew(snapStates.notificationsShowNew); diff --git a/src/pages/public.jsx b/src/pages/public.jsx index c8aabf0..52437f3 100644 --- a/src/pages/public.jsx +++ b/src/pages/public.jsx @@ -41,7 +41,7 @@ function Public({ local, columnMode, ...props }) { latestItem.current = value[0].id; } - value = filteredItems(value, 'public'); + // value = filteredItems(value, 'public'); value.forEach((item) => { saveStatus(item, instance); }); @@ -91,7 +91,8 @@ function Public({ local, columnMode, ...props }) { useItemID headerStart={<>} boostsCarousel={snapStates.settings.boostsCarousel} - allowFilters + // allowFilters + filterContext="public" headerEnd={ { saveStatus(item, instance); }); @@ -257,7 +257,8 @@ function Trending({ columnMode, ...props }) { useItemID headerStart={<>} boostsCarousel={snapStates.settings.boostsCarousel} - allowFilters + // allowFilters + filterContext="public" timelineStart={TimelineStart} headerEnd={ { const { filter } = f; const hasContext = filter.context.includes(filterContext); @@ -12,19 +10,35 @@ export function filteredItem(item, filterContext, currentAccountID) { if (!filter.expiresAt) return hasContext; return new Date(filter.expiresAt) > new Date(); }); - if (!appliedFilters.length) return true; + if (!appliedFilters.length) return false; const isHidden = appliedFilters.some((f) => f.filter.filterAction === 'hide'); - console.log({ isHidden, filtered, appliedFilters, item }); - if (isHidden) return false; + if (isHidden) + return { + action: 'hide', + }; const isWarn = appliedFilters.some((f) => f.filter.filterAction === 'warn'); if (isWarn) { const filterTitles = appliedFilters.map((f) => f.filter.title); - item._filtered = { + return { + action: 'warn', titles: filterTitles, titlesStr: filterTitles.join(' • '), }; } - return isWarn; + return false; +} +export const isFiltered = mem(_isFiltered); + +export function filteredItem(item, filterContext, currentAccountID) { + const { filtered } = item; + if (!filtered?.length) return true; + const isSelf = currentAccountID && item.account?.id === currentAccountID; + if (isSelf) return true; + const filterState = isFiltered(filtered, filterContext); + if (!filterState) return true; + if (filterState.action === 'hide') return false; + // item._filtered = filterState; + return true; } export function filteredItems(items, filterContext) { if (!items?.length) return []; diff --git a/src/utils/mem.js b/src/utils/mem.js index 773a492..2295b97 100644 --- a/src/utils/mem.js +++ b/src/utils/mem.js @@ -1,5 +1,7 @@ import moize from 'moize'; +window._moize = moize; + export default function mem(fn, opts = {}) { - return moize(fn, { ...opts, maxSize: 100 }); + return moize(fn, { ...opts, maxSize: 100, isDeepEqual: true }); } diff --git a/src/utils/states.js b/src/utils/states.js index 2f29571..314af7f 100644 --- a/src/utils/states.js +++ b/src/utils/states.js @@ -168,7 +168,7 @@ export function saveStatus(status, instance, opts) { if (!override && oldStatus) return; const key = statusKey(status.id, instance); if (oldStatus?._pinned) status._pinned = oldStatus._pinned; - if (oldStatus?._filtered) status._filtered = oldStatus._filtered; + // if (oldStatus?._filtered) status._filtered = oldStatus._filtered; states.statuses[key] = status; if (status.reblog) { const key = statusKey(status.reblog.id, instance);