1
0
Fork 0

Breaking: rewrote filters implementation

This commit is contained in:
Lim Chee Aun 2023-11-03 21:45:31 +08:00
parent 1cdc4ebbe8
commit 0bc1b598c3
13 changed files with 287 additions and 207 deletions

View file

@ -1,9 +1,13 @@
import './media-post.css'; import './media-post.css';
import { memo } from 'preact/compat'; import { memo } from 'preact/compat';
import { useContext, useMemo } from 'preact/hooks';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import FilterContext from '../utils/filter-context';
import { isFiltered } from '../utils/filters';
import states, { statusKey } from '../utils/states'; import states, { statusKey } from '../utils/states';
import store from '../utils/store';
import Media from './media'; import Media from './media';
@ -13,7 +17,7 @@ function MediaPost({
status, status,
instance, instance,
parent, parent,
allowFilters, // allowFilters,
onMediaClick, onMediaClick,
}) { }) {
let sKey = statusKey(statusID, instance); let sKey = statusKey(statusID, instance);
@ -68,7 +72,7 @@ function MediaPost({
// Non-API props // Non-API props
_deleted, _deleted,
_pinned, _pinned,
_filtered, // _filtered,
} = status; } = status;
if (!mediaAttachments?.length) { 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); console.debug('RENDER Media post', id, status?.account.displayName);
// const readingExpandSpoilers = useMemo(() => { // const readingExpandSpoilers = useMemo(() => {
@ -95,6 +113,7 @@ function MediaPost({
return mediaAttachments.map((media, i) => { return mediaAttachments.map((media, i) => {
const mediaKey = `${sKey}-${media.id}`; const mediaKey = `${sKey}-${media.id}`;
const filterTitleStr = filterInfo?.titlesStr;
return ( return (
<Parent <Parent
onMouseEnter={debugHover} onMouseEnter={debugHover}
@ -102,10 +121,14 @@ function MediaPost({
data-spoiler-text={ data-spoiler-text={
spoilerText || (sensitive ? 'Sensitive media' : undefined) spoilerText || (sensitive ? 'Sensitive media' : undefined)
} }
data-filtered-text={_filtered ? 'Filtered' : undefined} data-filtered-text={
filterInfo
? `Filtered${filterTitleStr ? `: ${filterTitleStr}` : ''}`
: undefined
}
class={` class={`
media-post media-post
${allowFilters && _filtered ? 'filtered' : ''} ${filterInfo ? 'filtered' : ''}
${hasSpoiler ? 'has-spoiler' : ''} ${hasSpoiler ? 'has-spoiler' : ''}
`} `}
> >

View file

@ -13,6 +13,7 @@ import pThrottle from 'p-throttle';
import { memo } from 'preact/compat'; import { memo } from 'preact/compat';
import { import {
useCallback, useCallback,
useContext,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
@ -34,6 +35,8 @@ import Poll from '../components/poll';
import { api } from '../utils/api'; import { api } from '../utils/api';
import emojifyText from '../utils/emojify-text'; import emojifyText from '../utils/emojify-text';
import enhanceContent from '../utils/enhance-content'; 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 getTranslateTargetLanguage from '../utils/get-translate-target-language';
import getHTMLText from '../utils/getHTMLText'; import getHTMLText from '../utils/getHTMLText';
import handleContentLinks from '../utils/handle-content-links'; import handleContentLinks from '../utils/handle-content-links';
@ -90,7 +93,7 @@ function Status({
enableTranslate, enableTranslate,
forceTranslate: _forceTranslate, forceTranslate: _forceTranslate,
previewMode, previewMode,
allowFilters, // allowFilters,
onMediaClick, onMediaClick,
quoted, quoted,
onStatusLinkClick = () => {}, onStatusLinkClick = () => {},
@ -166,9 +169,24 @@ function Status({
// Non-API props // Non-API props
_deleted, _deleted,
_pinned, _pinned,
_filtered, // _filtered,
} = status; } = 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); console.debug('RENDER Status', id, status?.account.displayName, quoted);
const debugHover = (e) => { const debugHover = (e) => {
@ -179,11 +197,11 @@ function Status({
} }
}; };
if (allowFilters && size !== 'l' && _filtered) { if (/*allowFilters && */ size !== 'l' && filterInfo) {
return ( return (
<FilteredStatus <FilteredStatus
status={status} status={status}
filterInfo={_filtered} filterInfo={filterInfo}
instance={instance} instance={instance}
containerProps={{ containerProps={{
onMouseEnter: debugHover, onMouseEnter: debugHover,
@ -195,13 +213,6 @@ function Status({
const createdAtDate = new Date(createdAt); const createdAtDate = new Date(createdAt);
const editedAtDate = new Date(editedAt); const editedAtDate = new Date(editedAt);
const currentAccount = useMemo(() => {
return store.session.get('currentAccount');
}, []);
const isSelf = useMemo(() => {
return currentAccount && currentAccount === accountId;
}, [accountId, currentAccount]);
let inReplyToAccountRef = mentions?.find( let inReplyToAccountRef = mentions?.find(
(mention) => mention.id === inReplyToAccountId, (mention) => mention.id === inReplyToAccountId,
); );

View file

@ -4,6 +4,8 @@ import { InView } from 'react-intersection-observer';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import FilterContext from '../utils/filter-context';
import { isFiltered } from '../utils/filters';
import states, { statusKey } from '../utils/states'; import states, { statusKey } from '../utils/states';
import statusPeek from '../utils/status-peek'; import statusPeek from '../utils/status-peek';
import { groupBoosts, groupContext } from '../utils/timeline-utils'; import { groupBoosts, groupContext } from '../utils/timeline-utils';
@ -13,7 +15,6 @@ import useScroll from '../utils/useScroll';
import Icon from './icon'; import Icon from './icon';
import Link from './link'; import Link from './link';
import Media from './media';
import MediaPost from './media-post'; import MediaPost from './media-post';
import NavMenu from './nav-menu'; import NavMenu from './nav-menu';
import Status from './status'; import Status from './status';
@ -39,9 +40,10 @@ function Timeline({
headerStart, headerStart,
headerEnd, headerEnd,
timelineStart, timelineStart,
allowFilters, // allowFilters,
refresh, refresh,
view, view,
filterContext,
}) { }) {
const snapStates = useSnapshot(states); const snapStates = useSnapshot(states);
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
@ -285,6 +287,7 @@ function Timeline({
const hiddenUI = scrollDirection === 'end' && !nearReachStart; const hiddenUI = scrollDirection === 'end' && !nearReachStart;
return ( return (
<FilterContext.Provider value={filterContext}>
<div <div
id={`${id}-page`} id={`${id}-page`}
class="deck-container" class="deck-container"
@ -365,7 +368,8 @@ function Timeline({
status={status} status={status}
instance={instance} instance={instance}
useItemID={useItemID} useItemID={useItemID}
allowFilters={allowFilters} // allowFilters={allowFilters}
filterContext={filterContext}
key={status.id + status?._pinned} key={status.id + status?._pinned}
view={view} view={view}
/> />
@ -447,10 +451,18 @@ function Timeline({
)} )}
</div> </div>
</div> </div>
</FilterContext.Provider>
); );
} }
function TimelineItem({ status, instance, useItemID, allowFilters, view }) { function TimelineItem({
status,
instance,
useItemID,
// allowFilters,
filterContext,
view,
}) {
const { id: statusID, reblog, items, type, _pinned } = status; const { id: statusID, reblog, items, type, _pinned } = status;
const actualStatusID = reblog?.id || statusID; const actualStatusID = reblog?.id || statusID;
const url = instance const url = instance
@ -467,10 +479,18 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
if (isCarousel) { if (isCarousel) {
// Here, we don't hide filtered posts, but we sort them last // Here, we don't hide filtered posts, but we sort them last
items.sort((a, b) => { 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; return 1;
} }
if (!a._filtered && b._filtered) { if (!aFiltered && bFiltered) {
return -1; return -1;
} }
return 0; return 0;
@ -493,7 +513,7 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
instance={instance} instance={instance}
size="s" size="s"
contentTextWeight contentTextWeight
allowFilters={allowFilters} // allowFilters={allowFilters}
/> />
) : ( ) : (
<Status <Status
@ -501,7 +521,7 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
instance={instance} instance={instance}
size="s" size="s"
contentTextWeight contentTextWeight
allowFilters={allowFilters} // allowFilters={allowFilters}
/> />
)} )}
</Link> </Link>
@ -541,13 +561,13 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
<Status <Status
statusID={statusID} statusID={statusID}
instance={instance} instance={instance}
allowFilters={allowFilters} // allowFilters={allowFilters}
/> />
) : ( ) : (
<Status <Status
status={item} status={item}
instance={instance} instance={instance}
allowFilters={allowFilters} // allowFilters={allowFilters}
/> />
)} )}
</Link> </Link>
@ -566,7 +586,7 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
key={itemKey} key={itemKey}
statusID={statusID} statusID={statusID}
instance={instance} instance={instance}
allowFilters={allowFilters} // allowFilters={allowFilters}
/> />
) : ( ) : (
<MediaPost <MediaPost
@ -575,7 +595,7 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
key={itemKey} key={itemKey}
status={status} status={status}
instance={instance} instance={instance}
allowFilters={allowFilters} // allowFilters={allowFilters}
/> />
); );
} }
@ -587,13 +607,13 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
<Status <Status
statusID={statusID} statusID={statusID}
instance={instance} instance={instance}
allowFilters={allowFilters} // allowFilters={allowFilters}
/> />
) : ( ) : (
<Status <Status
status={status} status={status}
instance={instance} instance={instance}
allowFilters={allowFilters} // allowFilters={allowFilters}
/> />
)} )}
</Link> </Link>

View file

@ -32,7 +32,7 @@ function Following({ title, path, id, ...props }) {
console.log('First load', latestItem.current); console.log('First load', latestItem.current);
} }
value = filteredItems(value, 'home'); // value = filteredItems(value, 'home');
value.forEach((item) => { value.forEach((item) => {
saveStatus(item, instance); saveStatus(item, instance);
}); });
@ -115,7 +115,8 @@ function Following({ title, path, id, ...props }) {
useItemID useItemID
boostsCarousel={snapStates.settings.boostsCarousel} boostsCarousel={snapStates.settings.boostsCarousel}
{...props} {...props}
allowFilters // allowFilters
filterContext="home"
/> />
); );
} }

View file

@ -78,7 +78,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
latestItem.current = value[0].id; latestItem.current = value[0].id;
} }
value = filteredItems(value, 'public'); // value = filteredItems(value, 'public');
value.forEach((item) => { value.forEach((item) => {
saveStatus(item, instance, { saveStatus(item, instance, {
skipThreading: media, // If media view, no need to form threads skipThreading: media, // If media view, no need to form threads
@ -153,7 +153,8 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
useItemID useItemID
view={media ? 'media' : undefined} view={media ? 'media' : undefined}
refresh={media} refresh={media}
allowFilters // allowFilters
filterContext="public"
headerEnd={ headerEnd={
<Menu2 <Menu2
portal portal

View file

@ -43,7 +43,7 @@ function List(props) {
latestItem.current = value[0].id; latestItem.current = value[0].id;
} }
value = filteredItems(value, 'home'); // value = filteredItems(value, 'home');
value.forEach((item) => { value.forEach((item) => {
saveStatus(item, instance); saveStatus(item, instance);
}); });
@ -102,7 +102,8 @@ function List(props) {
checkForUpdates={checkForUpdates} checkForUpdates={checkForUpdates}
useItemID useItemID
boostsCarousel={snapStates.settings.boostsCarousel} boostsCarousel={snapStates.settings.boostsCarousel}
allowFilters // allowFilters
filterContext="home"
// refresh={reloadCount} // refresh={reloadCount}
headerStart={ headerStart={
<Link to="/l" class="button plain"> <Link to="/l" class="button plain">

View file

@ -195,6 +195,7 @@ function Notifications({ columnMode }) {
snapStates.notificationsShowNew && snapStates.notificationsShowNew &&
uiState !== 'loading' uiState !== 'loading'
) { ) {
setShowNew(false);
loadNotifications(true); loadNotifications(true);
} else { } else {
setShowNew(snapStates.notificationsShowNew); setShowNew(snapStates.notificationsShowNew);

View file

@ -41,7 +41,7 @@ function Public({ local, columnMode, ...props }) {
latestItem.current = value[0].id; latestItem.current = value[0].id;
} }
value = filteredItems(value, 'public'); // value = filteredItems(value, 'public');
value.forEach((item) => { value.forEach((item) => {
saveStatus(item, instance); saveStatus(item, instance);
}); });
@ -91,7 +91,8 @@ function Public({ local, columnMode, ...props }) {
useItemID useItemID
headerStart={<></>} headerStart={<></>}
boostsCarousel={snapStates.settings.boostsCarousel} boostsCarousel={snapStates.settings.boostsCarousel}
allowFilters // allowFilters
filterContext="public"
headerEnd={ headerEnd={
<Menu2 <Menu2
portal portal

View file

@ -85,7 +85,7 @@ function Trending({ columnMode, ...props }) {
latestItem.current = value[0].id; latestItem.current = value[0].id;
} }
value = filteredItems(value, 'public'); // Might not work here // value = filteredItems(value, 'public'); // Might not work here
value.forEach((item) => { value.forEach((item) => {
saveStatus(item, instance); saveStatus(item, instance);
}); });
@ -257,7 +257,8 @@ function Trending({ columnMode, ...props }) {
useItemID useItemID
headerStart={<></>} headerStart={<></>}
boostsCarousel={snapStates.settings.boostsCarousel} boostsCarousel={snapStates.settings.boostsCarousel}
allowFilters // allowFilters
filterContext="public"
timelineStart={TimelineStart} timelineStart={TimelineStart}
headerEnd={ headerEnd={
<Menu2 <Menu2

View file

@ -0,0 +1,4 @@
import { createContext } from 'preact';
const FilterContext = createContext();
export default FilterContext;

View file

@ -1,10 +1,8 @@
import mem from './mem';
import store from './store'; import store from './store';
export function filteredItem(item, filterContext, currentAccountID) { function _isFiltered(filtered, filterContext) {
const { filtered } = item; if (!filtered?.length) return false;
if (!filtered?.length) return true;
const isSelf = currentAccountID && item.account?.id === currentAccountID;
if (isSelf) return true;
const appliedFilters = filtered.filter((f) => { const appliedFilters = filtered.filter((f) => {
const { filter } = f; const { filter } = f;
const hasContext = filter.context.includes(filterContext); const hasContext = filter.context.includes(filterContext);
@ -12,19 +10,35 @@ export function filteredItem(item, filterContext, currentAccountID) {
if (!filter.expiresAt) return hasContext; if (!filter.expiresAt) return hasContext;
return new Date(filter.expiresAt) > new Date(); 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'); const isHidden = appliedFilters.some((f) => f.filter.filterAction === 'hide');
console.log({ isHidden, filtered, appliedFilters, item }); if (isHidden)
if (isHidden) return false; return {
action: 'hide',
};
const isWarn = appliedFilters.some((f) => f.filter.filterAction === 'warn'); const isWarn = appliedFilters.some((f) => f.filter.filterAction === 'warn');
if (isWarn) { if (isWarn) {
const filterTitles = appliedFilters.map((f) => f.filter.title); const filterTitles = appliedFilters.map((f) => f.filter.title);
item._filtered = { return {
action: 'warn',
titles: filterTitles, titles: filterTitles,
titlesStr: filterTitles.join(' • '), 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) { export function filteredItems(items, filterContext) {
if (!items?.length) return []; if (!items?.length) return [];

View file

@ -1,5 +1,7 @@
import moize from 'moize'; import moize from 'moize';
window._moize = moize;
export default function mem(fn, opts = {}) { export default function mem(fn, opts = {}) {
return moize(fn, { ...opts, maxSize: 100 }); return moize(fn, { ...opts, maxSize: 100, isDeepEqual: true });
} }

View file

@ -168,7 +168,7 @@ export function saveStatus(status, instance, opts) {
if (!override && oldStatus) return; if (!override && oldStatus) return;
const key = statusKey(status.id, instance); const key = statusKey(status.id, instance);
if (oldStatus?._pinned) status._pinned = oldStatus._pinned; 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; states.statuses[key] = status;
if (status.reblog) { if (status.reblog) {
const key = statusKey(status.reblog.id, instance); const key = statusKey(status.reblog.id, instance);