diff --git a/src/app.css b/src/app.css index 7d4c4c8e..292ff984 100644 --- a/src/app.css +++ b/src/app.css @@ -2127,6 +2127,48 @@ ul.link-list li a .icon { pointer-events: none; opacity: 0.5; } + + .filter-field { + flex-shrink: 0; + padding: 8px 16px; + border-radius: 999px; + color: var(--text-color); + background-color: var(--bg-color); + border: 2px solid transparent; + margin: 0; + appearance: none; + line-height: 1; + font-size: 90%; + + &:placeholder-shown { + color: var(--text-insignificant-color); + } + + &:is(:hover, :focus-visible) { + border-color: var(--link-light-color); + } + &:focus { + outline-color: var(--link-light-color); + } + &.is-active { + border-color: var(--link-color); + box-shadow: inset 0 0 8px var(--link-faded-color); + } + + :is(input, select) { + background-color: transparent; + border: 0; + padding: 0; + margin: 0; + color: inherit; + font-size: inherit; + line-height: inherit; + appearance: none; + border-radius: 0; + box-shadow: none; + outline: none; + } + } } .filter-bar.centered { justify-content: center; diff --git a/src/pages/account-statuses.jsx b/src/pages/account-statuses.jsx index 34704188..dfd94df5 100644 --- a/src/pages/account-statuses.jsx +++ b/src/pages/account-statuses.jsx @@ -17,17 +17,119 @@ import useTitle from '../utils/useTitle'; const LIMIT = 20; +const supportsInputMonth = (() => { + try { + const input = document.createElement('input'); + input.setAttribute('type', 'month'); + return input.type === 'month'; + } catch (e) { + return false; + } +})(); + function AccountStatuses() { const snapStates = useSnapshot(states); const { id, ...params } = useParams(); const [searchParams, setSearchParams] = useSearchParams(); + const month = searchParams.get('month'); const excludeReplies = !searchParams.get('replies'); const excludeBoosts = !!searchParams.get('boosts'); const tagged = searchParams.get('tagged'); const media = !!searchParams.get('media'); const { masto, instance, authenticated } = api({ instance: params.instance }); const accountStatusesIterator = useRef(); + + const allSearchParams = [month, excludeReplies, excludeBoosts, tagged, media]; + const [account, setAccount] = useState(); + const searchOffsetRef = useRef(0); + useEffect(() => { + searchOffsetRef.current = 0; + }, allSearchParams); + + const sameCurrentInstance = useMemo( + () => instance === api().instance, + [instance], + ); + const [searchEnabled, setSearchEnabled] = useState(false); + useEffect(() => { + // Only enable for current logged-in instance + // Most remote instances don't allow unauthenticated searches + if (!sameCurrentInstance) return; + if (!account?.acct) return; + (async () => { + const results = await masto.v2.search.fetch({ + q: `from:${account?.acct}`, + type: 'statuses', + limit: 1, + }); + setSearchEnabled(!!results?.statuses?.length); + })(); + }, [sameCurrentInstance, account?.acct]); + async function fetchAccountStatuses(firstLoad) { + if (/^\d{4}-[01]\d$/.test(month)) { + if (!account) { + return { + value: [], + done: true, + }; + } + const [_year, _month] = month.split('-'); + const monthIndex = parseInt(_month, 10) - 1; + // YYYY-MM (no day) + // Search options: + // - from:account + // - after:YYYY-MM-DD (non-inclusive) + // - before:YYYY-MM-DD (non-inclusive) + + // Last day of previous month + const after = new Date(_year, monthIndex, 0); + const afterStr = `${after.getFullYear()}-${(after.getMonth() + 1) + .toString() + .padStart(2, '0')}-${after.getDate().toString().padStart(2, '0')}`; + // First day of next month + const before = new Date(_year, monthIndex + 1, 1); + const beforeStr = `${before.getFullYear()}-${(before.getMonth() + 1) + .toString() + .padStart(2, '0')}-${before.getDate().toString().padStart(2, '0')}`; + console.log({ + month, + _year, + _month, + monthIndex, + after, + before, + afterStr, + beforeStr, + }); + + let limit; + if (firstLoad) { + limit = LIMIT + 1; + searchOffsetRef.current = 0; + } else { + limit = LIMIT + searchOffsetRef.current + 1; + searchOffsetRef.current += LIMIT; + } + + const searchResults = await masto.v2.search.fetch({ + q: `from:${account.acct} after:${afterStr} before:${beforeStr}`, + type: 'statuses', + limit, + offset: searchOffsetRef.current, + }); + if (searchResults?.statuses?.length) { + const value = searchResults.statuses.slice(0, LIMIT); + value.forEach((item) => { + saveStatus(item, instance); + }); + const done = searchResults.statuses.length <= LIMIT; + return { value, done }; + } else { + return { value: [], done: true }; + } + } + const results = []; if (firstLoad) { const { value: pinnedStatuses } = await masto.v1.accounts @@ -78,7 +180,6 @@ function AccountStatuses() { }; } - const [account, setAccount] = useState(); const [featuredTags, setFeaturedTags] = useState([]); useTitle( `${account?.displayName ? account.displayName + ' ' : ''}@${ @@ -112,7 +213,8 @@ function AccountStatuses() { const filterBarRef = useRef(); const TimelineStart = useMemo(() => { const cachedAccount = snapStates.accounts[`${id}@${instance}`]; - const filtered = !excludeReplies || excludeBoosts || tagged || media; + const filtered = + !excludeReplies || excludeBoosts || tagged || media || !!month; return ( <> {featuredTags.map((tag) => ( {tag.statusesCount} */} ))} + {searchEnabled && + (supportsInputMonth ? ( + { + const { value } = e.currentTarget; + setSearchParams( + value + ? { + month: value, + } + : {}, + ); + }} + /> + ) : ( + // Fallback to for year + { + const { value } = e; + setSearchParams( + value + ? { + month: value, + } + : {}, + ); + }} + /> + ))} ); @@ -199,11 +342,9 @@ function AccountStatuses() { id, instance, authenticated, - excludeReplies, - excludeBoosts, featuredTags, - tagged, - media, + searchEnabled, + ...allSearchParams, ]); useEffect(() => { @@ -258,7 +399,13 @@ function AccountStatuses() { useItemID boostsCarousel={snapStates.settings.boostsCarousel} timelineStart={TimelineStart} - refresh={[excludeReplies, excludeBoosts, tagged, media].toString()} + refresh={[ + excludeReplies, + excludeBoosts, + tagged, + media, + month + account?.acct, + ].toString()} headerEnd={ {}, + } = props; + const [_year, _month] = value?.split('-') || []; + const monthFieldRef = useRef(); + const yearFieldRef = useRef(); + + return ( +
+ {' '} + { + const { value } = e.currentTarget; + onInput({ + value: value ? `${value}-${monthFieldRef.current.value}` : '', + }); + }} + style={{ + width: '4.5em', + }} + /> +
+ ); +} + export default AccountStatuses;