mirror of
https://github.com/mastodon/mastodon.git
synced 2026-05-23 01:26:36 -05:00
Allow users to change how accounts are sorted when viewing a collection (#39073)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
Chromatic / Check for relevant changes (push) Waiting to run
Chromatic / Run Chromatic (push) Blocked by required conditions
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / test (3.4) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.4) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.29) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.19.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.29) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.4, docker.elastic.co/elasticsearch/elasticsearch:7.17.29) (push) Blocked by required conditions
Crowdin / Upload translations / upload-translations (push) Has been cancelled
CSS Linting / lint (push) Has been cancelled
Some checks failed
Check i18n / check-i18n (push) Waiting to run
Chromatic / Check for relevant changes (push) Waiting to run
Chromatic / Run Chromatic (push) Blocked by required conditions
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / test (3.4) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.4) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.29) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.19.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.29) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.4, docker.elastic.co/elasticsearch/elasticsearch:7.17.29) (push) Blocked by required conditions
Crowdin / Upload translations / upload-translations (push) Has been cancelled
CSS Linting / lint (push) Has been cancelled
This commit is contained in:
parent
07a05e1edf
commit
c26003af21
|
|
@ -48,10 +48,14 @@
|
|||
|
||||
.status {
|
||||
// If there's no content, we need to compensate for the parent's
|
||||
// flex gap to avoid extra spacing below the field.
|
||||
// flex gap to avoid extra spacing below or next to the field.
|
||||
&:empty {
|
||||
margin-top: calc(-1 * var(--form-field-label-gap));
|
||||
}
|
||||
|
||||
[data-input-placement^='inline'] &:empty {
|
||||
margin-inline-start: calc(-1 * var(--form-field-label-gap));
|
||||
}
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export interface FieldStatus {
|
|||
message?: string;
|
||||
}
|
||||
|
||||
interface FieldWrapperProps {
|
||||
export interface FieldWrapperProps {
|
||||
label: ReactNode;
|
||||
hint?: ReactNode;
|
||||
required?: boolean;
|
||||
|
|
|
|||
|
|
@ -4,11 +4,17 @@ import { forwardRef } from 'react';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { FormFieldWrapper } from './form_field_wrapper';
|
||||
import type { CommonFieldWrapperProps } from './form_field_wrapper';
|
||||
import type {
|
||||
CommonFieldWrapperProps,
|
||||
FieldWrapperProps,
|
||||
} from './form_field_wrapper';
|
||||
import classes from './select.module.scss';
|
||||
|
||||
interface Props
|
||||
extends ComponentPropsWithoutRef<'select'>, CommonFieldWrapperProps {}
|
||||
extends
|
||||
ComponentPropsWithoutRef<'select'>,
|
||||
CommonFieldWrapperProps,
|
||||
Pick<FieldWrapperProps, 'inputPlacement'> {}
|
||||
|
||||
/**
|
||||
* A simple form field for single-item selections.
|
||||
|
|
@ -19,13 +25,28 @@ interface Props
|
|||
*/
|
||||
|
||||
export const SelectField = forwardRef<HTMLSelectElement, Props>(
|
||||
({ id, label, hint, required, status, children, ...otherProps }, ref) => (
|
||||
(
|
||||
{
|
||||
id,
|
||||
label,
|
||||
hint,
|
||||
required,
|
||||
status,
|
||||
inputPlacement,
|
||||
children,
|
||||
wrapperClassName,
|
||||
...otherProps
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<FormFieldWrapper
|
||||
label={label}
|
||||
hint={hint}
|
||||
required={required}
|
||||
status={status}
|
||||
inputId={id}
|
||||
inputPlacement={inputPlacement}
|
||||
className={wrapperClassName}
|
||||
>
|
||||
{(inputProps) => (
|
||||
<Select {...otherProps} {...inputProps} ref={ref}>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { useCallback, useMemo, useRef } from 'react';
|
|||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { PendingBadge } from '@/mastodon/components/badge';
|
||||
import { SelectField } from '@/mastodon/components/form_fields';
|
||||
import { useSearchParam } from '@/mastodon/hooks/useSearchParam';
|
||||
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
|
||||
import type {
|
||||
ApiCollectionJSON,
|
||||
|
|
@ -53,6 +55,43 @@ const getCollectionItems = createAppSelector(
|
|||
),
|
||||
);
|
||||
|
||||
function sortAccounts(
|
||||
accounts: CollectionItemWithAccount[],
|
||||
sortBy?: string,
|
||||
): CollectionItemWithAccount[] {
|
||||
if (!sortBy || sortBy === 'date_added') {
|
||||
return accounts;
|
||||
}
|
||||
|
||||
const sorted = [...accounts];
|
||||
|
||||
switch (sortBy) {
|
||||
case 'alphabetical':
|
||||
return sorted.sort((a, b) => {
|
||||
const nameA = a.account?.display_name ?? '';
|
||||
const nameB = b.account?.display_name ?? '';
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
|
||||
case 'last_active':
|
||||
return sorted.sort((a, b) => {
|
||||
const dateA = a.account?.last_status_at ?? '';
|
||||
const dateB = b.account?.last_status_at ?? '';
|
||||
return new Date(dateB).getTime() - new Date(dateA).getTime();
|
||||
});
|
||||
|
||||
case 'most_followers':
|
||||
return sorted.sort((a, b) => {
|
||||
const followersA = a.account?.followers_count ?? 0;
|
||||
const followersB = b.account?.followers_count ?? 0;
|
||||
return followersB - followersA;
|
||||
});
|
||||
|
||||
default:
|
||||
return accounts;
|
||||
}
|
||||
}
|
||||
|
||||
export const CollectionAccountsList: React.FC<{
|
||||
collection: ApiCollectionJSON;
|
||||
}> = ({ collection }) => {
|
||||
|
|
@ -68,11 +107,20 @@ export const CollectionAccountsList: React.FC<{
|
|||
getCollectionItems(state, id),
|
||||
);
|
||||
|
||||
const [sortBy, setSortBy] = useSearchParam('sort', 'date_added');
|
||||
const changeSortBy = useCallback(
|
||||
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSortBy(event.target.value);
|
||||
},
|
||||
[setSortBy],
|
||||
);
|
||||
const sortedAccounts = sortAccounts(collectionAccounts, sortBy);
|
||||
|
||||
const { visibleAccounts, hiddenAccounts } = useMemo(() => {
|
||||
const visibleAccounts: CollectionItemWithAccount[] = [];
|
||||
const hiddenAccounts: CollectionItemWithAccount[] = [];
|
||||
|
||||
collectionAccounts.forEach((item) => {
|
||||
sortedAccounts.forEach((item) => {
|
||||
const { account, account_id } = item;
|
||||
|
||||
if (!isOwnCollection && !account) {
|
||||
|
|
@ -89,7 +137,7 @@ export const CollectionAccountsList: React.FC<{
|
|||
});
|
||||
|
||||
return { visibleAccounts, hiddenAccounts };
|
||||
}, [collectionAccounts, isOwnCollection, relationships]);
|
||||
}, [sortedAccounts, isOwnCollection, relationships]);
|
||||
|
||||
const renderAccountItemButton = useCallback(
|
||||
({ relationship, accountId }: RenderButtonOptions) => {
|
||||
|
|
@ -147,17 +195,57 @@ export const CollectionAccountsList: React.FC<{
|
|||
|
||||
return (
|
||||
<>
|
||||
<h3
|
||||
className={classes.columnSubheading}
|
||||
tabIndex={-1}
|
||||
ref={listHeadingRef}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='collections.account_count'
|
||||
defaultMessage='{count, plural, one {# account} other {# accounts}}'
|
||||
values={{ count: collection.item_count }}
|
||||
/>
|
||||
</h3>
|
||||
<div className={classes.subheadingWithSelect}>
|
||||
<h3
|
||||
className={classes.columnSubheading}
|
||||
tabIndex={-1}
|
||||
ref={listHeadingRef}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='collections.account_count'
|
||||
defaultMessage='{count, plural, one {# account} other {# accounts}}'
|
||||
values={{ count: collection.item_count }}
|
||||
/>
|
||||
</h3>
|
||||
<SelectField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.sort_by'
|
||||
defaultMessage='Sort by:'
|
||||
/>
|
||||
}
|
||||
value={sortBy}
|
||||
onChange={changeSortBy}
|
||||
inputPlacement='inline-end'
|
||||
className={classes.select}
|
||||
wrapperClassName={classes.selectWrapper}
|
||||
>
|
||||
<option value='alphabetical'>
|
||||
<FormattedMessage
|
||||
id='collections.sort_alphabetical'
|
||||
defaultMessage='Alphabetical'
|
||||
/>
|
||||
</option>
|
||||
<option value='last_active'>
|
||||
<FormattedMessage
|
||||
id='collections.sort_last_active'
|
||||
defaultMessage='Last active'
|
||||
/>
|
||||
</option>
|
||||
<option value='most_followers'>
|
||||
<FormattedMessage
|
||||
id='collections.sort_most_followers'
|
||||
defaultMessage='Most followers'
|
||||
/>
|
||||
</option>
|
||||
<option value='date_added'>
|
||||
<FormattedMessage
|
||||
id='collections.sort_date_added'
|
||||
defaultMessage='Date added'
|
||||
/>
|
||||
</option>
|
||||
</SelectField>
|
||||
</div>
|
||||
<ItemList emptyMessage={intl.formatMessage(messages.empty)}>
|
||||
<TruncatedListItems
|
||||
visibleItems={visibleAccounts}
|
||||
|
|
|
|||
|
|
@ -67,8 +67,27 @@
|
|||
margin-inline: 16px;
|
||||
}
|
||||
|
||||
.columnSubheading {
|
||||
.subheadingWithSelect {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 16px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.selectWrapper {
|
||||
// Align to right edge even when wrapped to new line
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.select {
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
background-color: var(--color-bg-primary);
|
||||
}
|
||||
|
||||
.columnSubheading {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -425,6 +425,11 @@
|
|||
"collections.search_accounts_max_reached": "You have added the maximum number of accounts",
|
||||
"collections.sensitive": "Sensitive",
|
||||
"collections.share_short": "Share",
|
||||
"collections.sort_alphabetical": "Alphabetical",
|
||||
"collections.sort_by": "Sort by:",
|
||||
"collections.sort_date_added": "Date added",
|
||||
"collections.sort_last_active": "Last active",
|
||||
"collections.sort_most_followers": "Most followers",
|
||||
"collections.suggestions.can_not_add": "Can’t be added",
|
||||
"collections.suggestions.can_not_add_desc": "These accounts may have opted out of discovery, or they might be on a server that doesn’t support collections.",
|
||||
"collections.suggestions.must_follow": "Must follow first",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user