this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Breaking: rewrote filters implementation

+286 -206
+27 -4
src/components/media-post.jsx
··· 1 1 import './media-post.css'; 2 2 3 3 import { memo } from 'preact/compat'; 4 + import { useContext, useMemo } from 'preact/hooks'; 4 5 import { useSnapshot } from 'valtio'; 5 6 7 + import FilterContext from '../utils/filter-context'; 8 + import { isFiltered } from '../utils/filters'; 6 9 import states, { statusKey } from '../utils/states'; 10 + import store from '../utils/store'; 7 11 8 12 import Media from './media'; 9 13 ··· 13 17 status, 14 18 instance, 15 19 parent, 16 - allowFilters, 20 + // allowFilters, 17 21 onMediaClick, 18 22 }) { 19 23 let sKey = statusKey(statusID, instance); ··· 68 72 // Non-API props 69 73 _deleted, 70 74 _pinned, 71 - _filtered, 75 + // _filtered, 72 76 } = status; 73 77 74 78 if (!mediaAttachments?.length) { ··· 83 87 } 84 88 }; 85 89 90 + const currentAccount = useMemo(() => { 91 + return store.session.get('currentAccount'); 92 + }, []); 93 + const isSelf = useMemo(() => { 94 + return currentAccount && currentAccount === accountId; 95 + }, [accountId, currentAccount]); 96 + 97 + const filterContext = useContext(FilterContext); 98 + const filterInfo = !isSelf && isFiltered(filtered, filterContext); 99 + 100 + if (filterInfo?.action === 'hide') { 101 + return null; 102 + } 103 + 86 104 console.debug('RENDER Media post', id, status?.account.displayName); 87 105 88 106 // const readingExpandSpoilers = useMemo(() => { ··· 95 113 96 114 return mediaAttachments.map((media, i) => { 97 115 const mediaKey = `${sKey}-${media.id}`; 116 + const filterTitleStr = filterInfo?.titlesStr; 98 117 return ( 99 118 <Parent 100 119 onMouseEnter={debugHover} ··· 102 121 data-spoiler-text={ 103 122 spoilerText || (sensitive ? 'Sensitive media' : undefined) 104 123 } 105 - data-filtered-text={_filtered ? 'Filtered' : undefined} 124 + data-filtered-text={ 125 + filterInfo 126 + ? `Filtered${filterTitleStr ? `: ${filterTitleStr}` : ''}` 127 + : undefined 128 + } 106 129 class={` 107 130 media-post 108 - ${allowFilters && _filtered ? 'filtered' : ''} 131 + ${filterInfo ? 'filtered' : ''} 109 132 ${hasSpoiler ? 'has-spoiler' : ''} 110 133 `} 111 134 >
+22 -11
src/components/status.jsx
··· 13 13 import { memo } from 'preact/compat'; 14 14 import { 15 15 useCallback, 16 + useContext, 16 17 useEffect, 17 18 useMemo, 18 19 useRef, ··· 34 35 import { api } from '../utils/api'; 35 36 import emojifyText from '../utils/emojify-text'; 36 37 import enhanceContent from '../utils/enhance-content'; 38 + import FilterContext from '../utils/filter-context'; 39 + import { isFiltered } from '../utils/filters'; 37 40 import getTranslateTargetLanguage from '../utils/get-translate-target-language'; 38 41 import getHTMLText from '../utils/getHTMLText'; 39 42 import handleContentLinks from '../utils/handle-content-links'; ··· 90 93 enableTranslate, 91 94 forceTranslate: _forceTranslate, 92 95 previewMode, 93 - allowFilters, 96 + // allowFilters, 94 97 onMediaClick, 95 98 quoted, 96 99 onStatusLinkClick = () => {}, ··· 166 169 // Non-API props 167 170 _deleted, 168 171 _pinned, 169 - _filtered, 172 + // _filtered, 170 173 } = status; 171 174 175 + const currentAccount = useMemo(() => { 176 + return store.session.get('currentAccount'); 177 + }, []); 178 + const isSelf = useMemo(() => { 179 + return currentAccount && currentAccount === accountId; 180 + }, [accountId, currentAccount]); 181 + 182 + const filterContext = useContext(FilterContext); 183 + const filterInfo = 184 + !isSelf && !readOnly && !previewMode && isFiltered(filtered, filterContext); 185 + 186 + if (filterInfo?.action === 'hide') { 187 + return null; 188 + } 189 + 172 190 console.debug('RENDER Status', id, status?.account.displayName, quoted); 173 191 174 192 const debugHover = (e) => { ··· 179 197 } 180 198 }; 181 199 182 - if (allowFilters && size !== 'l' && _filtered) { 200 + if (/*allowFilters && */ size !== 'l' && filterInfo) { 183 201 return ( 184 202 <FilteredStatus 185 203 status={status} 186 - filterInfo={_filtered} 204 + filterInfo={filterInfo} 187 205 instance={instance} 188 206 containerProps={{ 189 207 onMouseEnter: debugHover, ··· 194 212 195 213 const createdAtDate = new Date(createdAt); 196 214 const editedAtDate = new Date(editedAt); 197 - 198 - const currentAccount = useMemo(() => { 199 - return store.session.get('currentAccount'); 200 - }, []); 201 - const isSelf = useMemo(() => { 202 - return currentAccount && currentAccount === accountId; 203 - }, [accountId, currentAccount]); 204 215 205 216 let inReplyToAccountRef = mentions?.find( 206 217 (mention) => mention.id === inReplyToAccountId,
+189 -169
src/components/timeline.jsx
··· 4 4 import { useDebouncedCallback } from 'use-debounce'; 5 5 import { useSnapshot } from 'valtio'; 6 6 7 + import FilterContext from '../utils/filter-context'; 8 + import { isFiltered } from '../utils/filters'; 7 9 import states, { statusKey } from '../utils/states'; 8 10 import statusPeek from '../utils/status-peek'; 9 11 import { groupBoosts, groupContext } from '../utils/timeline-utils'; ··· 13 15 14 16 import Icon from './icon'; 15 17 import Link from './link'; 16 - import Media from './media'; 17 18 import MediaPost from './media-post'; 18 19 import NavMenu from './nav-menu'; 19 20 import Status from './status'; ··· 39 40 headerStart, 40 41 headerEnd, 41 42 timelineStart, 42 - allowFilters, 43 + // allowFilters, 43 44 refresh, 44 45 view, 46 + filterContext, 45 47 }) { 46 48 const snapStates = useSnapshot(states); 47 49 const [items, setItems] = useState([]); ··· 285 287 const hiddenUI = scrollDirection === 'end' && !nearReachStart; 286 288 287 289 return ( 288 - <div 289 - id={`${id}-page`} 290 - class="deck-container" 291 - ref={(node) => { 292 - scrollableRef.current = node; 293 - jRef.current = node; 294 - kRef.current = node; 295 - oRef.current = node; 296 - }} 297 - tabIndex="-1" 298 - > 299 - <div class="timeline-deck deck"> 300 - <header 301 - hidden={hiddenUI} 302 - onClick={(e) => { 303 - if (!e.target.closest('a, button')) { 304 - scrollableRef.current?.scrollTo({ 305 - top: 0, 306 - behavior: 'smooth', 307 - }); 308 - } 309 - }} 310 - onDblClick={(e) => { 311 - if (!e.target.closest('a, button')) { 312 - loadItems(true); 313 - } 314 - }} 315 - class={uiState === 'loading' ? 'loading' : ''} 316 - > 317 - <div class="header-grid"> 318 - <div class="header-side"> 319 - <NavMenu /> 320 - {headerStart !== null && headerStart !== undefined ? ( 321 - headerStart 322 - ) : ( 323 - <Link to="/" class="button plain home-button"> 324 - <Icon icon="home" size="l" /> 325 - </Link> 290 + <FilterContext.Provider value={filterContext}> 291 + <div 292 + id={`${id}-page`} 293 + class="deck-container" 294 + ref={(node) => { 295 + scrollableRef.current = node; 296 + jRef.current = node; 297 + kRef.current = node; 298 + oRef.current = node; 299 + }} 300 + tabIndex="-1" 301 + > 302 + <div class="timeline-deck deck"> 303 + <header 304 + hidden={hiddenUI} 305 + onClick={(e) => { 306 + if (!e.target.closest('a, button')) { 307 + scrollableRef.current?.scrollTo({ 308 + top: 0, 309 + behavior: 'smooth', 310 + }); 311 + } 312 + }} 313 + onDblClick={(e) => { 314 + if (!e.target.closest('a, button')) { 315 + loadItems(true); 316 + } 317 + }} 318 + class={uiState === 'loading' ? 'loading' : ''} 319 + > 320 + <div class="header-grid"> 321 + <div class="header-side"> 322 + <NavMenu /> 323 + {headerStart !== null && headerStart !== undefined ? ( 324 + headerStart 325 + ) : ( 326 + <Link to="/" class="button plain home-button"> 327 + <Icon icon="home" size="l" /> 328 + </Link> 329 + )} 330 + </div> 331 + {title && (titleComponent ? titleComponent : <h1>{title}</h1>)} 332 + <div class="header-side"> 333 + {/* <Loader hidden={uiState !== 'loading'} /> */} 334 + {!!headerEnd && headerEnd} 335 + </div> 336 + </div> 337 + {items.length > 0 && 338 + uiState !== 'loading' && 339 + !hiddenUI && 340 + showNew && ( 341 + <button 342 + class="updates-button shiny-pill" 343 + type="button" 344 + onClick={() => { 345 + loadItems(true); 346 + scrollableRef.current?.scrollTo({ 347 + top: 0, 348 + behavior: 'smooth', 349 + }); 350 + }} 351 + > 352 + <Icon icon="arrow-up" /> New posts 353 + </button> 326 354 )} 355 + </header> 356 + {!!timelineStart && ( 357 + <div 358 + class={`timeline-start ${uiState === 'loading' ? 'loading' : ''}`} 359 + > 360 + {timelineStart} 327 361 </div> 328 - {title && (titleComponent ? titleComponent : <h1>{title}</h1>)} 329 - <div class="header-side"> 330 - {/* <Loader hidden={uiState !== 'loading'} /> */} 331 - {!!headerEnd && headerEnd} 332 - </div> 333 - </div> 334 - {items.length > 0 && 335 - uiState !== 'loading' && 336 - !hiddenUI && 337 - showNew && ( 338 - <button 339 - class="updates-button shiny-pill" 340 - type="button" 341 - onClick={() => { 342 - loadItems(true); 343 - scrollableRef.current?.scrollTo({ 344 - top: 0, 345 - behavior: 'smooth', 346 - }); 347 - }} 348 - > 349 - <Icon icon="arrow-up" /> New posts 350 - </button> 351 - )} 352 - </header> 353 - {!!timelineStart && ( 354 - <div 355 - class={`timeline-start ${uiState === 'loading' ? 'loading' : ''}`} 356 - > 357 - {timelineStart} 358 - </div> 359 - )} 360 - {!!items.length ? ( 361 - <> 362 - <ul class={`timeline ${view ? `timeline-${view}` : ''}`}> 363 - {items.map((status) => ( 364 - <TimelineItem 365 - status={status} 366 - instance={instance} 367 - useItemID={useItemID} 368 - allowFilters={allowFilters} 369 - key={status.id + status?._pinned} 370 - view={view} 371 - /> 372 - ))} 373 - {showMore && 374 - uiState === 'loading' && 375 - (view === 'media' ? null : ( 376 - <> 377 - <li 378 - style={{ 379 - height: '20vh', 380 - }} 381 - > 382 - <Status skeleton /> 383 - </li> 384 - <li 385 - style={{ 386 - height: '25vh', 387 - }} 362 + )} 363 + {!!items.length ? ( 364 + <> 365 + <ul class={`timeline ${view ? `timeline-${view}` : ''}`}> 366 + {items.map((status) => ( 367 + <TimelineItem 368 + status={status} 369 + instance={instance} 370 + useItemID={useItemID} 371 + // allowFilters={allowFilters} 372 + filterContext={filterContext} 373 + key={status.id + status?._pinned} 374 + view={view} 375 + /> 376 + ))} 377 + {showMore && 378 + uiState === 'loading' && 379 + (view === 'media' ? null : ( 380 + <> 381 + <li 382 + style={{ 383 + height: '20vh', 384 + }} 385 + > 386 + <Status skeleton /> 387 + </li> 388 + <li 389 + style={{ 390 + height: '25vh', 391 + }} 392 + > 393 + <Status skeleton /> 394 + </li> 395 + </> 396 + ))} 397 + </ul> 398 + {uiState === 'default' && 399 + (showMore ? ( 400 + <InView 401 + onChange={(inView) => { 402 + if (inView) { 403 + loadItems(); 404 + } 405 + }} 406 + > 407 + <button 408 + type="button" 409 + class="plain block" 410 + onClick={() => loadItems()} 411 + style={{ marginBlockEnd: '6em' }} 388 412 > 389 - <Status skeleton /> 390 - </li> 391 - </> 413 + Show more&hellip; 414 + </button> 415 + </InView> 416 + ) : ( 417 + <p class="ui-state insignificant">The end.</p> 392 418 ))} 419 + </> 420 + ) : uiState === 'loading' ? ( 421 + <ul class="timeline"> 422 + {Array.from({ length: 5 }).map((_, i) => 423 + view === 'media' ? ( 424 + <div 425 + style={{ 426 + height: '50vh', 427 + }} 428 + /> 429 + ) : ( 430 + <li key={i}> 431 + <Status skeleton /> 432 + </li> 433 + ), 434 + )} 393 435 </ul> 394 - {uiState === 'default' && 395 - (showMore ? ( 396 - <InView 397 - onChange={(inView) => { 398 - if (inView) { 399 - loadItems(); 400 - } 401 - }} 402 - > 403 - <button 404 - type="button" 405 - class="plain block" 406 - onClick={() => loadItems()} 407 - style={{ marginBlockEnd: '6em' }} 408 - > 409 - Show more&hellip; 410 - </button> 411 - </InView> 412 - ) : ( 413 - <p class="ui-state insignificant">The end.</p> 414 - ))} 415 - </> 416 - ) : uiState === 'loading' ? ( 417 - <ul class="timeline"> 418 - {Array.from({ length: 5 }).map((_, i) => 419 - view === 'media' ? ( 420 - <div 421 - style={{ 422 - height: '50vh', 423 - }} 424 - /> 425 - ) : ( 426 - <li key={i}> 427 - <Status skeleton /> 428 - </li> 429 - ), 430 - )} 431 - </ul> 432 - ) : ( 433 - uiState !== 'error' && <p class="ui-state">{emptyText}</p> 434 - )} 435 - {uiState === 'error' && ( 436 - <p class="ui-state"> 437 - {errorText} 438 - <br /> 439 - <br /> 440 - <button 441 - class="button plain" 442 - onClick={() => loadItems(!items.length)} 443 - > 444 - Try again 445 - </button> 446 - </p> 447 - )} 436 + ) : ( 437 + uiState !== 'error' && <p class="ui-state">{emptyText}</p> 438 + )} 439 + {uiState === 'error' && ( 440 + <p class="ui-state"> 441 + {errorText} 442 + <br /> 443 + <br /> 444 + <button 445 + class="button plain" 446 + onClick={() => loadItems(!items.length)} 447 + > 448 + Try again 449 + </button> 450 + </p> 451 + )} 452 + </div> 448 453 </div> 449 - </div> 454 + </FilterContext.Provider> 450 455 ); 451 456 } 452 457 453 - function TimelineItem({ status, instance, useItemID, allowFilters, view }) { 458 + function TimelineItem({ 459 + status, 460 + instance, 461 + useItemID, 462 + // allowFilters, 463 + filterContext, 464 + view, 465 + }) { 454 466 const { id: statusID, reblog, items, type, _pinned } = status; 455 467 const actualStatusID = reblog?.id || statusID; 456 468 const url = instance ··· 467 479 if (isCarousel) { 468 480 // Here, we don't hide filtered posts, but we sort them last 469 481 items.sort((a, b) => { 470 - if (a._filtered && !b._filtered) { 482 + // if (a._filtered && !b._filtered) { 483 + // return 1; 484 + // } 485 + // if (!a._filtered && b._filtered) { 486 + // return -1; 487 + // } 488 + const aFiltered = isFiltered(a.filtered, filterContext); 489 + const bFiltered = isFiltered(b.filtered, filterContext); 490 + if (aFiltered && !bFiltered) { 471 491 return 1; 472 492 } 473 - if (!a._filtered && b._filtered) { 493 + if (!aFiltered && bFiltered) { 474 494 return -1; 475 495 } 476 496 return 0; ··· 493 513 instance={instance} 494 514 size="s" 495 515 contentTextWeight 496 - allowFilters={allowFilters} 516 + // allowFilters={allowFilters} 497 517 /> 498 518 ) : ( 499 519 <Status ··· 501 521 instance={instance} 502 522 size="s" 503 523 contentTextWeight 504 - allowFilters={allowFilters} 524 + // allowFilters={allowFilters} 505 525 /> 506 526 )} 507 527 </Link> ··· 541 561 <Status 542 562 statusID={statusID} 543 563 instance={instance} 544 - allowFilters={allowFilters} 564 + // allowFilters={allowFilters} 545 565 /> 546 566 ) : ( 547 567 <Status 548 568 status={item} 549 569 instance={instance} 550 - allowFilters={allowFilters} 570 + // allowFilters={allowFilters} 551 571 /> 552 572 )} 553 573 </Link> ··· 566 586 key={itemKey} 567 587 statusID={statusID} 568 588 instance={instance} 569 - allowFilters={allowFilters} 589 + // allowFilters={allowFilters} 570 590 /> 571 591 ) : ( 572 592 <MediaPost ··· 575 595 key={itemKey} 576 596 status={status} 577 597 instance={instance} 578 - allowFilters={allowFilters} 598 + // allowFilters={allowFilters} 579 599 /> 580 600 ); 581 601 } ··· 587 607 <Status 588 608 statusID={statusID} 589 609 instance={instance} 590 - allowFilters={allowFilters} 610 + // allowFilters={allowFilters} 591 611 /> 592 612 ) : ( 593 613 <Status 594 614 status={status} 595 615 instance={instance} 596 - allowFilters={allowFilters} 616 + // allowFilters={allowFilters} 597 617 /> 598 618 )} 599 619 </Link>
+3 -2
src/pages/following.jsx
··· 32 32 console.log('First load', latestItem.current); 33 33 } 34 34 35 - value = filteredItems(value, 'home'); 35 + // value = filteredItems(value, 'home'); 36 36 value.forEach((item) => { 37 37 saveStatus(item, instance); 38 38 }); ··· 115 115 useItemID 116 116 boostsCarousel={snapStates.settings.boostsCarousel} 117 117 {...props} 118 - allowFilters 118 + // allowFilters 119 + filterContext="home" 119 120 /> 120 121 ); 121 122 }
+3 -2
src/pages/hashtag.jsx
··· 78 78 latestItem.current = value[0].id; 79 79 } 80 80 81 - value = filteredItems(value, 'public'); 81 + // value = filteredItems(value, 'public'); 82 82 value.forEach((item) => { 83 83 saveStatus(item, instance, { 84 84 skipThreading: media, // If media view, no need to form threads ··· 153 153 useItemID 154 154 view={media ? 'media' : undefined} 155 155 refresh={media} 156 - allowFilters 156 + // allowFilters 157 + filterContext="public" 157 158 headerEnd={ 158 159 <Menu2 159 160 portal
+3 -2
src/pages/list.jsx
··· 43 43 latestItem.current = value[0].id; 44 44 } 45 45 46 - value = filteredItems(value, 'home'); 46 + // value = filteredItems(value, 'home'); 47 47 value.forEach((item) => { 48 48 saveStatus(item, instance); 49 49 }); ··· 102 102 checkForUpdates={checkForUpdates} 103 103 useItemID 104 104 boostsCarousel={snapStates.settings.boostsCarousel} 105 - allowFilters 105 + // allowFilters 106 + filterContext="home" 106 107 // refresh={reloadCount} 107 108 headerStart={ 108 109 <Link to="/l" class="button plain">
+1
src/pages/notifications.jsx
··· 195 195 snapStates.notificationsShowNew && 196 196 uiState !== 'loading' 197 197 ) { 198 + setShowNew(false); 198 199 loadNotifications(true); 199 200 } else { 200 201 setShowNew(snapStates.notificationsShowNew);
+3 -2
src/pages/public.jsx
··· 41 41 latestItem.current = value[0].id; 42 42 } 43 43 44 - value = filteredItems(value, 'public'); 44 + // value = filteredItems(value, 'public'); 45 45 value.forEach((item) => { 46 46 saveStatus(item, instance); 47 47 }); ··· 91 91 useItemID 92 92 headerStart={<></>} 93 93 boostsCarousel={snapStates.settings.boostsCarousel} 94 - allowFilters 94 + // allowFilters 95 + filterContext="public" 95 96 headerEnd={ 96 97 <Menu2 97 98 portal
+3 -2
src/pages/trending.jsx
··· 85 85 latestItem.current = value[0].id; 86 86 } 87 87 88 - value = filteredItems(value, 'public'); // Might not work here 88 + // value = filteredItems(value, 'public'); // Might not work here 89 89 value.forEach((item) => { 90 90 saveStatus(item, instance); 91 91 }); ··· 257 257 useItemID 258 258 headerStart={<></>} 259 259 boostsCarousel={snapStates.settings.boostsCarousel} 260 - allowFilters 260 + // allowFilters 261 + filterContext="public" 261 262 timelineStart={TimelineStart} 262 263 headerEnd={ 263 264 <Menu2
+4
src/utils/filter-context.js
··· 1 + import { createContext } from 'preact'; 2 + 3 + const FilterContext = createContext(); 4 + export default FilterContext;
+24 -10
src/utils/filters.jsx
··· 1 + import mem from './mem'; 1 2 import store from './store'; 2 3 3 - export function filteredItem(item, filterContext, currentAccountID) { 4 - const { filtered } = item; 5 - if (!filtered?.length) return true; 6 - const isSelf = currentAccountID && item.account?.id === currentAccountID; 7 - if (isSelf) return true; 4 + function _isFiltered(filtered, filterContext) { 5 + if (!filtered?.length) return false; 8 6 const appliedFilters = filtered.filter((f) => { 9 7 const { filter } = f; 10 8 const hasContext = filter.context.includes(filterContext); ··· 12 10 if (!filter.expiresAt) return hasContext; 13 11 return new Date(filter.expiresAt) > new Date(); 14 12 }); 15 - if (!appliedFilters.length) return true; 13 + if (!appliedFilters.length) return false; 16 14 const isHidden = appliedFilters.some((f) => f.filter.filterAction === 'hide'); 17 - console.log({ isHidden, filtered, appliedFilters, item }); 18 - if (isHidden) return false; 15 + if (isHidden) 16 + return { 17 + action: 'hide', 18 + }; 19 19 const isWarn = appliedFilters.some((f) => f.filter.filterAction === 'warn'); 20 20 if (isWarn) { 21 21 const filterTitles = appliedFilters.map((f) => f.filter.title); 22 - item._filtered = { 22 + return { 23 + action: 'warn', 23 24 titles: filterTitles, 24 25 titlesStr: filterTitles.join(' • '), 25 26 }; 26 27 } 27 - return isWarn; 28 + return false; 29 + } 30 + export const isFiltered = mem(_isFiltered); 31 + 32 + export function filteredItem(item, filterContext, currentAccountID) { 33 + const { filtered } = item; 34 + if (!filtered?.length) return true; 35 + const isSelf = currentAccountID && item.account?.id === currentAccountID; 36 + if (isSelf) return true; 37 + const filterState = isFiltered(filtered, filterContext); 38 + if (!filterState) return true; 39 + if (filterState.action === 'hide') return false; 40 + // item._filtered = filterState; 41 + return true; 28 42 } 29 43 export function filteredItems(items, filterContext) { 30 44 if (!items?.length) return [];
+3 -1
src/utils/mem.js
··· 1 1 import moize from 'moize'; 2 2 3 + window._moize = moize; 4 + 3 5 export default function mem(fn, opts = {}) { 4 - return moize(fn, { ...opts, maxSize: 100 }); 6 + return moize(fn, { ...opts, maxSize: 100, isDeepEqual: true }); 5 7 }
+1 -1
src/utils/states.js
··· 168 168 if (!override && oldStatus) return; 169 169 const key = statusKey(status.id, instance); 170 170 if (oldStatus?._pinned) status._pinned = oldStatus._pinned; 171 - if (oldStatus?._filtered) status._filtered = oldStatus._filtered; 171 + // if (oldStatus?._filtered) status._filtered = oldStatus._filtered; 172 172 states.statuses[key] = status; 173 173 if (status.reblog) { 174 174 const key = statusKey(status.reblog.id, instance);