Merge pull request #3489 from glitch-soc/glitch-soc/merge-upstream
Merge upstream changes up to 3473b8a65278783bc74ce2738aea98cca0c7a5ed
This commit is contained in:
commit
785d612ab2
@ -587,7 +587,7 @@ GEM
|
|||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostruct (0.6.3)
|
ostruct (0.6.3)
|
||||||
ox (2.14.23)
|
ox (2.14.25)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
parallel (1.28.0)
|
parallel (1.28.0)
|
||||||
parser (3.3.11.1)
|
parser (3.3.11.1)
|
||||||
|
|||||||
@ -62,7 +62,7 @@ module Admin
|
|||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params
|
params
|
||||||
.expect(user_role: [:name, :color, :highlighted, :position, :require_2fa, permissions_as_keys: []])
|
.expect(user_role: [:name, :color, :highlighted, :position, :require_2fa, :collection_limit, permissions_as_keys: []])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -17,7 +17,10 @@ class CollectionsController < ApplicationController
|
|||||||
|
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
# TODO: format.html
|
format.html do
|
||||||
|
expires_in expiration_duration, public: true unless user_signed_in?
|
||||||
|
render template: 'home/index'
|
||||||
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
expires_in expiration_duration, public: true if public_fetch_mode?
|
expires_in expiration_duration, public: true if public_fetch_mode?
|
||||||
@ -28,8 +31,17 @@ class CollectionsController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
if account_id_param.present?
|
||||||
|
@account = Account.local.find(account_id_param)
|
||||||
|
else
|
||||||
|
@collection = Collection.find(params[:id])
|
||||||
|
@account = @collection.account
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_collection
|
def set_collection
|
||||||
@collection = @account.collections.find(params[:id])
|
@collection ||= @account.collections.find(params[:id])
|
||||||
authorize @collection, :show?
|
authorize @collection, :show?
|
||||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
|
|||||||
@ -161,10 +161,11 @@ export function resetCompose() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const focusCompose = (defaultText = '') => (dispatch, getState) => {
|
export const focusCompose = (defaultText = '', caretStart = false) => (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_FOCUS,
|
type: COMPOSE_FOCUS,
|
||||||
defaultText,
|
defaultText,
|
||||||
|
caretStart,
|
||||||
});
|
});
|
||||||
|
|
||||||
ensureComposeIsVisible(getState);
|
ensureComposeIsVisible(getState);
|
||||||
|
|||||||
@ -11,7 +11,8 @@ export interface ApiCollectionJSON {
|
|||||||
account_id: string;
|
account_id: string;
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
uri: string | null;
|
uri: string;
|
||||||
|
url: string;
|
||||||
local: boolean;
|
local: boolean;
|
||||||
item_count: number;
|
item_count: number;
|
||||||
|
|
||||||
@ -56,7 +57,7 @@ export interface CollectionAccountItem {
|
|||||||
id: string;
|
id: string;
|
||||||
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
|
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
|
||||||
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
|
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
|
||||||
position: number;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WrappedCollectionAccountItem {
|
export interface WrappedCollectionAccountItem {
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { useState, useCallback, useRef, useId } from 'react';
|
|||||||
|
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
OffsetValue,
|
OffsetValue,
|
||||||
UsePopperOptions,
|
UsePopperOptions,
|
||||||
@ -18,9 +20,10 @@ import classes from './styles.module.scss';
|
|||||||
const offset = [0, 4] as OffsetValue;
|
const offset = [0, 4] as OffsetValue;
|
||||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||||
|
|
||||||
export const AltTextBadge: React.FC<{ description: string }> = ({
|
export const AltTextBadge: React.FC<{
|
||||||
description,
|
description: string;
|
||||||
}) => {
|
className?: string;
|
||||||
|
}> = ({ description, className }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const uniqueId = useId();
|
const uniqueId = useId();
|
||||||
const popoverId = `${uniqueId}-popover`;
|
const popoverId = `${uniqueId}-popover`;
|
||||||
@ -48,7 +51,7 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
|
|||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
className='media-gallery__alt__label'
|
className={classNames('media-gallery__alt__label', className)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-controls={popoverId}
|
aria-controls={popoverId}
|
||||||
|
|||||||
@ -24,9 +24,14 @@ export const NumberFieldsItem: React.FC<ItemProps> = ({
|
|||||||
link,
|
link,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
...restProps
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<li className={classNames(classes.item, className)} title={hint}>
|
<li
|
||||||
|
{...restProps}
|
||||||
|
className={classNames(classes.item, className)}
|
||||||
|
title={hint}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
{link ? (
|
{link ? (
|
||||||
<NavLink exact to={link}>
|
<NavLink exact to={link}>
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a,
|
||||||
|
button,
|
||||||
strong {
|
strong {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -25,10 +26,21 @@
|
|||||||
a {
|
a {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
button {
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
appearance: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { PureComponent } from 'react';
|
|||||||
|
|
||||||
import { FormattedMessage, defineMessages } from 'react-intl';
|
import { FormattedMessage, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ import { injectIntl } from './intl';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
|
aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
|
||||||
|
aboutThisServer: { id: 'server_banner.more_about_this_server', defaultMessage: 'More about this server'},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
@ -47,9 +48,14 @@ class ServerBanner extends PureComponent {
|
|||||||
<FormattedMessage id='server_banner.is_one_of_many' defaultMessage='{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank' rel='noopener'>Mastodon</a> }} />
|
<FormattedMessage id='server_banner.is_one_of_many' defaultMessage='{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank' rel='noopener'>Mastodon</a> }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to='/about'>
|
<NavLink to='/about'>
|
||||||
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
|
<ServerHeroImage
|
||||||
</Link>
|
blurhash={server.getIn(['thumbnail', 'blurhash'])}
|
||||||
|
src={server.getIn(['thumbnail', 'url'])}
|
||||||
|
alt={intl.formatMessage(messages.aboutThisServer)}
|
||||||
|
className='server-banner__hero'
|
||||||
|
/>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
<div className='server-banner__description'>
|
<div className='server-banner__description'>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@ -2,9 +2,14 @@ import { useCallback, useState } from 'react';
|
|||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Blurhash } from './blurhash';
|
import { AltTextBadge } from '../alt_text_badge';
|
||||||
|
import { Blurhash } from '../blurhash';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
withAltBadge?: boolean;
|
||||||
|
alt: string;
|
||||||
src: string;
|
src: string;
|
||||||
srcSet?: string;
|
srcSet?: string;
|
||||||
blurhash?: string;
|
blurhash?: string;
|
||||||
@ -12,9 +17,11 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ServerHeroImage: React.FC<Props> = ({
|
export const ServerHeroImage: React.FC<Props> = ({
|
||||||
|
alt,
|
||||||
src,
|
src,
|
||||||
srcSet,
|
srcSet,
|
||||||
blurhash,
|
blurhash,
|
||||||
|
withAltBadge,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
@ -24,12 +31,12 @@ export const ServerHeroImage: React.FC<Props> = ({
|
|||||||
}, [setLoaded]);
|
}, [setLoaded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classNames('image', { loaded }, className)}>
|
||||||
className={classNames('image', { loaded }, className)}
|
|
||||||
role='presentation'
|
|
||||||
>
|
|
||||||
{blurhash && <Blurhash hash={blurhash} className='image__preview' />}
|
{blurhash && <Blurhash hash={blurhash} className='image__preview' />}
|
||||||
<img src={src} srcSet={srcSet} alt='' onLoad={handleLoad} />
|
<img src={src} srcSet={srcSet} alt={alt} onLoad={handleLoad} />
|
||||||
|
{withAltBadge && alt && (
|
||||||
|
<AltTextBadge description={alt} className={classes.altBadge} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
.altBadge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
inset-inline-end: 8px;
|
||||||
|
}
|
||||||
@ -30,6 +30,7 @@ import StatusActionBar from './status_action_bar';
|
|||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
import StatusIcons from './status_icons';
|
import StatusIcons from './status_icons';
|
||||||
import StatusPrepend from './status_prepend';
|
import StatusPrepend from './status_prepend';
|
||||||
|
import { CollectionPreviewCard } from '../features/collections/components/collection_preview_card';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
@ -642,14 +643,30 @@ class Status extends ImmutablePureComponent {
|
|||||||
mediaIcons.push('video-camera');
|
mediaIcons.push('video-camera');
|
||||||
}
|
}
|
||||||
} else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted && !status.get('quote')) {
|
} else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted && !status.get('quote')) {
|
||||||
media.push(
|
const cardUrl = status.getIn(['card', 'url']);
|
||||||
<Card
|
|
||||||
key={`${status.get('id')}-${status.get('edited_at')}`}
|
const taggedCollection = (
|
||||||
card={status.get('card')}
|
status.get('tagged_collections')
|
||||||
sensitive={status.get('sensitive')}
|
).find((item) => compareUrls(item.get('url'), cardUrl));
|
||||||
/>,
|
if (taggedCollection) {
|
||||||
);
|
media.push(<CollectionPreviewCard collection={taggedCollection} />);
|
||||||
|
} else {
|
||||||
|
media.push(
|
||||||
|
<Card
|
||||||
|
key={`${status.get('id')}-${status.get('edited_at')}`}
|
||||||
|
card={status.get('card')}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
mediaIcons.push('link');
|
mediaIcons.push('link');
|
||||||
|
} else if (status.get('tagged_collections').size && settings.get('inline_preview_cards') && !this.props.muted) {
|
||||||
|
const firstLinkedCollection = status.get('tagged_collections').first();
|
||||||
|
if (firstLinkedCollection) {
|
||||||
|
media = (
|
||||||
|
<CollectionPreviewCard collection={firstLinkedCollection.toJS()} />
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('poll')) {
|
if (status.get('poll')) {
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import type { ComponentProps, FC } from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { ApiCollectionJSON } from '@/flavours/glitch/api_types/collections';
|
||||||
import type { ApiMentionJSON } from '@/flavours/glitch/api_types/statuses';
|
import type { ApiMentionJSON } from '@/flavours/glitch/api_types/statuses';
|
||||||
|
import { getCollectionPath } from '@/flavours/glitch/features/collections/utils';
|
||||||
import { useAppSelector } from '@/flavours/glitch/store';
|
import { useAppSelector } from '@/flavours/glitch/store';
|
||||||
import type { OnElementHandler } from '@/flavours/glitch/utils/html';
|
import type { OnElementHandler } from '@/flavours/glitch/utils/html';
|
||||||
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
|
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
|
||||||
@ -15,6 +17,7 @@ export interface HandledLinkProps {
|
|||||||
prevText?: string;
|
prevText?: string;
|
||||||
hashtagAccountId?: string;
|
hashtagAccountId?: string;
|
||||||
mention?: Pick<ApiMentionJSON, 'id' | 'acct' | 'username'>;
|
mention?: Pick<ApiMentionJSON, 'id' | 'acct' | 'username'>;
|
||||||
|
collection?: Pick<ApiCollectionJSON, 'id'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const textMatchesTarget = (text: string, origin: string, host: string) => {
|
const textMatchesTarget = (text: string, origin: string, host: string) => {
|
||||||
@ -111,6 +114,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
|||||||
prevText,
|
prevText,
|
||||||
hashtagAccountId,
|
hashtagAccountId,
|
||||||
mention,
|
mention,
|
||||||
|
collection,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
@ -180,6 +184,15 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
|||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
} else if (collection) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={classNames(className)}
|
||||||
|
to={getCollectionPath(collection.id)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-absolute paths treated as internal links. This shouldn't happen, but just in case.
|
// Non-absolute paths treated as internal links. This shouldn't happen, but just in case.
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { languages as preloadedLanguages } from 'flavours/glitch/initial_state';
|
|||||||
import { EmojiHTML } from './emoji/html';
|
import { EmojiHTML } from './emoji/html';
|
||||||
import { injectIntl } from './intl';
|
import { injectIntl } from './intl';
|
||||||
import { HandledLink } from './status/handled_link';
|
import { HandledLink } from './status/handled_link';
|
||||||
|
import { compareUrls } from '../utils/compare_urls';
|
||||||
|
|
||||||
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||||
|
|
||||||
@ -71,17 +72,6 @@ const mapStateToProps = state => ({
|
|||||||
languages: state.getIn(['server', 'translationLanguages', 'items']),
|
languages: state.getIn(['server', 'translationLanguages', 'items']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const compareUrls = (href1, href2) => {
|
|
||||||
try {
|
|
||||||
const url1 = new URL(href1);
|
|
||||||
const url2 = new URL(href2);
|
|
||||||
|
|
||||||
return url1.origin === url2.origin && url1.pathname === url2.pathname && url1.search === url2.search;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class StatusContent extends PureComponent {
|
class StatusContent extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
identity: identityContextPropShape,
|
identity: identityContextPropShape,
|
||||||
@ -166,7 +156,13 @@ class StatusContent extends PureComponent {
|
|||||||
|
|
||||||
handleElement = (element, { key, ...props }, children) => {
|
handleElement = (element, { key, ...props }, children) => {
|
||||||
if (element instanceof HTMLAnchorElement) {
|
if (element instanceof HTMLAnchorElement) {
|
||||||
const mention = this.props.status.get('mentions').find(item => compareUrls(element.href, item.get('url')));
|
const mention = this.props.status.get('mentions').find(
|
||||||
|
item => compareUrls(element.href, item.get('url'))
|
||||||
|
);
|
||||||
|
const taggedCollection = this.props.status.get('tagged_collections').find(
|
||||||
|
item => compareUrls(element.href, item.get('url'))
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HandledLink
|
<HandledLink
|
||||||
{...props}
|
{...props}
|
||||||
@ -174,6 +170,7 @@ class StatusContent extends PureComponent {
|
|||||||
text={element.innerText}
|
text={element.innerText}
|
||||||
hashtagAccountId={this.props.status.getIn(['account', 'id'])}
|
hashtagAccountId={this.props.status.getIn(['account', 'id'])}
|
||||||
mention={mention?.toJSON()}
|
mention={mention?.toJSON()}
|
||||||
|
collection={taggedCollection?.toJSON()}
|
||||||
key={key}
|
key={key}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -82,7 +82,14 @@ class About extends PureComponent {
|
|||||||
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
|
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
|
||||||
<div className='scrollable about'>
|
<div className='scrollable about'>
|
||||||
<div className='about__header'>
|
<div className='about__header'>
|
||||||
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
|
<ServerHeroImage
|
||||||
|
withAltBadge
|
||||||
|
alt={server.getIn(['thumbnail', 'description']) ?? ''}
|
||||||
|
blurhash={server.getIn(['thumbnail', 'blurhash'])}
|
||||||
|
src={server.getIn(['thumbnail', 'url'])}
|
||||||
|
srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')}
|
||||||
|
className='about__header__hero'
|
||||||
|
/>
|
||||||
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
|
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
|
||||||
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank' rel='noopener'>Mastodon</a> }} /></p>
|
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank' rel='noopener'>Mastodon</a> }} /></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export const AccountEditEmptyColumn: FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} className={classes.column}>
|
<Column bindToDocument={!multiColumn}>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
@ -38,7 +38,7 @@ export const AccountEditColumn: FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Column bindToDocument={!multiColumn} className={classes.column}>
|
<Column bindToDocument={!multiColumn}>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
title={title}
|
title={title}
|
||||||
className={classes.columnHeader}
|
className={classes.columnHeader}
|
||||||
@ -53,7 +53,7 @@ export const AccountEditColumn: FC<{
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{children}
|
<div className='scrollable'>{children}</div>
|
||||||
</Column>
|
</Column>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { ToggleField } from '@/flavours/glitch/components/form_fields';
|
|||||||
import { useElementHandledLink } from '@/flavours/glitch/components/status/handled_link';
|
import { useElementHandledLink } from '@/flavours/glitch/components/status/handled_link';
|
||||||
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
||||||
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
|
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
|
||||||
|
import { useCustomEmojis } from '@/flavours/glitch/hooks/useCustomEmojis';
|
||||||
import { autoPlayGif } from '@/flavours/glitch/initial_state';
|
import { autoPlayGif } from '@/flavours/glitch/initial_state';
|
||||||
import {
|
import {
|
||||||
fetchProfile,
|
fetchProfile,
|
||||||
@ -175,7 +176,7 @@ export const AccountEdit: FC = () => {
|
|||||||
}, [dispatch, profile?.bot]);
|
}, [dispatch, profile?.bot]);
|
||||||
|
|
||||||
// Normally we would use the account emoji, but we want all custom emojis to be available to render after editing.
|
// Normally we would use the account emoji, but we want all custom emojis to be available to render after editing.
|
||||||
const emojis = useAppSelector((state) => state.custom_emojis);
|
const emojis = useCustomEmojis();
|
||||||
const htmlHandlers = useElementHandledLink({
|
const htmlHandlers = useElementHandledLink({
|
||||||
hashtagAccountId: profile?.id,
|
hashtagAccountId: profile?.id,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
import { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import type { FC, FocusEventHandler } from 'react';
|
import type { FC, FocusEventHandler } from 'react';
|
||||||
|
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
@ -9,6 +15,7 @@ import { closeModal } from '@/flavours/glitch/actions/modal';
|
|||||||
import { Button } from '@/flavours/glitch/components/button';
|
import { Button } from '@/flavours/glitch/components/button';
|
||||||
import type { FieldStatus } from '@/flavours/glitch/components/form_fields';
|
import type { FieldStatus } from '@/flavours/glitch/components/form_fields';
|
||||||
import { EmojiTextInputField } from '@/flavours/glitch/components/form_fields';
|
import { EmojiTextInputField } from '@/flavours/glitch/components/form_fields';
|
||||||
|
import { useCustomEmojis } from '@/flavours/glitch/hooks/useCustomEmojis';
|
||||||
import {
|
import {
|
||||||
removeField,
|
removeField,
|
||||||
selectFieldById,
|
selectFieldById,
|
||||||
@ -104,11 +111,6 @@ const selectFieldLimits = createAppSelector(
|
|||||||
|
|
||||||
const RECOMMENDED_LIMIT = 40;
|
const RECOMMENDED_LIMIT = 40;
|
||||||
|
|
||||||
const selectEmojiCodes = createAppSelector(
|
|
||||||
[(state) => state.custom_emojis],
|
|
||||||
(emojis) => emojis.map((emoji) => emoji.get('shortcode')).toArray(),
|
|
||||||
);
|
|
||||||
|
|
||||||
interface ConfirmationMessage {
|
interface ConfirmationMessage {
|
||||||
message: string;
|
message: string;
|
||||||
confirm: string;
|
confirm: string;
|
||||||
@ -143,7 +145,11 @@ export const EditFieldModal = forwardRef<
|
|||||||
value?: FieldStatus;
|
value?: FieldStatus;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
const customEmojiCodes = useAppSelector(selectEmojiCodes);
|
const customEmojis = useCustomEmojis();
|
||||||
|
const customEmojiCodes = useMemo(
|
||||||
|
() => Object.keys(customEmojis ?? {}),
|
||||||
|
[customEmojis],
|
||||||
|
);
|
||||||
const checkField = useCallback(
|
const checkField = useCallback(
|
||||||
(value: string): FieldStatus | null => {
|
(value: string): FieldStatus | null => {
|
||||||
if (!value.trim()) {
|
if (!value.trim()) {
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import { CSS } from '@dnd-kit/utilities';
|
|||||||
import { CustomEmojiProvider } from '@/flavours/glitch/components/emoji/context';
|
import { CustomEmojiProvider } from '@/flavours/glitch/components/emoji/context';
|
||||||
import { normalizeKey } from '@/flavours/glitch/components/hotkeys/utils';
|
import { normalizeKey } from '@/flavours/glitch/components/hotkeys/utils';
|
||||||
import { Icon } from '@/flavours/glitch/components/icon';
|
import { Icon } from '@/flavours/glitch/components/icon';
|
||||||
|
import { useCustomEmojis } from '@/flavours/glitch/hooks/useCustomEmojis';
|
||||||
import type { FieldData } from '@/flavours/glitch/reducers/slices/profile_edit';
|
import type { FieldData } from '@/flavours/glitch/reducers/slices/profile_edit';
|
||||||
import {
|
import {
|
||||||
patchProfile,
|
patchProfile,
|
||||||
@ -217,7 +218,7 @@ export const ReorderFieldsModal: FC<DialogModalProps> = ({ onClose }) => {
|
|||||||
void dispatch(patchProfile({ fields_attributes: newFields })).then(onClose);
|
void dispatch(patchProfile({ fields_attributes: newFields })).then(onClose);
|
||||||
}, [dispatch, fieldKeys, fields, onClose]);
|
}, [dispatch, fieldKeys, fields, onClose]);
|
||||||
|
|
||||||
const emojis = useAppSelector((state) => state.custom_emojis);
|
const emojis = useCustomEmojis();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Add a wrapper here in the capture phase, so that it can be intercepted before the window listener in ModalRoot.
|
// Add a wrapper here in the capture phase, so that it can be intercepted before the window listener in ModalRoot.
|
||||||
|
|||||||
@ -126,11 +126,6 @@
|
|||||||
|
|
||||||
// Column component
|
// Column component
|
||||||
|
|
||||||
.column {
|
|
||||||
border: 1px solid var(--color-border-primary);
|
|
||||||
border-top-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.columnHeader {
|
.columnHeader {
|
||||||
:global(.column-header__buttons) {
|
:global(.column-header__buttons) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -258,6 +253,10 @@
|
|||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-bottom: 1px solid var(--color-border-primary);
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionHeader {
|
.sectionHeader {
|
||||||
|
|||||||
@ -25,12 +25,9 @@ import Column from 'flavours/glitch/features/ui/components/column';
|
|||||||
import { useAccount } from 'flavours/glitch/hooks/useAccount';
|
import { useAccount } from 'flavours/glitch/hooks/useAccount';
|
||||||
import { useAccountId } from 'flavours/glitch/hooks/useAccountId';
|
import { useAccountId } from 'flavours/glitch/hooks/useAccountId';
|
||||||
import { useAccountVisibility } from 'flavours/glitch/hooks/useAccountVisibility';
|
import { useAccountVisibility } from 'flavours/glitch/hooks/useAccountVisibility';
|
||||||
import {
|
|
||||||
fetchAccountCollections,
|
|
||||||
selectAccountCollections,
|
|
||||||
} from 'flavours/glitch/reducers/slices/collections';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import { useAccountCollections } from '../collections';
|
||||||
import { CollectionListItem } from '../collections/components/collection_list_item';
|
import { CollectionListItem } from '../collections/components/collection_list_item';
|
||||||
import { areCollectionsEnabled } from '../collections/utils';
|
import { areCollectionsEnabled } from '../collections/utils';
|
||||||
|
|
||||||
@ -59,10 +56,6 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
void dispatch(fetchEndorsedAccounts({ accountId }));
|
void dispatch(fetchEndorsedAccounts({ accountId }));
|
||||||
|
|
||||||
if (collectionsEnabled) {
|
|
||||||
void dispatch(fetchAccountCollections({ accountId }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [accountId, dispatch]);
|
}, [accountId, dispatch]);
|
||||||
|
|
||||||
@ -73,9 +66,8 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
|||||||
ImmutableList(),
|
ImmutableList(),
|
||||||
) as ImmutableList<string>,
|
) as ImmutableList<string>,
|
||||||
);
|
);
|
||||||
const { collections, status: collectionsLoadStatus } = useAppSelector(
|
const { collections, status: collectionsLoadStatus } =
|
||||||
(state) => selectAccountCollections(state, accountId ?? null),
|
useAccountCollections(accountId);
|
||||||
);
|
|
||||||
|
|
||||||
const { listedCollections = [], unlistedCollections = [] } = Object.groupBy(
|
const { listedCollections = [], unlistedCollections = [] } = Object.groupBy(
|
||||||
collections,
|
collections,
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { openModal } from '@/flavours/glitch/actions/modal';
|
||||||
import { FormattedDateWrapper } from '@/flavours/glitch/components/formatted_date';
|
import { FormattedDateWrapper } from '@/flavours/glitch/components/formatted_date';
|
||||||
import {
|
import {
|
||||||
NumberFields,
|
NumberFields,
|
||||||
@ -10,6 +11,7 @@ import {
|
|||||||
} from '@/flavours/glitch/components/number_fields';
|
} from '@/flavours/glitch/components/number_fields';
|
||||||
import { ShortNumber } from '@/flavours/glitch/components/short_number';
|
import { ShortNumber } from '@/flavours/glitch/components/short_number';
|
||||||
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
||||||
|
import { useAppDispatch } from '@/flavours/glitch/store';
|
||||||
|
|
||||||
export const AccountNumberFields: FC<{ accountId: string }> = ({
|
export const AccountNumberFields: FC<{ accountId: string }> = ({
|
||||||
accountId,
|
accountId,
|
||||||
@ -21,6 +23,13 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({
|
|||||||
[account?.created_at],
|
[account?.created_at],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const showJoinModal = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
openModal({ modalType: 'ACCOUNT_JOIN_DATE', modalProps: { accountId } }),
|
||||||
|
);
|
||||||
|
}, [accountId, dispatch]);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -60,15 +69,17 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({
|
|||||||
}
|
}
|
||||||
hint={intl.formatDate(account.created_at)}
|
hint={intl.formatDate(account.created_at)}
|
||||||
>
|
>
|
||||||
{createdThisYear ? (
|
<button type='button' onClick={showJoinModal}>
|
||||||
<FormattedDateWrapper
|
{createdThisYear ? (
|
||||||
value={account.created_at}
|
<FormattedDateWrapper
|
||||||
month='short'
|
value={account.created_at}
|
||||||
day='2-digit'
|
month='short'
|
||||||
/>
|
day='2-digit'
|
||||||
) : (
|
/>
|
||||||
<FormattedDateWrapper value={account.created_at} year='numeric' />
|
) : (
|
||||||
)}
|
<FormattedDateWrapper value={account.created_at} year='numeric' />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</NumberFieldsItem>
|
</NumberFieldsItem>
|
||||||
</NumberFields>
|
</NumberFields>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
import type { AccountField } from '../common';
|
import type { AccountField } from '../common';
|
||||||
import { useFieldHtml } from '../hooks/useFieldHtml';
|
import { useFieldHtml } from '../hooks/useFieldHtml';
|
||||||
|
|
||||||
import classes from './styles.module.css';
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
export const AccountFieldModal: FC<{
|
export const AccountFieldModal: FC<{
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|||||||
@ -0,0 +1,273 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { focusCompose, resetCompose } from '@/flavours/glitch/actions/compose';
|
||||||
|
import { closeModal } from '@/flavours/glitch/actions/modal';
|
||||||
|
import { Button } from '@/flavours/glitch/components/button';
|
||||||
|
import { DisplayNameSimple } from '@/flavours/glitch/components/display_name/simple';
|
||||||
|
import { FormattedDateWrapper } from '@/flavours/glitch/components/formatted_date';
|
||||||
|
import { IconButton } from '@/flavours/glitch/components/icon_button';
|
||||||
|
import {
|
||||||
|
ModalShell,
|
||||||
|
ModalShellBody,
|
||||||
|
} from '@/flavours/glitch/components/modal_shell';
|
||||||
|
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
||||||
|
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
|
||||||
|
import {
|
||||||
|
createAppSelector,
|
||||||
|
useAppDispatch,
|
||||||
|
useAppSelector,
|
||||||
|
} from '@/flavours/glitch/store';
|
||||||
|
import AnniversaryImage from '@/images/anniversary.svg?react';
|
||||||
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
const closeMessage = defineMessage({
|
||||||
|
id: 'lightbox.close',
|
||||||
|
defaultMessage: 'Close',
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectServerName = createAppSelector(
|
||||||
|
[
|
||||||
|
(state) => state.accounts,
|
||||||
|
(_, accountId: string) => accountId,
|
||||||
|
(state) => state.server.getIn(['server', 'domain']) as string | undefined,
|
||||||
|
],
|
||||||
|
(accounts, accountId, serverDomain) => {
|
||||||
|
const acct = accounts.getIn([accountId, 'acct']) as string | undefined;
|
||||||
|
if (!acct) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = acct.split('@').at(1);
|
||||||
|
if (domain) {
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverDomain;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AccountJoinModal: FC<{
|
||||||
|
accountId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}> = ({ accountId, onClose }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const account = useAccount(accountId);
|
||||||
|
const currentId = useCurrentAccountId();
|
||||||
|
const isMe = accountId === currentId;
|
||||||
|
|
||||||
|
const createdAtStr = account?.created_at;
|
||||||
|
const anniversary = useMemo(() => {
|
||||||
|
if (!createdAtStr) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
const createdAt = new Date(createdAtStr);
|
||||||
|
if (
|
||||||
|
now.getMonth() === createdAt.getMonth() &&
|
||||||
|
now.getDate() === createdAt.getDate()
|
||||||
|
) {
|
||||||
|
return now.getFullYear() - createdAt.getFullYear();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [createdAtStr]);
|
||||||
|
|
||||||
|
const domain = useAppSelector((state) => selectServerName(state, accountId));
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handle = account?.acct;
|
||||||
|
const handleShare = useCallback(() => {
|
||||||
|
if (anniversary === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shareText = '#Fediversary';
|
||||||
|
if (anniversary === 0) {
|
||||||
|
shareText = isMe ? '#firstday' : '#welcome';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMe && handle) {
|
||||||
|
shareText = `@${handle} ${shareText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(resetCompose());
|
||||||
|
dispatch(focusCompose(`\n\n${shareText}`, true));
|
||||||
|
dispatch(closeModal({ modalType: 'ACCOUNT_JOIN_DATE', ignoreFocus: true }));
|
||||||
|
}, [anniversary, handle, dispatch, isMe]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalShell className={classes.joinShell}>
|
||||||
|
<ModalShellBody className={classes.joinWrapper}>
|
||||||
|
<AccountAnniversaryImage anniversary={anniversary} />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<AccountJoinMessage
|
||||||
|
name={<DisplayNameSimple account={account} />}
|
||||||
|
isMe={isMe}
|
||||||
|
serverName={domain}
|
||||||
|
anniversary={anniversary}
|
||||||
|
/>
|
||||||
|
<h1>
|
||||||
|
<FormattedDateWrapper
|
||||||
|
value={account?.created_at}
|
||||||
|
month='short'
|
||||||
|
day='numeric'
|
||||||
|
year='numeric'
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AccountAnniversaryShare
|
||||||
|
anniversary={anniversary}
|
||||||
|
onShare={handleShare}
|
||||||
|
isMe={isMe}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
iconComponent={CloseIcon}
|
||||||
|
icon='times'
|
||||||
|
onClick={onClose}
|
||||||
|
title={intl.formatMessage(closeMessage)}
|
||||||
|
className={classes.joinClose}
|
||||||
|
/>
|
||||||
|
</ModalShellBody>
|
||||||
|
</ModalShell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountJoinMessage: FC<{
|
||||||
|
name: React.JSX.Element;
|
||||||
|
isMe: boolean;
|
||||||
|
serverName?: string;
|
||||||
|
anniversary: number | null;
|
||||||
|
}> = ({ name, isMe, serverName, anniversary }) => {
|
||||||
|
if (anniversary === 0) {
|
||||||
|
if (isMe) {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.me_today'
|
||||||
|
defaultMessage='It’s your first day on {server}!'
|
||||||
|
tagName='p'
|
||||||
|
values={{
|
||||||
|
server: serverName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.other_today'
|
||||||
|
defaultMessage='It’s {name}’s first day on {server}!'
|
||||||
|
tagName='p'
|
||||||
|
values={{
|
||||||
|
name,
|
||||||
|
server: serverName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMe) {
|
||||||
|
if (anniversary !== null && anniversary > 0) {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.me_anniversary'
|
||||||
|
defaultMessage='Happy Fediversary! You joined {server} on'
|
||||||
|
tagName='p'
|
||||||
|
values={{
|
||||||
|
server: serverName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.me'
|
||||||
|
defaultMessage='You joined {server} on'
|
||||||
|
tagName='p'
|
||||||
|
values={{
|
||||||
|
server: serverName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.other'
|
||||||
|
defaultMessage='{name} joined {server} on'
|
||||||
|
tagName='p'
|
||||||
|
values={{
|
||||||
|
name,
|
||||||
|
server: serverName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountAnniversaryImage: FC<{ anniversary: number | null }> = ({
|
||||||
|
anniversary,
|
||||||
|
}) => {
|
||||||
|
if (anniversary === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.joinBanner}>
|
||||||
|
<AnniversaryImage role='presentation' />
|
||||||
|
<h2>{anniversary || 1}</h2>
|
||||||
|
{anniversary === 0 && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.day'
|
||||||
|
defaultMessage='Day'
|
||||||
|
tagName='h3'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{anniversary > 0 && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.years'
|
||||||
|
defaultMessage='{number, plural, one {year} other {years}}'
|
||||||
|
values={{ number: anniversary }}
|
||||||
|
tagName='h3'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountAnniversaryShare: FC<{
|
||||||
|
anniversary: number | null;
|
||||||
|
onShare: () => void;
|
||||||
|
isMe: boolean;
|
||||||
|
}> = ({ anniversary, onShare, isMe }) => {
|
||||||
|
if (anniversary === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={onShare}>
|
||||||
|
{anniversary === 0 && isMe && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.share.intro'
|
||||||
|
defaultMessage='Share an intro post'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{anniversary === 0 && !isMe && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.share.welcome'
|
||||||
|
defaultMessage='Share a welcome post'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{anniversary > 0 && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.share.celebrate'
|
||||||
|
defaultMessage='Share a celebratory post'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -13,7 +13,7 @@ import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
|||||||
|
|
||||||
import { ConfirmationModal } from '../../ui/components/confirmation_modals';
|
import { ConfirmationModal } from '../../ui/components/confirmation_modals';
|
||||||
|
|
||||||
import classes from './styles.module.css';
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
newTitle: {
|
newTitle: {
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
.noteCallout {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noteInput {
|
|
||||||
min-height: 70px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: var(--color-bg-primary);
|
|
||||||
border: 1px solid var(--color-border-primary);
|
|
||||||
appearance: none;
|
|
||||||
resize: none;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noteInput:focus-visible {
|
|
||||||
outline: var(--outline-focus-default);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldName,
|
|
||||||
.fieldValue {
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldValue {
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
@use '@/styles/mastodon/variables' as *;
|
||||||
|
|
||||||
|
.noteCallout {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noteInput {
|
||||||
|
min-height: 70px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
appearance: none;
|
||||||
|
resize: none;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noteInput:focus-visible {
|
||||||
|
outline: var(--outline-focus-default);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldName,
|
||||||
|
.fieldValue {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldValue {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: ($mobile-breakpoint + 1)) {
|
||||||
|
.joinShell {
|
||||||
|
> :global(.safety-action-modal__top) {
|
||||||
|
border-bottom-left-radius: 16px;
|
||||||
|
border-bottom-right-radius: 16px;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.joinWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
min-height: 120px;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
p,
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.joinBanner {
|
||||||
|
width: 120px;
|
||||||
|
height: 110px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 40px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.joinClose {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
@ -13,6 +13,8 @@ import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp
|
|||||||
import { useAccount } from 'flavours/glitch/hooks/useAccount';
|
import { useAccount } from 'flavours/glitch/hooks/useAccount';
|
||||||
import { domain } from 'flavours/glitch/initial_state';
|
import { domain } from 'flavours/glitch/initial_state';
|
||||||
|
|
||||||
|
import { getCollectionPath } from '../utils';
|
||||||
|
|
||||||
import classes from './collection_lockup.module.scss';
|
import classes from './collection_lockup.module.scss';
|
||||||
|
|
||||||
export const AvatarGrid: React.FC<{
|
export const AvatarGrid: React.FC<{
|
||||||
@ -27,10 +29,10 @@ export const AvatarGrid: React.FC<{
|
|||||||
sensitive ? classes.avatarGridSensitive : null,
|
sensitive ? classes.avatarGridSensitive : null,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{avatarIds.map((id) => (
|
{avatarIds.map((id, index) => (
|
||||||
<AvatarById
|
<AvatarById
|
||||||
animate={false}
|
animate={false}
|
||||||
key={id}
|
key={index}
|
||||||
accountId={id}
|
accountId={id}
|
||||||
className={classes.avatar}
|
className={classes.avatar}
|
||||||
size={25}
|
size={25}
|
||||||
@ -67,7 +69,7 @@ export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
|||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h2 id={linkId}>
|
<h2 id={linkId}>
|
||||||
<Link to={`/collections/${id}`} className={classes.link}>
|
<Link to={getCollectionPath(id)} className={classes.link}>
|
||||||
{name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import type { MenuItem } from 'flavours/glitch/models/dropdown_menu';
|
|||||||
import { useAppDispatch } from 'flavours/glitch/store';
|
import { useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import { messages as editorMessages } from '../editor';
|
import { messages as editorMessages } from '../editor';
|
||||||
|
import { getCollectionPath } from '../utils';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
view: {
|
view: {
|
||||||
@ -120,7 +121,7 @@ export const CollectionMenu: React.FC<{
|
|||||||
const menu = useMemo(() => {
|
const menu = useMemo(() => {
|
||||||
const viewCollectionItem: MenuItem = {
|
const viewCollectionItem: MenuItem = {
|
||||||
text: intl.formatMessage(messages.view),
|
text: intl.formatMessage(messages.view),
|
||||||
to: `/collections/${id}`,
|
to: getCollectionPath(id),
|
||||||
};
|
};
|
||||||
const shareItems: MenuItem[] = [
|
const shareItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
@ -130,7 +131,7 @@ export const CollectionMenu: React.FC<{
|
|||||||
{
|
{
|
||||||
text: intl.formatMessage(messages.copyLink),
|
text: intl.formatMessage(messages.copyLink),
|
||||||
action: () => {
|
action: () => {
|
||||||
void navigator.clipboard.writeText(`/collections/${id}`);
|
void navigator.clipboard.writeText(getCollectionPath(id));
|
||||||
dispatch(showAlert({ message: messages.copyLinkConfirmation }));
|
dispatch(showAlert({ message: messages.copyLinkConfirmation }));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import type { CollectionLockupProps } from 'flavours/glitch/features/collections/components/collection_lockup';
|
import type { CollectionLockupProps } from 'flavours/glitch/features/collections/components/collection_lockup';
|
||||||
import { CollectionLockup } from 'flavours/glitch/features/collections/components/collection_lockup';
|
import { CollectionLockup } from 'flavours/glitch/features/collections/components/collection_lockup';
|
||||||
|
|
||||||
@ -13,7 +15,7 @@ export const CollectionPreviewCard: React.FC<CollectionPreviewCardProps> = ({
|
|||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={classes.wrapper}>
|
<div className={classNames(classes.wrapper, 'collection-preview')}>
|
||||||
<CollectionLockup collection={collection} {...otherProps} />
|
<CollectionLockup collection={collection} {...otherProps} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import { useHistory } from 'react-router-dom';
|
|||||||
import { isFulfilled } from '@reduxjs/toolkit';
|
import { isFulfilled } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { ComboboxMenuItem } from '@/flavours/glitch/components/form_fields/combobox_field';
|
import { ComboboxMenuItem } from '@/flavours/glitch/components/form_fields/combobox_field';
|
||||||
|
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
||||||
|
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
|
||||||
import { languages } from '@/flavours/glitch/initial_state';
|
import { languages } from '@/flavours/glitch/initial_state';
|
||||||
import {
|
import {
|
||||||
hasSpecialCharacters,
|
hasSpecialCharacters,
|
||||||
@ -36,6 +38,8 @@ import {
|
|||||||
} from 'flavours/glitch/reducers/slices/collections';
|
} from 'flavours/glitch/reducers/slices/collections';
|
||||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import { getCollectionPath } from '../utils';
|
||||||
|
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
import { WizardStepTitle } from './wizard_step_title';
|
import { WizardStepTitle } from './wizard_step_title';
|
||||||
|
|
||||||
@ -93,6 +97,9 @@ export const CollectionDetails: React.FC = () => {
|
|||||||
[dispatch],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const accountId = useCurrentAccountId();
|
||||||
|
const { acct: currentUserName } = useAccount(accountId) ?? {};
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(e: React.FormEvent) => {
|
(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -128,8 +135,8 @@ export const CollectionDetails: React.FC = () => {
|
|||||||
}),
|
}),
|
||||||
).then((result) => {
|
).then((result) => {
|
||||||
if (isFulfilled(result)) {
|
if (isFulfilled(result)) {
|
||||||
history.replace(`/collections`);
|
history.replace(`/@${currentUserName}/collections`);
|
||||||
history.push(`/collections/${result.payload.collection.id}`, {
|
history.push(getCollectionPath(result.payload.collection.id), {
|
||||||
newCollection: true,
|
newCollection: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -146,6 +153,7 @@ export const CollectionDetails: React.FC = () => {
|
|||||||
dispatch,
|
dispatch,
|
||||||
history,
|
history,
|
||||||
accountIds,
|
accountIds,
|
||||||
|
currentUserName,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import {
|
import {
|
||||||
@ -12,6 +12,9 @@ import {
|
|||||||
useLocation,
|
useLocation,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Callout } from '@/flavours/glitch/components/callout';
|
||||||
|
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
|
||||||
|
import { initialState } from '@/flavours/glitch/initial_state';
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
import { Column } from 'flavours/glitch/components/column';
|
import { Column } from 'flavours/glitch/components/column';
|
||||||
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||||
@ -22,8 +25,11 @@ import {
|
|||||||
} from 'flavours/glitch/reducers/slices/collections';
|
} from 'flavours/glitch/reducers/slices/collections';
|
||||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import { useAccountCollections } from '..';
|
||||||
|
|
||||||
import { CollectionAccounts } from './accounts';
|
import { CollectionAccounts } from './accounts';
|
||||||
import { CollectionDetails } from './details';
|
import { CollectionDetails } from './details';
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
create: {
|
create: {
|
||||||
@ -61,11 +67,14 @@ function usePageTitle(id: string | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const userCollectionLimit = initialState?.role?.collection_limit ?? 0;
|
||||||
|
|
||||||
export const CollectionEditorPage: React.FC<{
|
export const CollectionEditorPage: React.FC<{
|
||||||
multiColumn?: boolean;
|
multiColumn?: boolean;
|
||||||
}> = ({ multiColumn }) => {
|
}> = ({ multiColumn }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const accountId = useCurrentAccountId();
|
||||||
const { id = null } = useParams<{ id?: string }>();
|
const { id = null } = useParams<{ id?: string }>();
|
||||||
const { path } = useRouteMatch();
|
const { path } = useRouteMatch();
|
||||||
const collection = useAppSelector((state) =>
|
const collection = useAppSelector((state) =>
|
||||||
@ -73,7 +82,18 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
);
|
);
|
||||||
const editorStateId = useAppSelector((state) => state.collections.editor.id);
|
const editorStateId = useAppSelector((state) => state.collections.editor.id);
|
||||||
const isEditMode = !!id;
|
const isEditMode = !!id;
|
||||||
const isLoading = isEditMode && !collection;
|
|
||||||
|
// When creating a new collection, we load the current account's collections
|
||||||
|
// to determine if they're allowed to create more.
|
||||||
|
const { collections: collectionList, status: collectionListStatus } =
|
||||||
|
useAccountCollections(isEditMode ? null : accountId);
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
(isEditMode && !collection) ||
|
||||||
|
(!isEditMode && collectionListStatus === 'loading');
|
||||||
|
|
||||||
|
const canCreateMoreCollections =
|
||||||
|
isEditMode || collectionList.length < userCollectionLimit;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@ -108,7 +128,7 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
<div className='scrollable'>
|
<div className='scrollable'>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
) : (
|
) : canCreateMoreCollections ? (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
@ -123,6 +143,8 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
render={() => <CollectionDetails />}
|
render={() => <CollectionDetails />}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
) : (
|
||||||
|
<MaxCollectionsCallout />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -133,3 +155,21 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MaxCollectionsCallout: React.FC = () => (
|
||||||
|
<Callout
|
||||||
|
className={classes.maxCollectionsError}
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.maximum_collection_count_reached'
|
||||||
|
defaultMessage='You have created the maximum number of collections'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.maximum_collection_count_description'
|
||||||
|
defaultMessage='Your server allows creation of up to {count} collections.'
|
||||||
|
values={{ count: userCollectionLimit }}
|
||||||
|
/>
|
||||||
|
</Callout>
|
||||||
|
);
|
||||||
|
|||||||
@ -77,3 +77,7 @@
|
|||||||
.suggestionGroup {
|
.suggestionGroup {
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maxCollectionsError {
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|||||||
@ -28,7 +28,12 @@ import {
|
|||||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import { CollectionListItem } from './components/collection_list_item';
|
import { CollectionListItem } from './components/collection_list_item';
|
||||||
import { messages as editorMessages } from './editor';
|
import {
|
||||||
|
messages as editorMessages,
|
||||||
|
MaxCollectionsCallout,
|
||||||
|
userCollectionLimit,
|
||||||
|
} from './editor';
|
||||||
|
import { areCollectionsEnabled } from './utils';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
headingMe: { id: 'column.my_collections', defaultMessage: 'My collections' },
|
headingMe: { id: 'column.my_collections', defaultMessage: 'My collections' },
|
||||||
@ -38,24 +43,27 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function useAccountCollections(accountId: string | null | undefined) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accountId && areCollectionsEnabled()) {
|
||||||
|
void dispatch(fetchAccountCollections({ accountId }));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId]);
|
||||||
|
|
||||||
|
return useAppSelector((state) => selectAccountCollections(state, accountId));
|
||||||
|
}
|
||||||
|
|
||||||
export const Collections: React.FC<{
|
export const Collections: React.FC<{
|
||||||
multiColumn?: boolean;
|
multiColumn?: boolean;
|
||||||
}> = ({ multiColumn }) => {
|
}> = ({ multiColumn }) => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const me = useCurrentAccountId();
|
const me = useCurrentAccountId();
|
||||||
const accountId = useAccountId();
|
const accountId = useAccountId();
|
||||||
const account = useAccount(accountId);
|
const account = useAccount(accountId);
|
||||||
|
|
||||||
const { collections, status } = useAppSelector((state) =>
|
const { collections, status } = useAccountCollections(accountId);
|
||||||
selectAccountCollections(state, accountId),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (accountId) {
|
|
||||||
void dispatch(fetchAccountCollections({ accountId }));
|
|
||||||
}
|
|
||||||
}, [dispatch, accountId]);
|
|
||||||
|
|
||||||
const emptyMessage =
|
const emptyMessage =
|
||||||
status === 'error' || !accountId ? (
|
status === 'error' || !accountId ? (
|
||||||
@ -82,6 +90,7 @@ export const Collections: React.FC<{
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canCreateMoreCollections = collections.length < userCollectionLimit;
|
||||||
const isOwnCollection = accountId === me;
|
const isOwnCollection = accountId === me;
|
||||||
const titleMessage = isOwnCollection
|
const titleMessage = isOwnCollection
|
||||||
? messages.headingMe
|
? messages.headingMe
|
||||||
@ -102,7 +111,9 @@ export const Collections: React.FC<{
|
|||||||
iconComponent={CollectionsFilledIcon}
|
iconComponent={CollectionsFilledIcon}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
extraButton={
|
extraButton={
|
||||||
isOwnCollection && (
|
isOwnCollection &&
|
||||||
|
status === 'idle' &&
|
||||||
|
canCreateMoreCollections && (
|
||||||
<Link
|
<Link
|
||||||
to='/collections/new'
|
to='/collections/new'
|
||||||
className='column-header__button'
|
className='column-header__button'
|
||||||
@ -116,6 +127,9 @@ export const Collections: React.FC<{
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Scrollable>
|
<Scrollable>
|
||||||
|
{status === 'idle' && !canCreateMoreCollections && (
|
||||||
|
<MaxCollectionsCallout />
|
||||||
|
)}
|
||||||
<ItemList emptyMessage={emptyMessage} isLoading={status === 'loading'}>
|
<ItemList emptyMessage={emptyMessage} isLoading={status === 'loading'}>
|
||||||
{collections.map((item, index) => (
|
{collections.map((item, index) => (
|
||||||
<CollectionListItem
|
<CollectionListItem
|
||||||
|
|||||||
@ -3,3 +3,5 @@ import { isServerFeatureEnabled } from '@/flavours/glitch/utils/environment';
|
|||||||
export function areCollectionsEnabled() {
|
export function areCollectionsEnabled() {
|
||||||
return isServerFeatureEnabled('collections');
|
return isServerFeatureEnabled('collections');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getCollectionPath = (id: string) => `/collections/${id}`;
|
||||||
|
|||||||
@ -267,6 +267,11 @@ export async function searchCustomEmojisByShortcodes(shortcodes: string[]) {
|
|||||||
return results.filter((emoji) => shortcodes.includes(emoji.shortcode));
|
return results.filter((emoji) => shortcodes.includes(emoji.shortcode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadAllCustomEmoji() {
|
||||||
|
const db = await loadDB();
|
||||||
|
return db.getAll('custom');
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadLegacyShortcodesByShortcode(shortcode: string) {
|
export async function loadLegacyShortcodesByShortcode(shortcode: string) {
|
||||||
const db = await loadDB();
|
const db = await loadDB();
|
||||||
return db.getFromIndex(
|
return db.getFromIndex(
|
||||||
|
|||||||
@ -185,21 +185,16 @@ export function cleanExtraEmojis(extraEmojis?: CustomEmojiMapArg | null) {
|
|||||||
if (!extraEmojis) {
|
if (!extraEmojis) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (Array.isArray(extraEmojis)) {
|
if (!Array.isArray(extraEmojis) && !isList(extraEmojis)) {
|
||||||
return extraEmojis.reduce<ExtraCustomEmojiMap>(
|
return extraEmojis;
|
||||||
(acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (isList(extraEmojis)) {
|
const emojis: ExtraCustomEmojiMap = {};
|
||||||
return extraEmojis
|
const emojiArray = isList(extraEmojis) ? extraEmojis.toJS() : extraEmojis;
|
||||||
.toJS()
|
for (const emoji of emojiArray) {
|
||||||
.reduce<ExtraCustomEmojiMap>(
|
emojis[emoji.shortcode] = emoji;
|
||||||
(acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return extraEmojis;
|
|
||||||
|
return emojis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import type { Map, List } from 'immutable';
|
|||||||
import type { RenderSlideFn } from '@/flavours/glitch/components/carousel';
|
import type { RenderSlideFn } from '@/flavours/glitch/components/carousel';
|
||||||
import { Carousel } from '@/flavours/glitch/components/carousel';
|
import { Carousel } from '@/flavours/glitch/components/carousel';
|
||||||
import { CustomEmojiProvider } from '@/flavours/glitch/components/emoji/context';
|
import { CustomEmojiProvider } from '@/flavours/glitch/components/emoji/context';
|
||||||
|
import { useCustomEmojis } from '@/flavours/glitch/hooks/useCustomEmojis';
|
||||||
import { mascot } from '@/flavours/glitch/initial_state';
|
import { mascot } from '@/flavours/glitch/initial_state';
|
||||||
import { createAppSelector, useAppSelector } from '@/flavours/glitch/store';
|
import { createAppSelector, useAppSelector } from '@/flavours/glitch/store';
|
||||||
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
||||||
@ -23,7 +24,7 @@ const announcementSelector = createAppSelector(
|
|||||||
|
|
||||||
export const Announcements: FC = () => {
|
export const Announcements: FC = () => {
|
||||||
const announcements = useAppSelector(announcementSelector);
|
const announcements = useAppSelector(announcementSelector);
|
||||||
const emojis = useAppSelector((state) => state.custom_emojis);
|
const emojis = useCustomEmojis();
|
||||||
|
|
||||||
const renderSlide: RenderSlideFn<{
|
const renderSlide: RenderSlideFn<{
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@ -30,10 +30,13 @@ import StatusContent from 'flavours/glitch/components/status_content';
|
|||||||
import { QuotedStatus } from 'flavours/glitch/components/status_quoted';
|
import { QuotedStatus } from 'flavours/glitch/components/status_quoted';
|
||||||
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
|
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
|
||||||
import { Audio } from 'flavours/glitch/features/audio';
|
import { Audio } from 'flavours/glitch/features/audio';
|
||||||
|
import { CollectionPreviewCard } from 'flavours/glitch/features/collections/components/collection_preview_card';
|
||||||
import scheduleIdleTask from 'flavours/glitch/features/ui/util/schedule_idle_task';
|
import scheduleIdleTask from 'flavours/glitch/features/ui/util/schedule_idle_task';
|
||||||
import { Video } from 'flavours/glitch/features/video';
|
import { Video } from 'flavours/glitch/features/video';
|
||||||
import { useIdentity } from 'flavours/glitch/identity_context';
|
import { useIdentity } from 'flavours/glitch/identity_context';
|
||||||
|
import type { CollectionAttachment } from 'flavours/glitch/models/status';
|
||||||
import { useAppSelector } from 'flavours/glitch/store';
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
import { compareUrls } from 'flavours/glitch/utils/compare_urls';
|
||||||
|
|
||||||
import Card from './card';
|
import Card from './card';
|
||||||
|
|
||||||
@ -292,14 +295,34 @@ export const DetailedStatus: React.FC<{
|
|||||||
mediaIcons.push('video-camera');
|
mediaIcons.push('video-camera');
|
||||||
}
|
}
|
||||||
} else if (status.get('card') && !status.get('quote')) {
|
} else if (status.get('card') && !status.get('quote')) {
|
||||||
media = (
|
const cardUrl: string = status.getIn(['card', 'url']);
|
||||||
<Card
|
|
||||||
key={`${status.get('id')}-${status.get('edited_at')}`}
|
const taggedCollection = status
|
||||||
sensitive={status.get('sensitive')}
|
.get('tagged_collections')
|
||||||
card={status.get('card')}
|
.find((item: CollectionAttachment) =>
|
||||||
/>
|
compareUrls(item.get('url'), cardUrl),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (taggedCollection) {
|
||||||
|
media = <CollectionPreviewCard collection={taggedCollection} />;
|
||||||
|
} else {
|
||||||
|
media = (
|
||||||
|
<Card
|
||||||
|
key={`${status.get('id')}-${status.get('edited_at')}`}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
card={status.get('card')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
mediaIcons.push('link');
|
mediaIcons.push('link');
|
||||||
|
} else if (status.get('tagged_collections').size) {
|
||||||
|
const firstLinkedCollection = status.get('tagged_collections').first();
|
||||||
|
if (firstLinkedCollection) {
|
||||||
|
media = (
|
||||||
|
<CollectionPreviewCard collection={firstLinkedCollection.toJS()} />
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('poll')) {
|
if (status.get('poll')) {
|
||||||
|
|||||||
@ -101,6 +101,7 @@ export const MODAL_COMPONENTS = {
|
|||||||
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
|
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
|
||||||
'ACCOUNT_NOTE': () => import('@/flavours/glitch/features/account_timeline/modals/note_modal').then(module => ({ default: module.AccountNoteModal })),
|
'ACCOUNT_NOTE': () => import('@/flavours/glitch/features/account_timeline/modals/note_modal').then(module => ({ default: module.AccountNoteModal })),
|
||||||
'ACCOUNT_FIELD_OVERFLOW': () => import('@/flavours/glitch/features/account_timeline/modals/field_modal').then(module => ({ default: module.AccountFieldModal })),
|
'ACCOUNT_FIELD_OVERFLOW': () => import('@/flavours/glitch/features/account_timeline/modals/field_modal').then(module => ({ default: module.AccountFieldModal })),
|
||||||
|
'ACCOUNT_JOIN_DATE': () => import('@/flavours/glitch/features/account_timeline/modals/join_modal').then(module => ({ default: module.AccountJoinModal })),
|
||||||
'ACCOUNT_EDIT_NAME': accountEditModal('NameModal'),
|
'ACCOUNT_EDIT_NAME': accountEditModal('NameModal'),
|
||||||
'ACCOUNT_EDIT_BIO': accountEditModal('BioModal'),
|
'ACCOUNT_EDIT_BIO': accountEditModal('BioModal'),
|
||||||
'ACCOUNT_EDIT_PROFILE_DISPLAY': accountEditModal('ProfileDisplayModal'),
|
'ACCOUNT_EDIT_PROFILE_DISPLAY': accountEditModal('ProfileDisplayModal'),
|
||||||
|
|||||||
35
app/javascript/flavours/glitch/hooks/useCustomEmojis.ts
Normal file
35
app/javascript/flavours/glitch/hooks/useCustomEmojis.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import type { ExtraCustomEmojiMap } from '../features/emoji/types';
|
||||||
|
|
||||||
|
let emojis: ExtraCustomEmojiMap | null = null;
|
||||||
|
|
||||||
|
export function useCustomEmojis() {
|
||||||
|
const [, setLoaded] = useState(emojis !== null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!emojis) {
|
||||||
|
void loadEmojisIntoCache().then(() => {
|
||||||
|
setLoaded(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return emojis;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEmojisIntoCache() {
|
||||||
|
const { loadAllCustomEmoji } = await import('../features/emoji/database');
|
||||||
|
const emojisRaw = await loadAllCustomEmoji();
|
||||||
|
if (emojisRaw.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emojis = {};
|
||||||
|
for (const emoji of emojisRaw) {
|
||||||
|
emojis[emoji.shortcode] = {
|
||||||
|
url: emoji.url,
|
||||||
|
shortcode: emoji.shortcode,
|
||||||
|
static_url: emoji.static_url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -60,6 +60,7 @@ interface Role {
|
|||||||
permissions: string;
|
permissions: string;
|
||||||
color: string;
|
color: string;
|
||||||
highlighted: boolean;
|
highlighted: boolean;
|
||||||
|
collection_limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PollLimits {
|
interface PollLimits {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { RecordOf } from 'immutable';
|
import type { RecordOf } from 'immutable';
|
||||||
|
|
||||||
|
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
||||||
import type { ApiPreviewCardJSON } from 'flavours/glitch/api_types/statuses';
|
import type { ApiPreviewCardJSON } from 'flavours/glitch/api_types/statuses';
|
||||||
|
|
||||||
export type { StatusVisibility } from 'flavours/glitch/api_types/statuses';
|
export type { StatusVisibility } from 'flavours/glitch/api_types/statuses';
|
||||||
@ -10,3 +11,5 @@ export type Status = Immutable.Map<string, unknown>;
|
|||||||
export type Card = RecordOf<ApiPreviewCardJSON>;
|
export type Card = RecordOf<ApiPreviewCardJSON>;
|
||||||
|
|
||||||
export type MediaAttachment = Immutable.Map<string, unknown>;
|
export type MediaAttachment = Immutable.Map<string, unknown>;
|
||||||
|
|
||||||
|
export type CollectionAttachment = RecordOf<ApiCollectionJSON>;
|
||||||
|
|||||||
@ -732,7 +732,10 @@ export const composeReducer = (state = initialState, action) => {
|
|||||||
case COMPOSE_LANGUAGE_CHANGE:
|
case COMPOSE_LANGUAGE_CHANGE:
|
||||||
return state.set('language', action.language);
|
return state.set('language', action.language);
|
||||||
case COMPOSE_FOCUS:
|
case COMPOSE_FOCUS:
|
||||||
return state.set('focusDate', new Date()).update('text', text => text.length > 0 ? text : action.defaultText);
|
return state
|
||||||
|
.set('focusDate', new Date())
|
||||||
|
.update('text', text => text.length > 0 ? text : action.defaultText)
|
||||||
|
.update('caretPosition', position => action.caretStart ? 0 : position);
|
||||||
case COMPOSE_CHANGE_MEDIA_ORDER:
|
case COMPOSE_CHANGE_MEDIA_ORDER:
|
||||||
return state.update('media_attachments', list => {
|
return state.update('media_attachments', list => {
|
||||||
const indexA = list.findIndex(x => x.get('id') === action.a);
|
const indexA = list.findIndex(x => x.get('id') === action.a);
|
||||||
|
|||||||
@ -1558,7 +1558,8 @@ body > [data-popper-placement] {
|
|||||||
.media-gallery,
|
.media-gallery,
|
||||||
.video-player,
|
.video-player,
|
||||||
.audio-player,
|
.audio-player,
|
||||||
.attachment-list {
|
.attachment-list,
|
||||||
|
.collection-preview {
|
||||||
margin-top: 8px; // glitch: reduced margins
|
margin-top: 8px; // glitch: reduced margins
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1576,6 +1577,7 @@ body > [data-popper-placement] {
|
|||||||
& > .picture-in-picture-placeholder,
|
& > .picture-in-picture-placeholder,
|
||||||
& > .more-from-author,
|
& > .more-from-author,
|
||||||
& > .status-card,
|
& > .status-card,
|
||||||
|
& > .collection-preview,
|
||||||
& > .hashtag-bar,
|
& > .hashtag-bar,
|
||||||
& > .content-warning,
|
& > .content-warning,
|
||||||
& > .filter-warning,
|
& > .filter-warning,
|
||||||
@ -1853,7 +1855,8 @@ body > [data-popper-placement] {
|
|||||||
|
|
||||||
.media-gallery,
|
.media-gallery,
|
||||||
.video-player,
|
.video-player,
|
||||||
.audio-player {
|
.audio-player,
|
||||||
|
.collection-preview {
|
||||||
margin-top: 8px; // glitch: reduced margins
|
margin-top: 8px; // glitch: reduced margins
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6611,7 +6614,7 @@ a.status-card {
|
|||||||
|
|
||||||
&__top {
|
&__top {
|
||||||
border-radius: 16px 16px 0 0;
|
border-radius: 16px 16px 0 0;
|
||||||
border-bottom: 0;
|
border-bottom-width: 0;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
app/javascript/flavours/glitch/utils/compare_urls.ts
Normal file
14
app/javascript/flavours/glitch/utils/compare_urls.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export function compareUrls(href1: string, href2: string) {
|
||||||
|
try {
|
||||||
|
const url1 = new URL(href1);
|
||||||
|
const url2 = new URL(href2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
url1.origin === url2.origin &&
|
||||||
|
url1.pathname === url2.pathname &&
|
||||||
|
url1.search === url2.search
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
app/javascript/images/anniversary.svg
Normal file
1
app/javascript/images/anniversary.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.5 KiB |
@ -153,10 +153,11 @@ export function resetCompose() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const focusCompose = (defaultText = '') => (dispatch, getState) => {
|
export const focusCompose = (defaultText = '', caretStart = false) => (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_FOCUS,
|
type: COMPOSE_FOCUS,
|
||||||
defaultText,
|
defaultText,
|
||||||
|
caretStart,
|
||||||
});
|
});
|
||||||
|
|
||||||
ensureComposeIsVisible(getState);
|
ensureComposeIsVisible(getState);
|
||||||
|
|||||||
@ -11,7 +11,8 @@ export interface ApiCollectionJSON {
|
|||||||
account_id: string;
|
account_id: string;
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
uri: string | null;
|
uri: string;
|
||||||
|
url: string;
|
||||||
local: boolean;
|
local: boolean;
|
||||||
item_count: number;
|
item_count: number;
|
||||||
|
|
||||||
@ -56,7 +57,7 @@ export interface CollectionAccountItem {
|
|||||||
id: string;
|
id: string;
|
||||||
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
|
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
|
||||||
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
|
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
|
||||||
position: number;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WrappedCollectionAccountItem {
|
export interface WrappedCollectionAccountItem {
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { useState, useCallback, useRef, useId } from 'react';
|
|||||||
|
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
OffsetValue,
|
OffsetValue,
|
||||||
UsePopperOptions,
|
UsePopperOptions,
|
||||||
@ -18,9 +20,10 @@ import classes from './styles.module.scss';
|
|||||||
const offset = [0, 4] as OffsetValue;
|
const offset = [0, 4] as OffsetValue;
|
||||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||||
|
|
||||||
export const AltTextBadge: React.FC<{ description: string }> = ({
|
export const AltTextBadge: React.FC<{
|
||||||
description,
|
description: string;
|
||||||
}) => {
|
className?: string;
|
||||||
|
}> = ({ description, className }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const uniqueId = useId();
|
const uniqueId = useId();
|
||||||
const popoverId = `${uniqueId}-popover`;
|
const popoverId = `${uniqueId}-popover`;
|
||||||
@ -48,7 +51,7 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
|
|||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
className='media-gallery__alt__label'
|
className={classNames('media-gallery__alt__label', className)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-controls={popoverId}
|
aria-controls={popoverId}
|
||||||
|
|||||||
@ -24,9 +24,14 @@ export const NumberFieldsItem: React.FC<ItemProps> = ({
|
|||||||
link,
|
link,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
...restProps
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<li className={classNames(classes.item, className)} title={hint}>
|
<li
|
||||||
|
{...restProps}
|
||||||
|
className={classNames(classes.item, className)}
|
||||||
|
title={hint}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
{link ? (
|
{link ? (
|
||||||
<NavLink exact to={link}>
|
<NavLink exact to={link}>
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a,
|
||||||
|
button,
|
||||||
strong {
|
strong {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -25,10 +26,21 @@
|
|||||||
a {
|
a {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
button {
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
appearance: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { PureComponent } from 'react';
|
|||||||
|
|
||||||
import { FormattedMessage, defineMessages } from 'react-intl';
|
import { FormattedMessage, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ import { injectIntl } from './intl';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
|
aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
|
||||||
|
aboutThisServer: { id: 'server_banner.more_about_this_server', defaultMessage: 'More about this server'},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
@ -47,9 +48,14 @@ class ServerBanner extends PureComponent {
|
|||||||
<FormattedMessage id='server_banner.is_one_of_many' defaultMessage='{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank' rel='noopener'>Mastodon</a> }} />
|
<FormattedMessage id='server_banner.is_one_of_many' defaultMessage='{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank' rel='noopener'>Mastodon</a> }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link to='/about'>
|
<NavLink to='/about'>
|
||||||
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
|
<ServerHeroImage
|
||||||
</Link>
|
blurhash={server.getIn(['thumbnail', 'blurhash'])}
|
||||||
|
src={server.getIn(['thumbnail', 'url'])}
|
||||||
|
alt={intl.formatMessage(messages.aboutThisServer)}
|
||||||
|
className='server-banner__hero'
|
||||||
|
/>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
<div className='server-banner__description'>
|
<div className='server-banner__description'>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@ -2,9 +2,14 @@ import { useCallback, useState } from 'react';
|
|||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Blurhash } from './blurhash';
|
import { AltTextBadge } from '../alt_text_badge';
|
||||||
|
import { Blurhash } from '../blurhash';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
withAltBadge?: boolean;
|
||||||
|
alt: string;
|
||||||
src: string;
|
src: string;
|
||||||
srcSet?: string;
|
srcSet?: string;
|
||||||
blurhash?: string;
|
blurhash?: string;
|
||||||
@ -12,9 +17,11 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ServerHeroImage: React.FC<Props> = ({
|
export const ServerHeroImage: React.FC<Props> = ({
|
||||||
|
alt,
|
||||||
src,
|
src,
|
||||||
srcSet,
|
srcSet,
|
||||||
blurhash,
|
blurhash,
|
||||||
|
withAltBadge,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
@ -24,12 +31,12 @@ export const ServerHeroImage: React.FC<Props> = ({
|
|||||||
}, [setLoaded]);
|
}, [setLoaded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classNames('image', { loaded }, className)}>
|
||||||
className={classNames('image', { loaded }, className)}
|
|
||||||
role='presentation'
|
|
||||||
>
|
|
||||||
{blurhash && <Blurhash hash={blurhash} className='image__preview' />}
|
{blurhash && <Blurhash hash={blurhash} className='image__preview' />}
|
||||||
<img src={src} srcSet={srcSet} alt='' onLoad={handleLoad} />
|
<img src={src} srcSet={srcSet} alt={alt} onLoad={handleLoad} />
|
||||||
|
{withAltBadge && alt && (
|
||||||
|
<AltTextBadge description={alt} className={classes.altBadge} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
.altBadge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
inset-inline-end: 8px;
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@ import { getHashtagBarForStatus } from './hashtag_bar';
|
|||||||
import StatusActionBar from './status_action_bar';
|
import StatusActionBar from './status_action_bar';
|
||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
import { StatusThreadLabel } from './status_thread_label';
|
import { StatusThreadLabel } from './status_thread_label';
|
||||||
|
import { CollectionPreviewCard } from '../features/collections/components/collection_preview_card';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
@ -547,13 +548,30 @@ class Status extends ImmutablePureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (status.get('card') && !status.get('quote')) {
|
} else if (status.get('card') && !status.get('quote')) {
|
||||||
media = (
|
const cardUrl = status.getIn(['card', 'url']);
|
||||||
<Card
|
|
||||||
key={`${status.get('id')}-${status.get('edited_at')}`}
|
const taggedCollection = (
|
||||||
card={status.get('card')}
|
status.get('tagged_collections')
|
||||||
sensitive={status.get('sensitive')}
|
).find((item) => compareUrls(item.get('url'), cardUrl));
|
||||||
/>
|
|
||||||
);
|
if (taggedCollection) {
|
||||||
|
media = <CollectionPreviewCard collection={taggedCollection} />;
|
||||||
|
} else {
|
||||||
|
media = (
|
||||||
|
<Card
|
||||||
|
key={`${status.get('id')}-${status.get('edited_at')}`}
|
||||||
|
card={status.get('card')}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (status.get('tagged_collections').size) {
|
||||||
|
const firstLinkedCollection = status.get('tagged_collections').first();
|
||||||
|
if (firstLinkedCollection) {
|
||||||
|
media = (
|
||||||
|
<CollectionPreviewCard collection={firstLinkedCollection.toJS()} />
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import type { ComponentProps, FC } from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { ApiCollectionJSON } from '@/mastodon/api_types/collections';
|
||||||
import type { ApiMentionJSON } from '@/mastodon/api_types/statuses';
|
import type { ApiMentionJSON } from '@/mastodon/api_types/statuses';
|
||||||
|
import { getCollectionPath } from '@/mastodon/features/collections/utils';
|
||||||
import type { OnElementHandler } from '@/mastodon/utils/html';
|
import type { OnElementHandler } from '@/mastodon/utils/html';
|
||||||
|
|
||||||
export interface HandledLinkProps {
|
export interface HandledLinkProps {
|
||||||
@ -13,6 +15,7 @@ export interface HandledLinkProps {
|
|||||||
prevText?: string;
|
prevText?: string;
|
||||||
hashtagAccountId?: string;
|
hashtagAccountId?: string;
|
||||||
mention?: Pick<ApiMentionJSON, 'id' | 'acct'>;
|
mention?: Pick<ApiMentionJSON, 'id' | 'acct'>;
|
||||||
|
collection?: Pick<ApiCollectionJSON, 'id'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
||||||
@ -21,6 +24,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
|||||||
prevText,
|
prevText,
|
||||||
hashtagAccountId,
|
hashtagAccountId,
|
||||||
mention,
|
mention,
|
||||||
|
collection,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
@ -57,6 +61,15 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
|||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
} else if (collection) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={classNames(className)}
|
||||||
|
to={getCollectionPath(collection.id)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-absolute paths treated as internal links. This shouldn't happen, but just in case.
|
// Non-absolute paths treated as internal links. This shouldn't happen, but just in case.
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { languages as preloadedLanguages } from 'mastodon/initial_state';
|
|||||||
import { EmojiHTML } from './emoji/html';
|
import { EmojiHTML } from './emoji/html';
|
||||||
import { injectIntl } from './intl';
|
import { injectIntl } from './intl';
|
||||||
import { HandledLink } from './status/handled_link';
|
import { HandledLink } from './status/handled_link';
|
||||||
|
import { compareUrls } from '../utils/compare_urls';
|
||||||
|
|
||||||
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||||
|
|
||||||
@ -71,17 +72,6 @@ const mapStateToProps = state => ({
|
|||||||
languages: state.getIn(['server', 'translationLanguages', 'items']),
|
languages: state.getIn(['server', 'translationLanguages', 'items']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const compareUrls = (href1, href2) => {
|
|
||||||
try {
|
|
||||||
const url1 = new URL(href1);
|
|
||||||
const url2 = new URL(href2);
|
|
||||||
|
|
||||||
return url1.origin === url2.origin && url1.pathname === url2.pathname && url1.search === url2.search;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class StatusContent extends PureComponent {
|
class StatusContent extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
identity: identityContextPropShape,
|
identity: identityContextPropShape,
|
||||||
@ -166,7 +156,13 @@ class StatusContent extends PureComponent {
|
|||||||
|
|
||||||
handleElement = (element, { key, ...props }, children) => {
|
handleElement = (element, { key, ...props }, children) => {
|
||||||
if (element instanceof HTMLAnchorElement) {
|
if (element instanceof HTMLAnchorElement) {
|
||||||
const mention = this.props.status.get('mentions').find(item => compareUrls(element.href, item.get('url')));
|
const mention = this.props.status.get('mentions').find(
|
||||||
|
item => compareUrls(element.href, item.get('url'))
|
||||||
|
);
|
||||||
|
const taggedCollection = this.props.status.get('tagged_collections').find(
|
||||||
|
item => compareUrls(element.href, item.get('url'))
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HandledLink
|
<HandledLink
|
||||||
{...props}
|
{...props}
|
||||||
@ -174,6 +170,7 @@ class StatusContent extends PureComponent {
|
|||||||
text={element.innerText}
|
text={element.innerText}
|
||||||
hashtagAccountId={this.props.status.getIn(['account', 'id'])}
|
hashtagAccountId={this.props.status.getIn(['account', 'id'])}
|
||||||
mention={mention?.toJSON()}
|
mention={mention?.toJSON()}
|
||||||
|
collection={taggedCollection?.toJSON()}
|
||||||
key={key}
|
key={key}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -82,7 +82,14 @@ class About extends PureComponent {
|
|||||||
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
|
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
|
||||||
<div className='scrollable about'>
|
<div className='scrollable about'>
|
||||||
<div className='about__header'>
|
<div className='about__header'>
|
||||||
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
|
<ServerHeroImage
|
||||||
|
withAltBadge
|
||||||
|
alt={server.getIn(['thumbnail', 'description']) ?? ''}
|
||||||
|
blurhash={server.getIn(['thumbnail', 'blurhash'])}
|
||||||
|
src={server.getIn(['thumbnail', 'url'])}
|
||||||
|
srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')}
|
||||||
|
className='about__header__hero'
|
||||||
|
/>
|
||||||
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
|
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
|
||||||
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank' rel='noopener'>Mastodon</a> }} /></p>
|
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank' rel='noopener'>Mastodon</a> }} /></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export const AccountEditEmptyColumn: FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} className={classes.column}>
|
<Column bindToDocument={!multiColumn}>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
@ -38,7 +38,7 @@ export const AccountEditColumn: FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Column bindToDocument={!multiColumn} className={classes.column}>
|
<Column bindToDocument={!multiColumn}>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
title={title}
|
title={title}
|
||||||
className={classes.columnHeader}
|
className={classes.columnHeader}
|
||||||
@ -53,7 +53,7 @@ export const AccountEditColumn: FC<{
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{children}
|
<div className='scrollable'>{children}</div>
|
||||||
</Column>
|
</Column>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { ToggleField } from '@/mastodon/components/form_fields';
|
|||||||
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
||||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||||
|
import { useCustomEmojis } from '@/mastodon/hooks/useCustomEmojis';
|
||||||
import { autoPlayGif } from '@/mastodon/initial_state';
|
import { autoPlayGif } from '@/mastodon/initial_state';
|
||||||
import {
|
import {
|
||||||
fetchProfile,
|
fetchProfile,
|
||||||
@ -175,7 +176,7 @@ export const AccountEdit: FC = () => {
|
|||||||
}, [dispatch, profile?.bot]);
|
}, [dispatch, profile?.bot]);
|
||||||
|
|
||||||
// Normally we would use the account emoji, but we want all custom emojis to be available to render after editing.
|
// Normally we would use the account emoji, but we want all custom emojis to be available to render after editing.
|
||||||
const emojis = useAppSelector((state) => state.custom_emojis);
|
const emojis = useCustomEmojis();
|
||||||
const htmlHandlers = useElementHandledLink({
|
const htmlHandlers = useElementHandledLink({
|
||||||
hashtagAccountId: profile?.id,
|
hashtagAccountId: profile?.id,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
import { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import type { FC, FocusEventHandler } from 'react';
|
import type { FC, FocusEventHandler } from 'react';
|
||||||
|
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
@ -9,6 +15,7 @@ import { closeModal } from '@/mastodon/actions/modal';
|
|||||||
import { Button } from '@/mastodon/components/button';
|
import { Button } from '@/mastodon/components/button';
|
||||||
import type { FieldStatus } from '@/mastodon/components/form_fields';
|
import type { FieldStatus } from '@/mastodon/components/form_fields';
|
||||||
import { EmojiTextInputField } from '@/mastodon/components/form_fields';
|
import { EmojiTextInputField } from '@/mastodon/components/form_fields';
|
||||||
|
import { useCustomEmojis } from '@/mastodon/hooks/useCustomEmojis';
|
||||||
import {
|
import {
|
||||||
removeField,
|
removeField,
|
||||||
selectFieldById,
|
selectFieldById,
|
||||||
@ -104,11 +111,6 @@ const selectFieldLimits = createAppSelector(
|
|||||||
|
|
||||||
const RECOMMENDED_LIMIT = 40;
|
const RECOMMENDED_LIMIT = 40;
|
||||||
|
|
||||||
const selectEmojiCodes = createAppSelector(
|
|
||||||
[(state) => state.custom_emojis],
|
|
||||||
(emojis) => emojis.map((emoji) => emoji.get('shortcode')).toArray(),
|
|
||||||
);
|
|
||||||
|
|
||||||
interface ConfirmationMessage {
|
interface ConfirmationMessage {
|
||||||
message: string;
|
message: string;
|
||||||
confirm: string;
|
confirm: string;
|
||||||
@ -143,7 +145,11 @@ export const EditFieldModal = forwardRef<
|
|||||||
value?: FieldStatus;
|
value?: FieldStatus;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
const customEmojiCodes = useAppSelector(selectEmojiCodes);
|
const customEmojis = useCustomEmojis();
|
||||||
|
const customEmojiCodes = useMemo(
|
||||||
|
() => Object.keys(customEmojis ?? {}),
|
||||||
|
[customEmojis],
|
||||||
|
);
|
||||||
const checkField = useCallback(
|
const checkField = useCallback(
|
||||||
(value: string): FieldStatus | null => {
|
(value: string): FieldStatus | null => {
|
||||||
if (!value.trim()) {
|
if (!value.trim()) {
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import { CSS } from '@dnd-kit/utilities';
|
|||||||
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||||
import { normalizeKey } from '@/mastodon/components/hotkeys/utils';
|
import { normalizeKey } from '@/mastodon/components/hotkeys/utils';
|
||||||
import { Icon } from '@/mastodon/components/icon';
|
import { Icon } from '@/mastodon/components/icon';
|
||||||
|
import { useCustomEmojis } from '@/mastodon/hooks/useCustomEmojis';
|
||||||
import type { FieldData } from '@/mastodon/reducers/slices/profile_edit';
|
import type { FieldData } from '@/mastodon/reducers/slices/profile_edit';
|
||||||
import {
|
import {
|
||||||
patchProfile,
|
patchProfile,
|
||||||
@ -217,7 +218,7 @@ export const ReorderFieldsModal: FC<DialogModalProps> = ({ onClose }) => {
|
|||||||
void dispatch(patchProfile({ fields_attributes: newFields })).then(onClose);
|
void dispatch(patchProfile({ fields_attributes: newFields })).then(onClose);
|
||||||
}, [dispatch, fieldKeys, fields, onClose]);
|
}, [dispatch, fieldKeys, fields, onClose]);
|
||||||
|
|
||||||
const emojis = useAppSelector((state) => state.custom_emojis);
|
const emojis = useCustomEmojis();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Add a wrapper here in the capture phase, so that it can be intercepted before the window listener in ModalRoot.
|
// Add a wrapper here in the capture phase, so that it can be intercepted before the window listener in ModalRoot.
|
||||||
|
|||||||
@ -126,11 +126,6 @@
|
|||||||
|
|
||||||
// Column component
|
// Column component
|
||||||
|
|
||||||
.column {
|
|
||||||
border: 1px solid var(--color-border-primary);
|
|
||||||
border-top-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.columnHeader {
|
.columnHeader {
|
||||||
:global(.column-header__buttons) {
|
:global(.column-header__buttons) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -258,6 +253,10 @@
|
|||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-bottom: 1px solid var(--color-border-primary);
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionHeader {
|
.sectionHeader {
|
||||||
|
|||||||
@ -25,12 +25,9 @@ import Column from 'mastodon/features/ui/components/column';
|
|||||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||||
import { useAccountId } from 'mastodon/hooks/useAccountId';
|
import { useAccountId } from 'mastodon/hooks/useAccountId';
|
||||||
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
|
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
|
||||||
import {
|
|
||||||
fetchAccountCollections,
|
|
||||||
selectAccountCollections,
|
|
||||||
} from 'mastodon/reducers/slices/collections';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { useAccountCollections } from '../collections';
|
||||||
import { CollectionListItem } from '../collections/components/collection_list_item';
|
import { CollectionListItem } from '../collections/components/collection_list_item';
|
||||||
import { areCollectionsEnabled } from '../collections/utils';
|
import { areCollectionsEnabled } from '../collections/utils';
|
||||||
|
|
||||||
@ -59,10 +56,6 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
void dispatch(fetchEndorsedAccounts({ accountId }));
|
void dispatch(fetchEndorsedAccounts({ accountId }));
|
||||||
|
|
||||||
if (collectionsEnabled) {
|
|
||||||
void dispatch(fetchAccountCollections({ accountId }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [accountId, dispatch]);
|
}, [accountId, dispatch]);
|
||||||
|
|
||||||
@ -73,9 +66,8 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
|||||||
ImmutableList(),
|
ImmutableList(),
|
||||||
) as ImmutableList<string>,
|
) as ImmutableList<string>,
|
||||||
);
|
);
|
||||||
const { collections, status: collectionsLoadStatus } = useAppSelector(
|
const { collections, status: collectionsLoadStatus } =
|
||||||
(state) => selectAccountCollections(state, accountId ?? null),
|
useAccountCollections(accountId);
|
||||||
);
|
|
||||||
|
|
||||||
const { listedCollections = [], unlistedCollections = [] } = Object.groupBy(
|
const { listedCollections = [], unlistedCollections = [] } = Object.groupBy(
|
||||||
collections,
|
collections,
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { openModal } from '@/mastodon/actions/modal';
|
||||||
import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
|
import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
|
||||||
import {
|
import {
|
||||||
NumberFields,
|
NumberFields,
|
||||||
@ -10,6 +11,7 @@ import {
|
|||||||
} from '@/mastodon/components/number_fields';
|
} from '@/mastodon/components/number_fields';
|
||||||
import { ShortNumber } from '@/mastodon/components/short_number';
|
import { ShortNumber } from '@/mastodon/components/short_number';
|
||||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||||
|
import { useAppDispatch } from '@/mastodon/store';
|
||||||
|
|
||||||
export const AccountNumberFields: FC<{ accountId: string }> = ({
|
export const AccountNumberFields: FC<{ accountId: string }> = ({
|
||||||
accountId,
|
accountId,
|
||||||
@ -21,6 +23,13 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({
|
|||||||
[account?.created_at],
|
[account?.created_at],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const showJoinModal = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
openModal({ modalType: 'ACCOUNT_JOIN_DATE', modalProps: { accountId } }),
|
||||||
|
);
|
||||||
|
}, [accountId, dispatch]);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -60,15 +69,17 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({
|
|||||||
}
|
}
|
||||||
hint={intl.formatDate(account.created_at)}
|
hint={intl.formatDate(account.created_at)}
|
||||||
>
|
>
|
||||||
{createdThisYear ? (
|
<button type='button' onClick={showJoinModal}>
|
||||||
<FormattedDateWrapper
|
{createdThisYear ? (
|
||||||
value={account.created_at}
|
<FormattedDateWrapper
|
||||||
month='short'
|
value={account.created_at}
|
||||||
day='2-digit'
|
month='short'
|
||||||
/>
|
day='2-digit'
|
||||||
) : (
|
/>
|
||||||
<FormattedDateWrapper value={account.created_at} year='numeric' />
|
) : (
|
||||||
)}
|
<FormattedDateWrapper value={account.created_at} year='numeric' />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</NumberFieldsItem>
|
</NumberFieldsItem>
|
||||||
</NumberFields>
|
</NumberFields>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
import type { AccountField } from '../common';
|
import type { AccountField } from '../common';
|
||||||
import { useFieldHtml } from '../hooks/useFieldHtml';
|
import { useFieldHtml } from '../hooks/useFieldHtml';
|
||||||
|
|
||||||
import classes from './styles.module.css';
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
export const AccountFieldModal: FC<{
|
export const AccountFieldModal: FC<{
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|||||||
@ -0,0 +1,270 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import AnniversaryImage from '@/images/anniversary.svg?react';
|
||||||
|
import { focusCompose, resetCompose } from '@/mastodon/actions/compose';
|
||||||
|
import { closeModal } from '@/mastodon/actions/modal';
|
||||||
|
import { Button } from '@/mastodon/components/button';
|
||||||
|
import { DisplayNameSimple } from '@/mastodon/components/display_name/simple';
|
||||||
|
import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
|
||||||
|
import { IconButton } from '@/mastodon/components/icon_button';
|
||||||
|
import { ModalShell, ModalShellBody } from '@/mastodon/components/modal_shell';
|
||||||
|
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||||
|
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||||
|
import {
|
||||||
|
createAppSelector,
|
||||||
|
useAppDispatch,
|
||||||
|
useAppSelector,
|
||||||
|
} from '@/mastodon/store';
|
||||||
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
const closeMessage = defineMessage({
|
||||||
|
id: 'lightbox.close',
|
||||||
|
defaultMessage: 'Close',
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectServerName = createAppSelector(
|
||||||
|
[
|
||||||
|
(state) => state.accounts,
|
||||||
|
(_, accountId: string) => accountId,
|
||||||
|
(state) => state.server.getIn(['server', 'domain']) as string | undefined,
|
||||||
|
],
|
||||||
|
(accounts, accountId, serverDomain) => {
|
||||||
|
const acct = accounts.getIn([accountId, 'acct']) as string | undefined;
|
||||||
|
if (!acct) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = acct.split('@').at(1);
|
||||||
|
if (domain) {
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverDomain;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AccountJoinModal: FC<{
|
||||||
|
accountId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}> = ({ accountId, onClose }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const account = useAccount(accountId);
|
||||||
|
const currentId = useCurrentAccountId();
|
||||||
|
const isMe = accountId === currentId;
|
||||||
|
|
||||||
|
const createdAtStr = account?.created_at;
|
||||||
|
const anniversary = useMemo(() => {
|
||||||
|
if (!createdAtStr) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
const createdAt = new Date(createdAtStr);
|
||||||
|
if (
|
||||||
|
now.getMonth() === createdAt.getMonth() &&
|
||||||
|
now.getDate() === createdAt.getDate()
|
||||||
|
) {
|
||||||
|
return now.getFullYear() - createdAt.getFullYear();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [createdAtStr]);
|
||||||
|
|
||||||
|
const domain = useAppSelector((state) => selectServerName(state, accountId));
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handle = account?.acct;
|
||||||
|
const handleShare = useCallback(() => {
|
||||||
|
if (anniversary === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shareText = '#Fediversary';
|
||||||
|
if (anniversary === 0) {
|
||||||
|
shareText = isMe ? '#firstday' : '#welcome';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMe && handle) {
|
||||||
|
shareText = `@${handle} ${shareText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(resetCompose());
|
||||||
|
dispatch(focusCompose(`\n\n${shareText}`, true));
|
||||||
|
dispatch(closeModal({ modalType: 'ACCOUNT_JOIN_DATE', ignoreFocus: true }));
|
||||||
|
}, [anniversary, handle, dispatch, isMe]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalShell className={classes.joinShell}>
|
||||||
|
<ModalShellBody className={classes.joinWrapper}>
|
||||||
|
<AccountAnniversaryImage anniversary={anniversary} />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<AccountJoinMessage
|
||||||
|
name={<DisplayNameSimple account={account} />}
|
||||||
|
isMe={isMe}
|
||||||
|
serverName={domain}
|
||||||
|
anniversary={anniversary}
|
||||||
|
/>
|
||||||
|
<h1>
|
||||||
|
<FormattedDateWrapper
|
||||||
|
value={account?.created_at}
|
||||||
|
month='short'
|
||||||
|
day='numeric'
|
||||||
|
year='numeric'
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AccountAnniversaryShare
|
||||||
|
anniversary={anniversary}
|
||||||
|
onShare={handleShare}
|
||||||
|
isMe={isMe}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
iconComponent={CloseIcon}
|
||||||
|
icon='times'
|
||||||
|
onClick={onClose}
|
||||||
|
title={intl.formatMessage(closeMessage)}
|
||||||
|
className={classes.joinClose}
|
||||||
|
/>
|
||||||
|
</ModalShellBody>
|
||||||
|
</ModalShell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountJoinMessage: FC<{
|
||||||
|
name: React.JSX.Element;
|
||||||
|
isMe: boolean;
|
||||||
|
serverName?: string;
|
||||||
|
anniversary: number | null;
|
||||||
|
}> = ({ name, isMe, serverName, anniversary }) => {
|
||||||
|
if (anniversary === 0) {
|
||||||
|
if (isMe) {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.me_today'
|
||||||
|
defaultMessage='It’s your first day on {server}!'
|
||||||
|
tagName='p'
|
||||||
|
values={{
|
||||||
|
server: serverName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.other_today'
|
||||||
|
defaultMessage='It’s {name}’s first day on {server}!'
|
||||||
|
tagName='p'
|
||||||
|
values={{
|
||||||
|
name,
|
||||||
|
server: serverName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMe) {
|
||||||
|
if (anniversary !== null && anniversary > 0) {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.me_anniversary'
|
||||||
|
defaultMessage='Happy Fediversary! You joined {server} on'
|
||||||
|
tagName='p'
|
||||||
|
values={{
|
||||||
|
server: serverName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.me'
|
||||||
|
defaultMessage='You joined {server} on'
|
||||||
|
tagName='p'
|
||||||
|
values={{
|
||||||
|
server: serverName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.other'
|
||||||
|
defaultMessage='{name} joined {server} on'
|
||||||
|
tagName='p'
|
||||||
|
values={{
|
||||||
|
name,
|
||||||
|
server: serverName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountAnniversaryImage: FC<{ anniversary: number | null }> = ({
|
||||||
|
anniversary,
|
||||||
|
}) => {
|
||||||
|
if (anniversary === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.joinBanner}>
|
||||||
|
<AnniversaryImage role='presentation' />
|
||||||
|
<h2>{anniversary || 1}</h2>
|
||||||
|
{anniversary === 0 && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.day'
|
||||||
|
defaultMessage='Day'
|
||||||
|
tagName='h3'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{anniversary > 0 && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.years'
|
||||||
|
defaultMessage='{number, plural, one {year} other {years}}'
|
||||||
|
values={{ number: anniversary }}
|
||||||
|
tagName='h3'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountAnniversaryShare: FC<{
|
||||||
|
anniversary: number | null;
|
||||||
|
onShare: () => void;
|
||||||
|
isMe: boolean;
|
||||||
|
}> = ({ anniversary, onShare, isMe }) => {
|
||||||
|
if (anniversary === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={onShare}>
|
||||||
|
{anniversary === 0 && isMe && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.share.intro'
|
||||||
|
defaultMessage='Share an intro post'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{anniversary === 0 && !isMe && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.share.welcome'
|
||||||
|
defaultMessage='Share a welcome post'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{anniversary > 0 && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.join_modal.share.celebrate'
|
||||||
|
defaultMessage='Share a celebratory post'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -13,7 +13,7 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
|||||||
|
|
||||||
import { ConfirmationModal } from '../../ui/components/confirmation_modals';
|
import { ConfirmationModal } from '../../ui/components/confirmation_modals';
|
||||||
|
|
||||||
import classes from './styles.module.css';
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
newTitle: {
|
newTitle: {
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
.noteCallout {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noteInput {
|
|
||||||
min-height: 70px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: var(--color-bg-primary);
|
|
||||||
border: 1px solid var(--color-border-primary);
|
|
||||||
appearance: none;
|
|
||||||
resize: none;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noteInput:focus-visible {
|
|
||||||
outline: var(--outline-focus-default);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldName,
|
|
||||||
.fieldValue {
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldValue {
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
@use '@/styles/mastodon/variables' as *;
|
||||||
|
|
||||||
|
.noteCallout {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noteInput {
|
||||||
|
min-height: 70px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
appearance: none;
|
||||||
|
resize: none;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noteInput:focus-visible {
|
||||||
|
outline: var(--outline-focus-default);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldName,
|
||||||
|
.fieldValue {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldValue {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: ($mobile-breakpoint + 1)) {
|
||||||
|
.joinShell {
|
||||||
|
> :global(.safety-action-modal__top) {
|
||||||
|
border-bottom-left-radius: 16px;
|
||||||
|
border-bottom-right-radius: 16px;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.joinWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
min-height: 120px;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
p,
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.joinBanner {
|
||||||
|
width: 120px;
|
||||||
|
height: 110px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 40px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.joinClose {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
@ -13,6 +13,8 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
|||||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||||
import { domain } from 'mastodon/initial_state';
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
import { getCollectionPath } from '../utils';
|
||||||
|
|
||||||
import classes from './collection_lockup.module.scss';
|
import classes from './collection_lockup.module.scss';
|
||||||
|
|
||||||
export const AvatarGrid: React.FC<{
|
export const AvatarGrid: React.FC<{
|
||||||
@ -27,10 +29,10 @@ export const AvatarGrid: React.FC<{
|
|||||||
sensitive ? classes.avatarGridSensitive : null,
|
sensitive ? classes.avatarGridSensitive : null,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{avatarIds.map((id) => (
|
{avatarIds.map((id, index) => (
|
||||||
<AvatarById
|
<AvatarById
|
||||||
animate={false}
|
animate={false}
|
||||||
key={id}
|
key={index}
|
||||||
accountId={id}
|
accountId={id}
|
||||||
className={classes.avatar}
|
className={classes.avatar}
|
||||||
size={25}
|
size={25}
|
||||||
@ -67,7 +69,7 @@ export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
|||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h2 id={linkId}>
|
<h2 id={linkId}>
|
||||||
<Link to={`/collections/${id}`} className={classes.link}>
|
<Link to={getCollectionPath(id)} className={classes.link}>
|
||||||
{name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import type { MenuItem } from 'mastodon/models/dropdown_menu';
|
|||||||
import { useAppDispatch } from 'mastodon/store';
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
import { messages as editorMessages } from '../editor';
|
import { messages as editorMessages } from '../editor';
|
||||||
|
import { getCollectionPath } from '../utils';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
view: {
|
view: {
|
||||||
@ -120,7 +121,7 @@ export const CollectionMenu: React.FC<{
|
|||||||
const menu = useMemo(() => {
|
const menu = useMemo(() => {
|
||||||
const viewCollectionItem: MenuItem = {
|
const viewCollectionItem: MenuItem = {
|
||||||
text: intl.formatMessage(messages.view),
|
text: intl.formatMessage(messages.view),
|
||||||
to: `/collections/${id}`,
|
to: getCollectionPath(id),
|
||||||
};
|
};
|
||||||
const shareItems: MenuItem[] = [
|
const shareItems: MenuItem[] = [
|
||||||
{
|
{
|
||||||
@ -130,7 +131,7 @@ export const CollectionMenu: React.FC<{
|
|||||||
{
|
{
|
||||||
text: intl.formatMessage(messages.copyLink),
|
text: intl.formatMessage(messages.copyLink),
|
||||||
action: () => {
|
action: () => {
|
||||||
void navigator.clipboard.writeText(`/collections/${id}`);
|
void navigator.clipboard.writeText(getCollectionPath(id));
|
||||||
dispatch(showAlert({ message: messages.copyLinkConfirmation }));
|
dispatch(showAlert({ message: messages.copyLinkConfirmation }));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import type { CollectionLockupProps } from 'mastodon/features/collections/components/collection_lockup';
|
import type { CollectionLockupProps } from 'mastodon/features/collections/components/collection_lockup';
|
||||||
import { CollectionLockup } from 'mastodon/features/collections/components/collection_lockup';
|
import { CollectionLockup } from 'mastodon/features/collections/components/collection_lockup';
|
||||||
|
|
||||||
@ -13,7 +15,7 @@ export const CollectionPreviewCard: React.FC<CollectionPreviewCardProps> = ({
|
|||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={classes.wrapper}>
|
<div className={classNames(classes.wrapper, 'collection-preview')}>
|
||||||
<CollectionLockup collection={collection} {...otherProps} />
|
<CollectionLockup collection={collection} {...otherProps} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import { useHistory } from 'react-router-dom';
|
|||||||
import { isFulfilled } from '@reduxjs/toolkit';
|
import { isFulfilled } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { ComboboxMenuItem } from '@/mastodon/components/form_fields/combobox_field';
|
import { ComboboxMenuItem } from '@/mastodon/components/form_fields/combobox_field';
|
||||||
|
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||||
|
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||||
import { languages } from '@/mastodon/initial_state';
|
import { languages } from '@/mastodon/initial_state';
|
||||||
import {
|
import {
|
||||||
hasSpecialCharacters,
|
hasSpecialCharacters,
|
||||||
@ -36,6 +38,8 @@ import {
|
|||||||
} from 'mastodon/reducers/slices/collections';
|
} from 'mastodon/reducers/slices/collections';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { getCollectionPath } from '../utils';
|
||||||
|
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
import { WizardStepTitle } from './wizard_step_title';
|
import { WizardStepTitle } from './wizard_step_title';
|
||||||
|
|
||||||
@ -93,6 +97,9 @@ export const CollectionDetails: React.FC = () => {
|
|||||||
[dispatch],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const accountId = useCurrentAccountId();
|
||||||
|
const { acct: currentUserName } = useAccount(accountId) ?? {};
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(e: React.FormEvent) => {
|
(e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -128,8 +135,8 @@ export const CollectionDetails: React.FC = () => {
|
|||||||
}),
|
}),
|
||||||
).then((result) => {
|
).then((result) => {
|
||||||
if (isFulfilled(result)) {
|
if (isFulfilled(result)) {
|
||||||
history.replace(`/collections`);
|
history.replace(`/@${currentUserName}/collections`);
|
||||||
history.push(`/collections/${result.payload.collection.id}`, {
|
history.push(getCollectionPath(result.payload.collection.id), {
|
||||||
newCollection: true,
|
newCollection: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -146,6 +153,7 @@ export const CollectionDetails: React.FC = () => {
|
|||||||
dispatch,
|
dispatch,
|
||||||
history,
|
history,
|
||||||
accountIds,
|
accountIds,
|
||||||
|
currentUserName,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import {
|
import {
|
||||||
@ -12,6 +12,9 @@ import {
|
|||||||
useLocation,
|
useLocation,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Callout } from '@/mastodon/components/callout';
|
||||||
|
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||||
|
import { initialState } from '@/mastodon/initial_state';
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
import { Column } from 'mastodon/components/column';
|
import { Column } from 'mastodon/components/column';
|
||||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||||
@ -22,8 +25,11 @@ import {
|
|||||||
} from 'mastodon/reducers/slices/collections';
|
} from 'mastodon/reducers/slices/collections';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { useAccountCollections } from '..';
|
||||||
|
|
||||||
import { CollectionAccounts } from './accounts';
|
import { CollectionAccounts } from './accounts';
|
||||||
import { CollectionDetails } from './details';
|
import { CollectionDetails } from './details';
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
create: {
|
create: {
|
||||||
@ -61,11 +67,14 @@ function usePageTitle(id: string | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const userCollectionLimit = initialState?.role?.collection_limit ?? 0;
|
||||||
|
|
||||||
export const CollectionEditorPage: React.FC<{
|
export const CollectionEditorPage: React.FC<{
|
||||||
multiColumn?: boolean;
|
multiColumn?: boolean;
|
||||||
}> = ({ multiColumn }) => {
|
}> = ({ multiColumn }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const accountId = useCurrentAccountId();
|
||||||
const { id = null } = useParams<{ id?: string }>();
|
const { id = null } = useParams<{ id?: string }>();
|
||||||
const { path } = useRouteMatch();
|
const { path } = useRouteMatch();
|
||||||
const collection = useAppSelector((state) =>
|
const collection = useAppSelector((state) =>
|
||||||
@ -73,7 +82,18 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
);
|
);
|
||||||
const editorStateId = useAppSelector((state) => state.collections.editor.id);
|
const editorStateId = useAppSelector((state) => state.collections.editor.id);
|
||||||
const isEditMode = !!id;
|
const isEditMode = !!id;
|
||||||
const isLoading = isEditMode && !collection;
|
|
||||||
|
// When creating a new collection, we load the current account's collections
|
||||||
|
// to determine if they're allowed to create more.
|
||||||
|
const { collections: collectionList, status: collectionListStatus } =
|
||||||
|
useAccountCollections(isEditMode ? null : accountId);
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
(isEditMode && !collection) ||
|
||||||
|
(!isEditMode && collectionListStatus === 'loading');
|
||||||
|
|
||||||
|
const canCreateMoreCollections =
|
||||||
|
isEditMode || collectionList.length < userCollectionLimit;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@ -108,7 +128,7 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
<div className='scrollable'>
|
<div className='scrollable'>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
) : (
|
) : canCreateMoreCollections ? (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
@ -123,6 +143,8 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
render={() => <CollectionDetails />}
|
render={() => <CollectionDetails />}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
) : (
|
||||||
|
<MaxCollectionsCallout />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -133,3 +155,21 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MaxCollectionsCallout: React.FC = () => (
|
||||||
|
<Callout
|
||||||
|
className={classes.maxCollectionsError}
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.maximum_collection_count_reached'
|
||||||
|
defaultMessage='You have created the maximum number of collections'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.maximum_collection_count_description'
|
||||||
|
defaultMessage='Your server allows creation of up to {count} collections.'
|
||||||
|
values={{ count: userCollectionLimit }}
|
||||||
|
/>
|
||||||
|
</Callout>
|
||||||
|
);
|
||||||
|
|||||||
@ -77,3 +77,7 @@
|
|||||||
.suggestionGroup {
|
.suggestionGroup {
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maxCollectionsError {
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|||||||
@ -25,7 +25,12 @@ import {
|
|||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
import { CollectionListItem } from './components/collection_list_item';
|
import { CollectionListItem } from './components/collection_list_item';
|
||||||
import { messages as editorMessages } from './editor';
|
import {
|
||||||
|
messages as editorMessages,
|
||||||
|
MaxCollectionsCallout,
|
||||||
|
userCollectionLimit,
|
||||||
|
} from './editor';
|
||||||
|
import { areCollectionsEnabled } from './utils';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
headingMe: { id: 'column.my_collections', defaultMessage: 'My collections' },
|
headingMe: { id: 'column.my_collections', defaultMessage: 'My collections' },
|
||||||
@ -35,24 +40,27 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function useAccountCollections(accountId: string | null | undefined) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accountId && areCollectionsEnabled()) {
|
||||||
|
void dispatch(fetchAccountCollections({ accountId }));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId]);
|
||||||
|
|
||||||
|
return useAppSelector((state) => selectAccountCollections(state, accountId));
|
||||||
|
}
|
||||||
|
|
||||||
export const Collections: React.FC<{
|
export const Collections: React.FC<{
|
||||||
multiColumn?: boolean;
|
multiColumn?: boolean;
|
||||||
}> = ({ multiColumn }) => {
|
}> = ({ multiColumn }) => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const me = useCurrentAccountId();
|
const me = useCurrentAccountId();
|
||||||
const accountId = useAccountId();
|
const accountId = useAccountId();
|
||||||
const account = useAccount(accountId);
|
const account = useAccount(accountId);
|
||||||
|
|
||||||
const { collections, status } = useAppSelector((state) =>
|
const { collections, status } = useAccountCollections(accountId);
|
||||||
selectAccountCollections(state, accountId),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (accountId) {
|
|
||||||
void dispatch(fetchAccountCollections({ accountId }));
|
|
||||||
}
|
|
||||||
}, [dispatch, accountId]);
|
|
||||||
|
|
||||||
const emptyMessage =
|
const emptyMessage =
|
||||||
status === 'error' || !accountId ? (
|
status === 'error' || !accountId ? (
|
||||||
@ -79,6 +87,7 @@ export const Collections: React.FC<{
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canCreateMoreCollections = collections.length < userCollectionLimit;
|
||||||
const isOwnCollection = accountId === me;
|
const isOwnCollection = accountId === me;
|
||||||
const titleMessage = isOwnCollection
|
const titleMessage = isOwnCollection
|
||||||
? messages.headingMe
|
? messages.headingMe
|
||||||
@ -99,7 +108,9 @@ export const Collections: React.FC<{
|
|||||||
iconComponent={CollectionsFilledIcon}
|
iconComponent={CollectionsFilledIcon}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
extraButton={
|
extraButton={
|
||||||
isOwnCollection && (
|
isOwnCollection &&
|
||||||
|
status === 'idle' &&
|
||||||
|
canCreateMoreCollections && (
|
||||||
<Link
|
<Link
|
||||||
to='/collections/new'
|
to='/collections/new'
|
||||||
className='column-header__button'
|
className='column-header__button'
|
||||||
@ -113,6 +124,9 @@ export const Collections: React.FC<{
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Scrollable>
|
<Scrollable>
|
||||||
|
{status === 'idle' && !canCreateMoreCollections && (
|
||||||
|
<MaxCollectionsCallout />
|
||||||
|
)}
|
||||||
<ItemList emptyMessage={emptyMessage} isLoading={status === 'loading'}>
|
<ItemList emptyMessage={emptyMessage} isLoading={status === 'loading'}>
|
||||||
{collections.map((item, index) => (
|
{collections.map((item, index) => (
|
||||||
<CollectionListItem
|
<CollectionListItem
|
||||||
|
|||||||
@ -3,3 +3,5 @@ import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
|
|||||||
export function areCollectionsEnabled() {
|
export function areCollectionsEnabled() {
|
||||||
return isServerFeatureEnabled('collections');
|
return isServerFeatureEnabled('collections');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getCollectionPath = (id: string) => `/collections/${id}`;
|
||||||
|
|||||||
@ -267,6 +267,11 @@ export async function searchCustomEmojisByShortcodes(shortcodes: string[]) {
|
|||||||
return results.filter((emoji) => shortcodes.includes(emoji.shortcode));
|
return results.filter((emoji) => shortcodes.includes(emoji.shortcode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadAllCustomEmoji() {
|
||||||
|
const db = await loadDB();
|
||||||
|
return db.getAll('custom');
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadLegacyShortcodesByShortcode(shortcode: string) {
|
export async function loadLegacyShortcodesByShortcode(shortcode: string) {
|
||||||
const db = await loadDB();
|
const db = await loadDB();
|
||||||
return db.getFromIndex(
|
return db.getFromIndex(
|
||||||
|
|||||||
@ -185,21 +185,16 @@ export function cleanExtraEmojis(extraEmojis?: CustomEmojiMapArg | null) {
|
|||||||
if (!extraEmojis) {
|
if (!extraEmojis) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (Array.isArray(extraEmojis)) {
|
if (!Array.isArray(extraEmojis) && !isList(extraEmojis)) {
|
||||||
return extraEmojis.reduce<ExtraCustomEmojiMap>(
|
return extraEmojis;
|
||||||
(acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (isList(extraEmojis)) {
|
const emojis: ExtraCustomEmojiMap = {};
|
||||||
return extraEmojis
|
const emojiArray = isList(extraEmojis) ? extraEmojis.toJS() : extraEmojis;
|
||||||
.toJS()
|
for (const emoji of emojiArray) {
|
||||||
.reduce<ExtraCustomEmojiMap>(
|
emojis[emoji.shortcode] = emoji;
|
||||||
(acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return extraEmojis;
|
|
||||||
|
return emojis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
|||||||
import type { RenderSlideFn } from '@/mastodon/components/carousel';
|
import type { RenderSlideFn } from '@/mastodon/components/carousel';
|
||||||
import { Carousel } from '@/mastodon/components/carousel';
|
import { Carousel } from '@/mastodon/components/carousel';
|
||||||
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||||
|
import { useCustomEmojis } from '@/mastodon/hooks/useCustomEmojis';
|
||||||
import { mascot } from '@/mastodon/initial_state';
|
import { mascot } from '@/mastodon/initial_state';
|
||||||
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ const announcementSelector = createAppSelector(
|
|||||||
|
|
||||||
export const Announcements: FC = () => {
|
export const Announcements: FC = () => {
|
||||||
const announcements = useAppSelector(announcementSelector);
|
const announcements = useAppSelector(announcementSelector);
|
||||||
const emojis = useAppSelector((state) => state.custom_emojis);
|
const emojis = useCustomEmojis();
|
||||||
|
|
||||||
const renderSlide: RenderSlideFn<{
|
const renderSlide: RenderSlideFn<{
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@ -29,9 +29,12 @@ import StatusContent from 'mastodon/components/status_content';
|
|||||||
import { QuotedStatus } from 'mastodon/components/status_quoted';
|
import { QuotedStatus } from 'mastodon/components/status_quoted';
|
||||||
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
|
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
|
||||||
import { Audio } from 'mastodon/features/audio';
|
import { Audio } from 'mastodon/features/audio';
|
||||||
|
import { CollectionPreviewCard } from 'mastodon/features/collections/components/collection_preview_card';
|
||||||
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
|
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
|
||||||
import { Video } from 'mastodon/features/video';
|
import { Video } from 'mastodon/features/video';
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
|
import type { CollectionAttachment } from 'mastodon/models/status';
|
||||||
|
import { compareUrls } from 'mastodon/utils/compare_urls';
|
||||||
|
|
||||||
import Card from './card';
|
import Card from './card';
|
||||||
|
|
||||||
@ -260,13 +263,32 @@ export const DetailedStatus: React.FC<{
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (status.get('card') && !status.get('quote')) {
|
} else if (status.get('card') && !status.get('quote')) {
|
||||||
media = (
|
const cardUrl: string = status.getIn(['card', 'url']);
|
||||||
<Card
|
|
||||||
key={`${status.get('id')}-${status.get('edited_at')}`}
|
const taggedCollection = status
|
||||||
sensitive={status.get('sensitive')}
|
.get('tagged_collections')
|
||||||
card={status.get('card')}
|
.find((item: CollectionAttachment) =>
|
||||||
/>
|
compareUrls(item.get('url'), cardUrl),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (taggedCollection) {
|
||||||
|
media = <CollectionPreviewCard collection={taggedCollection} />;
|
||||||
|
} else {
|
||||||
|
media = (
|
||||||
|
<Card
|
||||||
|
key={`${status.get('id')}-${status.get('edited_at')}`}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
card={status.get('card')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (status.get('tagged_collections').size) {
|
||||||
|
const firstLinkedCollection = status.get('tagged_collections').first();
|
||||||
|
if (firstLinkedCollection) {
|
||||||
|
media = (
|
||||||
|
<CollectionPreviewCard collection={firstLinkedCollection.toJS()} />
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('application')) {
|
if (status.get('application')) {
|
||||||
|
|||||||
@ -93,6 +93,7 @@ export const MODAL_COMPONENTS = {
|
|||||||
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
|
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
|
||||||
'ACCOUNT_NOTE': () => import('@/mastodon/features/account_timeline/modals/note_modal').then(module => ({ default: module.AccountNoteModal })),
|
'ACCOUNT_NOTE': () => import('@/mastodon/features/account_timeline/modals/note_modal').then(module => ({ default: module.AccountNoteModal })),
|
||||||
'ACCOUNT_FIELD_OVERFLOW': () => import('@/mastodon/features/account_timeline/modals/field_modal').then(module => ({ default: module.AccountFieldModal })),
|
'ACCOUNT_FIELD_OVERFLOW': () => import('@/mastodon/features/account_timeline/modals/field_modal').then(module => ({ default: module.AccountFieldModal })),
|
||||||
|
'ACCOUNT_JOIN_DATE': () => import('@/mastodon/features/account_timeline/modals/join_modal').then(module => ({ default: module.AccountJoinModal })),
|
||||||
'ACCOUNT_EDIT_NAME': accountEditModal('NameModal'),
|
'ACCOUNT_EDIT_NAME': accountEditModal('NameModal'),
|
||||||
'ACCOUNT_EDIT_BIO': accountEditModal('BioModal'),
|
'ACCOUNT_EDIT_BIO': accountEditModal('BioModal'),
|
||||||
'ACCOUNT_EDIT_PROFILE_DISPLAY': accountEditModal('ProfileDisplayModal'),
|
'ACCOUNT_EDIT_PROFILE_DISPLAY': accountEditModal('ProfileDisplayModal'),
|
||||||
|
|||||||
35
app/javascript/mastodon/hooks/useCustomEmojis.ts
Normal file
35
app/javascript/mastodon/hooks/useCustomEmojis.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import type { ExtraCustomEmojiMap } from '../features/emoji/types';
|
||||||
|
|
||||||
|
let emojis: ExtraCustomEmojiMap | null = null;
|
||||||
|
|
||||||
|
export function useCustomEmojis() {
|
||||||
|
const [, setLoaded] = useState(emojis !== null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!emojis) {
|
||||||
|
void loadEmojisIntoCache().then(() => {
|
||||||
|
setLoaded(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return emojis;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEmojisIntoCache() {
|
||||||
|
const { loadAllCustomEmoji } = await import('../features/emoji/database');
|
||||||
|
const emojisRaw = await loadAllCustomEmoji();
|
||||||
|
if (emojisRaw.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emojis = {};
|
||||||
|
for (const emoji of emojisRaw) {
|
||||||
|
emojis[emoji.shortcode] = {
|
||||||
|
url: emoji.url,
|
||||||
|
shortcode: emoji.shortcode,
|
||||||
|
static_url: emoji.static_url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -57,6 +57,7 @@ interface Role {
|
|||||||
permissions: string;
|
permissions: string;
|
||||||
color: string;
|
color: string;
|
||||||
highlighted: boolean;
|
highlighted: boolean;
|
||||||
|
collection_limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitialWrapstodonState {
|
interface InitialWrapstodonState {
|
||||||
|
|||||||
@ -18,8 +18,12 @@
|
|||||||
"account.add_note": "إضافة ملاحظة شخصية",
|
"account.add_note": "إضافة ملاحظة شخصية",
|
||||||
"account.add_or_remove_from_list": "الإضافة أو الإزالة من القائمة",
|
"account.add_or_remove_from_list": "الإضافة أو الإزالة من القائمة",
|
||||||
"account.badges.admin": "مدير",
|
"account.badges.admin": "مدير",
|
||||||
|
"account.badges.blocked": "محظور",
|
||||||
"account.badges.bot": "آلي",
|
"account.badges.bot": "آلي",
|
||||||
|
"account.badges.domain_blocked": "النطاق محظور",
|
||||||
"account.badges.group": "فريق",
|
"account.badges.group": "فريق",
|
||||||
|
"account.badges.muted": "مكتوم",
|
||||||
|
"account.badges.muted_until": "مكتوم إلى غاية {until}",
|
||||||
"account.block": "احجب @{name}",
|
"account.block": "احجب @{name}",
|
||||||
"account.block_domain": "حظر اسم النِّطاق {domain}",
|
"account.block_domain": "حظر اسم النِّطاق {domain}",
|
||||||
"account.block_short": "حظر",
|
"account.block_short": "حظر",
|
||||||
@ -30,6 +34,7 @@
|
|||||||
"account.direct": "إشارة خاصة لـ @{name}",
|
"account.direct": "إشارة خاصة لـ @{name}",
|
||||||
"account.disable_notifications": "توقف عن إشعاري عندما ينشر @{name}",
|
"account.disable_notifications": "توقف عن إشعاري عندما ينشر @{name}",
|
||||||
"account.domain_blocking": "نطاق محظور",
|
"account.domain_blocking": "نطاق محظور",
|
||||||
|
"account.edit_note": "تعديل الملاحظة الشخصية",
|
||||||
"account.edit_profile": "تعديل الملف الشخصي",
|
"account.edit_profile": "تعديل الملف الشخصي",
|
||||||
"account.edit_profile_short": "تعديل",
|
"account.edit_profile_short": "تعديل",
|
||||||
"account.enable_notifications": "أشعرني عندما ينشر @{name}",
|
"account.enable_notifications": "أشعرني عندما ينشر @{name}",
|
||||||
@ -40,6 +45,8 @@
|
|||||||
"account.featured": "معروض",
|
"account.featured": "معروض",
|
||||||
"account.featured.accounts": "ملفات شخصية",
|
"account.featured.accounts": "ملفات شخصية",
|
||||||
"account.filters.all": "جميع الأنشطة",
|
"account.filters.all": "جميع الأنشطة",
|
||||||
|
"account.filters.boosts_toggle": "اعرض المعاد نشرها",
|
||||||
|
"account.filters.posts_boosts": "المنشورات والمعاد نشرها",
|
||||||
"account.filters.posts_only": "منشورات",
|
"account.filters.posts_only": "منشورات",
|
||||||
"account.filters.posts_replies": "المنشورات والردود",
|
"account.filters.posts_replies": "المنشورات والردود",
|
||||||
"account.filters.replies_toggle": "اعرض الردود",
|
"account.filters.replies_toggle": "اعرض الردود",
|
||||||
@ -49,6 +56,7 @@
|
|||||||
"account.follow_request": "طلب المتابعة",
|
"account.follow_request": "طلب المتابعة",
|
||||||
"account.follow_request_cancel": "إلغاء الطلب",
|
"account.follow_request_cancel": "إلغاء الطلب",
|
||||||
"account.follow_request_cancel_short": "إلغاء",
|
"account.follow_request_cancel_short": "إلغاء",
|
||||||
|
"account.follow_request_short": "طلب المتابعة",
|
||||||
"account.followers": "مُتابِعون",
|
"account.followers": "مُتابِعون",
|
||||||
"account.followers.empty": "لا أحدَ يُتابع هذا المُستخدم إلى حد الآن.",
|
"account.followers.empty": "لا أحدَ يُتابع هذا المُستخدم إلى حد الآن.",
|
||||||
"account.followers_counter": "{count, plural, zero{لا مُتابع} one {مُتابعٌ واحِد} two {مُتابعانِ اِثنان} few {{counter} مُتابِعين} many {{counter} مُتابِعًا} other {{counter} مُتابع}}",
|
"account.followers_counter": "{count, plural, zero{لا مُتابع} one {مُتابعٌ واحِد} two {مُتابعانِ اِثنان} few {{counter} مُتابِعين} many {{counter} مُتابِعًا} other {{counter} مُتابع}}",
|
||||||
@ -62,11 +70,26 @@
|
|||||||
"account.in_memoriam": "في الذكرى.",
|
"account.in_memoriam": "في الذكرى.",
|
||||||
"account.joined_short": "انضم في",
|
"account.joined_short": "انضم في",
|
||||||
"account.languages": "تغيير اللغات المشترَك فيها",
|
"account.languages": "تغيير اللغات المشترَك فيها",
|
||||||
|
"account.last_active": "آخر نشاط",
|
||||||
"account.link_verified_on": "تمَّ التَّحقق مِن مِلْكيّة هذا الرابط بتاريخ {date}",
|
"account.link_verified_on": "تمَّ التَّحقق مِن مِلْكيّة هذا الرابط بتاريخ {date}",
|
||||||
"account.locked_info": "تم ضبط حالة خصوصية هذا الحساب على أنه مؤمّن. إذ يراجع صاحبه يدويًا من يُسمح له بالمتابعة.",
|
"account.locked_info": "تم ضبط حالة خصوصية هذا الحساب على أنه مؤمّن. إذ يراجع صاحبه يدويًا من يُسمح له بالمتابعة.",
|
||||||
"account.media": "وسائط",
|
"account.media": "وسائط",
|
||||||
"account.mention": "أذكُر @{name}",
|
"account.mention": "أذكُر @{name}",
|
||||||
|
"account.menu.add_to_list": "إضافة إلى القائمة…",
|
||||||
|
"account.menu.block": "حظر الحساب",
|
||||||
|
"account.menu.block_domain": "حظر {domain}",
|
||||||
|
"account.menu.copied": "تم نسخ رابط الحساب إلى الحافظة",
|
||||||
"account.menu.copy": "نسخ الرابط",
|
"account.menu.copy": "نسخ الرابط",
|
||||||
|
"account.menu.direct": "إشارة خاصة لـ",
|
||||||
|
"account.menu.mention": "إشارة",
|
||||||
|
"account.menu.mute": "كتم الحساب",
|
||||||
|
"account.menu.note.description": "مرئي لك فقط",
|
||||||
|
"account.menu.remove_follower": "إزالة المتابِع",
|
||||||
|
"account.menu.report": "الإبلاغ عن الحساب",
|
||||||
|
"account.menu.share": "مشاركة…",
|
||||||
|
"account.menu.unblock": "رفع الحظر عن الحساب",
|
||||||
|
"account.menu.unblock_domain": "رفع الحظر عن {domain}",
|
||||||
|
"account.menu.unmute": "إلغاء كتم الحساب",
|
||||||
"account.moved_to": "أشار {name} إلى أن حسابه الجديد الآن:",
|
"account.moved_to": "أشار {name} إلى أن حسابه الجديد الآن:",
|
||||||
"account.mute": "أكتم @{name}",
|
"account.mute": "أكتم @{name}",
|
||||||
"account.mute_notifications_short": "كتم الإشعارات",
|
"account.mute_notifications_short": "كتم الإشعارات",
|
||||||
@ -74,7 +97,11 @@
|
|||||||
"account.muted": "مَكتوم",
|
"account.muted": "مَكتوم",
|
||||||
"account.muting": "مكتوم",
|
"account.muting": "مكتوم",
|
||||||
"account.mutual": "أنتم تتابعون بعضكم البعض",
|
"account.mutual": "أنتم تتابعون بعضكم البعض",
|
||||||
|
"account.name.copy": "نسخ المعرف",
|
||||||
|
"account.name_info": "ما معنى ذلك؟",
|
||||||
"account.no_bio": "لم يتم تقديم وصف.",
|
"account.no_bio": "لم يتم تقديم وصف.",
|
||||||
|
"account.node_modal.edit_title": "تعديل الملاحظة الشخصية",
|
||||||
|
"account.node_modal.error_unknown": "تعذر حفظ الملاحظة",
|
||||||
"account.node_modal.field_label": "ملاحظة شخصية",
|
"account.node_modal.field_label": "ملاحظة شخصية",
|
||||||
"account.node_modal.save": "حفظ",
|
"account.node_modal.save": "حفظ",
|
||||||
"account.node_modal.title": "إضافة ملاحظة شخصية",
|
"account.node_modal.title": "إضافة ملاحظة شخصية",
|
||||||
@ -89,6 +116,8 @@
|
|||||||
"account.share": "شارِك الملف التعريفي لـ @{name}",
|
"account.share": "شارِك الملف التعريفي لـ @{name}",
|
||||||
"account.show_reblogs": "اعرض إعادات نشر @{name}",
|
"account.show_reblogs": "اعرض إعادات نشر @{name}",
|
||||||
"account.statuses_counter": "{count, plural, zero {}one {{counter} مشور} two {{counter} منشور} few {{counter} منشور} many {{counter} منشور} other {{counter} منشور}}",
|
"account.statuses_counter": "{count, plural, zero {}one {{counter} مشور} two {{counter} منشور} few {{counter} منشور} many {{counter} منشور} other {{counter} منشور}}",
|
||||||
|
"account.timeline.pinned": "مثبّت",
|
||||||
|
"account.timeline.pinned.view_all": "عرض جميع المنشورات المثبتة",
|
||||||
"account.unblock": "إلغاء الحَظر عن @{name}",
|
"account.unblock": "إلغاء الحَظر عن @{name}",
|
||||||
"account.unblock_domain": "إلغاء الحَظر عن النِّطاق {domain}",
|
"account.unblock_domain": "إلغاء الحَظر عن النِّطاق {domain}",
|
||||||
"account.unblock_domain_short": "رفع الحظر",
|
"account.unblock_domain_short": "رفع الحظر",
|
||||||
@ -98,20 +127,57 @@
|
|||||||
"account.unmute": "إلغاء الكَتم عن @{name}",
|
"account.unmute": "إلغاء الكَتم عن @{name}",
|
||||||
"account.unmute_notifications_short": "إلغاء كَتم الإشعارات",
|
"account.unmute_notifications_short": "إلغاء كَتم الإشعارات",
|
||||||
"account.unmute_short": "إلغاء الكتم",
|
"account.unmute_short": "إلغاء الكتم",
|
||||||
|
"account_edit.advanced_settings.bot_label": "حساب آلي",
|
||||||
|
"account_edit.advanced_settings.title": "الإعدادات المتقدمة",
|
||||||
"account_edit.bio.add_label": "إضافة سيرة ذاتية",
|
"account_edit.bio.add_label": "إضافة سيرة ذاتية",
|
||||||
|
"account_edit.bio.edit_label": "تعديل السيرة الذاتية",
|
||||||
|
"account_edit.bio.title": "نبذة عنك",
|
||||||
"account_edit.bio_modal.add_title": "إضافة سيرة ذاتية",
|
"account_edit.bio_modal.add_title": "إضافة سيرة ذاتية",
|
||||||
|
"account_edit.bio_modal.edit_title": "تعديل السيرة الذاتية",
|
||||||
|
"account_edit.column_button": "تمّ",
|
||||||
|
"account_edit.column_title": "تعديل الملف الشخصي",
|
||||||
"account_edit.custom_fields.add_label": "إضافة حقل",
|
"account_edit.custom_fields.add_label": "إضافة حقل",
|
||||||
|
"account_edit.custom_fields.edit_label": "تعديل الحقل",
|
||||||
|
"account_edit.custom_fields.placeholder": "أضف ضمائر أو روابط خارجية أو أي شيء آخر ترغب في مشاركته.",
|
||||||
|
"account_edit.custom_fields.reorder_button": "إعادة ترتيب الحقول",
|
||||||
|
"account_edit.custom_fields.tip_title": "نصيحة: إضافة روابط متحقق منها",
|
||||||
|
"account_edit.custom_fields.title": "الحقول المخصصة",
|
||||||
|
"account_edit.custom_fields.verified_hint": "كيف يمكنني إضافة رابط متحقق منه؟",
|
||||||
|
"account_edit.display_name.add_label": "إضافة اسم علني",
|
||||||
|
"account_edit.display_name.edit_label": "تعديل الاسم العلني",
|
||||||
|
"account_edit.display_name.title": "الاسم العلني",
|
||||||
|
"account_edit.featured_hashtags.edit_label": "إضافة وسوم",
|
||||||
|
"account_edit.featured_hashtags.title": "الوسوم المروّجة",
|
||||||
|
"account_edit.field_actions.delete": "حذف الحقل",
|
||||||
"account_edit.field_actions.edit": "تعديل الحقل",
|
"account_edit.field_actions.edit": "تعديل الحقل",
|
||||||
"account_edit.field_delete_modal.delete_button": "حذف",
|
"account_edit.field_delete_modal.delete_button": "حذف",
|
||||||
|
"account_edit.field_delete_modal.title": "أتريد حذف الحقل المخصص؟",
|
||||||
|
"account_edit.field_edit_modal.add_title": "إضافة حقل مخصص",
|
||||||
|
"account_edit.field_edit_modal.discard_confirm": "تجاهل",
|
||||||
|
"account_edit.field_edit_modal.edit_title": "تعديل الحقل المخصص",
|
||||||
|
"account_edit.field_edit_modal.name_hint": "على سبيل المثال \"الموقع الشخصي\"",
|
||||||
|
"account_edit.field_edit_modal.name_label": "التسمية",
|
||||||
|
"account_edit.field_edit_modal.url_warning": "لإضافة رابط ، يرجى تضمين {protocol} في البداية.",
|
||||||
|
"account_edit.field_edit_modal.value_hint": "على سبيل المثال \"https://example.me\"",
|
||||||
"account_edit.field_edit_modal.value_label": "قيمة",
|
"account_edit.field_edit_modal.value_label": "قيمة",
|
||||||
|
"account_edit.image_alt_modal.add_title": "إضافة نص بديل",
|
||||||
|
"account_edit.image_alt_modal.edit_title": "تعديل نص بديل",
|
||||||
"account_edit.image_alt_modal.text_label": "نص بديل",
|
"account_edit.image_alt_modal.text_label": "نص بديل",
|
||||||
"account_edit.image_delete_modal.delete_button": "حذف",
|
"account_edit.image_delete_modal.delete_button": "حذف",
|
||||||
"account_edit.image_edit.add_button": "إضافة صورة",
|
"account_edit.image_edit.add_button": "إضافة صورة",
|
||||||
"account_edit.image_edit.alt_add_button": "إضافة نص بديل",
|
"account_edit.image_edit.alt_add_button": "إضافة نص بديل",
|
||||||
|
"account_edit.image_edit.alt_edit_button": "تعديل نص بديل",
|
||||||
|
"account_edit.image_edit.remove_button": "إزالة الصورة",
|
||||||
|
"account_edit.image_edit.replace_button": "استبدال الصورة",
|
||||||
|
"account_edit.item_list.delete": "حذف {name}",
|
||||||
|
"account_edit.item_list.edit": "تعديل {name}",
|
||||||
|
"account_edit.profile_tab.button_label": "تخصيص",
|
||||||
"account_edit.save": "حفظ",
|
"account_edit.save": "حفظ",
|
||||||
"account_edit.upload_modal.back": "العودة",
|
"account_edit.upload_modal.back": "العودة",
|
||||||
"account_edit.upload_modal.done": "تمّ",
|
"account_edit.upload_modal.done": "تمّ",
|
||||||
"account_edit.upload_modal.next": "التالي",
|
"account_edit.upload_modal.next": "التالي",
|
||||||
|
"account_edit.upload_modal.step_crop.zoom": "تكبير",
|
||||||
|
"account_edit.upload_modal.step_upload.button": "تصفح الملفات",
|
||||||
"account_edit.upload_modal.step_upload.dragging": "إسقاط للتحميل",
|
"account_edit.upload_modal.step_upload.dragging": "إسقاط للتحميل",
|
||||||
"account_edit.upload_modal.step_upload.header": "اختيار صورة",
|
"account_edit.upload_modal.step_upload.header": "اختيار صورة",
|
||||||
"account_edit.upload_modal.title_add.avatar": "إضافة صورة الملف الشخصي",
|
"account_edit.upload_modal.title_add.avatar": "إضافة صورة الملف الشخصي",
|
||||||
@ -119,6 +185,7 @@
|
|||||||
"account_edit.upload_modal.title_replace.avatar": "استبدال صورة الملف الشخصي",
|
"account_edit.upload_modal.title_replace.avatar": "استبدال صورة الملف الشخصي",
|
||||||
"account_edit.upload_modal.title_replace.header": "استبدال صورة الغلاف",
|
"account_edit.upload_modal.title_replace.header": "استبدال صورة الغلاف",
|
||||||
"account_edit_tags.add_tag": "إضافة #{tagName}",
|
"account_edit_tags.add_tag": "إضافة #{tagName}",
|
||||||
|
"account_edit_tags.suggestions": "الاقتراحات:",
|
||||||
"account_note.placeholder": "اضغط لإضافة مُلاحظة",
|
"account_note.placeholder": "اضغط لإضافة مُلاحظة",
|
||||||
"admin.dashboard.daily_retention": "معدّل بقاء المستخدمين بعد إنشاء الحسابات، بالأيام",
|
"admin.dashboard.daily_retention": "معدّل بقاء المستخدمين بعد إنشاء الحسابات، بالأيام",
|
||||||
"admin.dashboard.monthly_retention": "معدّل بقاء المستخدمين بعد إنشاء الحسابات، بالشهور",
|
"admin.dashboard.monthly_retention": "معدّل بقاء المستخدمين بعد إنشاء الحسابات، بالشهور",
|
||||||
@ -145,6 +212,7 @@
|
|||||||
"annual_report.announcement.action_dismiss": "لا شكراً",
|
"annual_report.announcement.action_dismiss": "لا شكراً",
|
||||||
"annual_report.nav_item.badge": "جديد",
|
"annual_report.nav_item.badge": "جديد",
|
||||||
"annual_report.shared_page.donate": "تبرع",
|
"annual_report.shared_page.donate": "تبرع",
|
||||||
|
"annual_report.summary.archetype.oracle.name": "الحكيم",
|
||||||
"annual_report.summary.archetype.replier.name": "الفراشة",
|
"annual_report.summary.archetype.replier.name": "الفراشة",
|
||||||
"annual_report.summary.close": "اغلق",
|
"annual_report.summary.close": "اغلق",
|
||||||
"annual_report.summary.copy_link": "نسخ الرابط",
|
"annual_report.summary.copy_link": "نسخ الرابط",
|
||||||
@ -186,17 +254,33 @@
|
|||||||
"closed_registrations_modal.find_another_server": "ابحث على خادم آخر",
|
"closed_registrations_modal.find_another_server": "ابحث على خادم آخر",
|
||||||
"closed_registrations_modal.preamble": "ماستدون لامركزي، لذلك بغض النظر عن مكان إنشاء حسابك، سيكون بإمكانك المتابعة والتفاعل مع أي شخص على هذا الخادم. يمكنك حتى أن تستضيفه ذاتياً!",
|
"closed_registrations_modal.preamble": "ماستدون لامركزي، لذلك بغض النظر عن مكان إنشاء حسابك، سيكون بإمكانك المتابعة والتفاعل مع أي شخص على هذا الخادم. يمكنك حتى أن تستضيفه ذاتياً!",
|
||||||
"closed_registrations_modal.title": "إنشاء حساب على ماستدون",
|
"closed_registrations_modal.title": "إنشاء حساب على ماستدون",
|
||||||
|
"collection.share_modal.share_link_label": "مشاركة الرابط",
|
||||||
|
"collection.share_modal.share_via_post": "نشر على ماستدون",
|
||||||
|
"collections.block_collection_owner": "حظر الحساب",
|
||||||
|
"collections.by_account": "مِن {account_handle}",
|
||||||
"collections.collection_description": "الوصف",
|
"collections.collection_description": "الوصف",
|
||||||
"collections.collection_language": "اللغة",
|
"collections.collection_language": "اللغة",
|
||||||
|
"collections.collection_language_none": "لا شيء",
|
||||||
"collections.collection_name": "الاسم",
|
"collections.collection_name": "الاسم",
|
||||||
"collections.collection_topic": "الموضوع",
|
"collections.collection_topic": "الموضوع",
|
||||||
"collections.content_warning": "تحذير عن المحتوى",
|
"collections.content_warning": "تحذير عن المحتوى",
|
||||||
"collections.continue": "مواصلة",
|
"collections.continue": "مواصلة",
|
||||||
|
"collections.copy_link": "نسخ الرابط",
|
||||||
|
"collections.copy_link_confirmation": "نسخ الرابط إلى الحافظة",
|
||||||
|
"collections.create.basic_details_title": "المعلومات الأساسية",
|
||||||
"collections.create.steps": "الخطوة {step}/{total}",
|
"collections.create.steps": "الخطوة {step}/{total}",
|
||||||
"collections.detail.accounts_heading": "الحسابات",
|
"collections.detail.accounts_heading": "الحسابات",
|
||||||
|
"collections.detail.revoke_inclusion": "أزلني",
|
||||||
"collections.detail.sensitive_content": "محتوى حساس",
|
"collections.detail.sensitive_content": "محتوى حساس",
|
||||||
"collections.edit_details": "تعديل التفاصيل",
|
"collections.edit_details": "تعديل التفاصيل",
|
||||||
|
"collections.hints.accounts_counter": "{count}/{max} حسابات",
|
||||||
"collections.manage_accounts": "إدارة الحسابات",
|
"collections.manage_accounts": "إدارة الحسابات",
|
||||||
|
"collections.remove_account": "إزالة",
|
||||||
|
"collections.sensitive": "حساس",
|
||||||
|
"collections.share_short": "مشاركة",
|
||||||
|
"collections.visibility_public": "للعامة",
|
||||||
|
"collections.visibility_title": "مدى الظهور",
|
||||||
|
"collections.visibility_unlisted": "غير مُدرَج",
|
||||||
"column.about": "عن",
|
"column.about": "عن",
|
||||||
"column.blocks": "المُستَخدِمون المَحظورون",
|
"column.blocks": "المُستَخدِمون المَحظورون",
|
||||||
"column.bookmarks": "الفواصل المرجعية",
|
"column.bookmarks": "الفواصل المرجعية",
|
||||||
@ -260,6 +344,7 @@
|
|||||||
"confirmations.delete.message": "هل أنتَ مُتأكدٌ أنك تُريدُ حَذفَ هذا المنشور؟",
|
"confirmations.delete.message": "هل أنتَ مُتأكدٌ أنك تُريدُ حَذفَ هذا المنشور؟",
|
||||||
"confirmations.delete.title": "أتريد حذف المنشور؟",
|
"confirmations.delete.title": "أتريد حذف المنشور؟",
|
||||||
"confirmations.delete_collection.confirm": "حذف",
|
"confirmations.delete_collection.confirm": "حذف",
|
||||||
|
"confirmations.delete_collection.title": "حذف \"{name}\"؟",
|
||||||
"confirmations.delete_list.confirm": "حذف",
|
"confirmations.delete_list.confirm": "حذف",
|
||||||
"confirmations.delete_list.message": "هل أنتَ مُتأكدٌ أنكَ تُريدُ حَذفَ هذِهِ القائمة بشكلٍ دائم؟",
|
"confirmations.delete_list.message": "هل أنتَ مُتأكدٌ أنكَ تُريدُ حَذفَ هذِهِ القائمة بشكلٍ دائم؟",
|
||||||
"confirmations.delete_list.title": "أتريد حذف القائمة؟",
|
"confirmations.delete_list.title": "أتريد حذف القائمة؟",
|
||||||
@ -283,6 +368,9 @@
|
|||||||
"confirmations.missing_alt_text.secondary": "انشر على أي حال",
|
"confirmations.missing_alt_text.secondary": "انشر على أي حال",
|
||||||
"confirmations.missing_alt_text.title": "أضف نصًا بديلًا؟",
|
"confirmations.missing_alt_text.title": "أضف نصًا بديلًا؟",
|
||||||
"confirmations.mute.confirm": "أكتم",
|
"confirmations.mute.confirm": "أكتم",
|
||||||
|
"confirmations.private_quote_notify.cancel": "العودة إلى التحرير",
|
||||||
|
"confirmations.private_quote_notify.confirm": "نشر المنشور",
|
||||||
|
"confirmations.private_quote_notify.do_not_show_again": "لا تظهر علي هذه الرسالة مجددًا",
|
||||||
"confirmations.quiet_post_quote_info.dismiss": "لا تُذكرني مرة أخرى",
|
"confirmations.quiet_post_quote_info.dismiss": "لا تُذكرني مرة أخرى",
|
||||||
"confirmations.quiet_post_quote_info.got_it": "مفهوم",
|
"confirmations.quiet_post_quote_info.got_it": "مفهوم",
|
||||||
"confirmations.quiet_post_quote_info.message": "عندما تقتبس منشورا هادئا للعامة، فإن منشورك سيكون أيضا مخفيا عن الخيوط الزمنية الرائجة.",
|
"confirmations.quiet_post_quote_info.message": "عندما تقتبس منشورا هادئا للعامة، فإن منشورك سيكون أيضا مخفيا عن الخيوط الزمنية الرائجة.",
|
||||||
@ -293,6 +381,7 @@
|
|||||||
"confirmations.remove_from_followers.confirm": "إزالة المتابع",
|
"confirmations.remove_from_followers.confirm": "إزالة المتابع",
|
||||||
"confirmations.remove_from_followers.message": "سيتوقف {name} عن متابعتك. هل بالتأكيد تريد المتابعة؟",
|
"confirmations.remove_from_followers.message": "سيتوقف {name} عن متابعتك. هل بالتأكيد تريد المتابعة؟",
|
||||||
"confirmations.remove_from_followers.title": "إزالة المتابع؟",
|
"confirmations.remove_from_followers.title": "إزالة المتابع؟",
|
||||||
|
"confirmations.revoke_collection_inclusion.confirm": "أزلني",
|
||||||
"confirmations.revoke_quote.confirm": "إزالة المنشور",
|
"confirmations.revoke_quote.confirm": "إزالة المنشور",
|
||||||
"confirmations.revoke_quote.message": "لا يمكن التراجع عن هذا الإجراء.",
|
"confirmations.revoke_quote.message": "لا يمكن التراجع عن هذا الإجراء.",
|
||||||
"confirmations.revoke_quote.title": "أتريد إزالة المنشور؟",
|
"confirmations.revoke_quote.title": "أتريد إزالة المنشور؟",
|
||||||
@ -343,6 +432,8 @@
|
|||||||
"dropdown.empty": "حدد خيارا",
|
"dropdown.empty": "حدد خيارا",
|
||||||
"email_subscriptions.email": "البريد الإلكتروني",
|
"email_subscriptions.email": "البريد الإلكتروني",
|
||||||
"email_subscriptions.form.action": "اشترك",
|
"email_subscriptions.form.action": "اشترك",
|
||||||
|
"email_subscriptions.submitted.title": "خطوة واحدة أخرى",
|
||||||
|
"email_subscriptions.validation.email.invalid": "عنوان البريد الإلكتروني غير صالح",
|
||||||
"embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.",
|
"embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.",
|
||||||
"embed.preview": "إليك ما سيبدو عليه:",
|
"embed.preview": "إليك ما سيبدو عليه:",
|
||||||
"emoji_button.activity": "الأنشطة",
|
"emoji_button.activity": "الأنشطة",
|
||||||
@ -437,6 +528,7 @@
|
|||||||
"follow_suggestions.view_all": "عرض الكل",
|
"follow_suggestions.view_all": "عرض الكل",
|
||||||
"follow_suggestions.who_to_follow": "حسابات للمُتابَعة",
|
"follow_suggestions.who_to_follow": "حسابات للمُتابَعة",
|
||||||
"followed_tags": "الوسوم المتابَعة",
|
"followed_tags": "الوسوم المتابَعة",
|
||||||
|
"following.title": "يتبعه {name}",
|
||||||
"footer.about": "عن",
|
"footer.about": "عن",
|
||||||
"footer.about_mastodon": "عن ماستدون",
|
"footer.about_mastodon": "عن ماستدون",
|
||||||
"footer.about_server": "عن {domain}",
|
"footer.about_server": "عن {domain}",
|
||||||
@ -750,12 +842,14 @@
|
|||||||
"notifications_permission_banner.title": "لا تفوت شيئاً أبداً",
|
"notifications_permission_banner.title": "لا تفوت شيئاً أبداً",
|
||||||
"onboarding.follows.back": "عودة",
|
"onboarding.follows.back": "عودة",
|
||||||
"onboarding.follows.empty": "نأسف، لا يمكن عرض نتائج في الوقت الحالي. جرب البحث أو انتقل لصفحة الاستكشاف لإيجاد أشخاص للمتابعة، أو حاول مرة أخرى.",
|
"onboarding.follows.empty": "نأسف، لا يمكن عرض نتائج في الوقت الحالي. جرب البحث أو انتقل لصفحة الاستكشاف لإيجاد أشخاص للمتابعة، أو حاول مرة أخرى.",
|
||||||
|
"onboarding.follows.next": "التالي: إعداد ملفك الشخصي",
|
||||||
"onboarding.follows.search": "بحث",
|
"onboarding.follows.search": "بحث",
|
||||||
"onboarding.follows.title": "للبدء قم بمتابعة أشخاص",
|
"onboarding.follows.title": "للبدء قم بمتابعة أشخاص",
|
||||||
"onboarding.profile.discoverable": "اجعل ملفي الشخصي قابلاً للاكتشاف",
|
"onboarding.profile.discoverable": "اجعل ملفي الشخصي قابلاً للاكتشاف",
|
||||||
"onboarding.profile.discoverable_hint": "عندما تختار تفعيل إمكانية الاكتشاف على ماستدون، قد تظهر منشوراتك في نتائج البحث والمواضيع الرائجة، وقد يتم اقتراح ملفك الشخصي لأشخاص ذوي اهتمامات مماثلة معك.",
|
"onboarding.profile.discoverable_hint": "عندما تختار تفعيل إمكانية الاكتشاف على ماستدون، قد تظهر منشوراتك في نتائج البحث والمواضيع الرائجة، وقد يتم اقتراح ملفك الشخصي لأشخاص ذوي اهتمامات مماثلة معك.",
|
||||||
"onboarding.profile.display_name": "الاسم العلني",
|
"onboarding.profile.display_name": "الاسم العلني",
|
||||||
"onboarding.profile.display_name_hint": "اسمك الكامل أو اسمك المرح…",
|
"onboarding.profile.display_name_hint": "اسمك الكامل أو اسمك المرح…",
|
||||||
|
"onboarding.profile.finish": "إنهاء",
|
||||||
"onboarding.profile.note": "نبذة عنك",
|
"onboarding.profile.note": "نبذة عنك",
|
||||||
"onboarding.profile.note_hint": "يمكنك @ذِكر أشخاص آخرين أو استعمال #الوسوم…",
|
"onboarding.profile.note_hint": "يمكنك @ذِكر أشخاص آخرين أو استعمال #الوسوم…",
|
||||||
"onboarding.profile.title": "إعداد الملف الشخصي",
|
"onboarding.profile.title": "إعداد الملف الشخصي",
|
||||||
@ -896,6 +990,7 @@
|
|||||||
"sign_in_banner.mastodon_is": "ماستودون هو أفضل وسيلة لمواكبة الأحداث.",
|
"sign_in_banner.mastodon_is": "ماستودون هو أفضل وسيلة لمواكبة الأحداث.",
|
||||||
"sign_in_banner.sign_in": "تسجيل الدخول",
|
"sign_in_banner.sign_in": "تسجيل الدخول",
|
||||||
"sign_in_banner.sso_redirect": "تسجيل الدخول أو إنشاء حساب",
|
"sign_in_banner.sso_redirect": "تسجيل الدخول أو إنشاء حساب",
|
||||||
|
"skip_links.skip_to_content": "تخطي إلى المحتوى الرئيسي",
|
||||||
"status.admin_account": "افتح الواجهة الإدارية لـ @{name}",
|
"status.admin_account": "افتح الواجهة الإدارية لـ @{name}",
|
||||||
"status.admin_domain": "فتح واجهة الإشراف لـ {domain}",
|
"status.admin_domain": "فتح واجهة الإشراف لـ {domain}",
|
||||||
"status.admin_status": "افتح هذا المنشور على واجهة الإشراف",
|
"status.admin_status": "افتح هذا المنشور على واجهة الإشراف",
|
||||||
@ -905,6 +1000,10 @@
|
|||||||
"status.cancel_reblog_private": "إلغاء إعادة النشر",
|
"status.cancel_reblog_private": "إلغاء إعادة النشر",
|
||||||
"status.cannot_quote": "غير مصرح لك باقتباس هذا المنشور",
|
"status.cannot_quote": "غير مصرح لك باقتباس هذا المنشور",
|
||||||
"status.cannot_reblog": "لا يمكن إعادة نشر هذا المنشور",
|
"status.cannot_reblog": "لا يمكن إعادة نشر هذا المنشور",
|
||||||
|
"status.contains_quote": "يحتوي على اقتباس",
|
||||||
|
"status.context.loading": "تحميل المزيد من الردود",
|
||||||
|
"status.context.loading_success": "تم تحميل ردود جديدة",
|
||||||
|
"status.context.more_replies_found": "تم العثور على المزيد من الردود",
|
||||||
"status.context.retry": "حاول مجددًا",
|
"status.context.retry": "حاول مجددًا",
|
||||||
"status.context.show": "إظهار",
|
"status.context.show": "إظهار",
|
||||||
"status.continued_thread": "تكملة للخيط",
|
"status.continued_thread": "تكملة للخيط",
|
||||||
|
|||||||
@ -71,6 +71,8 @@
|
|||||||
"account.go_to_profile": "Перайсці да профілю",
|
"account.go_to_profile": "Перайсці да профілю",
|
||||||
"account.hide_reblogs": "Схаваць пашырэнні ад @{name}",
|
"account.hide_reblogs": "Схаваць пашырэнні ад @{name}",
|
||||||
"account.in_memoriam": "У памяць.",
|
"account.in_memoriam": "У памяць.",
|
||||||
|
"account.join_modal.me": "Вы далучыліся да {server}",
|
||||||
|
"account.join_modal.years": "{number, plural, one {# год} few {# гады} many {# гадоў} other {# гады}}",
|
||||||
"account.joined_short": "Далучыўся",
|
"account.joined_short": "Далучыўся",
|
||||||
"account.languages": "Змяніць выбраныя мовы",
|
"account.languages": "Змяніць выбраныя мовы",
|
||||||
"account.last_active": "Апошняя актыўнасць",
|
"account.last_active": "Апошняя актыўнасць",
|
||||||
@ -398,6 +400,8 @@
|
|||||||
"collections.manage_accounts": "Кіраванне ўліковымі запісамі",
|
"collections.manage_accounts": "Кіраванне ўліковымі запісамі",
|
||||||
"collections.mark_as_sensitive": "Пазначыць як адчувальную",
|
"collections.mark_as_sensitive": "Пазначыць як адчувальную",
|
||||||
"collections.mark_as_sensitive_hint": "Схаваць апісанне калекцыі і ўліковыя запісы за банерам з папярэджаннем. Назва калекцыі застанецца бачнай.",
|
"collections.mark_as_sensitive_hint": "Схаваць апісанне калекцыі і ўліковыя запісы за банерам з папярэджаннем. Назва калекцыі застанецца бачнай.",
|
||||||
|
"collections.maximum_collection_count_description": "Ваш сервер дазваляе ствараць да {count} калекцый.",
|
||||||
|
"collections.maximum_collection_count_reached": "Вы стварылі максімальную колькасць калекцый",
|
||||||
"collections.name_length_hint": "Максімум 40 сімвалаў",
|
"collections.name_length_hint": "Максімум 40 сімвалаў",
|
||||||
"collections.new_collection": "Новая калекцыя",
|
"collections.new_collection": "Новая калекцыя",
|
||||||
"collections.no_collections_yet": "Пакуль няма калекцый.",
|
"collections.no_collections_yet": "Пакуль няма калекцый.",
|
||||||
|
|||||||
@ -71,6 +71,16 @@
|
|||||||
"account.go_to_profile": "Gå til profil",
|
"account.go_to_profile": "Gå til profil",
|
||||||
"account.hide_reblogs": "Skjul fremhævelser fra @{name}",
|
"account.hide_reblogs": "Skjul fremhævelser fra @{name}",
|
||||||
"account.in_memoriam": "Til minde om.",
|
"account.in_memoriam": "Til minde om.",
|
||||||
|
"account.join_modal.day": "Dag",
|
||||||
|
"account.join_modal.me": "Du tilmeldte dig {server}",
|
||||||
|
"account.join_modal.me_anniversary": "Tillykke med dit fedivers-jubilæum! Du tilmeldte dig {server}",
|
||||||
|
"account.join_modal.me_today": "Det er din første dag på {server}!",
|
||||||
|
"account.join_modal.other": "{name} tilmeldte sig {server}",
|
||||||
|
"account.join_modal.other_today": "Det er {name}’s første dag på {server}!",
|
||||||
|
"account.join_modal.share.celebrate": "Del et indlæg for at fejre begivenheden",
|
||||||
|
"account.join_modal.share.intro": "Del et introindlæg",
|
||||||
|
"account.join_modal.share.welcome": "Del et velkomstindlæg",
|
||||||
|
"account.join_modal.years": "{number, plural, one {år} other {år}}",
|
||||||
"account.joined_short": "Oprettet",
|
"account.joined_short": "Oprettet",
|
||||||
"account.languages": "Skift abonnementssprog",
|
"account.languages": "Skift abonnementssprog",
|
||||||
"account.last_active": "Senest aktiv",
|
"account.last_active": "Senest aktiv",
|
||||||
@ -398,6 +408,8 @@
|
|||||||
"collections.manage_accounts": "Administrer konti",
|
"collections.manage_accounts": "Administrer konti",
|
||||||
"collections.mark_as_sensitive": "Markér som sensitiv",
|
"collections.mark_as_sensitive": "Markér som sensitiv",
|
||||||
"collections.mark_as_sensitive_hint": "Skjuler samlingens beskrivelse og konti bag en indholdsadvarsel. Samlingens navn vil stadig være synligt.",
|
"collections.mark_as_sensitive_hint": "Skjuler samlingens beskrivelse og konti bag en indholdsadvarsel. Samlingens navn vil stadig være synligt.",
|
||||||
|
"collections.maximum_collection_count_description": "Din server tillader oprettelse af op til {count} samlinger.",
|
||||||
|
"collections.maximum_collection_count_reached": "Du har oprettet det maksimale antal samlinger",
|
||||||
"collections.name_length_hint": "Begrænset til 40 tegn",
|
"collections.name_length_hint": "Begrænset til 40 tegn",
|
||||||
"collections.new_collection": "Ny samling",
|
"collections.new_collection": "Ny samling",
|
||||||
"collections.no_collections_yet": "Ingen samlinger endnu.",
|
"collections.no_collections_yet": "Ingen samlinger endnu.",
|
||||||
|
|||||||
@ -71,6 +71,16 @@
|
|||||||
"account.go_to_profile": "Profil aufrufen",
|
"account.go_to_profile": "Profil aufrufen",
|
||||||
"account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden",
|
"account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden",
|
||||||
"account.in_memoriam": "Zum Andenken.",
|
"account.in_memoriam": "Zum Andenken.",
|
||||||
|
"account.join_modal.day": "Tag",
|
||||||
|
"account.join_modal.me": "Du bist auf {server} dabei seit",
|
||||||
|
"account.join_modal.me_anniversary": "Happy Fediversary! Du bist auf {server} dabei seit",
|
||||||
|
"account.join_modal.me_today": "Dein erster Tag auf {server}!",
|
||||||
|
"account.join_modal.other": "{name} ist auf {server} dabei seit",
|
||||||
|
"account.join_modal.other_today": "{name} ist seit einem Tag auf {server}!",
|
||||||
|
"account.join_modal.share.celebrate": "Teile die Feierlichkeit",
|
||||||
|
"account.join_modal.share.intro": "Teile einen Einblick über dich",
|
||||||
|
"account.join_modal.share.welcome": "Teile ein herzliches Willkommen",
|
||||||
|
"account.join_modal.years": "{number, plural, one {Jahr} other {Jahre}}",
|
||||||
"account.joined_short": "Dabei seit",
|
"account.joined_short": "Dabei seit",
|
||||||
"account.languages": "Sprachen verwalten",
|
"account.languages": "Sprachen verwalten",
|
||||||
"account.last_active": "Zuletzt aktiv",
|
"account.last_active": "Zuletzt aktiv",
|
||||||
@ -398,6 +408,8 @@
|
|||||||
"collections.manage_accounts": "Profile verwalten",
|
"collections.manage_accounts": "Profile verwalten",
|
||||||
"collections.mark_as_sensitive": "Mit Inhaltswarnung versehen",
|
"collections.mark_as_sensitive": "Mit Inhaltswarnung versehen",
|
||||||
"collections.mark_as_sensitive_hint": "Die Beschreibung sowie enthaltenen Profile werden durch eine Inhaltswarnung ausgeblendet. Der Titel bleibt weiterhin sichtbar.",
|
"collections.mark_as_sensitive_hint": "Die Beschreibung sowie enthaltenen Profile werden durch eine Inhaltswarnung ausgeblendet. Der Titel bleibt weiterhin sichtbar.",
|
||||||
|
"collections.maximum_collection_count_description": "Auf deinem Server darfst du bis zu {count} Sammlungen erstellen.",
|
||||||
|
"collections.maximum_collection_count_reached": "Du hast die Höchstzahl an Sammlungen erreicht",
|
||||||
"collections.name_length_hint": "Maximal 40 Zeichen",
|
"collections.name_length_hint": "Maximal 40 Zeichen",
|
||||||
"collections.new_collection": "Neue Sammlung",
|
"collections.new_collection": "Neue Sammlung",
|
||||||
"collections.no_collections_yet": "Bisher keine Sammlungen vorhanden.",
|
"collections.no_collections_yet": "Bisher keine Sammlungen vorhanden.",
|
||||||
|
|||||||
@ -71,6 +71,16 @@
|
|||||||
"account.go_to_profile": "Μετάβαση στο προφίλ",
|
"account.go_to_profile": "Μετάβαση στο προφίλ",
|
||||||
"account.hide_reblogs": "Απόκρυψη ενισχύσεων από @{name}",
|
"account.hide_reblogs": "Απόκρυψη ενισχύσεων από @{name}",
|
||||||
"account.in_memoriam": "Εις μνήμην.",
|
"account.in_memoriam": "Εις μνήμην.",
|
||||||
|
"account.join_modal.day": "Ημέρα",
|
||||||
|
"account.join_modal.me": "Έγινες μέλος στο {server} στις",
|
||||||
|
"account.join_modal.me_anniversary": "Ευτυχισμένο Fediversary! Έγινες μέλος στο {server} στις",
|
||||||
|
"account.join_modal.me_today": "Είναι η πρώτη σου μέρα στο {server}!",
|
||||||
|
"account.join_modal.other": "Ο/Η {name} έγινε μέλος στο {server} στις",
|
||||||
|
"account.join_modal.other_today": "Είναι η πρώτη μέρα του/της {name} στο {server}!",
|
||||||
|
"account.join_modal.share.celebrate": "Μοιράσου μια εορταστική ανάρτηση",
|
||||||
|
"account.join_modal.share.intro": "Μοιράσου μια εισαγωγική ανάρτηση",
|
||||||
|
"account.join_modal.share.welcome": "Μοιράσου μια ανάρτηση καλωσορίσματος",
|
||||||
|
"account.join_modal.years": "{number, plural, one {χρόνος} other {χρόνια}}",
|
||||||
"account.joined_short": "Έγινε μέλος",
|
"account.joined_short": "Έγινε μέλος",
|
||||||
"account.languages": "Αλλαγή εγγεγραμμένων γλωσσών",
|
"account.languages": "Αλλαγή εγγεγραμμένων γλωσσών",
|
||||||
"account.last_active": "Τελευταία ενεργός",
|
"account.last_active": "Τελευταία ενεργός",
|
||||||
@ -398,6 +408,8 @@
|
|||||||
"collections.manage_accounts": "Διαχείριση λογαριασμών",
|
"collections.manage_accounts": "Διαχείριση λογαριασμών",
|
||||||
"collections.mark_as_sensitive": "Σήμανση ως ευαίσθητο",
|
"collections.mark_as_sensitive": "Σήμανση ως ευαίσθητο",
|
||||||
"collections.mark_as_sensitive_hint": "Κρύβει την περιγραφή και τους λογαριασμούς της συλλογής πίσω από μια προειδοποίηση περιεχομένου. Το όνομα της συλλογής θα είναι ακόμη ορατό.",
|
"collections.mark_as_sensitive_hint": "Κρύβει την περιγραφή και τους λογαριασμούς της συλλογής πίσω από μια προειδοποίηση περιεχομένου. Το όνομα της συλλογής θα είναι ακόμη ορατό.",
|
||||||
|
"collections.maximum_collection_count_description": "Ο εξυπηρετητής σου επιτρέπει την δημιουργία μέχρι {count} συλλογών.",
|
||||||
|
"collections.maximum_collection_count_reached": "Έχεις δημιουργήσει τον μέγιστο αριθμό συλλογών",
|
||||||
"collections.name_length_hint": "Όριο 40 χαρακτήρων",
|
"collections.name_length_hint": "Όριο 40 χαρακτήρων",
|
||||||
"collections.new_collection": "Νέα συλλογή",
|
"collections.new_collection": "Νέα συλλογή",
|
||||||
"collections.no_collections_yet": "Καμία συλλογή ακόμη.",
|
"collections.no_collections_yet": "Καμία συλλογή ακόμη.",
|
||||||
|
|||||||
@ -71,6 +71,16 @@
|
|||||||
"account.go_to_profile": "Go to profile",
|
"account.go_to_profile": "Go to profile",
|
||||||
"account.hide_reblogs": "Hide boosts from @{name}",
|
"account.hide_reblogs": "Hide boosts from @{name}",
|
||||||
"account.in_memoriam": "In Memoriam.",
|
"account.in_memoriam": "In Memoriam.",
|
||||||
|
"account.join_modal.day": "Day",
|
||||||
|
"account.join_modal.me": "You joined {server} on",
|
||||||
|
"account.join_modal.me_anniversary": "Happy Fediversary! You joined {server} on",
|
||||||
|
"account.join_modal.me_today": "It’s your first day on {server}!",
|
||||||
|
"account.join_modal.other": "{name} joined {server} on",
|
||||||
|
"account.join_modal.other_today": "It’s {name}’s first day on {server}!",
|
||||||
|
"account.join_modal.share.celebrate": "Share a celebratory post",
|
||||||
|
"account.join_modal.share.intro": "Share an intro post",
|
||||||
|
"account.join_modal.share.welcome": "Share a welcome post",
|
||||||
|
"account.join_modal.years": "{number, plural, one {year} other {years}}",
|
||||||
"account.joined_short": "Joined",
|
"account.joined_short": "Joined",
|
||||||
"account.languages": "Change subscribed languages",
|
"account.languages": "Change subscribed languages",
|
||||||
"account.last_active": "Last active",
|
"account.last_active": "Last active",
|
||||||
@ -398,6 +408,8 @@
|
|||||||
"collections.manage_accounts": "Manage accounts",
|
"collections.manage_accounts": "Manage accounts",
|
||||||
"collections.mark_as_sensitive": "Mark as sensitive",
|
"collections.mark_as_sensitive": "Mark as sensitive",
|
||||||
"collections.mark_as_sensitive_hint": "Hides the collection's description and accounts behind a content warning. The collection name will still be visible.",
|
"collections.mark_as_sensitive_hint": "Hides the collection's description and accounts behind a content warning. The collection name will still be visible.",
|
||||||
|
"collections.maximum_collection_count_description": "Your server allows creation of up to {count} collections.",
|
||||||
|
"collections.maximum_collection_count_reached": "You have created the maximum number of collections",
|
||||||
"collections.name_length_hint": "40 characters limit",
|
"collections.name_length_hint": "40 characters limit",
|
||||||
"collections.new_collection": "New collection",
|
"collections.new_collection": "New collection",
|
||||||
"collections.no_collections_yet": "No collections yet.",
|
"collections.no_collections_yet": "No collections yet.",
|
||||||
@ -1184,6 +1196,7 @@
|
|||||||
"server_banner.active_users": "active users",
|
"server_banner.active_users": "active users",
|
||||||
"server_banner.administered_by": "Administered by:",
|
"server_banner.administered_by": "Administered by:",
|
||||||
"server_banner.is_one_of_many": "{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.",
|
"server_banner.is_one_of_many": "{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.",
|
||||||
|
"server_banner.more_about_this_server": "More about this server",
|
||||||
"server_banner.server_stats": "Server stats:",
|
"server_banner.server_stats": "Server stats:",
|
||||||
"sign_in_banner.create_account": "Create account",
|
"sign_in_banner.create_account": "Create account",
|
||||||
"sign_in_banner.follow_anyone": "Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.",
|
"sign_in_banner.follow_anyone": "Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.",
|
||||||
|
|||||||
@ -71,6 +71,16 @@
|
|||||||
"account.go_to_profile": "Ir al perfil",
|
"account.go_to_profile": "Ir al perfil",
|
||||||
"account.hide_reblogs": "Ocultar adhesiones de @{name}",
|
"account.hide_reblogs": "Ocultar adhesiones de @{name}",
|
||||||
"account.in_memoriam": "Cuenta conmemorativa.",
|
"account.in_memoriam": "Cuenta conmemorativa.",
|
||||||
|
"account.join_modal.day": "Día",
|
||||||
|
"account.join_modal.me": "Te uniste a {server} el",
|
||||||
|
"account.join_modal.me_anniversary": "¡Feliz Fediversario! Te uniste a {server} el",
|
||||||
|
"account.join_modal.me_today": "¡Es tu primer día en {server}!",
|
||||||
|
"account.join_modal.other": "{name} se unió a {server} el",
|
||||||
|
"account.join_modal.other_today": "¡Es el primer día de {name} en {server}!",
|
||||||
|
"account.join_modal.share.celebrate": "Compartí un mensaje conmemorativo",
|
||||||
|
"account.join_modal.share.intro": "Compartí un mensaje introductorio",
|
||||||
|
"account.join_modal.share.welcome": "Compartí un mensaje de bienvenida",
|
||||||
|
"account.join_modal.years": "{number, plural, one {# año} other {# años}}",
|
||||||
"account.joined_short": "En este servidor desde el",
|
"account.joined_short": "En este servidor desde el",
|
||||||
"account.languages": "Cambiar idiomas suscritos",
|
"account.languages": "Cambiar idiomas suscritos",
|
||||||
"account.last_active": "Última actividad",
|
"account.last_active": "Última actividad",
|
||||||
@ -398,6 +408,8 @@
|
|||||||
"collections.manage_accounts": "Administrar cuentas",
|
"collections.manage_accounts": "Administrar cuentas",
|
||||||
"collections.mark_as_sensitive": "Marcar como sensible",
|
"collections.mark_as_sensitive": "Marcar como sensible",
|
||||||
"collections.mark_as_sensitive_hint": "Oculta la descripción de la colección y las cuentas detrás de una advertencia de contenido. El nombre de la colección seguirá siendo visible.",
|
"collections.mark_as_sensitive_hint": "Oculta la descripción de la colección y las cuentas detrás de una advertencia de contenido. El nombre de la colección seguirá siendo visible.",
|
||||||
|
"collections.maximum_collection_count_description": "Tu servidor permite creaciones de hasta {count} colecciones.",
|
||||||
|
"collections.maximum_collection_count_reached": "Creaste el número máximo de colecciones",
|
||||||
"collections.name_length_hint": "Límite de 40 caracteres",
|
"collections.name_length_hint": "Límite de 40 caracteres",
|
||||||
"collections.new_collection": "Nueva colección",
|
"collections.new_collection": "Nueva colección",
|
||||||
"collections.no_collections_yet": "No hay colecciones aún.",
|
"collections.no_collections_yet": "No hay colecciones aún.",
|
||||||
|
|||||||
@ -71,6 +71,16 @@
|
|||||||
"account.go_to_profile": "Ir al perfil",
|
"account.go_to_profile": "Ir al perfil",
|
||||||
"account.hide_reblogs": "Ocultar impulsos de @{name}",
|
"account.hide_reblogs": "Ocultar impulsos de @{name}",
|
||||||
"account.in_memoriam": "En memoria.",
|
"account.in_memoriam": "En memoria.",
|
||||||
|
"account.join_modal.day": "Día",
|
||||||
|
"account.join_modal.me": "Te uniste a {server} el",
|
||||||
|
"account.join_modal.me_anniversary": "¡Feliz Fediversario! Te uniste a {server} el",
|
||||||
|
"account.join_modal.me_today": "¡Es tu primer día en {server}!",
|
||||||
|
"account.join_modal.other": "{name} se unió a {server} el",
|
||||||
|
"account.join_modal.other_today": "¡Es el primer día de {name} en {server}!",
|
||||||
|
"account.join_modal.share.celebrate": "Compartir una publicación para celebrarlo",
|
||||||
|
"account.join_modal.share.intro": "Compartir una publicación de presentación",
|
||||||
|
"account.join_modal.share.welcome": "Compartir una publicación de bienvenida",
|
||||||
|
"account.join_modal.years": "{number, plural, one {año} other {# años}}",
|
||||||
"account.joined_short": "Se unió",
|
"account.joined_short": "Se unió",
|
||||||
"account.languages": "Cambiar idiomas suscritos",
|
"account.languages": "Cambiar idiomas suscritos",
|
||||||
"account.last_active": "Última actividad",
|
"account.last_active": "Última actividad",
|
||||||
@ -398,6 +408,8 @@
|
|||||||
"collections.manage_accounts": "Administrar cuentas",
|
"collections.manage_accounts": "Administrar cuentas",
|
||||||
"collections.mark_as_sensitive": "Marcar como sensible",
|
"collections.mark_as_sensitive": "Marcar como sensible",
|
||||||
"collections.mark_as_sensitive_hint": "Oculta la descripción y las cuentas de la colección detrás de una advertencia de contenido. El nombre de la colección seguirá siendo visible.",
|
"collections.mark_as_sensitive_hint": "Oculta la descripción y las cuentas de la colección detrás de una advertencia de contenido. El nombre de la colección seguirá siendo visible.",
|
||||||
|
"collections.maximum_collection_count_description": "Tu servidor permite la creación de hasta {count} colecciones.",
|
||||||
|
"collections.maximum_collection_count_reached": "Has creado el número máximo de colecciones",
|
||||||
"collections.name_length_hint": "Limitado a 40 caracteres",
|
"collections.name_length_hint": "Limitado a 40 caracteres",
|
||||||
"collections.new_collection": "Nueva colección",
|
"collections.new_collection": "Nueva colección",
|
||||||
"collections.no_collections_yet": "No hay colecciones todavía.",
|
"collections.no_collections_yet": "No hay colecciones todavía.",
|
||||||
|
|||||||
@ -71,6 +71,16 @@
|
|||||||
"account.go_to_profile": "Ir al perfil",
|
"account.go_to_profile": "Ir al perfil",
|
||||||
"account.hide_reblogs": "Ocultar impulsos de @{name}",
|
"account.hide_reblogs": "Ocultar impulsos de @{name}",
|
||||||
"account.in_memoriam": "Cuenta conmemorativa.",
|
"account.in_memoriam": "Cuenta conmemorativa.",
|
||||||
|
"account.join_modal.day": "Día",
|
||||||
|
"account.join_modal.me": "Te uniste a {server} el",
|
||||||
|
"account.join_modal.me_anniversary": "¡Feliz Fediversario! Te uniste a {server} el",
|
||||||
|
"account.join_modal.me_today": "¡Es tu primer día en {server}!",
|
||||||
|
"account.join_modal.other": "{name} se unió a {server} el",
|
||||||
|
"account.join_modal.other_today": "¡Es el primer día de {name} en {server}!",
|
||||||
|
"account.join_modal.share.celebrate": "Compartir una publicación para celebrarlo",
|
||||||
|
"account.join_modal.share.intro": "Compartir una publicación de presentación",
|
||||||
|
"account.join_modal.share.welcome": "Compartir una publicación de bienvenida",
|
||||||
|
"account.join_modal.years": "{number, plural, one {año} other {# años}}",
|
||||||
"account.joined_short": "Se unió",
|
"account.joined_short": "Se unió",
|
||||||
"account.languages": "Cambiar idiomas suscritos",
|
"account.languages": "Cambiar idiomas suscritos",
|
||||||
"account.last_active": "Última actividad",
|
"account.last_active": "Última actividad",
|
||||||
@ -398,6 +408,8 @@
|
|||||||
"collections.manage_accounts": "Administrar cuentas",
|
"collections.manage_accounts": "Administrar cuentas",
|
||||||
"collections.mark_as_sensitive": "Marcar como sensible",
|
"collections.mark_as_sensitive": "Marcar como sensible",
|
||||||
"collections.mark_as_sensitive_hint": "Oculta la descripción de la colección y las cuentas detrás de una advertencia de contenido. El nombre de la colección seguirá siendo visible.",
|
"collections.mark_as_sensitive_hint": "Oculta la descripción de la colección y las cuentas detrás de una advertencia de contenido. El nombre de la colección seguirá siendo visible.",
|
||||||
|
"collections.maximum_collection_count_description": "Tu servidor permite la creación de hasta {count} colecciones.",
|
||||||
|
"collections.maximum_collection_count_reached": "Has creado el número máximo de colecciones",
|
||||||
"collections.name_length_hint": "Límite de 40 caracteres",
|
"collections.name_length_hint": "Límite de 40 caracteres",
|
||||||
"collections.new_collection": "Nueva colección",
|
"collections.new_collection": "Nueva colección",
|
||||||
"collections.no_collections_yet": "Aún no hay colecciones.",
|
"collections.no_collections_yet": "Aún no hay colecciones.",
|
||||||
|
|||||||
@ -71,6 +71,10 @@
|
|||||||
"account.go_to_profile": "Vaata profiili",
|
"account.go_to_profile": "Vaata profiili",
|
||||||
"account.hide_reblogs": "Peida @{name} jagamised",
|
"account.hide_reblogs": "Peida @{name} jagamised",
|
||||||
"account.in_memoriam": "In Memoriam.",
|
"account.in_memoriam": "In Memoriam.",
|
||||||
|
"account.join_modal.day": "Päev",
|
||||||
|
"account.join_modal.me": "Ühinesid {server} kohas",
|
||||||
|
"account.join_modal.me_anniversary": "Rõõmsat Födiversumist! Ühinesid {server} kohas",
|
||||||
|
"account.join_modal.me_today": "See on sinu esimene päev kohas {server}!",
|
||||||
"account.joined_short": "Liitus",
|
"account.joined_short": "Liitus",
|
||||||
"account.languages": "Muuda tellitud keeli",
|
"account.languages": "Muuda tellitud keeli",
|
||||||
"account.last_active": "Viimati aktiivne",
|
"account.last_active": "Viimati aktiivne",
|
||||||
|
|||||||
@ -71,6 +71,16 @@
|
|||||||
"account.go_to_profile": "Siirry profiiliin",
|
"account.go_to_profile": "Siirry profiiliin",
|
||||||
"account.hide_reblogs": "Piilota käyttäjän @{name} tehostukset",
|
"account.hide_reblogs": "Piilota käyttäjän @{name} tehostukset",
|
||||||
"account.in_memoriam": "Muistoissamme.",
|
"account.in_memoriam": "Muistoissamme.",
|
||||||
|
"account.join_modal.day": "päivä",
|
||||||
|
"account.join_modal.me": "Liityit palvelimelle {server}",
|
||||||
|
"account.join_modal.me_anniversary": "Hyvää fediversumin vuosipäivää! Liityit palvelimelle {server}",
|
||||||
|
"account.join_modal.me_today": "On ensimmäinen päiväsi palvelimella {server}!",
|
||||||
|
"account.join_modal.other": "{name} liittyi palvelimelle {server}",
|
||||||
|
"account.join_modal.other_today": "On käyttäjän {name} ensimmäinen päivä palvelimella {server}!",
|
||||||
|
"account.join_modal.share.celebrate": "Jaa juhlajulkaisu",
|
||||||
|
"account.join_modal.share.intro": "Jaa esittelyjulkaisu",
|
||||||
|
"account.join_modal.share.welcome": "Jaa tervetulojulkaisu",
|
||||||
|
"account.join_modal.years": "{number, plural, one {vuosi} other {vuotta}}",
|
||||||
"account.joined_short": "Liittynyt",
|
"account.joined_short": "Liittynyt",
|
||||||
"account.languages": "Vaihda tilattuja kieliä",
|
"account.languages": "Vaihda tilattuja kieliä",
|
||||||
"account.last_active": "Viimeksi aktiivisena",
|
"account.last_active": "Viimeksi aktiivisena",
|
||||||
@ -398,6 +408,8 @@
|
|||||||
"collections.manage_accounts": "Hallitse tilejä",
|
"collections.manage_accounts": "Hallitse tilejä",
|
||||||
"collections.mark_as_sensitive": "Merkitse arkaluonteiseksi",
|
"collections.mark_as_sensitive": "Merkitse arkaluonteiseksi",
|
||||||
"collections.mark_as_sensitive_hint": "Piilottaa kokoelman kuvauksen ja tilit sisältövaroituksen taakse. Kokoelman nimi jää esiin.",
|
"collections.mark_as_sensitive_hint": "Piilottaa kokoelman kuvauksen ja tilit sisältövaroituksen taakse. Kokoelman nimi jää esiin.",
|
||||||
|
"collections.maximum_collection_count_description": "Palvelimesi sallii enintään {count} kokoelman luomisen.",
|
||||||
|
"collections.maximum_collection_count_reached": "Olet luonut enimmäismäärän kokoelmia",
|
||||||
"collections.name_length_hint": "40 merkin rajoitus",
|
"collections.name_length_hint": "40 merkin rajoitus",
|
||||||
"collections.new_collection": "Uusi kokoelma",
|
"collections.new_collection": "Uusi kokoelma",
|
||||||
"collections.no_collections_yet": "Ei vielä kokoelmia.",
|
"collections.no_collections_yet": "Ei vielä kokoelmia.",
|
||||||
|
|||||||
@ -327,8 +327,8 @@
|
|||||||
"block_modal.title": "Bloquer l'utilisateur·ice ?",
|
"block_modal.title": "Bloquer l'utilisateur·ice ?",
|
||||||
"block_modal.you_wont_see_mentions": "Vous ne verrez plus les messages qui le ou la mentionnent.",
|
"block_modal.you_wont_see_mentions": "Vous ne verrez plus les messages qui le ou la mentionnent.",
|
||||||
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour sauter ceci la prochaine fois",
|
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour sauter ceci la prochaine fois",
|
||||||
"boost_modal.reblog": "Booster le message ?",
|
"boost_modal.reblog": "Partager le message ?",
|
||||||
"boost_modal.undo_reblog": "Annuler le boost du message ?",
|
"boost_modal.undo_reblog": "Annuler le partage du message ?",
|
||||||
"bundle_column_error.copy_stacktrace": "Copier le rapport d'erreur",
|
"bundle_column_error.copy_stacktrace": "Copier le rapport d'erreur",
|
||||||
"bundle_column_error.error.body": "La page demandée n'a pas pu être affichée. Cela pourrait être dû à un bogue dans notre code, ou à un problème de compatibilité avec le navigateur.",
|
"bundle_column_error.error.body": "La page demandée n'a pas pu être affichée. Cela pourrait être dû à un bogue dans notre code, ou à un problème de compatibilité avec le navigateur.",
|
||||||
"bundle_column_error.error.title": "Oh non!",
|
"bundle_column_error.error.title": "Oh non!",
|
||||||
@ -343,7 +343,7 @@
|
|||||||
"bundle_modal_error.retry": "Réessayer",
|
"bundle_modal_error.retry": "Réessayer",
|
||||||
"callout.dismiss": "Rejeter",
|
"callout.dismiss": "Rejeter",
|
||||||
"carousel.current": "<sr>Diapositive</sr> {current, number} / {max, number}",
|
"carousel.current": "<sr>Diapositive</sr> {current, number} / {max, number}",
|
||||||
"carousel.slide": "Diapositive {current, number} de {max, number}",
|
"carousel.slide": "Diapositive {current, number} sur {max, number}",
|
||||||
"character_counter.recommended": "{currentLength}/{maxLength} caractères recommandés",
|
"character_counter.recommended": "{currentLength}/{maxLength} caractères recommandés",
|
||||||
"character_counter.required": "{currentLength}/{maxLength} caractères",
|
"character_counter.required": "{currentLength}/{maxLength} caractères",
|
||||||
"closed_registrations.other_server_instructions": "Puisque Mastodon est décentralisé, vous pouvez créer un compte sur un autre serveur et interagir quand même avec celui-ci.",
|
"closed_registrations.other_server_instructions": "Puisque Mastodon est décentralisé, vous pouvez créer un compte sur un autre serveur et interagir quand même avec celui-ci.",
|
||||||
@ -949,7 +949,7 @@
|
|||||||
"notification.poll": "Un sondage auquel vous avez participé vient de se terminer",
|
"notification.poll": "Un sondage auquel vous avez participé vient de se terminer",
|
||||||
"notification.quoted_update": "{name} a modifié un message que vous avez cité",
|
"notification.quoted_update": "{name} a modifié un message que vous avez cité",
|
||||||
"notification.reblog": "{name} a boosté votre message",
|
"notification.reblog": "{name} a boosté votre message",
|
||||||
"notification.reblog.name_and_others_with_link": "{name} et <a>{count, plural, one {# autre} other {# autres}}</a> ont boosté votre message",
|
"notification.reblog.name_and_others_with_link": "{name} et <a>{count, plural, one {# autre} other {# autres}}</a> ont partagé votre message",
|
||||||
"notification.relationships_severance_event": "Connexions perdues avec {name}",
|
"notification.relationships_severance_event": "Connexions perdues avec {name}",
|
||||||
"notification.relationships_severance_event.account_suspension": "Un·e administrateur·rice de {from} a suspendu {target}, ce qui signifie que vous ne pourrez plus recevoir de mises à jour ou interagir avec lui.",
|
"notification.relationships_severance_event.account_suspension": "Un·e administrateur·rice de {from} a suspendu {target}, ce qui signifie que vous ne pourrez plus recevoir de mises à jour ou interagir avec lui.",
|
||||||
"notification.relationships_severance_event.domain_block": "Un·e administrateur·rice de {from} en a bloqué {target}, comprenant {followersCount} de vos abonné·e·s et {followingCount, plural, one {# compte} other {# comptes}} vous suivez.",
|
"notification.relationships_severance_event.domain_block": "Un·e administrateur·rice de {from} en a bloqué {target}, comprenant {followersCount} de vos abonné·e·s et {followingCount, plural, one {# compte} other {# comptes}} vous suivez.",
|
||||||
|
|||||||
@ -327,8 +327,8 @@
|
|||||||
"block_modal.title": "Bloquer l'utilisateur·ice ?",
|
"block_modal.title": "Bloquer l'utilisateur·ice ?",
|
||||||
"block_modal.you_wont_see_mentions": "Vous ne verrez plus les messages qui le ou la mentionnent.",
|
"block_modal.you_wont_see_mentions": "Vous ne verrez plus les messages qui le ou la mentionnent.",
|
||||||
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois",
|
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois",
|
||||||
"boost_modal.reblog": "Booster le message ?",
|
"boost_modal.reblog": "Partager le message ?",
|
||||||
"boost_modal.undo_reblog": "Annuler le boost du message ?",
|
"boost_modal.undo_reblog": "Annuler le partage du message ?",
|
||||||
"bundle_column_error.copy_stacktrace": "Copier le rapport d'erreur",
|
"bundle_column_error.copy_stacktrace": "Copier le rapport d'erreur",
|
||||||
"bundle_column_error.error.body": "La page demandée n'a pas pu être affichée. Cela peut être dû à un bogue dans notre code, ou à un problème de compatibilité avec le navigateur.",
|
"bundle_column_error.error.body": "La page demandée n'a pas pu être affichée. Cela peut être dû à un bogue dans notre code, ou à un problème de compatibilité avec le navigateur.",
|
||||||
"bundle_column_error.error.title": "Oh non !",
|
"bundle_column_error.error.title": "Oh non !",
|
||||||
@ -343,7 +343,7 @@
|
|||||||
"bundle_modal_error.retry": "Réessayer",
|
"bundle_modal_error.retry": "Réessayer",
|
||||||
"callout.dismiss": "Rejeter",
|
"callout.dismiss": "Rejeter",
|
||||||
"carousel.current": "<sr>Diapositive</sr> {current, number} / {max, number}",
|
"carousel.current": "<sr>Diapositive</sr> {current, number} / {max, number}",
|
||||||
"carousel.slide": "Diapositive {current, number} de {max, number}",
|
"carousel.slide": "Diapositive {current, number} sur {max, number}",
|
||||||
"character_counter.recommended": "{currentLength}/{maxLength} caractères recommandés",
|
"character_counter.recommended": "{currentLength}/{maxLength} caractères recommandés",
|
||||||
"character_counter.required": "{currentLength}/{maxLength} caractères",
|
"character_counter.required": "{currentLength}/{maxLength} caractères",
|
||||||
"closed_registrations.other_server_instructions": "Puisque Mastodon est décentralisé, vous pouvez créer un compte sur un autre serveur et interagir quand même avec celui-ci.",
|
"closed_registrations.other_server_instructions": "Puisque Mastodon est décentralisé, vous pouvez créer un compte sur un autre serveur et interagir quand même avec celui-ci.",
|
||||||
@ -949,7 +949,7 @@
|
|||||||
"notification.poll": "Un sondage auquel vous avez participé vient de se terminer",
|
"notification.poll": "Un sondage auquel vous avez participé vient de se terminer",
|
||||||
"notification.quoted_update": "{name} a modifié un message que vous avez cité",
|
"notification.quoted_update": "{name} a modifié un message que vous avez cité",
|
||||||
"notification.reblog": "{name} a partagé votre message",
|
"notification.reblog": "{name} a partagé votre message",
|
||||||
"notification.reblog.name_and_others_with_link": "{name} et <a>{count, plural, one {# autre} other {# autres}}</a> ont boosté votre message",
|
"notification.reblog.name_and_others_with_link": "{name} et <a>{count, plural, one {# autre} other {# autres}}</a> ont partagé votre message",
|
||||||
"notification.relationships_severance_event": "Connexions perdues avec {name}",
|
"notification.relationships_severance_event": "Connexions perdues avec {name}",
|
||||||
"notification.relationships_severance_event.account_suspension": "Un·e administrateur·rice de {from} a suspendu {target}, ce qui signifie que vous ne pourrez plus recevoir de mises à jour ou interagir avec lui.",
|
"notification.relationships_severance_event.account_suspension": "Un·e administrateur·rice de {from} a suspendu {target}, ce qui signifie que vous ne pourrez plus recevoir de mises à jour ou interagir avec lui.",
|
||||||
"notification.relationships_severance_event.domain_block": "Un·e administrateur·rice de {from} en a bloqué {target}, comprenant {followersCount} de vos abonné·e·s et {followingCount, plural, one {# compte} other {# comptes}} vous suivez.",
|
"notification.relationships_severance_event.domain_block": "Un·e administrateur·rice de {from} en a bloqué {target}, comprenant {followersCount} de vos abonné·e·s et {followingCount, plural, one {# compte} other {# comptes}} vous suivez.",
|
||||||
|
|||||||
@ -71,6 +71,16 @@
|
|||||||
"account.go_to_profile": "Téigh go dtí próifíl",
|
"account.go_to_profile": "Téigh go dtí próifíl",
|
||||||
"account.hide_reblogs": "Folaigh moltaí ó @{name}",
|
"account.hide_reblogs": "Folaigh moltaí ó @{name}",
|
||||||
"account.in_memoriam": "Ón tseanaimsir.",
|
"account.in_memoriam": "Ón tseanaimsir.",
|
||||||
|
"account.join_modal.day": "Lá",
|
||||||
|
"account.join_modal.me": "Chuaigh tú isteach i {server} ar",
|
||||||
|
"account.join_modal.me_anniversary": "Lá Féile sona duit! Chuaigh tú isteach i {server} ar",
|
||||||
|
"account.join_modal.me_today": "Seo é do chéad lá ar {server}!",
|
||||||
|
"account.join_modal.other": "Chuaigh {name} isteach i {server} ar",
|
||||||
|
"account.join_modal.other_today": "Seo é an chéad lá ag {name} ar {server}!",
|
||||||
|
"account.join_modal.share.celebrate": "Roinn post ceiliúrtha",
|
||||||
|
"account.join_modal.share.intro": "Comhroinn post réamhrá",
|
||||||
|
"account.join_modal.share.welcome": "Roinn post fáilte",
|
||||||
|
"account.join_modal.years": "{number, plural, one {bliain} two {blianta} few {blianta} many {blianta} other {blianta}}",
|
||||||
"account.joined_short": "Cláraithe",
|
"account.joined_short": "Cláraithe",
|
||||||
"account.languages": "Athraigh teangacha foscríofa",
|
"account.languages": "Athraigh teangacha foscríofa",
|
||||||
"account.last_active": "Gníomhach deireanach",
|
"account.last_active": "Gníomhach deireanach",
|
||||||
@ -398,6 +408,8 @@
|
|||||||
"collections.manage_accounts": "Bainistigh cuntais",
|
"collections.manage_accounts": "Bainistigh cuntais",
|
||||||
"collections.mark_as_sensitive": "Marcáil mar íogair",
|
"collections.mark_as_sensitive": "Marcáil mar íogair",
|
||||||
"collections.mark_as_sensitive_hint": "Folaíonn sé cur síos agus cuntais an bhailiúcháin taobh thiar de rabhadh ábhair. Beidh ainm an bhailiúcháin le feiceáil fós.",
|
"collections.mark_as_sensitive_hint": "Folaíonn sé cur síos agus cuntais an bhailiúcháin taobh thiar de rabhadh ábhair. Beidh ainm an bhailiúcháin le feiceáil fós.",
|
||||||
|
"collections.maximum_collection_count_description": "Ceadaíonn do fhreastalaí cruthú suas le {count} bailiúchán.",
|
||||||
|
"collections.maximum_collection_count_reached": "Tá an líon uasta bailiúcháin cruthaithe agat",
|
||||||
"collections.name_length_hint": "Teorainn 40 carachtar",
|
"collections.name_length_hint": "Teorainn 40 carachtar",
|
||||||
"collections.new_collection": "Bailiúchán nua",
|
"collections.new_collection": "Bailiúchán nua",
|
||||||
"collections.no_collections_yet": "Gan aon bhailiúcháin fós.",
|
"collections.no_collections_yet": "Gan aon bhailiúcháin fós.",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user