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 { 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 (
<Parent
onMouseEnter={debugHover}
@ -102,10 +121,14 @@ function MediaPost({
data-spoiler-text={
spoilerText || (sensitive ? 'Sensitive media' : undefined)
}
data-filtered-text={_filtered ? 'Filtered' : undefined}
data-filtered-text={
filterInfo
? `Filtered${filterTitleStr ? `: ${filterTitleStr}` : ''}`
: undefined
}
class={`
media-post
${allowFilters && _filtered ? 'filtered' : ''}
${filterInfo ? 'filtered' : ''}
${hasSpoiler ? 'has-spoiler' : ''}
`}
>

View file

@ -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 (
<FilteredStatus
status={status}
filterInfo={_filtered}
filterInfo={filterInfo}
instance={instance}
containerProps={{
onMouseEnter: debugHover,
@ -195,13 +213,6 @@ function Status({
const createdAtDate = new Date(createdAt);
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(
(mention) => mention.id === inReplyToAccountId,
);

View file

@ -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,6 +287,7 @@ function Timeline({
const hiddenUI = scrollDirection === 'end' && !nearReachStart;
return (
<FilterContext.Provider value={filterContext}>
<div
id={`${id}-page`}
class="deck-container"
@ -365,7 +368,8 @@ function Timeline({
status={status}
instance={instance}
useItemID={useItemID}
allowFilters={allowFilters}
// allowFilters={allowFilters}
filterContext={filterContext}
key={status.id + status?._pinned}
view={view}
/>
@ -447,10 +451,18 @@ function Timeline({
)}
</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 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}
/>
) : (
<Status
@ -501,7 +521,7 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
instance={instance}
size="s"
contentTextWeight
allowFilters={allowFilters}
// allowFilters={allowFilters}
/>
)}
</Link>
@ -541,13 +561,13 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
<Status
statusID={statusID}
instance={instance}
allowFilters={allowFilters}
// allowFilters={allowFilters}
/>
) : (
<Status
status={item}
instance={instance}
allowFilters={allowFilters}
// allowFilters={allowFilters}
/>
)}
</Link>
@ -566,7 +586,7 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
key={itemKey}
statusID={statusID}
instance={instance}
allowFilters={allowFilters}
// allowFilters={allowFilters}
/>
) : (
<MediaPost
@ -575,7 +595,7 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
key={itemKey}
status={status}
instance={instance}
allowFilters={allowFilters}
// allowFilters={allowFilters}
/>
);
}
@ -587,13 +607,13 @@ function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
<Status
statusID={statusID}
instance={instance}
allowFilters={allowFilters}
// allowFilters={allowFilters}
/>
) : (
<Status
status={status}
instance={instance}
allowFilters={allowFilters}
// allowFilters={allowFilters}
/>
)}
</Link>

View file

@ -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"
/>
);
}

View file

@ -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={
<Menu2
portal

View file

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

View file

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

View file

@ -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={
<Menu2
portal

View file

@ -85,7 +85,7 @@ function Trending({ columnMode, ...props }) {
latestItem.current = value[0].id;
}
value = filteredItems(value, 'public'); // Might not work here
// value = filteredItems(value, 'public'); // Might not work here
value.forEach((item) => {
saveStatus(item, instance);
});
@ -257,7 +257,8 @@ function Trending({ columnMode, ...props }) {
useItemID
headerStart={<></>}
boostsCarousel={snapStates.settings.boostsCarousel}
allowFilters
// allowFilters
filterContext="public"
timelineStart={TimelineStart}
headerEnd={
<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';
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;
function _isFiltered(filtered, filterContext) {
if (!filtered?.length) return false;
const appliedFilters = filtered.filter((f) => {
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 [];

View file

@ -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 });
}

View file

@ -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);