Experiment: month filter for account statuses
This commit is contained in:
parent
d1aedcaef2
commit
ab7df0f66c
2 changed files with 259 additions and 7 deletions
42
src/app.css
42
src/app.css
|
@ -2127,6 +2127,48 @@ ul.link-list li a .icon {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
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 {
|
.filter-bar.centered {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -17,17 +17,119 @@ import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
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() {
|
function AccountStatuses() {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const { id, ...params } = useParams();
|
const { id, ...params } = useParams();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const month = searchParams.get('month');
|
||||||
const excludeReplies = !searchParams.get('replies');
|
const excludeReplies = !searchParams.get('replies');
|
||||||
const excludeBoosts = !!searchParams.get('boosts');
|
const excludeBoosts = !!searchParams.get('boosts');
|
||||||
const tagged = searchParams.get('tagged');
|
const tagged = searchParams.get('tagged');
|
||||||
const media = !!searchParams.get('media');
|
const media = !!searchParams.get('media');
|
||||||
const { masto, instance, authenticated } = api({ instance: params.instance });
|
const { masto, instance, authenticated } = api({ instance: params.instance });
|
||||||
const accountStatusesIterator = useRef();
|
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) {
|
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 = [];
|
const results = [];
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
const { value: pinnedStatuses } = await masto.v1.accounts
|
const { value: pinnedStatuses } = await masto.v1.accounts
|
||||||
|
@ -78,7 +180,6 @@ function AccountStatuses() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const [account, setAccount] = useState();
|
|
||||||
const [featuredTags, setFeaturedTags] = useState([]);
|
const [featuredTags, setFeaturedTags] = useState([]);
|
||||||
useTitle(
|
useTitle(
|
||||||
`${account?.displayName ? account.displayName + ' ' : ''}@${
|
`${account?.displayName ? account.displayName + ' ' : ''}@${
|
||||||
|
@ -112,7 +213,8 @@ function AccountStatuses() {
|
||||||
const filterBarRef = useRef();
|
const filterBarRef = useRef();
|
||||||
const TimelineStart = useMemo(() => {
|
const TimelineStart = useMemo(() => {
|
||||||
const cachedAccount = snapStates.accounts[`${id}@${instance}`];
|
const cachedAccount = snapStates.accounts[`${id}@${instance}`];
|
||||||
const filtered = !excludeReplies || excludeBoosts || tagged || media;
|
const filtered =
|
||||||
|
!excludeReplies || excludeBoosts || tagged || media || !!month;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AccountInfo
|
<AccountInfo
|
||||||
|
@ -170,6 +272,7 @@ function AccountStatuses() {
|
||||||
</Link>
|
</Link>
|
||||||
{featuredTags.map((tag) => (
|
{featuredTags.map((tag) => (
|
||||||
<Link
|
<Link
|
||||||
|
key={tag.id}
|
||||||
to={`/${instance}/a/${id}${
|
to={`/${instance}/a/${id}${
|
||||||
tagged === tag.name
|
tagged === tag.name
|
||||||
? ''
|
? ''
|
||||||
|
@ -192,6 +295,46 @@ function AccountStatuses() {
|
||||||
{/* <span class="filter-count">{tag.statusesCount}</span> */}
|
{/* <span class="filter-count">{tag.statusesCount}</span> */}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
{searchEnabled &&
|
||||||
|
(supportsInputMonth ? (
|
||||||
|
<input
|
||||||
|
type="month"
|
||||||
|
class={`filter-field ${month ? 'is-active' : ''}`}
|
||||||
|
disabled={!account?.acct}
|
||||||
|
value={month || ''}
|
||||||
|
min="1983-01" // Birth of the Internet
|
||||||
|
max={new Date().toISOString().slice(0, 7)}
|
||||||
|
onInput={(e) => {
|
||||||
|
const { value } = e.currentTarget;
|
||||||
|
setSearchParams(
|
||||||
|
value
|
||||||
|
? {
|
||||||
|
month: value,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
// Fallback to <select> for month and <input type="number"> for year
|
||||||
|
<MonthPicker
|
||||||
|
class={`filter-field ${month ? 'is-active' : ''}`}
|
||||||
|
disabled={!account?.acct}
|
||||||
|
value={month || ''}
|
||||||
|
min="1983-01" // Birth of the Internet
|
||||||
|
max={new Date().toISOString().slice(0, 7)}
|
||||||
|
onInput={(e) => {
|
||||||
|
const { value } = e;
|
||||||
|
setSearchParams(
|
||||||
|
value
|
||||||
|
? {
|
||||||
|
month: value,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -199,11 +342,9 @@ function AccountStatuses() {
|
||||||
id,
|
id,
|
||||||
instance,
|
instance,
|
||||||
authenticated,
|
authenticated,
|
||||||
excludeReplies,
|
|
||||||
excludeBoosts,
|
|
||||||
featuredTags,
|
featuredTags,
|
||||||
tagged,
|
searchEnabled,
|
||||||
media,
|
...allSearchParams,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -258,7 +399,13 @@ function AccountStatuses() {
|
||||||
useItemID
|
useItemID
|
||||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||||
timelineStart={TimelineStart}
|
timelineStart={TimelineStart}
|
||||||
refresh={[excludeReplies, excludeBoosts, tagged, media].toString()}
|
refresh={[
|
||||||
|
excludeReplies,
|
||||||
|
excludeBoosts,
|
||||||
|
tagged,
|
||||||
|
media,
|
||||||
|
month + account?.acct,
|
||||||
|
].toString()}
|
||||||
headerEnd={
|
headerEnd={
|
||||||
<Menu2
|
<Menu2
|
||||||
portal
|
portal
|
||||||
|
@ -303,4 +450,67 @@ function AccountStatuses() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MonthPicker(props) {
|
||||||
|
const {
|
||||||
|
class: className,
|
||||||
|
disabled,
|
||||||
|
value,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
onInput = () => {},
|
||||||
|
} = props;
|
||||||
|
const [_year, _month] = value?.split('-') || [];
|
||||||
|
const monthFieldRef = useRef();
|
||||||
|
const yearFieldRef = useRef();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={className}>
|
||||||
|
<select
|
||||||
|
ref={monthFieldRef}
|
||||||
|
disabled={disabled}
|
||||||
|
value={_month || ''}
|
||||||
|
onInput={(e) => {
|
||||||
|
const { value } = e.currentTarget;
|
||||||
|
onInput({
|
||||||
|
value: value ? `${yearFieldRef.current.value}-${value}` : '',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">Month</option>
|
||||||
|
<option disabled>-----</option>
|
||||||
|
{Array.from({ length: 12 }, (_, i) => (
|
||||||
|
<option
|
||||||
|
value={
|
||||||
|
// Month is 1-indexed
|
||||||
|
(i + 1).toString().padStart(2, '0')
|
||||||
|
}
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
{new Date(0, i).toLocaleString('default', {
|
||||||
|
month: 'long',
|
||||||
|
})}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>{' '}
|
||||||
|
<input
|
||||||
|
ref={yearFieldRef}
|
||||||
|
type="number"
|
||||||
|
disabled={disabled}
|
||||||
|
value={_year || new Date().getFullYear()}
|
||||||
|
min={min?.slice(0, 4) || '1983'}
|
||||||
|
max={max?.slice(0, 4) || new Date().getFullYear()}
|
||||||
|
onInput={(e) => {
|
||||||
|
const { value } = e.currentTarget;
|
||||||
|
onInput({
|
||||||
|
value: value ? `${value}-${monthFieldRef.current.value}` : '',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: '4.5em',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default AccountStatuses;
|
export default AccountStatuses;
|
||||||
|
|
Loading…
Add table
Reference in a new issue