Merge commit 'd20d04922672ed016029c40ed64798f341ebca91' into glitch-soc/merge-upstream
This commit is contained in:
commit
06176deee2
@ -16,6 +16,7 @@ class Api::V1::Admin::ReportsController < Api::BaseController
|
|||||||
|
|
||||||
FILTER_PARAMS = %i(
|
FILTER_PARAMS = %i(
|
||||||
resolved
|
resolved
|
||||||
|
unresolved
|
||||||
account_id
|
account_id
|
||||||
target_account_id
|
target_account_id
|
||||||
).freeze
|
).freeze
|
||||||
|
|||||||
@ -329,7 +329,7 @@ export function uploadCompose(files) {
|
|||||||
dispatch(showAlert({ message: messages.uploadQuote }));
|
dispatch(showAlert({ message: messages.uploadQuote }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']);
|
const uploadLimit = getState().getIn(['server', 'server', 'item', 'configuration', 'statuses', 'max_media_attachments']);
|
||||||
const media = getState().getIn(['compose', 'media_attachments']);
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
||||||
const progress = new Array(files.length).fill(0);
|
const progress = new Array(files.length).fill(0);
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji';
|
import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji';
|
||||||
import { loadCustomEmoji } from '@/mastodon/features/emoji';
|
import { loadCustomEmoji } from '@/mastodon/features/emoji';
|
||||||
|
import { emojiLogger } from '@/mastodon/features/emoji/utils';
|
||||||
|
|
||||||
|
const log = emojiLogger('actions');
|
||||||
|
|
||||||
export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) {
|
export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) {
|
||||||
if (emojis.length === 0) {
|
if (emojis.length === 0) {
|
||||||
@ -18,5 +21,11 @@ export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) {
|
|||||||
if (existingEmojis.length < emojis.length) {
|
if (existingEmojis.length < emojis.length) {
|
||||||
await clearCache('custom');
|
await clearCache('custom');
|
||||||
await loadCustomEmoji();
|
await loadCustomEmoji();
|
||||||
|
|
||||||
|
const { reloadCustomEmojis } =
|
||||||
|
await import('@/mastodon/features/emoji/picker');
|
||||||
|
await reloadCustomEmojis();
|
||||||
|
|
||||||
|
log('Custom emojis updated, reloaded cache and picker data.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,19 +16,33 @@ export const fetchServer = createDataLoadingThunk(
|
|||||||
dispatch(importFetchedAccount(instance.contact.account));
|
dispatch(importFetchedAccount(instance.contact.account));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
condition: (_, { getState }) => !getState().server.server.isLoading,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fetchExtendedDescription = createDataLoadingThunk(
|
export const fetchExtendedDescription = createDataLoadingThunk(
|
||||||
'server/extended_description',
|
'server/extended_description',
|
||||||
() => apiGetExtendedDescription(),
|
() => apiGetExtendedDescription(),
|
||||||
|
{
|
||||||
|
condition: (_, { getState }) =>
|
||||||
|
!getState().server.extendedDescription.isLoading,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fetchServerTranslationLanguages = createDataLoadingThunk(
|
export const fetchServerTranslationLanguages = createDataLoadingThunk(
|
||||||
'server/translation_languages',
|
'server/translation_languages',
|
||||||
() => apiGetTranslationLanguages(),
|
() => apiGetTranslationLanguages(),
|
||||||
|
{
|
||||||
|
condition: (_, { getState }) =>
|
||||||
|
!getState().server.translationLanguages.isLoading,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fetchDomainBlocks = createDataLoadingThunk(
|
export const fetchDomainBlocks = createDataLoadingThunk(
|
||||||
'server/domain_blocks',
|
'server/domain_blocks',
|
||||||
() => apiGetDomainBlocks(),
|
() => apiGetDomainBlocks(),
|
||||||
|
{
|
||||||
|
condition: (_, { getState }) => !getState().server.domainBlocks.isLoading,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -480,6 +480,7 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bannerBaseCentered {
|
.bannerBaseCentered {
|
||||||
|
|||||||
@ -144,11 +144,16 @@ export const HoverCardController: React.FC = () => {
|
|||||||
setScrollTimeout(handleScrollEnd, 100);
|
setScrollTimeout(handleScrollEnd, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseMove = () => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
if (isUsingTouch) {
|
if (isUsingTouch) {
|
||||||
isUsingTouch = false;
|
isUsingTouch = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasMoved = Math.max(e.movementX, e.movementY) > 0;
|
||||||
|
if (!hasMoved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
delayEnterTimeout(enterDelay);
|
delayEnterTimeout(enterDelay);
|
||||||
|
|
||||||
cancelMoveTimeout();
|
cancelMoveTimeout();
|
||||||
|
|||||||
@ -285,7 +285,7 @@ class ScrollableList extends PureComponent {
|
|||||||
if (this.props.bindToDocument) {
|
if (this.props.bindToDocument) {
|
||||||
document.removeEventListener('scroll', this.handleScroll);
|
document.removeEventListener('scroll', this.handleScroll);
|
||||||
document.removeEventListener('wheel', this.handleWheel, listenerOptions);
|
document.removeEventListener('wheel', this.handleWheel, listenerOptions);
|
||||||
} else {
|
} else if (this.node) {
|
||||||
this.node.removeEventListener('scroll', this.handleScroll);
|
this.node.removeEventListener('scroll', this.handleScroll);
|
||||||
this.node.removeEventListener('wheel', this.handleWheel, listenerOptions);
|
this.node.removeEventListener('wheel', this.handleWheel, listenerOptions);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,8 @@ export interface StatusHeaderProps {
|
|||||||
status: Status;
|
status: Status;
|
||||||
account?: Account;
|
account?: Account;
|
||||||
avatarSize?: number;
|
avatarSize?: number;
|
||||||
children?: ReactNode;
|
contentBeforeDate?: ReactNode;
|
||||||
|
contentAfterDate?: ReactNode;
|
||||||
wrapperProps?: HTMLAttributes<HTMLDivElement>;
|
wrapperProps?: HTMLAttributes<HTMLDivElement>;
|
||||||
displayNameProps?: DisplayNameProps;
|
displayNameProps?: DisplayNameProps;
|
||||||
onHeaderClick?: MouseEventHandler<HTMLDivElement>;
|
onHeaderClick?: MouseEventHandler<HTMLDivElement>;
|
||||||
@ -33,10 +34,11 @@ export type StatusHeaderRenderFn = (args: StatusHeaderProps) => ReactNode;
|
|||||||
export const StatusHeader: FC<StatusHeaderProps> = ({
|
export const StatusHeader: FC<StatusHeaderProps> = ({
|
||||||
status,
|
status,
|
||||||
account,
|
account,
|
||||||
children,
|
|
||||||
className,
|
className,
|
||||||
avatarSize = 48,
|
avatarSize = 48,
|
||||||
wrapperProps,
|
wrapperProps,
|
||||||
|
contentBeforeDate,
|
||||||
|
contentAfterDate,
|
||||||
onHeaderClick,
|
onHeaderClick,
|
||||||
}) => {
|
}) => {
|
||||||
const statusAccount = status.get('account') as Account | undefined;
|
const statusAccount = status.get('account') as Account | undefined;
|
||||||
@ -51,6 +53,14 @@ export const StatusHeader: FC<StatusHeaderProps> = ({
|
|||||||
className={classNames('status__info', className)}
|
className={classNames('status__info', className)}
|
||||||
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||||
>
|
>
|
||||||
|
<StatusDisplayName
|
||||||
|
statusAccount={statusAccount}
|
||||||
|
friendAccount={account}
|
||||||
|
avatarSize={avatarSize}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{contentBeforeDate}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to={`/@${statusAccount?.acct}/${status.get('id') as string}`}
|
to={`/@${statusAccount?.acct}/${status.get('id') as string}`}
|
||||||
className='status__relative-time'
|
className='status__relative-time'
|
||||||
@ -60,13 +70,7 @@ export const StatusHeader: FC<StatusHeaderProps> = ({
|
|||||||
{editedAt && <StatusEditedAt editedAt={editedAt} />}
|
{editedAt && <StatusEditedAt editedAt={editedAt} />}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<StatusDisplayName
|
{contentAfterDate}
|
||||||
statusAccount={statusAccount}
|
|
||||||
friendAccount={account}
|
|
||||||
avatarSize={avatarSize}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -69,7 +69,7 @@ class TranslateButton extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
languages: state.server.translationLanguages.items,
|
languages: state.server.translationLanguages.item,
|
||||||
});
|
});
|
||||||
|
|
||||||
class StatusContent extends PureComponent {
|
class StatusContent extends PureComponent {
|
||||||
@ -187,7 +187,7 @@ class StatusContent extends PureComponent {
|
|||||||
|
|
||||||
const renderReadMore = this.props.onClick && status.get('collapsed');
|
const renderReadMore = this.props.onClick && status.get('collapsed');
|
||||||
const contentLocale = intl.locale.replace(/[_-].*/, '');
|
const contentLocale = intl.locale.replace(/[_-].*/, '');
|
||||||
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
|
const targetLanguages = this.props.languages?.[status.get('language') || 'und'];
|
||||||
const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
|
const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
|
||||||
|
|
||||||
const content = statusContent ?? getStatusContent(status);
|
const content = statusContent ?? getStatusContent(status);
|
||||||
|
|||||||
@ -225,17 +225,20 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const headerRenderFn: StatusHeaderRenderFn = useCallback(
|
const headerRenderFn: StatusHeaderRenderFn = useCallback(
|
||||||
(props) => (
|
(props) => (
|
||||||
<StatusHeader {...props}>
|
<StatusHeader
|
||||||
{onQuoteCancel && (
|
{...props}
|
||||||
<IconButton
|
contentAfterDate={
|
||||||
onClick={onQuoteCancel}
|
onQuoteCancel && (
|
||||||
className='status__quote-cancel'
|
<IconButton
|
||||||
title={intl.formatMessage(quoteCancelMessage)}
|
onClick={onQuoteCancel}
|
||||||
icon='cancel-fill'
|
className='status__quote-cancel'
|
||||||
iconComponent={CancelFillIcon}
|
title={intl.formatMessage(quoteCancelMessage)}
|
||||||
/>
|
icon='cancel-fill'
|
||||||
)}
|
iconComponent={CancelFillIcon}
|
||||||
</StatusHeader>
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
[intl, onQuoteCancel],
|
[intl, onQuoteCancel],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -22,18 +22,22 @@ export const renderPinnedStatusHeader: StatusHeaderRenderFn = ({
|
|||||||
return <StatusHeader {...args} />;
|
return <StatusHeader {...args} />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<StatusHeader {...args} className={classes.pinnedStatusHeader}>
|
<StatusHeader
|
||||||
<Badge
|
{...args}
|
||||||
className={classes.pinnedBadge}
|
className={classes.pinnedStatusHeader}
|
||||||
icon={<Icon id='pinned' icon={IconPinned} />}
|
contentBeforeDate={
|
||||||
label={
|
<Badge
|
||||||
<FormattedMessage
|
className={classes.pinnedBadge}
|
||||||
id='account.timeline.pinned'
|
icon={<Icon id='pinned' icon={IconPinned} />}
|
||||||
defaultMessage='Pinned'
|
label={
|
||||||
/>
|
<FormattedMessage
|
||||||
}
|
id='account.timeline.pinned'
|
||||||
/>
|
defaultMessage='Pinned'
|
||||||
</StatusHeader>
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -126,4 +126,7 @@
|
|||||||
|
|
||||||
.pinnedBadge {
|
.pinnedBadge {
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
|
|
||||||
|
// Allow "click to open post" event to pass through
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,14 +27,14 @@ export const CollectionListItem: React.FC<CollectionListItemProps> = ({
|
|||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
const uniqueId = useId();
|
const uniqueId = useId();
|
||||||
const linkId = `${uniqueId}-link`;
|
const titleId = `${uniqueId}-title`;
|
||||||
const infoId = `${uniqueId}-info`;
|
const subtitleId = `${uniqueId}-info`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Article
|
<Article
|
||||||
focusable
|
focusable
|
||||||
aria-labelledby={linkId}
|
aria-labelledby={titleId}
|
||||||
aria-describedby={infoId}
|
aria-describedby={subtitleId}
|
||||||
aria-posinset={positionInList}
|
aria-posinset={positionInList}
|
||||||
aria-setsize={listSize}
|
aria-setsize={listSize}
|
||||||
>
|
>
|
||||||
@ -52,6 +52,8 @@ export const CollectionListItem: React.FC<CollectionListItemProps> = ({
|
|||||||
className={classes.menuButton}
|
className={classes.menuButton}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
titleId={titleId}
|
||||||
|
subtitleId={subtitleId}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
</Article>
|
</Article>
|
||||||
|
|||||||
@ -48,6 +48,8 @@ export interface CollectionLockupProps {
|
|||||||
sideContent?: React.ReactNode;
|
sideContent?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
headingLevel?: 'h2' | 'h3' | 'h4';
|
headingLevel?: 'h2' | 'h3' | 'h4';
|
||||||
|
titleId?: string;
|
||||||
|
subtitleId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
||||||
@ -56,6 +58,8 @@ export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
|||||||
withTimestamp,
|
withTimestamp,
|
||||||
sideContent,
|
sideContent,
|
||||||
headingLevel = 'h3',
|
headingLevel = 'h3',
|
||||||
|
titleId,
|
||||||
|
subtitleId,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const { id, name } = collection;
|
const { id, name } = collection;
|
||||||
@ -74,6 +78,7 @@ export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
|||||||
<ListItemLink
|
<ListItemLink
|
||||||
as={headingLevel}
|
as={headingLevel}
|
||||||
to={getCollectionPath(id)}
|
to={getCollectionPath(id)}
|
||||||
|
id={titleId}
|
||||||
subtitle={
|
subtitle={
|
||||||
<CollectionInfo
|
<CollectionInfo
|
||||||
collection={collection}
|
collection={collection}
|
||||||
@ -81,6 +86,7 @@ export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
|||||||
withTimestamp={withTimestamp}
|
withTimestamp={withTimestamp}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
subtitleId={subtitleId}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
|
|||||||
@ -248,7 +248,15 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
const { intl, onPaste, onDrop, autoFocus, withoutNavigation, maxChars, isSubmitting } = this.props;
|
const { intl, onPaste, onDrop, autoFocus, withoutNavigation, maxChars, isSubmitting } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className='compose-form' onSubmit={this.handleSubmit}>
|
<form
|
||||||
|
className='compose-form'
|
||||||
|
role='region'
|
||||||
|
aria-label={intl.formatMessage({
|
||||||
|
id: 'tabs_bar.publish',
|
||||||
|
defaultMessage: 'New Post'
|
||||||
|
})}
|
||||||
|
onSubmit={this.handleSubmit}
|
||||||
|
>
|
||||||
<ReplyIndicator />
|
<ReplyIndicator />
|
||||||
{!withoutNavigation && <NavigationBar />}
|
{!withoutNavigation && <NavigationBar />}
|
||||||
<Warning />
|
<Warning />
|
||||||
|
|||||||
@ -58,7 +58,7 @@ const Option = ({ multipleChoice, index, title, autoFocus }) => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
|
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
|
||||||
const lang = useSelector(state => state.getIn(['compose', 'language']));
|
const lang = useSelector(state => state.getIn(['compose', 'language']));
|
||||||
const maxOptions = useSelector(state => state.getIn(['server', 'server', 'configuration', 'polls', 'max_options']));
|
const maxOptions = useSelector(state => state.getIn(['server', 'server', 'item', 'configuration', 'polls', 'max_options']));
|
||||||
|
|
||||||
const handleChange = useCallback(({ target: { value } }) => {
|
const handleChange = useCallback(({ target: { value } }) => {
|
||||||
dispatch(changePollOption(index, value, maxOptions));
|
dispatch(changePollOption(index, value, maxOptions));
|
||||||
|
|||||||
@ -547,11 +547,15 @@ export const Search: React.FC<{
|
|||||||
const searchOptionsHeading = useId();
|
const searchOptionsHeading = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form ref={formRef} className={classNames('search', { active: expanded })}>
|
<form
|
||||||
|
role='search'
|
||||||
|
ref={formRef}
|
||||||
|
className={classNames('search', { active: expanded })}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
className='search__input'
|
className='search__input'
|
||||||
type='text'
|
type='search'
|
||||||
placeholder={intl.formatMessage(
|
placeholder={intl.formatMessage(
|
||||||
signedIn ? messages.placeholderSignedIn : messages.placeholder,
|
signedIn ? messages.placeholderSignedIn : messages.placeholder,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const mapStateToProps = state => {
|
|||||||
const readyAttachmentsSize = state.getIn(['compose', 'media_attachments']).size ?? 0;
|
const readyAttachmentsSize = state.getIn(['compose', 'media_attachments']).size ?? 0;
|
||||||
const pendingAttachmentsSize = state.getIn(['compose', 'pending_media_attachments']).size ?? 0;
|
const pendingAttachmentsSize = state.getIn(['compose', 'pending_media_attachments']).size ?? 0;
|
||||||
const attachmentsSize = readyAttachmentsSize + pendingAttachmentsSize;
|
const attachmentsSize = readyAttachmentsSize + pendingAttachmentsSize;
|
||||||
const isOverLimit = attachmentsSize > state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments'])-1;
|
const isOverLimit = attachmentsSize > state.getIn(['server', 'server', 'item', 'configuration', 'statuses', 'max_media_attachments'])-1;
|
||||||
const hasVideoOrAudio = state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')));
|
const hasVideoOrAudio = state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')));
|
||||||
const hasQuote = !!state.compose.get('quoted_status_id');
|
const hasQuote = !!state.compose.get('quoted_status_id');
|
||||||
|
|
||||||
|
|||||||
@ -87,12 +87,11 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
|||||||
|
|
||||||
if (multiColumn) {
|
if (multiColumn) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='drawer'>
|
||||||
className='drawer'
|
<nav
|
||||||
role='region'
|
className='drawer__header'
|
||||||
aria-label={intl.formatMessage(navbarMessages.publish)}
|
aria-label={intl.formatMessage(navbarMessages.advancedUiQuickLinks)}
|
||||||
>
|
>
|
||||||
<nav className='drawer__header'>
|
|
||||||
<Link
|
<Link
|
||||||
to='/getting-started'
|
to='/getting-started'
|
||||||
className='drawer__tab'
|
className='drawer__tab'
|
||||||
@ -163,7 +162,11 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
|||||||
|
|
||||||
<Search singleColumn={false} />
|
<Search singleColumn={false} />
|
||||||
|
|
||||||
<div className='drawer__pager'>
|
<div
|
||||||
|
className='drawer__pager'
|
||||||
|
role='region'
|
||||||
|
aria-label={intl.formatMessage(navbarMessages.publish)}
|
||||||
|
>
|
||||||
<div className='drawer__inner'>
|
<div className='drawer__inner'>
|
||||||
<ComposeFormContainer />
|
<ComposeFormContainer />
|
||||||
|
|
||||||
|
|||||||
75
app/javascript/mastodon/features/custom_homepage/about.tsx
Normal file
75
app/javascript/mastodon/features/custom_homepage/about.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { fetchExtendedDescription } from 'mastodon/actions/server';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
const Placeholder = () => (
|
||||||
|
<div className={classes.placeholder}>
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
<Skeleton width='100%' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const About = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const server = useAppSelector((state) => state.server.server);
|
||||||
|
const extendedDescription = useAppSelector(
|
||||||
|
(state) => state.server.extendedDescription,
|
||||||
|
);
|
||||||
|
|
||||||
|
const accountId = server.item?.contact.account?.id ?? '';
|
||||||
|
const isLoading = extendedDescription.isLoading;
|
||||||
|
const hasContent = (extendedDescription.item?.content.length ?? 0) > 0;
|
||||||
|
const content = extendedDescription.item?.content ?? '';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void dispatch(fetchExtendedDescription());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classes.block}>
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage
|
||||||
|
id='custom_homepage.administered_by'
|
||||||
|
defaultMessage='Administered by'
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<Account id={accountId} size={36} minimal />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classes.block}>
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage
|
||||||
|
id='custom_homepage.about_this_server'
|
||||||
|
defaultMessage='About this server'
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
{isLoading ? (
|
||||||
|
<Placeholder />
|
||||||
|
) : hasContent ? (
|
||||||
|
<div
|
||||||
|
className='prose'
|
||||||
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className='prose'>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='about.not_available'
|
||||||
|
defaultMessage='This information has not been made available on this server.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { fetchServer } from 'mastodon/actions/server';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import classes from '../styles.module.scss';
|
||||||
|
|
||||||
|
export const Footer = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const server = useAppSelector((state) => state.server.server);
|
||||||
|
const email = server.item?.contact.email ?? '';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void dispatch(fetchServer());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className={classes.minimalFooter}>
|
||||||
|
<div className={classes.contact}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='custom_homepage.contact'
|
||||||
|
defaultMessage='Contact:'
|
||||||
|
/>
|
||||||
|
<a href={`mailto:${email}`}>{email}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link to='/privacy-policy' rel='privacy-policy'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='footer.privacy_policy'
|
||||||
|
defaultMessage='Privacy policy'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { domain, sso_redirect } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
import classes from '../styles.module.scss';
|
||||||
|
|
||||||
|
export const Header = () => (
|
||||||
|
<div className={classes.minimalHeader}>
|
||||||
|
<div className={classes.leftSide}>
|
||||||
|
<Link to='/overview'>{domain}</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classes.rightSide}>
|
||||||
|
<a
|
||||||
|
href={sso_redirect ?? '/auth/sign_in'}
|
||||||
|
data-method={sso_redirect ? 'post' : undefined}
|
||||||
|
className='button button-secondary'
|
||||||
|
>
|
||||||
|
<FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
71
app/javascript/mastodon/features/custom_homepage/index.tsx
Normal file
71
app/javascript/mastodon/features/custom_homepage/index.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Route, Switch, useRouteMatch } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Helmet } from '@unhead/react/helmet';
|
||||||
|
|
||||||
|
import { fetchServer } from 'mastodon/actions/server';
|
||||||
|
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
|
||||||
|
import { TabLink, TabList } from 'mastodon/components/tab_list';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { About } from './about';
|
||||||
|
import { LatestActivity } from './latest_activity';
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
export const CustomHomepage: React.FC = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const server = useAppSelector((state) => state.server.server);
|
||||||
|
const { path } = useRouteMatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void dispatch(fetchServer());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.page}>
|
||||||
|
<ServerHeroImage
|
||||||
|
alt={server.item?.thumbnail.description ?? ''}
|
||||||
|
blurhash={server.item?.thumbnail.blurhash ?? ''}
|
||||||
|
src={server.item?.thumbnail.url ?? ''}
|
||||||
|
srcSet={Object.keys(server.item?.thumbnail.versions ?? {})
|
||||||
|
.map(
|
||||||
|
(key) =>
|
||||||
|
`${server.item?.thumbnail.versions?.[key]} ${key.replace('@', '')}`,
|
||||||
|
)
|
||||||
|
.join(', ')}
|
||||||
|
className={classes.header}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={classes.topSection}>
|
||||||
|
<h1>{server.item?.domain}</h1>
|
||||||
|
<p>{server.item?.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TabList>
|
||||||
|
<TabLink to={path} exact>
|
||||||
|
<FormattedMessage
|
||||||
|
id='custom_homepage.latest_activity'
|
||||||
|
defaultMessage='Latest activity'
|
||||||
|
/>
|
||||||
|
</TabLink>
|
||||||
|
|
||||||
|
<TabLink to={`${path}/about`} exact>
|
||||||
|
<FormattedMessage id='custom_homepage.about' defaultMessage='About' />
|
||||||
|
</TabLink>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<Switch>
|
||||||
|
<Route path={path} exact component={LatestActivity} />
|
||||||
|
<Route path={`${path}/about`} exact component={About} />
|
||||||
|
</Switch>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{server.item?.domain}</title>
|
||||||
|
<meta name='robots' content='all' />
|
||||||
|
</Helmet>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { expandCommunityTimeline } from 'mastodon/actions/timelines';
|
||||||
|
import { Callout } from 'mastodon/components/callout';
|
||||||
|
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
|
||||||
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
export const LatestActivity = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void dispatch(expandCommunityTimeline());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusListContainer
|
||||||
|
prepend={
|
||||||
|
<Callout className={classes.banner}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='custom_homepage.these_are_the_latest_posts'
|
||||||
|
defaultMessage='These are the latest 40 posts from accounts on this server.'
|
||||||
|
/>
|
||||||
|
</Callout>
|
||||||
|
}
|
||||||
|
scrollKey='custom_homepage'
|
||||||
|
timelineId='community'
|
||||||
|
maxItems={40}
|
||||||
|
bindToDocument
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,145 @@
|
|||||||
|
.page {
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
min-height: 100%;
|
||||||
|
|
||||||
|
:global(.item-list) article:last-child :global(.status) {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (width <= 1175px) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
aspect-ratio: 40/21;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
|
||||||
|
@media screen and (width <= 1175px) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
margin: 16px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topSection {
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 30px;
|
||||||
|
letter-spacing: -0.12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 22.4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.account) {
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
padding: 18px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
--avatar-border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
padding: 4px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
:global(.skeleton) {
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--color-bg-overlay-highlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimalHeader {
|
||||||
|
padding-top: 24px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media screen and (width <= 1175px) {
|
||||||
|
padding-inline-start: 12px;
|
||||||
|
padding-inline-end: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSide {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
max-width: 300px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightSide {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimalFooter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 22.4px;
|
||||||
|
padding-top: 28px;
|
||||||
|
padding-bottom: 54px;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (width <= 1175px) {
|
||||||
|
justify-content: center;
|
||||||
|
padding-inline-start: 12px;
|
||||||
|
padding-inline-end: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
@ -85,13 +85,16 @@ export async function search({
|
|||||||
// Only query the range for the last token to allow partial matches.
|
// Only query the range for the last token to allow partial matches.
|
||||||
const range =
|
const range =
|
||||||
i === queryTokens.length - 1
|
i === queryTokens.length - 1
|
||||||
? IDBKeyRange.bound(token, token + '\uffff')
|
? IDBKeyRange.lowerBound(token)
|
||||||
: IDBKeyRange.only(token);
|
: IDBKeyRange.only(token);
|
||||||
|
|
||||||
const [unicodeResults, customResults] = await Promise.all([
|
const [unicodeResults, customResults, shortcodeResults] = await Promise.all(
|
||||||
db.getAllFromIndex(locale, 'tokens', range),
|
[
|
||||||
db.getAllFromIndex('custom', 'tokens', range),
|
db.getAllFromIndex(locale, 'tokens', range),
|
||||||
]);
|
db.getAllFromIndex('custom', 'tokens', range),
|
||||||
|
db.getAllFromIndex('shortcodes', 'shortcodes', range),
|
||||||
|
],
|
||||||
|
);
|
||||||
const resultMap: ScoreMap = new Map();
|
const resultMap: ScoreMap = new Map();
|
||||||
for (const emoji of unicodeResults) {
|
for (const emoji of unicodeResults) {
|
||||||
const score = getScoreForEmoji(emoji, token);
|
const score = getScoreForEmoji(emoji, token);
|
||||||
@ -107,6 +110,22 @@ export async function search({
|
|||||||
}
|
}
|
||||||
resultMap.set(emoji.shortcode, { ...emoji, score });
|
resultMap.set(emoji.shortcode, { ...emoji, score });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const shortcodeResult of shortcodeResults) {
|
||||||
|
if (resultMap.has(shortcodeResult.hexcode)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const emoji = await db.get(locale, shortcodeResult.hexcode);
|
||||||
|
if (!emoji) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const score = getScoreForEmoji(emoji, token);
|
||||||
|
if (score === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
resultMap.set(emoji.hexcode, { ...emoji, score });
|
||||||
|
}
|
||||||
|
|
||||||
log('found %d results for token "%s"', resultMap.size, token);
|
log('found %d results for token "%s"', resultMap.size, token);
|
||||||
resultArrays.push(resultMap);
|
resultArrays.push(resultMap);
|
||||||
}
|
}
|
||||||
@ -147,7 +166,7 @@ function getScoreForEmoji(emoji: AnyEmojiData, query: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let index = 1;
|
let index = 1;
|
||||||
for (const token of [id, emoji.tokens]) {
|
for (const token of [id, ...emoji.tokens]) {
|
||||||
const tokenIndex = token.indexOf(query);
|
const tokenIndex = token.indexOf(query);
|
||||||
if (tokenIndex !== -1) {
|
if (tokenIndex !== -1) {
|
||||||
return index + tokenIndex / token.length;
|
return index + tokenIndex / token.length;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { initialState } from '@/mastodon/initial_state';
|
|||||||
|
|
||||||
import type { EMOJI_DB_NAME_SHORTCODES } from './constants';
|
import type { EMOJI_DB_NAME_SHORTCODES } from './constants';
|
||||||
import { toSupportedLocale } from './locale';
|
import { toSupportedLocale } from './locale';
|
||||||
|
import { reloadCustomEmojis } from './picker';
|
||||||
import type { LocaleOrCustom } from './types';
|
import type { LocaleOrCustom } from './types';
|
||||||
import { emojiLogger } from './utils';
|
import { emojiLogger } from './utils';
|
||||||
|
|
||||||
@ -90,6 +91,7 @@ export async function loadCustomEmoji() {
|
|||||||
const emojis = await importCustomEmojiData();
|
const emojis = await importCustomEmojiData();
|
||||||
if (emojis && emojis.length > 0) {
|
if (emojis && emojis.length > 0) {
|
||||||
log('loaded %d custom emojis', emojis.length);
|
log('loaded %d custom emojis', emojis.length);
|
||||||
|
await reloadCustomEmojis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
EMOJIS_REQUIRING_INVERSION_IN_DARK_MODE,
|
EMOJIS_REQUIRING_INVERSION_IN_DARK_MODE,
|
||||||
EMOJI_MIN_TOKEN_LENGTH,
|
EMOJI_MIN_TOKEN_LENGTH,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
import { localeToSegmenter } from './locale';
|
||||||
import type {
|
import type {
|
||||||
CustomEmojiData,
|
CustomEmojiData,
|
||||||
CustomEmojiMapArg,
|
CustomEmojiMapArg,
|
||||||
@ -92,10 +93,11 @@ export function transformEmojiData(
|
|||||||
export function transformCustomEmojiData(
|
export function transformCustomEmojiData(
|
||||||
emoji: ApiCustomEmojiJSON,
|
emoji: ApiCustomEmojiJSON,
|
||||||
): CustomEmojiData {
|
): CustomEmojiData {
|
||||||
const tokens = emoji.shortcode
|
const tokens = extractTokens(emoji.shortcode, localeToSegmenter('en'));
|
||||||
.split('_')
|
if (!tokens.includes(emoji.shortcode)) {
|
||||||
.filter((word) => word.length >= EMOJI_MIN_TOKEN_LENGTH)
|
tokens.unshift(emoji.shortcode);
|
||||||
.map((word) => word.toLowerCase());
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...emoji,
|
...emoji,
|
||||||
tokens,
|
tokens,
|
||||||
@ -215,7 +217,9 @@ export function extractTokens(
|
|||||||
// Prefer to use Intl.Segmenter if available for better locale support.
|
// Prefer to use Intl.Segmenter if available for better locale support.
|
||||||
if (segmenter) {
|
if (segmenter) {
|
||||||
for (const { isWordLike, segment } of segmenter.segment(
|
for (const { isWordLike, segment } of segmenter.segment(
|
||||||
input.replaceAll('_', ' '), // Handle underscores from shortcodes.
|
input
|
||||||
|
.replaceAll(/[_-]+/g, ' ') // Handle underscores from shortcodes.
|
||||||
|
.replaceAll(/([a-z])([A-Z])/g, '$1 $2'), // Handle camelCase.
|
||||||
)) {
|
)) {
|
||||||
if (isWordLike && segment.length >= EMOJI_MIN_TOKEN_LENGTH) {
|
if (isWordLike && segment.length >= EMOJI_MIN_TOKEN_LENGTH) {
|
||||||
tokens.push(segment.toLowerCase());
|
tokens.push(segment.toLowerCase());
|
||||||
|
|||||||
@ -82,13 +82,22 @@ type LegacyEmoji =
|
|||||||
custom: true;
|
custom: true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function reloadCustomEmojis() {
|
||||||
|
customEmojis = null;
|
||||||
|
|
||||||
|
const { loadEmojisIntoCache } =
|
||||||
|
await import('@/mastodon/hooks/useCustomEmojis');
|
||||||
|
|
||||||
|
await Promise.all([fetchCustomEmojiData(), loadEmojisIntoCache()]);
|
||||||
|
}
|
||||||
|
|
||||||
// Replicates the old legacy search function.
|
// Replicates the old legacy search function.
|
||||||
export async function emojiMartSearch(
|
export async function emojiMartSearch(
|
||||||
token: string,
|
token: string,
|
||||||
locale: string,
|
locale: string,
|
||||||
limit = 5,
|
limit = 5,
|
||||||
): Promise<LegacyEmoji[]> {
|
): Promise<LegacyEmoji[]> {
|
||||||
const query = token.replace(':', '').toLowerCase().trim();
|
const query = token.replace(':', '').trim();
|
||||||
if (!query.length) {
|
if (!query.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,14 +4,16 @@ import { Helmet } from '@unhead/react/helmet';
|
|||||||
|
|
||||||
import { Column } from 'mastodon/components/column';
|
import { Column } from 'mastodon/components/column';
|
||||||
|
|
||||||
import { NavigationPanel } from '../navigation_panel';
|
import { NavigationPanel, messages } from '../navigation_panel';
|
||||||
import { LinkFooter } from '../ui/components/link_footer';
|
import { LinkFooter } from '../ui/components/link_footer';
|
||||||
|
|
||||||
const GettingStarted: React.FC = () => {
|
const GettingStarted: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<NavigationPanel multiColumn />
|
<nav aria-label={intl.formatMessage(messages.main)}>
|
||||||
|
<NavigationPanel multiColumn />
|
||||||
|
</nav>
|
||||||
|
|
||||||
<LinkFooter context='multi-column' />
|
<LinkFooter context='multi-column' />
|
||||||
|
|
||||||
|
|||||||
@ -60,7 +60,7 @@ import { MoreLink } from './components/more_link';
|
|||||||
import { SignInBanner } from './components/sign_in_banner';
|
import { SignInBanner } from './components/sign_in_banner';
|
||||||
import { Trends } from './components/trends';
|
import { Trends } from './components/trends';
|
||||||
|
|
||||||
const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
notifications: {
|
notifications: {
|
||||||
id: 'tabs_bar.notifications',
|
id: 'tabs_bar.notifications',
|
||||||
@ -72,6 +72,12 @@ const messages = defineMessages({
|
|||||||
id: 'column.firehose_singular',
|
id: 'column.firehose_singular',
|
||||||
defaultMessage: 'Live feed',
|
defaultMessage: 'Live feed',
|
||||||
},
|
},
|
||||||
|
main: {
|
||||||
|
id: 'navigation_bar.main',
|
||||||
|
defaultMessage: 'Main',
|
||||||
|
description:
|
||||||
|
'Label for the main navigation; should not contain the word "navigation".',
|
||||||
|
},
|
||||||
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
|
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
|
||||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
|
||||||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
@ -419,6 +425,7 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CollapsibleNavigationPanel: React.FC = () => {
|
export const CollapsibleNavigationPanel: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const open = useAppSelector((state) => state.navigation.open);
|
const open = useAppSelector((state) => state.navigation.open);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const openable = useBreakpoint('openable');
|
const openable = useBreakpoint('openable');
|
||||||
@ -527,7 +534,8 @@ export const CollapsibleNavigationPanel: React.FC = () => {
|
|||||||
const showOverlay = openable && open;
|
const showOverlay = openable && open;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<nav
|
||||||
|
aria-label={intl.formatMessage(messages.main)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational',
|
'columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational',
|
||||||
{ 'columns-area__panels__pane--overlay': showOverlay },
|
{ 'columns-area__panels__pane--overlay': showOverlay },
|
||||||
@ -541,6 +549,6 @@ export const CollapsibleNavigationPanel: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<NavigationPanel />
|
<NavigationPanel />
|
||||||
</animated.div>
|
</animated.div>
|
||||||
</div>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import classNames from 'classnames';
|
|||||||
import type { List, Record } from 'immutable';
|
import type { List, Record } from 'immutable';
|
||||||
|
|
||||||
import { useAppSelector } from '@/mastodon/store';
|
import { useAppSelector } from '@/mastodon/store';
|
||||||
|
import { Footer } from 'mastodon/features/custom_homepage/components/footer';
|
||||||
|
import { Header } from 'mastodon/features/custom_homepage/components/header';
|
||||||
import { CollapsibleNavigationPanel } from 'mastodon/features/navigation_panel';
|
import { CollapsibleNavigationPanel } from 'mastodon/features/navigation_panel';
|
||||||
|
|
||||||
import { useBreakpoint } from '../hooks/useBreakpoint';
|
import { useBreakpoint } from '../hooks/useBreakpoint';
|
||||||
@ -85,9 +87,10 @@ export const ColumnsArea = forwardRef<
|
|||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{
|
{
|
||||||
singleColumn?: boolean;
|
singleColumn?: boolean;
|
||||||
|
minimalShell?: boolean;
|
||||||
children: React.ReactElement | React.ReactElement[];
|
children: React.ReactElement | React.ReactElement[];
|
||||||
}
|
}
|
||||||
>(({ children, singleColumn }, ref) => {
|
>(({ children, minimalShell, singleColumn }, ref) => {
|
||||||
const renderComposePanel = !useBreakpoint('full');
|
const renderComposePanel = !useBreakpoint('full');
|
||||||
const columns = useAppSelector((state) =>
|
const columns = useAppSelector((state) =>
|
||||||
(state.settings as Record<{ columns: List<Record<Column>> }>).get(
|
(state.settings as Record<{ columns: List<Record<Column>> }>).get(
|
||||||
@ -98,6 +101,24 @@ export const ColumnsArea = forwardRef<
|
|||||||
(state) => !state.modal.get('stack').isEmpty(),
|
(state) => !state.modal.get('stack').isEmpty(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (minimalShell) {
|
||||||
|
return (
|
||||||
|
<div className='columns-area__panels'>
|
||||||
|
<div className='columns-area__panels__main'>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<div className='tabs-bar__wrapper'>
|
||||||
|
<TabsBarPortal />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (singleColumn) {
|
if (singleColumn) {
|
||||||
return (
|
return (
|
||||||
<div className='columns-area__panels'>
|
<div className='columns-area__panels'>
|
||||||
@ -108,12 +129,13 @@ export const ColumnsArea = forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='columns-area__panels__main'>
|
<main className='columns-area__panels__main'>
|
||||||
<div className='tabs-bar__wrapper'>
|
<div className='tabs-bar__wrapper'>
|
||||||
<TabsBarPortal />
|
<TabsBarPortal />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
<CollapsibleNavigationPanel />
|
<CollapsibleNavigationPanel />
|
||||||
</div>
|
</div>
|
||||||
@ -121,7 +143,7 @@ export const ColumnsArea = forwardRef<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<main
|
||||||
className={classNames('columns-area', { unscrollable: isModalOpen })}
|
className={classNames('columns-area', { unscrollable: isModalOpen })}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
tabIndex={isModalOpen ? undefined : 0}
|
tabIndex={isModalOpen ? undefined : 0}
|
||||||
@ -160,7 +182,7 @@ export const ColumnsArea = forwardRef<
|
|||||||
cloneElement(child, { multiColumn: true }),
|
cloneElement(child, { multiColumn: true }),
|
||||||
)}
|
)}
|
||||||
</ColumnIndexContext.Provider>
|
</ColumnIndexContext.Provider>
|
||||||
</div>
|
</main>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,10 @@
|
|||||||
|
|
||||||
&:not(:last-child)::after {
|
&:not(:last-child)::after {
|
||||||
content: ' · ';
|
content: ' · ';
|
||||||
|
|
||||||
|
@supports (content: 'x' / 'y') {
|
||||||
|
content: ' · ' / '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,10 @@ export const messages = defineMessages({
|
|||||||
defaultMessage: 'Notifications',
|
defaultMessage: 'Notifications',
|
||||||
},
|
},
|
||||||
menu: { id: 'tabs_bar.menu', defaultMessage: 'Menu' },
|
menu: { id: 'tabs_bar.menu', defaultMessage: 'Menu' },
|
||||||
|
advancedUiQuickLinks: {
|
||||||
|
id: 'tabs_bar.quick_links',
|
||||||
|
defaultMessage: 'Quick links',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const IconLabelButton: React.FC<{
|
const IconLabelButton: React.FC<{
|
||||||
|
|||||||
@ -11,7 +11,15 @@ import { me } from '@/mastodon/initial_state';
|
|||||||
|
|
||||||
const makeGetStatusIds = (pending = false) => createSelector([
|
const makeGetStatusIds = (pending = false) => createSelector([
|
||||||
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
||||||
(state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()),
|
(state, { type, maxItems }) => {
|
||||||
|
const items = state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList());
|
||||||
|
|
||||||
|
if (maxItems) {
|
||||||
|
return items.take(maxItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
},
|
||||||
(state) => state.get('statuses'),
|
(state) => state.get('statuses'),
|
||||||
], (columnSettings, statusIds, statuses) => {
|
], (columnSettings, statusIds, statuses) => {
|
||||||
return statusIds.filter(id => {
|
return statusIds.filter(id => {
|
||||||
@ -41,8 +49,15 @@ const makeMapStateToProps = () => {
|
|||||||
const getStatusIds = makeGetStatusIds();
|
const getStatusIds = makeGetStatusIds();
|
||||||
const getPendingStatusIds = makeGetStatusIds(true);
|
const getPendingStatusIds = makeGetStatusIds(true);
|
||||||
|
|
||||||
const mapStateToProps = (state, { timelineId, initialLoadingState = true }) => ({
|
/**
|
||||||
statusIds: getStatusIds(state, { type: timelineId }),
|
* @param {import('mastodon/store').RootState} state
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {string} props.timelineId
|
||||||
|
* @param {boolean} [props.initialLoadingState]
|
||||||
|
* @param {number} [props.maxItems]
|
||||||
|
*/
|
||||||
|
const mapStateToProps = (state, { timelineId, initialLoadingState = true, maxItems }) => ({
|
||||||
|
statusIds: getStatusIds(state, { type: timelineId, maxItems }),
|
||||||
lastId: state.getIn(['timelines', timelineId, 'items'])?.last(),
|
lastId: state.getIn(['timelines', timelineId, 'items'])?.last(),
|
||||||
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], initialLoadingState),
|
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], initialLoadingState),
|
||||||
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../act
|
|||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards } from '../../initial_state';
|
import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards, domain } from '../../initial_state';
|
||||||
|
|
||||||
import BundleColumnError from './components/bundle_column_error';
|
import BundleColumnError from './components/bundle_column_error';
|
||||||
import { NavigationBar } from './components/navigation_bar';
|
import { NavigationBar } from './components/navigation_bar';
|
||||||
@ -88,6 +88,7 @@ import {
|
|||||||
import { ColumnsContextProvider } from './util/columns_context';
|
import { ColumnsContextProvider } from './util/columns_context';
|
||||||
import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem } from './util/focusUtils';
|
import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem } from './util/focusUtils';
|
||||||
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||||
|
import { CustomHomepage } from 'mastodon/features/custom_homepage';
|
||||||
|
|
||||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
// Without this it ends up in ~8 very commonly used bundles.
|
// Without this it ends up in ~8 very commonly used bundles.
|
||||||
@ -105,7 +106,7 @@ const mapStateToProps = state => ({
|
|||||||
hasComposingContents: state.getIn(['compose', 'text']).trim().length !== 0 || state.getIn(['compose', 'media_attachments']).size > 0 || state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'quoted_status_id']) !== null,
|
hasComposingContents: state.getIn(['compose', 'text']).trim().length !== 0 || state.getIn(['compose', 'media_attachments']).size > 0 || state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'quoted_status_id']) !== null,
|
||||||
canUploadMore:
|
canUploadMore:
|
||||||
!state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type')))
|
!state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type')))
|
||||||
&& state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']),
|
&& state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'item', 'configuration', 'statuses', 'max_media_attachments']),
|
||||||
isUploadEnabled:
|
isUploadEnabled:
|
||||||
state.getIn(['compose', 'isDragDisabled']) !== true,
|
state.getIn(['compose', 'isDragDisabled']) !== true,
|
||||||
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
|
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
|
||||||
@ -177,13 +178,15 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
rootRedirect = '/explore';
|
rootRedirect = '/explore';
|
||||||
} else if (localLiveFeedAccess === 'public' && landingPage === 'local_feed') {
|
} else if (localLiveFeedAccess === 'public' && landingPage === 'local_feed') {
|
||||||
rootRedirect = '/public/local';
|
rootRedirect = '/public/local';
|
||||||
|
} else if (landingPage === 'overview') {
|
||||||
|
rootRedirect = '/overview';
|
||||||
} else {
|
} else {
|
||||||
rootRedirect = '/about';
|
rootRedirect = '/about';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnsContextProvider multiColumn={!singleColumn}>
|
<ColumnsContextProvider multiColumn={!singleColumn}>
|
||||||
<ColumnsArea ref={this.setRef} singleColumn={singleColumn}>
|
<ColumnsArea ref={this.setRef} singleColumn={singleColumn} domain={domain} minimalShell={!signedIn && landingPage === 'overview'}>
|
||||||
<WrappedSwitch>
|
<WrappedSwitch>
|
||||||
<Redirect from='/' to={{pathname: rootRedirect, state: this.props.location.state}} exact />
|
<Redirect from='/' to={{pathname: rootRedirect, state: this.props.location.state}} exact />
|
||||||
|
|
||||||
@ -262,6 +265,8 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
||||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||||
|
|
||||||
|
<Route path='/overview' component={CustomHomepage} />
|
||||||
<Route component={BundleColumnError} />
|
<Route component={BundleColumnError} />
|
||||||
</WrappedSwitch>
|
</WrappedSwitch>
|
||||||
</ColumnsArea>
|
</ColumnsArea>
|
||||||
@ -633,13 +638,18 @@ class UI extends PureComponent {
|
|||||||
cheat: this.handleDonate,
|
cheat: this.handleDonate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const minimalShell = !this.props.identity.signedIn && landingPage === 'overview';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Hotkeys global handlers={handlers}>
|
<Hotkeys global handlers={handlers}>
|
||||||
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
|
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
|
||||||
<SkipLinks
|
{!minimalShell && (
|
||||||
multiColumn={layout === 'multi-column'}
|
<SkipLinks
|
||||||
onFocusGettingStartedColumn={this.handleHotkeyGoToStart}
|
multiColumn={layout === 'multi-column'}
|
||||||
/>
|
onFocusGettingStartedColumn={this.handleHotkeyGoToStart}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<SwitchingColumnsArea
|
<SwitchingColumnsArea
|
||||||
identity={this.props.identity}
|
identity={this.props.identity}
|
||||||
location={location}
|
location={location}
|
||||||
@ -650,7 +660,7 @@ class UI extends PureComponent {
|
|||||||
{children}
|
{children}
|
||||||
</SwitchingColumnsArea>
|
</SwitchingColumnsArea>
|
||||||
|
|
||||||
<NavigationBar />
|
{!minimalShell && <NavigationBar />}
|
||||||
{layout !== 'mobile' && <PictureInPicture />}
|
{layout !== 'mobile' && <PictureInPicture />}
|
||||||
<AlertsController />
|
<AlertsController />
|
||||||
{!disableHoverCards && <HoverCardController />}
|
{!disableHoverCards && <HoverCardController />}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export function useCustomEmojis() {
|
|||||||
return emojis;
|
return emojis;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadEmojisIntoCache() {
|
export async function loadEmojisIntoCache() {
|
||||||
const { loadAllCustomEmoji } = await import('../features/emoji/database');
|
const { loadAllCustomEmoji } = await import('../features/emoji/database');
|
||||||
const emojisRaw = await loadAllCustomEmoji();
|
const emojisRaw = await loadAllCustomEmoji();
|
||||||
if (emojisRaw === null) {
|
if (emojisRaw === null) {
|
||||||
|
|||||||
@ -584,6 +584,12 @@
|
|||||||
"copy_icon_button.copy_this_text": "Copy link to clipboard",
|
"copy_icon_button.copy_this_text": "Copy link to clipboard",
|
||||||
"copypaste.copied": "Copied",
|
"copypaste.copied": "Copied",
|
||||||
"copypaste.copy_to_clipboard": "Copy to clipboard",
|
"copypaste.copy_to_clipboard": "Copy to clipboard",
|
||||||
|
"custom_homepage.about": "About",
|
||||||
|
"custom_homepage.about_this_server": "About this server",
|
||||||
|
"custom_homepage.administered_by": "Administered by",
|
||||||
|
"custom_homepage.contact": "Contact:",
|
||||||
|
"custom_homepage.latest_activity": "Latest activity",
|
||||||
|
"custom_homepage.these_are_the_latest_posts": "These are the latest 40 posts from accounts on this server.",
|
||||||
"directory.federated": "From known fediverse",
|
"directory.federated": "From known fediverse",
|
||||||
"directory.local": "From {domain} only",
|
"directory.local": "From {domain} only",
|
||||||
"directory.new_arrivals": "New arrivals",
|
"directory.new_arrivals": "New arrivals",
|
||||||
@ -908,6 +914,7 @@
|
|||||||
"navigation_bar.live_feed_local": "Live feed (local)",
|
"navigation_bar.live_feed_local": "Live feed (local)",
|
||||||
"navigation_bar.live_feed_public": "Live feed (public)",
|
"navigation_bar.live_feed_public": "Live feed (public)",
|
||||||
"navigation_bar.logout": "Logout",
|
"navigation_bar.logout": "Logout",
|
||||||
|
"navigation_bar.main": "Main",
|
||||||
"navigation_bar.moderation": "Moderation",
|
"navigation_bar.moderation": "Moderation",
|
||||||
"navigation_bar.more": "More",
|
"navigation_bar.more": "More",
|
||||||
"navigation_bar.mutes": "Muted users",
|
"navigation_bar.mutes": "Muted users",
|
||||||
@ -1305,6 +1312,7 @@
|
|||||||
"tabs_bar.menu": "Menu",
|
"tabs_bar.menu": "Menu",
|
||||||
"tabs_bar.notifications": "Notifications",
|
"tabs_bar.notifications": "Notifications",
|
||||||
"tabs_bar.publish": "New Post",
|
"tabs_bar.publish": "New Post",
|
||||||
|
"tabs_bar.quick_links": "Quick links",
|
||||||
"tabs_bar.search": "Search",
|
"tabs_bar.search": "Search",
|
||||||
"tag.remove": "Remove",
|
"tag.remove": "Remove",
|
||||||
"terms_of_service.effective_as_of": "Effective as of {date}",
|
"terms_of_service.effective_as_of": "Effective as of {date}",
|
||||||
|
|||||||
@ -411,7 +411,7 @@ $content-width: 840px;
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > ul {
|
& > nav > ul {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
&.visible {
|
&.visible {
|
||||||
|
|||||||
@ -1613,7 +1613,6 @@ body > [data-popper-placement] {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
order: 2;
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
@ -1666,7 +1665,6 @@ body > [data-popper-placement] {
|
|||||||
|
|
||||||
.status__quote-cancel {
|
.status__quote-cancel {
|
||||||
align-self: self-start;
|
align-self: self-start;
|
||||||
order: 5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__info {
|
.status__info {
|
||||||
@ -10778,6 +10776,7 @@ noscript {
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
max-height: 2 * 20px;
|
max-height: 2 * 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|||||||
@ -109,7 +109,7 @@ class Form::AdminSettings
|
|||||||
REGISTRATION_MODES = %w(open approved none).freeze
|
REGISTRATION_MODES = %w(open approved none).freeze
|
||||||
FEED_ACCESS_MODES = %w(public authenticated disabled).freeze
|
FEED_ACCESS_MODES = %w(public authenticated disabled).freeze
|
||||||
ALTERNATE_FEED_ACCESS_MODES = %w(public authenticated).freeze
|
ALTERNATE_FEED_ACCESS_MODES = %w(public authenticated).freeze
|
||||||
LANDING_PAGE = %w(trends about local_feed).freeze
|
LANDING_PAGE = %w(trends overview local_feed about).freeze
|
||||||
|
|
||||||
attr_accessor(*KEYS)
|
attr_accessor(*KEYS)
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
class ReportFilter
|
class ReportFilter
|
||||||
KEYS = %i(
|
KEYS = %i(
|
||||||
resolved
|
resolved
|
||||||
|
unresolved
|
||||||
account_id
|
account_id
|
||||||
target_account_id
|
target_account_id
|
||||||
by_target_domain
|
by_target_domain
|
||||||
@ -16,7 +17,7 @@ class ReportFilter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def results
|
def results
|
||||||
scope = Report.unresolved
|
scope = status_scope
|
||||||
|
|
||||||
relevant_params.each do |key, value|
|
relevant_params.each do |key, value|
|
||||||
scope = scope.merge scope_for(key, value)
|
scope = scope.merge scope_for(key, value)
|
||||||
@ -28,7 +29,7 @@ class ReportFilter
|
|||||||
private
|
private
|
||||||
|
|
||||||
def relevant_params
|
def relevant_params
|
||||||
params.tap do |args|
|
params.except(:resolved, :unresolved).tap do |args|
|
||||||
args.delete(:target_origin) if origin_is_remote_and_domain_present?
|
args.delete(:target_origin) if origin_is_remote_and_domain_present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -37,12 +38,20 @@ class ReportFilter
|
|||||||
params[:target_origin] == 'remote' && params[:by_target_domain].present?
|
params[:target_origin] == 'remote' && params[:by_target_domain].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def status_scope
|
||||||
|
resolved = params.key?(:resolved)
|
||||||
|
unresolved = params.key?(:unresolved)
|
||||||
|
|
||||||
|
return Report.all if resolved && unresolved
|
||||||
|
return Report.resolved if resolved
|
||||||
|
|
||||||
|
Report.unresolved
|
||||||
|
end
|
||||||
|
|
||||||
def scope_for(key, value)
|
def scope_for(key, value)
|
||||||
case key.to_sym
|
case key.to_sym
|
||||||
when :by_target_domain
|
when :by_target_domain
|
||||||
Report.where(target_account: Account.where(domain: value))
|
Report.where(target_account: Account.where(domain: value))
|
||||||
when :resolved
|
|
||||||
Report.resolved
|
|
||||||
when :account_id
|
when :account_id
|
||||||
Report.where(account_id: value)
|
Report.where(account_id: value)
|
||||||
when :target_account_id
|
when :target_account_id
|
||||||
|
|||||||
@ -75,10 +75,11 @@
|
|||||||
|
|
||||||
.fields-row
|
.fields-row
|
||||||
= f.input :landing_page,
|
= f.input :landing_page,
|
||||||
|
as: :radio_buttons,
|
||||||
collection: f.object.class::LANDING_PAGE,
|
collection: f.object.class::LANDING_PAGE,
|
||||||
include_blank: false,
|
include_blank: false,
|
||||||
label_method: ->(page) { I18n.t("admin.settings.landing_page.values.#{page}") },
|
label_method: ->(page) { safe_join([I18n.t("admin.settings.landing_page.values.#{page}"), content_tag(:span, I18n.t("admin.settings.landing_page.hints.#{page}_html"), class: 'hint')]) },
|
||||||
wrapper: :with_label
|
wrapper: :with_block_label
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('generic.save_changes'), type: :submit
|
= f.button :button, t('generic.save_changes'), type: :submit
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
- content_for :content do
|
- content_for :content do
|
||||||
%a.navigation-skip-link{ href: '#content' }= t('admin.skip_to_content')
|
%a.navigation-skip-link{ href: '#content' }= t('admin.skip_to_content')
|
||||||
.admin-wrapper
|
.admin-wrapper
|
||||||
%nav.sidebar-wrapper
|
%header.sidebar-wrapper
|
||||||
.sidebar-wrapper__inner
|
.sidebar-wrapper__inner
|
||||||
.sidebar
|
.sidebar
|
||||||
= link_to root_path do
|
= link_to root_path do
|
||||||
@ -23,7 +23,8 @@
|
|||||||
= material_symbol 'menu'
|
= material_symbol 'menu'
|
||||||
= material_symbol 'close'
|
= material_symbol 'close'
|
||||||
|
|
||||||
= render_navigation
|
%nav
|
||||||
|
= render_navigation
|
||||||
|
|
||||||
%main.content-wrapper#content
|
%main.content-wrapper#content
|
||||||
.content
|
.content
|
||||||
|
|||||||
@ -956,10 +956,16 @@ en:
|
|||||||
disabled: Require specific user role
|
disabled: Require specific user role
|
||||||
public: Everyone
|
public: Everyone
|
||||||
landing_page:
|
landing_page:
|
||||||
|
hints:
|
||||||
|
about_html: A page with the description, contact information, rules and other information regarding this server.
|
||||||
|
local_feed_html: A live feed featuring most recent posts by users on this server.
|
||||||
|
overview_html: A page showcasing the description of your server alongside the most recent local posts by users on this server.
|
||||||
|
trends_html: A page featuring what's popular on this server right now.
|
||||||
values:
|
values:
|
||||||
about: About
|
about: About page
|
||||||
local_feed: Local feed
|
local_feed: Local live feed
|
||||||
trends: Trends
|
overview: Overview
|
||||||
|
trends: Trending
|
||||||
registrations:
|
registrations:
|
||||||
moderation_recommandation: Please make sure you have an adequate and reactive moderation team before you open registrations to everyone!
|
moderation_recommandation: Please make sure you have an adequate and reactive moderation team before you open registrations to everyone!
|
||||||
preamble: Control who can create an account on your server.
|
preamble: Control who can create an account on your server.
|
||||||
|
|||||||
@ -34,4 +34,6 @@
|
|||||||
/search
|
/search
|
||||||
/start/(*any)
|
/start/(*any)
|
||||||
/statuses/(*any)
|
/statuses/(*any)
|
||||||
|
/overview
|
||||||
|
/overview/about
|
||||||
).each { |path| get path, to: 'home#index' }
|
).each { |path| get path, to: 'home#index' }
|
||||||
|
|||||||
@ -164,7 +164,7 @@
|
|||||||
"@vitest/browser-playwright": "^4.1.0",
|
"@vitest/browser-playwright": "^4.1.0",
|
||||||
"@vitest/coverage-v8": "^4.1.0",
|
"@vitest/coverage-v8": "^4.1.0",
|
||||||
"@vitest/ui": "^4.1.0",
|
"@vitest/ui": "^4.1.0",
|
||||||
"chromatic": "^16.0.0",
|
"chromatic": "^17.0.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-import-resolver-typescript": "^4.2.5",
|
"eslint-import-resolver-typescript": "^4.2.5",
|
||||||
"eslint-plugin-formatjs": "^6.0.0",
|
"eslint-plugin-formatjs": "^6.0.0",
|
||||||
|
|||||||
@ -78,6 +78,17 @@ RSpec.describe 'Reports' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with both resolved and unresolved params' do
|
||||||
|
let(:params) { { resolved: true, unresolved: true } }
|
||||||
|
let(:scope) { Report.all }
|
||||||
|
|
||||||
|
it 'returns all reports' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response.parsed_body).to match_array(expected_response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with account_id param' do
|
context 'with account_id param' do
|
||||||
let(:params) { { account_id: reporter.id } }
|
let(:params) { { account_id: reporter.id } }
|
||||||
let(:scope) { Report.unresolved.where(account: reporter) }
|
let(:scope) { Report.unresolved.where(account: reporter) }
|
||||||
|
|||||||
@ -12,6 +12,6 @@ RSpec.describe 'UnloggedBrowsing', :js, :streaming do
|
|||||||
it 'loads the home page' do
|
it 'loads the home page' do
|
||||||
expect(subject).to have_css('div.app-holder')
|
expect(subject).to have_css('div.app-holder')
|
||||||
|
|
||||||
expect(subject).to have_css('div.columns-area__panels__main')
|
expect(subject).to have_css('main.columns-area__panels__main')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
21
yarn.lock
21
yarn.lock
@ -2964,7 +2964,7 @@ __metadata:
|
|||||||
axios: "npm:^1.4.0"
|
axios: "npm:^1.4.0"
|
||||||
babel-plugin-transform-react-remove-prop-types: "npm:^0.4.24"
|
babel-plugin-transform-react-remove-prop-types: "npm:^0.4.24"
|
||||||
blurhash: "npm:^2.0.5"
|
blurhash: "npm:^2.0.5"
|
||||||
chromatic: "npm:^16.0.0"
|
chromatic: "npm:^17.0.0"
|
||||||
classnames: "npm:^2.3.2"
|
classnames: "npm:^2.3.2"
|
||||||
cocoon-js-vanilla: "npm:^1.5.1"
|
cocoon-js-vanilla: "npm:^1.5.1"
|
||||||
color-blend: "npm:^4.0.0"
|
color-blend: "npm:^4.0.0"
|
||||||
@ -6830,22 +6830,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"chromatic@npm:^16.0.0":
|
"chromatic@npm:^17.0.0":
|
||||||
version: 16.0.0
|
version: 17.0.0
|
||||||
resolution: "chromatic@npm:16.0.0"
|
resolution: "chromatic@npm:17.0.0"
|
||||||
|
dependencies:
|
||||||
|
semver: "npm:^7.3.5"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@chromatic-com/cypress": ^0.*.* || ^1.0.0
|
"@chromatic-com/cypress": ^0.*.* || ^1.0.0
|
||||||
"@chromatic-com/playwright": ^0.*.* || ^1.0.0
|
"@chromatic-com/playwright": ^0.*.* || ^1.0.0
|
||||||
|
"@chromatic-com/vitest": ^0.*.* || ^1.0.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
"@chromatic-com/cypress":
|
"@chromatic-com/cypress":
|
||||||
optional: true
|
optional: true
|
||||||
"@chromatic-com/playwright":
|
"@chromatic-com/playwright":
|
||||||
optional: true
|
optional: true
|
||||||
|
"@chromatic-com/vitest":
|
||||||
|
optional: true
|
||||||
bin:
|
bin:
|
||||||
chroma: dist/bin.js
|
chroma: dist/bin.cjs
|
||||||
chromatic: dist/bin.js
|
chromatic: dist/bin.cjs
|
||||||
chromatic-cli: dist/bin.js
|
chromatic-cli: dist/bin.cjs
|
||||||
checksum: 10c0/ebebbf1c7d57e1ee9863997416c5125aab0a1886dce60fcb0358d34a51e0e1a45edc4635c8f8fb56d9facbcf21cd48014320c550f723b4791da51dde8552ee2b
|
checksum: 10c0/962c86feec17b12757fa3327b7e98abff272a048c03227bb21ecb51c7f1d7ec3589386611ece8e2c413ac4049f26d71e9c9226a5fb7628fdcbb47e87905863a0
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user