this repo has no description
0
fork

Configure Feed

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

Experiment: month filter for account statuses

+259 -7
+42
src/app.css
··· 2127 2127 pointer-events: none; 2128 2128 opacity: 0.5; 2129 2129 } 2130 + 2131 + .filter-field { 2132 + flex-shrink: 0; 2133 + padding: 8px 16px; 2134 + border-radius: 999px; 2135 + color: var(--text-color); 2136 + background-color: var(--bg-color); 2137 + border: 2px solid transparent; 2138 + margin: 0; 2139 + appearance: none; 2140 + line-height: 1; 2141 + font-size: 90%; 2142 + 2143 + &:placeholder-shown { 2144 + color: var(--text-insignificant-color); 2145 + } 2146 + 2147 + &:is(:hover, :focus-visible) { 2148 + border-color: var(--link-light-color); 2149 + } 2150 + &:focus { 2151 + outline-color: var(--link-light-color); 2152 + } 2153 + &.is-active { 2154 + border-color: var(--link-color); 2155 + box-shadow: inset 0 0 8px var(--link-faded-color); 2156 + } 2157 + 2158 + :is(input, select) { 2159 + background-color: transparent; 2160 + border: 0; 2161 + padding: 0; 2162 + margin: 0; 2163 + color: inherit; 2164 + font-size: inherit; 2165 + line-height: inherit; 2166 + appearance: none; 2167 + border-radius: 0; 2168 + box-shadow: none; 2169 + outline: none; 2170 + } 2171 + } 2130 2172 } 2131 2173 .filter-bar.centered { 2132 2174 justify-content: center;
+217 -7
src/pages/account-statuses.jsx
··· 17 17 18 18 const LIMIT = 20; 19 19 20 + const supportsInputMonth = (() => { 21 + try { 22 + const input = document.createElement('input'); 23 + input.setAttribute('type', 'month'); 24 + return input.type === 'month'; 25 + } catch (e) { 26 + return false; 27 + } 28 + })(); 29 + 20 30 function AccountStatuses() { 21 31 const snapStates = useSnapshot(states); 22 32 const { id, ...params } = useParams(); 23 33 const [searchParams, setSearchParams] = useSearchParams(); 34 + const month = searchParams.get('month'); 24 35 const excludeReplies = !searchParams.get('replies'); 25 36 const excludeBoosts = !!searchParams.get('boosts'); 26 37 const tagged = searchParams.get('tagged'); 27 38 const media = !!searchParams.get('media'); 28 39 const { masto, instance, authenticated } = api({ instance: params.instance }); 29 40 const accountStatusesIterator = useRef(); 41 + 42 + const allSearchParams = [month, excludeReplies, excludeBoosts, tagged, media]; 43 + const [account, setAccount] = useState(); 44 + const searchOffsetRef = useRef(0); 45 + useEffect(() => { 46 + searchOffsetRef.current = 0; 47 + }, allSearchParams); 48 + 49 + const sameCurrentInstance = useMemo( 50 + () => instance === api().instance, 51 + [instance], 52 + ); 53 + const [searchEnabled, setSearchEnabled] = useState(false); 54 + useEffect(() => { 55 + // Only enable for current logged-in instance 56 + // Most remote instances don't allow unauthenticated searches 57 + if (!sameCurrentInstance) return; 58 + if (!account?.acct) return; 59 + (async () => { 60 + const results = await masto.v2.search.fetch({ 61 + q: `from:${account?.acct}`, 62 + type: 'statuses', 63 + limit: 1, 64 + }); 65 + setSearchEnabled(!!results?.statuses?.length); 66 + })(); 67 + }, [sameCurrentInstance, account?.acct]); 68 + 30 69 async function fetchAccountStatuses(firstLoad) { 70 + if (/^\d{4}-[01]\d$/.test(month)) { 71 + if (!account) { 72 + return { 73 + value: [], 74 + done: true, 75 + }; 76 + } 77 + const [_year, _month] = month.split('-'); 78 + const monthIndex = parseInt(_month, 10) - 1; 79 + // YYYY-MM (no day) 80 + // Search options: 81 + // - from:account 82 + // - after:YYYY-MM-DD (non-inclusive) 83 + // - before:YYYY-MM-DD (non-inclusive) 84 + 85 + // Last day of previous month 86 + const after = new Date(_year, monthIndex, 0); 87 + const afterStr = `${after.getFullYear()}-${(after.getMonth() + 1) 88 + .toString() 89 + .padStart(2, '0')}-${after.getDate().toString().padStart(2, '0')}`; 90 + // First day of next month 91 + const before = new Date(_year, monthIndex + 1, 1); 92 + const beforeStr = `${before.getFullYear()}-${(before.getMonth() + 1) 93 + .toString() 94 + .padStart(2, '0')}-${before.getDate().toString().padStart(2, '0')}`; 95 + console.log({ 96 + month, 97 + _year, 98 + _month, 99 + monthIndex, 100 + after, 101 + before, 102 + afterStr, 103 + beforeStr, 104 + }); 105 + 106 + let limit; 107 + if (firstLoad) { 108 + limit = LIMIT + 1; 109 + searchOffsetRef.current = 0; 110 + } else { 111 + limit = LIMIT + searchOffsetRef.current + 1; 112 + searchOffsetRef.current += LIMIT; 113 + } 114 + 115 + const searchResults = await masto.v2.search.fetch({ 116 + q: `from:${account.acct} after:${afterStr} before:${beforeStr}`, 117 + type: 'statuses', 118 + limit, 119 + offset: searchOffsetRef.current, 120 + }); 121 + if (searchResults?.statuses?.length) { 122 + const value = searchResults.statuses.slice(0, LIMIT); 123 + value.forEach((item) => { 124 + saveStatus(item, instance); 125 + }); 126 + const done = searchResults.statuses.length <= LIMIT; 127 + return { value, done }; 128 + } else { 129 + return { value: [], done: true }; 130 + } 131 + } 132 + 31 133 const results = []; 32 134 if (firstLoad) { 33 135 const { value: pinnedStatuses } = await masto.v1.accounts ··· 78 180 }; 79 181 } 80 182 81 - const [account, setAccount] = useState(); 82 183 const [featuredTags, setFeaturedTags] = useState([]); 83 184 useTitle( 84 185 `${account?.displayName ? account.displayName + ' ' : ''}@${ ··· 112 213 const filterBarRef = useRef(); 113 214 const TimelineStart = useMemo(() => { 114 215 const cachedAccount = snapStates.accounts[`${id}@${instance}`]; 115 - const filtered = !excludeReplies || excludeBoosts || tagged || media; 216 + const filtered = 217 + !excludeReplies || excludeBoosts || tagged || media || !!month; 116 218 return ( 117 219 <> 118 220 <AccountInfo ··· 170 272 </Link> 171 273 {featuredTags.map((tag) => ( 172 274 <Link 275 + key={tag.id} 173 276 to={`/${instance}/a/${id}${ 174 277 tagged === tag.name 175 278 ? '' ··· 192 295 {/* <span class="filter-count">{tag.statusesCount}</span> */} 193 296 </Link> 194 297 ))} 298 + {searchEnabled && 299 + (supportsInputMonth ? ( 300 + <input 301 + type="month" 302 + class={`filter-field ${month ? 'is-active' : ''}`} 303 + disabled={!account?.acct} 304 + value={month || ''} 305 + min="1983-01" // Birth of the Internet 306 + max={new Date().toISOString().slice(0, 7)} 307 + onInput={(e) => { 308 + const { value } = e.currentTarget; 309 + setSearchParams( 310 + value 311 + ? { 312 + month: value, 313 + } 314 + : {}, 315 + ); 316 + }} 317 + /> 318 + ) : ( 319 + // Fallback to <select> for month and <input type="number"> for year 320 + <MonthPicker 321 + class={`filter-field ${month ? 'is-active' : ''}`} 322 + disabled={!account?.acct} 323 + value={month || ''} 324 + min="1983-01" // Birth of the Internet 325 + max={new Date().toISOString().slice(0, 7)} 326 + onInput={(e) => { 327 + const { value } = e; 328 + setSearchParams( 329 + value 330 + ? { 331 + month: value, 332 + } 333 + : {}, 334 + ); 335 + }} 336 + /> 337 + ))} 195 338 </div> 196 339 </> 197 340 ); ··· 199 342 id, 200 343 instance, 201 344 authenticated, 202 - excludeReplies, 203 - excludeBoosts, 204 345 featuredTags, 205 - tagged, 206 - media, 346 + searchEnabled, 347 + ...allSearchParams, 207 348 ]); 208 349 209 350 useEffect(() => { ··· 258 399 useItemID 259 400 boostsCarousel={snapStates.settings.boostsCarousel} 260 401 timelineStart={TimelineStart} 261 - refresh={[excludeReplies, excludeBoosts, tagged, media].toString()} 402 + refresh={[ 403 + excludeReplies, 404 + excludeBoosts, 405 + tagged, 406 + media, 407 + month + account?.acct, 408 + ].toString()} 262 409 headerEnd={ 263 410 <Menu2 264 411 portal ··· 300 447 </Menu2> 301 448 } 302 449 /> 450 + ); 451 + } 452 + 453 + function MonthPicker(props) { 454 + const { 455 + class: className, 456 + disabled, 457 + value, 458 + min, 459 + max, 460 + onInput = () => {}, 461 + } = props; 462 + const [_year, _month] = value?.split('-') || []; 463 + const monthFieldRef = useRef(); 464 + const yearFieldRef = useRef(); 465 + 466 + return ( 467 + <div class={className}> 468 + <select 469 + ref={monthFieldRef} 470 + disabled={disabled} 471 + value={_month || ''} 472 + onInput={(e) => { 473 + const { value } = e.currentTarget; 474 + onInput({ 475 + value: value ? `${yearFieldRef.current.value}-${value}` : '', 476 + }); 477 + }} 478 + > 479 + <option value="">Month</option> 480 + <option disabled>-----</option> 481 + {Array.from({ length: 12 }, (_, i) => ( 482 + <option 483 + value={ 484 + // Month is 1-indexed 485 + (i + 1).toString().padStart(2, '0') 486 + } 487 + key={i} 488 + > 489 + {new Date(0, i).toLocaleString('default', { 490 + month: 'long', 491 + })} 492 + </option> 493 + ))} 494 + </select>{' '} 495 + <input 496 + ref={yearFieldRef} 497 + type="number" 498 + disabled={disabled} 499 + value={_year || new Date().getFullYear()} 500 + min={min?.slice(0, 4) || '1983'} 501 + max={max?.slice(0, 4) || new Date().getFullYear()} 502 + onInput={(e) => { 503 + const { value } = e.currentTarget; 504 + onInput({ 505 + value: value ? `${value}-${monthFieldRef.current.value}` : '', 506 + }); 507 + }} 508 + style={{ 509 + width: '4.5em', 510 + }} 511 + /> 512 + </div> 303 513 ); 304 514 } 305 515