4.2.0-Beta2 test update

This commit is contained in:
root
2023-08-29 09:06:32 -04:00
552 changed files with 23255 additions and 7073 deletions

View File

@@ -75,7 +75,10 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
normalStatus.hidden = normalOldStatus.get('hidden');
normalStatus.translation = normalOldStatus.get('translation');
if (normalOldStatus.get('translation')) {
normalStatus.translation = normalOldStatus.get('translation');
}
} else {
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');

View File

@@ -157,7 +157,7 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, tagged, max_id: maxId });
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);

View File

@@ -8,6 +8,7 @@ import classNames from 'classnames';
import api from 'flavours/glitch/api';
const messages = defineMessages({
legal: { id: 'report.categories.legal', defaultMessage: 'Legal' },
other: { id: 'report.categories.other', defaultMessage: 'Other' },
spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
@@ -150,6 +151,7 @@ class ReportReasonSelector extends PureComponent {
return (
<div className='report-reason-selector'>
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='legal' text={intl.formatMessage(messages.legal)} selected={category === 'legal'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}

View File

@@ -18,7 +18,19 @@ export default class Column extends PureComponent {
};
scrollTop () {
const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable');
let scrollable = null;
if (this.props.bindToDocument) {
scrollable = document.scrollingElement;
} else {
scrollable = this.node.querySelector('.scrollable');
// Some columns have nested `.scrollable` containers, with the outer one
// being a wrapper while the actual scrollable content is deeper.
if (scrollable.classList.contains('scrollable--flex')) {
scrollable = scrollable?.querySelector('.scrollable') || scrollable;
}
}
if (!scrollable) {
return;

View File

@@ -196,9 +196,9 @@ class Header extends ImmutablePureComponent {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className={classNames({ 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
actionBtn = <Button className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}

View File

@@ -23,6 +23,7 @@ export default class Story extends PureComponent {
author: PropTypes.string,
sharedTimes: PropTypes.number,
thumbnail: PropTypes.string,
thumbnailDescription: PropTypes.string,
blurhash: PropTypes.string,
expanded: PropTypes.bool,
};
@@ -34,7 +35,7 @@ export default class Story extends PureComponent {
handleImageLoad = () => this.setState({ thumbnailLoaded: true });
render () {
const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, blurhash } = this.props;
const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, thumbnailDescription, blurhash } = this.props;
const { thumbnailLoaded } = this.state;
@@ -50,7 +51,7 @@ export default class Story extends PureComponent {
{thumbnail ? (
<>
<div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div>
<img src={thumbnail} onLoad={this.handleImageLoad} alt='' role='presentation' />
<img src={thumbnail} onLoad={this.handleImageLoad} alt={thumbnailDescription} title={thumbnailDescription} lang={lang} />
</>
) : <Skeleton />}
</div>

View File

@@ -67,6 +67,7 @@ class Links extends PureComponent {
author={link.get('author_name')}
sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
thumbnail={link.get('image')}
thumbnailDescription={link.get('image_description')}
blurhash={link.get('blurhash')}
/>
))}

View File

@@ -110,10 +110,10 @@ class Results extends PureComponent {
return (
<>
<div className='account__section-headline'>
<button onClick={this.handleSelectAll} className={type === 'all' && 'active'}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
<button onClick={this.handleSelectAccounts} className={type === 'accounts' && 'active'}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' && 'active'}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
<button onClick={this.handleSelectStatuses} className={type === 'statuses' && 'active'}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
<button onClick={this.handleSelectAll} className={type === 'all' ? 'active' : undefined}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
<button onClick={this.handleSelectAccounts} className={type === 'accounts' ? 'active' : undefined}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' ? 'active' : undefined}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
<button onClick={this.handleSelectStatuses} className={type === 'statuses' ? 'active' : undefined}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
</div>
<div className='explore__search-results'>

View File

@@ -0,0 +1,79 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Button from 'flavours/glitch/components/button';
import { ShortNumber } from 'flavours/glitch/components/short_number';
const messages = defineMessages({
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
});
const usesRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_uses'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const peopleRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} participant} other {{counter} participants}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const usesTodayRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_uses_today'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}} today'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
export const HashtagHeader = injectIntl(({ tag, intl, disabled, onClick }) => {
if (!tag) {
return null;
}
const [uses, people] = tag.get('history').reduce((arr, day) => [arr[0] + day.get('uses') * 1, arr[1] + day.get('accounts') * 1], [0, 0]);
const dividingCircle = <span aria-hidden>{' · '}</span>;
return (
<div className='hashtag-header'>
<div className='hashtag-header__header'>
<h1>#{tag.get('name')}</h1>
<Button onClick={onClick} text={intl.formatMessage(tag.get('following') ? messages.unfollowHashtag : messages.followHashtag)} disabled={disabled} />
</div>
<div>
<ShortNumber value={uses} renderer={usesRenderer} />
{dividingCircle}
<ShortNumber value={people} renderer={peopleRenderer} />
{dividingCircle}
<ShortNumber value={tag.getIn(['history', 0, 'uses']) * 1} renderer={usesTodayRenderer} />
</div>
</div>
);
});
HashtagHeader.propTypes = {
tag: ImmutablePropTypes.map,
disabled: PropTypes.bool,
onClick: PropTypes.func,
intl: PropTypes.object,
};

View File

@@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import ImmutablePropTypes from 'react-immutable-proptypes';
@@ -17,17 +16,11 @@ import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/glitch/ac
import { expandHashtagTimeline, clearTimeline } from 'flavours/glitch/actions/timelines';
import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header';
import { Icon } from 'flavours/glitch/components/icon';
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
import { HashtagHeader } from './components/hashtag_header';
import ColumnSettingsContainer from './containers/column_settings_container';
const messages = defineMessages({
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
});
const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0,
tag: state.getIn(['tags', props.params.id]),
@@ -48,7 +41,6 @@ class HashtagTimeline extends PureComponent {
hasUnread: PropTypes.bool,
tag: ImmutablePropTypes.map,
multiColumn: PropTypes.bool,
intl: PropTypes.object,
};
handlePin = () => {
@@ -188,27 +180,11 @@ class HashtagTimeline extends PureComponent {
};
render () {
const { hasUnread, columnId, multiColumn, tag, intl } = this.props;
const { hasUnread, columnId, multiColumn, tag } = this.props;
const { id, local } = this.props.params;
const pinned = !!columnId;
const { signedIn } = this.context.identity;
let followButton;
if (tag) {
const following = tag.get('following');
const classes = classNames('column-header__button', {
active: following,
});
followButton = (
<button className={classes} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)}>
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
</button>
);
}
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
<ColumnHeader
@@ -220,13 +196,14 @@ class HashtagTimeline extends PureComponent {
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
extraButton={followButton}
showBackButton
>
{columnId && <ColumnSettingsContainer columnId={columnId} />}
</ColumnHeader>
<StatusListContainer
prepend={pinned ? null : <HashtagHeader tag={tag} disabled={!signedIn} onClick={this.handleFollow} />}
alwaysPrepend
trackScroll={!pinned}
scrollKey={`hashtag_timeline-${columnId}`}
timelineId={`hashtag:${id}${local ? ':local' : ''}`}
@@ -245,4 +222,4 @@ class HashtagTimeline extends PureComponent {
}
export default connect(mapStateToProps)(injectIntl(HashtagTimeline));
export default connect(mapStateToProps)(HashtagTimeline);

View File

@@ -13,7 +13,7 @@ import { openModal, closeModal } from 'flavours/glitch/actions/modal';
import api from 'flavours/glitch/api';
import Button from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
import { registrationsOpen } from 'flavours/glitch/initial_state';
import { registrationsOpen, sso_redirect } from 'flavours/glitch/initial_state';
const messages = defineMessages({
loginPrompt: { id: 'interaction_modal.login.prompt', defaultMessage: 'Domain of your home server, e.g. mastodon.social' },
@@ -21,12 +21,16 @@ const messages = defineMessages({
const mapStateToProps = (state, { accountId }) => ({
displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']),
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
});
const mapDispatchToProps = (dispatch) => ({
onSignupClick() {
dispatch(closeModal());
dispatch(openModal('CLOSED_REGISTRATIONS'));
dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
}));
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
},
});
@@ -250,6 +254,9 @@ class LoginForm extends React.PureComponent {
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
autocomplete='off'
autocapitalize='off'
spellcheck='false'
/>
<Button onClick={this.handleSubmit} disabled={isSubmitting}><FormattedMessage id='interaction_modal.login.action' defaultMessage='Take me home' /></Button>
@@ -291,6 +298,7 @@ class InteractionModal extends React.PureComponent {
url: PropTypes.string,
type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow']),
onSignupClick: PropTypes.func.isRequired,
signupUrl: PropTypes.string.isRequired,
};
handleSignupClick = () => {
@@ -298,7 +306,7 @@ class InteractionModal extends React.PureComponent {
};
render () {
const { url, type, displayNameHtml } = this.props;
const { url, type, displayNameHtml, signupUrl } = this.props;
const name = <bdi dangerouslySetInnerHTML={{ __html: displayNameHtml }} />;
@@ -329,9 +337,15 @@ class InteractionModal extends React.PureComponent {
let signupButton;
if (registrationsOpen) {
if (sso_redirect) {
signupButton = (
<a href='/auth/sign_up' className='link-button'>
<a href={sso_redirect} data-method='post' className='link-button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='link-button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);

View File

@@ -178,7 +178,8 @@ export default class Card extends PureComponent {
dummy={!useBlurhash}
/>
);
let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
const thumbnailDescription = card.get('image_description');
const thumbnail = <img src={card.get('image')} alt={thumbnailDescription} title={thumbnailDescription} lang={language} style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
let spoilerButton = (
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
<span className='spoiler-button__overlay__label'>

View File

@@ -595,7 +595,7 @@ class Status extends ImmutablePureComponent {
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
previousId={i > 0 && list.get(i - 1)}
previousId={i > 0 ? list.get(i - 1) : undefined}
nextId={list.get(i + 1) || (ancestors && statusId)}
rootId={statusId}
/>

View File

@@ -1,761 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { HotKeys } from 'react-hotkeys';
import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initBoostModal } from 'flavours/glitch/actions/boosts';
import {
replyCompose,
mentionCompose,
directCompose,
} from 'flavours/glitch/actions/compose';
import {
favourite,
unfavourite,
bookmark,
unbookmark,
reblog,
unreblog,
pin,
unpin,
} from 'flavours/glitch/actions/interactions';
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
import { openModal } from 'flavours/glitch/actions/modal';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { initReport } from 'flavours/glitch/actions/reports';
import {
fetchStatus,
muteStatus,
unmuteStatus,
deleteStatus,
editStatus,
hideStatus,
revealStatus,
translateStatus,
undoStatusTranslation,
} from 'flavours/glitch/actions/statuses';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status';
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import StatusContainer from 'flavours/glitch/containers/status_container';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import Column from 'flavours/glitch/features/ui/components/column';
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/initial_state';
import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
import ColumnHeader from '../../components/column_header';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import ActionBar from './components/action_bar';
import DetailedStatus from './components/detailed_status';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' },
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
tootHeading: { id: 'account.posts_with_replies', defaultMessage: 'Posts and replies' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const getAncestorsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => {
let ancestorsIds = Immutable.List();
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId;
while (id && !mutable.includes(id)) {
mutable.unshift(id);
id = inReplyTos.get(id);
}
});
return ancestorsIds;
});
const getDescendantsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'replies']),
state => state.get('statuses'),
], (statusId, contextReplies, statuses) => {
let descendantsIds = [];
const ids = [statusId];
while (ids.length > 0) {
let id = ids.pop();
const replies = contextReplies.get(id);
if (statusId !== id) {
descendantsIds.push(id);
}
if (replies) {
replies.reverse().forEach(reply => {
if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
});
}
}
let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
if (insertAt !== -1) {
descendantsIds.forEach((id, idx) => {
if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) {
descendantsIds.splice(idx, 1);
descendantsIds.splice(insertAt, 0, id);
insertAt += 1;
}
});
}
return Immutable.List(descendantsIds);
});
const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId });
let ancestorsIds = Immutable.List();
let descendantsIds = Immutable.List();
if (status) {
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
descendantsIds = getDescendantsIds(state, { id: status.get('id') });
}
return {
isLoading: state.getIn(['statuses', props.params.statusId, 'isLoading']),
status,
ancestorsIds,
descendantsIds,
settings: state.get('local_settings'),
askReplyConfirmation: state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0,
domain: state.getIn(['meta', 'domain']),
pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
};
};
return mapStateToProps;
};
const truncate = (str, num) => {
const arr = Array.from(str);
if (arr.length > num) {
return arr.slice(0, num).join('') + '…';
} else {
return str;
}
};
const titleFromStatus = (intl, status) => {
const displayName = status.getIn(['account', 'display_name']);
const username = status.getIn(['account', 'username']);
const user = displayName.trim().length === 0 ? username : displayName;
const text = status.get('search_index');
const attachmentCount = status.get('media_attachments').size;
return text ? `${user}: "${truncate(text, 30)}"` : intl.formatMessage(messages.statusTitleWithAttachments, { user, attachmentCount });
};
class Status extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object,
};
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
status: ImmutablePropTypes.map,
isLoading: PropTypes.bool,
settings: ImmutablePropTypes.map.isRequired,
ancestorsIds: ImmutablePropTypes.list.isRequired,
descendantsIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool,
domain: PropTypes.string.isRequired,
pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
};
state = {
fullscreen: false,
isExpanded: undefined,
threadExpanded: undefined,
statusId: undefined,
loadedStatusId: undefined,
showMedia: undefined,
revealBehindCW: undefined,
};
componentDidMount () {
attachFullscreenListener(this.onFullScreenChange);
this.props.dispatch(fetchStatus(this.props.params.statusId));
}
static getDerivedStateFromProps(props, state) {
let update = {};
let updated = false;
if (props.params.statusId && state.statusId !== props.params.statusId) {
props.dispatch(fetchStatus(props.params.statusId));
update.threadExpanded = undefined;
update.statusId = props.params.statusId;
updated = true;
}
const revealBehindCW = props.settings.getIn(['media', 'reveal_behind_cw']);
if (revealBehindCW !== state.revealBehindCW) {
update.revealBehindCW = revealBehindCW;
if (revealBehindCW) update.showMedia = defaultMediaVisibility(props.status, props.settings);
updated = true;
}
if (props.status && state.loadedStatusId !== props.status.get('id')) {
update.showMedia = defaultMediaVisibility(props.status, props.settings);
update.loadedStatusId = props.status.get('id');
update.isExpanded = autoUnfoldCW(props.settings, props.status);
updated = true;
}
return updated ? update : null;
}
handleToggleHidden = () => {
const { status } = this.props;
if (this.props.settings.getIn(['content_warnings', 'shared_state'])) {
if (status.get('hidden')) {
this.props.dispatch(revealStatus(status.get('id')));
} else {
this.props.dispatch(hideStatus(status.get('id')));
}
} else if (this.props.status.get('spoiler_text')) {
this.setExpansion(!this.state.isExpanded);
}
};
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
};
handleModalFavourite = (status) => {
this.props.dispatch(favourite(status));
};
handleFavouriteClick = (status, e) => {
const { dispatch } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
if ((e && e.shiftKey) || !favouriteModal) {
this.handleModalFavourite(status);
} else {
dispatch(openModal({
modalType: 'FAVOURITE',
modalProps: {
status,
onFavourite: this.handleModalFavourite,
},
}));
}
}
} else {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'favourite',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
}));
}
};
handlePin = (status) => {
if (status.get('pinned')) {
this.props.dispatch(unpin(status));
} else {
this.props.dispatch(pin(status));
}
};
handleReplyClick = (status) => {
const { askReplyConfirmation, dispatch, intl } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
if (askReplyConfirmation) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)),
onConfirm: () => dispatch(replyCompose(status, this.context.router.history)),
},
}));
} else {
dispatch(replyCompose(status, this.context.router.history));
}
} else {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'reply',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
}));
}
};
handleModalReblog = (status, privacy) => {
const { dispatch } = this.props;
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
dispatch(reblog(status, privacy));
}
};
handleReblogClick = (status, e) => {
const { settings, dispatch } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
if (settings.get('confirm_boost_missing_media_description') && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
} else if ((e && e.shiftKey) || !boostModal) {
this.handleModalReblog(status);
} else {
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
}
} else {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
}));
}
};
handleBookmarkClick = (status) => {
if (status.get('bookmarked')) {
this.props.dispatch(unbookmark(status));
} else {
this.props.dispatch(bookmark(status));
}
};
handleDeleteClick = (status, history, withRedraft = false) => {
const { dispatch, intl } = this.props;
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
},
}));
}
};
handleEditClick = (status, history) => {
this.props.dispatch(editStatus(status.get('id'), history));
};
handleDirectClick = (account, router) => {
this.props.dispatch(directCompose(account, router));
};
handleMentionClick = (account, router) => {
this.props.dispatch(mentionCompose(account, router));
};
handleOpenMedia = (media, index, lang) => {
this.props.dispatch(openModal({
modalType: 'MEDIA',
modalProps: { statusId: this.props.status.get('id'), media, index, lang },
}));
};
handleOpenVideo = (media, lang, options) => {
this.props.dispatch(openModal({
modalType: 'VIDEO',
modalProps: { statusId: this.props.status.get('id'), media, lang, options },
}));
};
handleHotkeyOpenMedia = e => {
const { status } = this.props;
e.preventDefault();
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
} else {
this.handleOpenMedia(status.get('media_attachments'), 0);
}
}
};
handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account));
};
handleConversationMuteClick = (status) => {
if (status.get('muted')) {
this.props.dispatch(unmuteStatus(status.get('id')));
} else {
this.props.dispatch(muteStatus(status.get('id')));
}
};
handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds, settings } = this.props;
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
let { isExpanded } = this.state;
if (settings.getIn(['content_warnings', 'shared_state']))
isExpanded = !status.get('hidden');
if (!isExpanded) {
this.props.dispatch(revealStatus(statusIds));
} else {
this.props.dispatch(hideStatus(statusIds));
}
this.setState({ isExpanded: !isExpanded, threadExpanded: !isExpanded });
};
handleTranslate = status => {
const { dispatch } = this.props;
if (status.get('translation')) {
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
} else {
dispatch(translateStatus(status.get('id')));
}
};
handleBlockClick = (status) => {
const { dispatch } = this.props;
const account = status.get('account');
dispatch(initBlockModal(account));
};
handleReport = (status) => {
this.props.dispatch(initReport(status.get('account'), status));
};
handleEmbed = (status) => {
this.props.dispatch(openModal({
modalType: 'EMBED',
modalProps: { id: status.get('id') },
}));
};
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
};
handleHotkeyMoveUp = () => {
this.handleMoveUp(this.props.status.get('id'));
};
handleHotkeyMoveDown = () => {
this.handleMoveDown(this.props.status.get('id'));
};
handleHotkeyReply = e => {
e.preventDefault();
this.handleReplyClick(this.props.status);
};
handleHotkeyFavourite = () => {
this.handleFavouriteClick(this.props.status);
};
handleHotkeyBoost = () => {
this.handleReblogClick(this.props.status);
};
handleHotkeyBookmark = () => {
this.handleBookmarkClick(this.props.status);
};
handleHotkeyMention = e => {
e.preventDefault();
this.handleMentionClick(this.props.status);
};
handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
};
handleMoveUp = id => {
const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size - 1, true);
} else {
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index, true);
} else {
this._selectChild(index - 1, true);
}
}
};
handleMoveDown = id => {
const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size + 1, false);
} else {
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index + 2, false);
} else {
this._selectChild(index + 1, false);
}
}
};
_selectChild (index, align_top) {
const container = this.node;
const element = container.querySelectorAll('.focusable')[index];
if (element) {
if (align_top && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true);
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false);
}
element.focus();
}
}
handleHeaderClick = () => {
this.column.scrollTop();
};
renderChildren (list, ancestors) {
const { params: { statusId } } = this.props;
return list.map((id, i) => (
<StatusContainer
key={id}
id={id}
expanded={this.state.threadExpanded}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
previousId={i > 0 && list.get(i - 1)}
nextId={list.get(i + 1) || (ancestors && statusId)}
rootId={statusId}
/>
));
}
setExpansion = value => {
this.setState({ isExpanded: value });
};
setRef = c => {
this.node = c;
};
setColumnRef = c => {
this.column = c;
};
componentDidUpdate (prevProps) {
const { status, ancestorsIds, multiColumn } = this.props;
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
window.requestAnimationFrame(() => {
this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header,
// so compensate for that.
if (!multiColumn) {
const offset = document.querySelector('.column-header__wrapper')?.getBoundingClientRect()?.bottom;
if (offset) {
const scrollingElement = document.scrollingElement || document.body;
scrollingElement.scrollBy(0, -offset);
}
}
});
}
}
componentWillUnmount () {
detachFullscreenListener(this.onFullScreenChange);
}
onFullScreenChange = () => {
this.setState({ fullscreen: isFullscreen() });
};
render () {
let ancestors, descendants;
const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen } = this.state;
if (isLoading) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
if (status === null) {
return (
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
);
}
const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded;
if (ancestorsIds && ancestorsIds.size > 0) {
ancestors = <>{this.renderChildren(ancestorsIds, true)}</>;
}
if (descendantsIds && descendantsIds.size > 0) {
descendants = <>{this.renderChildren(descendantsIds)}</>;
}
const isLocal = status.getIn(['account', 'acct'], '').indexOf('@') === -1;
const isIndexable = !status.getIn(['account', 'noindex']);
const handlers = {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost,
bookmark: this.handleHotkeyBookmark,
mention: this.handleHotkeyMention,
openProfile: this.handleHotkeyOpenProfile,
toggleSpoiler: this.handleToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};
return (
<Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.detailedStatus)}>
<ColumnHeader
icon='comment'
title={intl.formatMessage(messages.tootHeading)}
onClick={this.handleHeaderClick}
showBackButton
multiColumn={multiColumn}
extraButton={(
<button type='button' className='column-header__button' title={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll}><Icon id={!isExpanded ? 'eye-slash' : 'eye'} /></button>
)}
/>
<ScrollContainer scrollKey='thread'>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
{ancestors}
<HotKeys handlers={handlers}>
<div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader(intl, status, false, isExpanded)}>
<DetailedStatus
key={`details-${status.get('id')}`}
status={status}
settings={settings}
onOpenVideo={this.handleOpenVideo}
onOpenMedia={this.handleOpenMedia}
expanded={isExpanded}
onToggleHidden={this.handleToggleHidden}
onTranslate={this.handleTranslate}
domain={domain}
showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture}
/>
<ActionBar
key={`action-bar-${status.get('id')}`}
status={status}
onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick}
onEdit={this.handleEditClick}
onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}
onMute={this.handleMuteClick}
onMuteConversation={this.handleConversationMuteClick}
onBlock={this.handleBlockClick}
onReport={this.handleReport}
onPin={this.handlePin}
onEmbed={this.handleEmbed}
/>
</div>
</HotKeys>
{descendants}
</div>
</ScrollContainer>
<Helmet>
<title>{titleFromStatus(intl, status)}</title>
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
<link rel='canonical' href={status.get('url')} />
</Helmet>
</Column>
);
}
}
export default injectIntl(connect(makeMapStateToProps)(Status));

View File

@@ -423,4 +423,4 @@ class FocalPointModal extends ImmutablePureComponent {
export default connect(mapStateToProps, mapDispatchToProps, null, {
forwardRef: true,
})(injectIntl(FocalPointModal, { withRef: true }));
})(injectIntl(FocalPointModal, { forwardRef: true }));

View File

@@ -13,7 +13,7 @@ import { Avatar } from 'flavours/glitch/components/avatar';
import { Icon } from 'flavours/glitch/components/icon';
import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo';
import Permalink from 'flavours/glitch/components/permalink';
import { registrationsOpen, me } from 'flavours/glitch/initial_state';
import { registrationsOpen, me, sso_redirect } from 'flavours/glitch/initial_state';
const Account = connect(state => ({
account: state.getIn(['accounts', me]),
@@ -74,28 +74,35 @@ class Header extends PureComponent {
</>
);
} else {
let signupButton;
if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
if (sso_redirect) {
content = (
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
)
} else {
signupButton = (
<button className='button' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
let signupButton;
if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else {
signupButton = (
<button className='button' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
);
}
content = (
<>
{signupButton}
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</>
);
}
content = (
<>
{signupButton}
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</>
);
}
return (

View File

@@ -105,14 +105,7 @@ export default class ModalRoot extends PureComponent {
handleClose = (ignoreFocus = false) => {
const { onClose } = this.props;
let message = null;
try {
message = this._modal?.getWrappedInstance?.().getCloseConfirmationMessage?.();
} catch (_) {
// injectIntl defines `getWrappedInstance` but errors out if `withRef`
// isn't set.
// This would be much smoother with react-intl 3+ and `forwardRef`.
}
const message = this._modal?.getCloseConfirmationMessage?.();
onClose(message, ignoreFocus);
};
@@ -133,7 +126,10 @@ export default class ModalRoot extends PureComponent {
{visible && (
<>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />}
{(SpecificComponent) => {
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />
}}
</BundleContainer>
<Helmet>

View File

@@ -63,7 +63,7 @@ class ReportModal extends ImmutablePureComponent {
dispatch(submitReport({
account_id: accountId,
status_ids: selectedStatusIds.toArray(),
selected_domains: selectedDomains.toArray(),
forward_to_domains: selectedDomains.toArray(),
comment,
forward: selectedDomains.size > 0,
category,

View File

@@ -3,7 +3,7 @@ import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { openModal } from 'flavours/glitch/actions/modal';
import { registrationsOpen } from 'flavours/glitch/initial_state';
import { registrationsOpen, sso_redirect } from 'flavours/glitch/initial_state';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
const SignInBanner = () => {
@@ -18,6 +18,15 @@ const SignInBanner = () => {
const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up');
if (sso_redirect) {
return (
<div className='sign-in-banner'>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p>
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
</div>
)
}
if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='button button--block'>

View File

@@ -82,6 +82,7 @@
* @property {boolean} use_blurhash
* @property {boolean=} use_pending_items
* @property {string} version
* @property {string} sso_redirect
* @property {boolean} translation_enabled
* @property {string} status_page_url
* @property {boolean} system_emoji_font
@@ -160,6 +161,7 @@ export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version');
export const languages = initialState?.languages;
export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect');
// Glitch-soc-specific settings
export const maxChars = (initialState && initialState.max_toot_chars) || 500;

View File

@@ -1,10 +1,43 @@
{
"compose.attach": "Vedhæft...",
"compose.attach.doodle": "Tegn noget",
"compose.attach.upload": "Upload en fil",
"compose_form.poll.multiple_choices": "Tillad flere valg",
"confirmations.missing_media_description.message": "Mindst én vedhæftet medie mangler en beskrivelse. Overvej at tilføje en beskrivelse af alle vedhæftede medier af hensyn til personer med nedsat syn, før du publicerer dit indlæg.",
"empty_column.follow_recommendations": "Det ser ud til, at der ikke kunne genereres forslag til dig. Du kan prøve med Søg for at lede efter personer, du måske kender, eller udforske hashtags.",
"follow_recommendations.done": "Udført",
"follow_recommendations.heading": "Følg personer du gerne vil se indlæg fra! Her er nogle forslag.",
"follow_recommendations.lead": "Indlæg, fra personer du følger, vil fremgå kronologisk ordnet i dit hjemmefeed. Vær ikke bange for at begå fejl, da du altid og meget nemt kan ændre dit valg!",
"home.column_settings.advanced": "Avanceret",
"home.column_settings.show_direct": "Vis private omtaler",
"navigation_bar.app_settings": "Appindstillinger",
"navigation_bar.misc": "Diverse",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
"settings.always_show_spoilers_field": "Vis altid feltet til indholdsadvarsel",
"settings.auto_collapse_media": "Indlæg med medier",
"settings.close": "Luk",
"settings.collapsed_statuses": "Sammenfoldede indlæg",
"settings.content_warnings": "Indholdsadvarsler",
"settings.content_warnings.regexp": "Regulært udtryk",
"settings.general": "Generelt",
"settings.image_backgrounds_media_hint": "Hvis et indlæg har vedhæftede medier, brug den første som baggrund",
"settings.media": "Medier",
"settings.preferences": "Præferencer",
"settings.rewrite_mentions": "Omskriv omtaler i viste indlæg",
"settings.rewrite_mentions_acct": "Omskriv med brugernavn og domæne (når brugeren ikke er lokal)",
"settings.rewrite_mentions_no": "Omskriv ikke omtaler",
"settings.rewrite_mentions_username": "Omskriv med brugernavn",
"settings.show_reply_counter": "Vis et estimat over antal svar",
"settings.status_icons": "Statusikoner",
"settings.status_icons_language": "Sprogindikator",
"settings.status_icons_local_only": "Kun lokal-indikator",
"settings.status_icons_media": "Medie- og afstemningsindikator",
"settings.status_icons_reply": "Svarindikator",
"settings.status_icons_visibility": "Statussynlighedsindikator",
"settings.tag_misleading_links": "Marker vildledende links",
"status.has_audio": "Har vedhæftede lydfiler",
"status.has_pictures": "Har vedhæftede billeder",
"status.has_preview_card": "Har en vedhæftet linkvisning",
"status.has_video": "Har vedhæftede videoer"
}

View File

@@ -4,7 +4,9 @@
"account.disclaimer_full": "Les informations ci-dessous peuvent être incomplètes.",
"account.follows": "Abonnements",
"account.joined": "Ici depuis {date}",
"account.mute_notifications": "Masquer les notifications de @{name}",
"account.suspended_disclaimer_full": "Cet utilisateur a été suspendu par un modérateur.",
"account.unmute_notifications": "Ne plus masquer les notifications de @{name}",
"account.view_full_profile": "Voir le profil complet",
"account_note.cancel": "Annuler",
"account_note.edit": "Éditer",
@@ -50,6 +52,7 @@
"empty_column.follow_recommendations": "Il semble quaucune suggestion nait pu être générée pour vous. Vous pouvez essayer dutiliser la recherche pour découvrir des personnes que vous pourriez connaître ou explorer les hashtags populaires.",
"endorsed_accounts_editor.endorsed_accounts": "Comptes mis en avant",
"favourite_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois",
"firehose.column_settings.allow_local_only": "Afficher les messages locaux dans \"Tous\"",
"follow_recommendations.done": "Terminé",
"follow_recommendations.heading": "Suivez les personnes dont vous aimeriez voir les publications! Voici quelques suggestions.",
"follow_recommendations.lead": "Les publication de personnes que vous suivez apparaîtront par ordre chronologique sur votre fil d'accueil. N'ayez pas peur de faire des erreurs, vous pouvez arrêter de suivre les gens aussi facilement n'importe quand!",
@@ -98,6 +101,7 @@
"settings.always_show_spoilers_field": "Toujours activer le champ de rédaction de l'avertissement de contenu",
"settings.auto_collapse": "Repliage automatique",
"settings.auto_collapse_all": "Tout",
"settings.auto_collapse_height": "Hauteur (en pixels) pour qu'un pouet soit considéré comme long",
"settings.auto_collapse_lengthy": "Posts longs",
"settings.auto_collapse_media": "Posts avec média",
"settings.auto_collapse_notifications": "Notifications",

View File

@@ -4,7 +4,9 @@
"account.disclaimer_full": "Les informations ci-dessous peuvent être incomplètes.",
"account.follows": "Abonnements",
"account.joined": "Ici depuis {date}",
"account.mute_notifications": "Masquer les notifications de @{name}",
"account.suspended_disclaimer_full": "Cet utilisateur a été suspendu par un modérateur.",
"account.unmute_notifications": "Ne plus masquer les notifications de @{name}",
"account.view_full_profile": "Voir le profil complet",
"account_note.cancel": "Annuler",
"account_note.edit": "Éditer",
@@ -50,6 +52,7 @@
"empty_column.follow_recommendations": "Il semble quaucune suggestion nait pu être générée pour vous. Vous pouvez essayer dutiliser la recherche pour découvrir des personnes que vous pourriez connaître ou explorer les hashtags tendance.",
"endorsed_accounts_editor.endorsed_accounts": "Comptes mis en avant",
"favourite_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois",
"firehose.column_settings.allow_local_only": "Afficher les messages locaux dans \"Tous\"",
"follow_recommendations.done": "Terminé",
"follow_recommendations.heading": "Suivez les personnes dont vous aimeriez voir les messages ! Voici quelques suggestions.",
"follow_recommendations.lead": "Les messages des personnes que vous suivez apparaîtront par ordre chronologique sur votre fil d'accueil. Ne craignez pas de faire des erreurs, vous pouvez arrêter de suivre les gens aussi facilement à tout moment !",
@@ -98,6 +101,7 @@
"settings.always_show_spoilers_field": "Toujours activer le champ de rédaction de l'avertissement de contenu",
"settings.auto_collapse": "Repliage automatique",
"settings.auto_collapse_all": "Tout",
"settings.auto_collapse_height": "Hauteur (en pixels) pour qu'un pouet soit considéré comme long",
"settings.auto_collapse_lengthy": "Posts longs",
"settings.auto_collapse_media": "Posts avec média",
"settings.auto_collapse_notifications": "Notifications",

View File

@@ -1,8 +1,12 @@
{
"about.fork_disclaimer": "Glitch-socはMastodonからフォークされたフリーなオープンソースソフトウェアです。",
"account.add_account_note": "@{name}のメモを追加",
"account.disclaimer_full": "このユーザー情報は不正確な可能性があります。",
"account.follows": "フォロー",
"account.joined": "{date} に登録",
"account.mute_notifications": "@{name}さんからの通知を受け取らない",
"account.suspended_disclaimer_full": "このユーザーはモデレータにより停止されました。",
"account.unmute_notifications": "@{name}さんからの通知を受け取る",
"account.view_full_profile": "正確な情報を見る",
"account_note.cancel": "キャンセル",
"account_note.edit": "編集",
@@ -16,20 +20,25 @@
"advanced_options.threaded_mode.short": "スレッドモード",
"advanced_options.threaded_mode.tooltip": "スレッドモードを有効にする",
"boost_modal.missing_description": "このトゥートには少なくとも1つの画像に説明が付与されていません",
"column.favourited_by": "お気に入りしたユーザー",
"column.heading": "その他",
"column.reblogged_by": "ブーストしたユーザー",
"column.subheading": "その他のオプション",
"column_header.profile": "プロフィール",
"column_subheading.lists": "リスト",
"column_subheading.navigation": "ナビゲーション",
"community.column_settings.allow_local_only": "ローカル限定投稿を表示する",
"compose.attach": "添付...",
"compose.attach.doodle": "お絵描きをする",
"compose.attach.upload": "ファイルをアップロード",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "マークダウン",
"compose.content-type.plain": "プレーンテキスト",
"compose_form.poll.multiple_choices": "複数回答を許可",
"compose_form.poll.single_choice": "単一回答を許可",
"compose_form.spoiler": "本文は警告の後ろに隠す",
"confirmation_modal.do_not_ask_again": "もう1度尋ねない",
"confirmations.deprecated_settings.confirm": "Mastodonの設定を使用",
"confirmations.missing_media_description.confirm": "このまま投稿",
"confirmations.missing_media_description.edit": "メディアを編集",
"confirmations.missing_media_description.message": "少なくとも1つの画像に視覚障害者のための画像説明が付与されていません。すべての画像に対して説明を付与することを望みます。",
@@ -38,6 +47,7 @@
"confirmations.unfilter.edit_filter": "フィルターを編集",
"confirmations.unfilter.filters": "適用されたフィルター",
"content-type.change": "コンテンツ形式を変更",
"direct.group_by_conversations": "会話でグループ化",
"empty_column.follow_recommendations": "おすすめを生成できませんでした。検索を使って知り合いを探したり、トレンドハッシュタグを見てみましょう。",
"endorsed_accounts_editor.endorsed_accounts": "紹介しているユーザー",
"favourite_modal.combo": "次からは {combo} を押せば、これをスキップできます。",
@@ -48,18 +58,22 @@
"home.column_settings.advanced": "高度",
"home.column_settings.filter_regex": "正規表現でフィルター",
"home.column_settings.show_direct": "DMを表示",
"home.settings": "カラムの設定",
"keyboard_shortcuts.bookmark": "ブックマーク",
"keyboard_shortcuts.secondary_toot": "セカンダリートゥートの公開範囲でトゥートする",
"keyboard_shortcuts.toggle_collapse": "折りたたむ/折りたたみを解除",
"media_gallery.sensitive": "閲覧注意",
"moved_to_warning": "このアカウント{moved_to_link}に引っ越したため、新しいフォロワーを受け入れていません。",
"navigation_bar.app_settings": "アプリ設定",
"navigation_bar.featured_users": "紹介しているアカウント",
"navigation_bar.keyboard_shortcuts": "キーボードショートカット",
"navigation_bar.misc": "その他",
"notification.markForDeletion": "選択",
"notification_purge.btn_all": "すべて\n選択",
"notification_purge.btn_apply": "選択したものを\n削除",
"notification_purge.btn_invert": "選択を\n反転",
"notification_purge.btn_none": "選択\n解除",
"notification_purge.start": "通知整理モードに入る",
"notifications.marked_clear": "選択した通知を削除する",
"notifications.marked_clear_confirmation": "削除した全ての通知を完全に削除してもよろしいですか?",
"onboarding.page_one.federation": "{domain}はMastodonのインスタンスです。Mastodonとは、独立したサーバが連携して作るソーシャルネットワークです。これらのサーバーをインスタンスと呼びます。",
@@ -68,6 +82,7 @@
"settings.always_show_spoilers_field": "常にコンテンツワーニング設定を表示する(指定がない場合は通常投稿)",
"settings.auto_collapse": "自動折りたたみ",
"settings.auto_collapse_all": "すべて",
"settings.auto_collapse_height": "トゥートが長いと見なされる高さ(ピクセル)",
"settings.auto_collapse_lengthy": "長いトゥート",
"settings.auto_collapse_media": "メディア付きトゥート",
"settings.auto_collapse_notifications": "通知",
@@ -82,6 +97,9 @@
"settings.content_warnings": "コンテンツワーニング",
"settings.content_warnings.regexp": "正規表現",
"settings.content_warnings_filter": "説明に指定した文字が含まれているものを自動で展開しないようにする",
"settings.content_warnings_media_outside": "コンテンツワーニングの外側にメディア添付ファイルを表示する",
"settings.content_warnings_shared_state": "すべてのコピーの内容を一度に表示/非表示",
"settings.content_warnings_unfold_opts": "自動展開オプション",
"settings.enable_collapsed": "トゥート折りたたみを有効にする",
"settings.enable_content_warnings_auto_unfold": "コンテンツワーニング指定されている投稿を常に表示する",
"settings.general": "一般",
@@ -119,10 +137,24 @@
"settings.side_arm_reply_mode.copy": "返信先の投稿範囲を利用する",
"settings.side_arm_reply_mode.keep": "セカンダリートゥートボタンの設定を維持する",
"settings.side_arm_reply_mode.restrict": "返信先の投稿範囲に制限する",
"settings.status_icons": "トゥートアイコン",
"settings.status_icons_language": "言語インジケータ",
"settings.status_icons_local_only": "ローカル限定インジケータ",
"settings.status_icons_media": "メディア・アンケートインジケータ",
"settings.status_icons_reply": "返信インジケータ",
"settings.status_icons_visibility": "公開範囲インジケータ",
"settings.swipe_to_change_columns": "スワイプでカラムを切り替え可能にする(モバイルのみ)",
"settings.tag_misleading_links": "誤解を招くリンクにタグをつける",
"settings.tag_misleading_links.hint": "明示的に言及していないすべてのリンクに、リンクターゲットホストを含む視覚的な表示を追加します",
"settings.wide_view": "ワイドビュー(デスクトップ レイアウトのみ)",
"status.collapse": "折りたたむ",
"status.has_audio": "添付されたオーディオファイルが表示されます",
"status.has_pictures": "添付された画像が表示されます",
"status.has_preview_card": "添付されたプレビューカードが表示されます",
"status.has_video": "添付動画が表示されます",
"status.in_reply_to": "このトゥートは返信です",
"status.is_poll": "このトゥートはアンケートです",
"status.local_only": "あなたのインスタンスのみに公開",
"status.sensitive_toggle": "クリックして表示",
"status.uncollapse": "折りたたみを解除"
}

View File

@@ -16,11 +16,17 @@
"advanced_options.local-only.long": "不要傳遞給其他實例",
"advanced_options.local-only.short": "僅限本地",
"advanced_options.local-only.tooltip": "此嘟文僅限本地",
"advanced_options.threaded_mode.long": "發佈時自動打開回覆",
"advanced_options.threaded_mode.short": "討論串模式",
"advanced_options.threaded_mode.tooltip": "已啟用討論串模式",
"boost_modal.missing_description": "此嘟文包含未加說明的媒體檔案",
"column.favourited_by": "誰按了最愛",
"column.heading": "雜項",
"column.reblogged_by": "被誰轉嘟",
"column.subheading": "其他選項",
"column_header.profile": "個人檔案",
"column_subheading.lists": "列表",
"column_subheading.navigation": "導覽",
"community.column_settings.allow_local_only": "顯示僅限本地的嘟文",
"compose.attach": "附加...",
"compose.attach.doodle": "塗鴉",
@@ -30,27 +36,66 @@
"compose.content-type.plain": "純文字",
"compose_form.poll.multiple_choices": "允許多重選擇",
"compose_form.poll.single_choice": "允許單一選擇",
"compose_form.spoiler": "將文字隱藏在內容警告後面",
"confirmation_modal.do_not_ask_again": "不要再顯示確認訊息",
"confirmations.deprecated_settings.confirm": "使用 Mastodon 偏好",
"confirmations.deprecated_settings.message": "您正在使用的某些特定於 glitch-soc 設備的 {app_settings} 已被 Mastodon {preferences} 所取代,並將被覆蓋:",
"confirmations.missing_media_description.confirm": "仍要張貼",
"confirmations.missing_media_description.edit": "編輯媒體",
"confirmations.missing_media_description.message": "至少有一個媒體附件缺少說明。 在發送嘟文之前,請考慮為視障人士在所有媒體附件加上說明。",
"confirmations.unfilter.author": "作者",
"confirmations.unfilter.confirm": "顯示",
"confirmations.unfilter.edit_filter": "編輯篩選器",
"content-type.change": "內容類型",
"direct.group_by_conversations": "以對話分組",
"empty_column.follow_recommendations": "似乎未能為您產生任何建議。您可以嘗試使用搜尋來尋找您可能認識的人,或是探索熱門主題標籤。",
"endorsed_accounts_editor.endorsed_accounts": "受推薦帳號",
"favourite_modal.combo": "下次您可以按 {combo} 跳過",
"firehose.column_settings.allow_local_only": "在「全部」顯示僅限本地的貼文",
"follow_recommendations.done": "完成",
"follow_recommendations.heading": "跟隨您想檢視其嘟文的人!這裡有一些建議。",
"follow_recommendations.lead": "來自您跟隨的人之嘟文將會按時間順序顯示在您的首頁時間軸上。不要害怕犯錯,您隨時都可以取消跟隨其他人!",
"getting_started.onboarding": "帶我四處看看",
"home.column_settings.advanced": "進階設定",
"home.column_settings.filter_regex": "以正規表達式進行過濾",
"home.column_settings.show_direct": "顯示私人提及",
"home.settings": "欄位設定",
"keyboard_shortcuts.bookmark": "到書籤",
"keyboard_shortcuts.secondary_toot": "使用次要隱私設定來發布嘟文",
"keyboard_shortcuts.toggle_collapse": "去折疊/展開嘟文",
"media_gallery.sensitive": "敏感",
"moved_to_warning": "此帳戶已標記為移至 {moved_to_link},因此可能不接受新的追隨者。",
"navigation_bar.app_settings": "應用程式設定",
"navigation_bar.featured_users": "被推薦的使用者",
"navigation_bar.keyboard_shortcuts": "鍵盤快速鍵",
"navigation_bar.misc": "雜項",
"notification.markForDeletion": "標記刪除",
"notification_purge.btn_all": "選取全部",
"notification_purge.btn_apply": "清除所選項目",
"notification_purge.btn_invert": "反向選擇",
"notification_purge.btn_none": "取消選取",
"notification_purge.start": "進入通知清理模式",
"notifications.marked_clear": "清除被選取的通知訊息",
"notifications.marked_clear_confirmation": "您確定要永久清除所有被選取的通知訊息嗎?",
"onboarding.done": "完成",
"onboarding.next": "下一個",
"onboarding.page_five.public_timelines": "本地時間軸顯示來自 {domain} 上所有人的公開貼文。聯合時間軸顯示 {domain} 上追隨的每個人發表的公開貼文。這些是公共時間軸,是發現新朋友的好方法。",
"onboarding.page_four.home": "首頁時間線會顯示你追隨的人發布的貼文。",
"onboarding.page_four.notifications": "當有人與您互動時會顯示在通知欄。",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_one.handle": "你的帳號在 {domain} ,所以你的帳號全名是 {handle}",
"onboarding.page_one.welcome": "歡迎來到 {domain} ",
"onboarding.page_six.admin": "您的站台管理者是 {admin} 。",
"onboarding.page_six.almost_done": "就快完成了…",
"onboarding.page_six.apps_available": "有適用於 iOS、Android 和其他平台的 {apps}。",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.guidelines": "社群規範",
"onboarding.page_six.read_guidelines": "請閱讀 {domain} 的 {guidelines}",
"onboarding.page_six.various_app": "手機應用程式",
"onboarding.page_three.profile": "編輯您的個人資料以更改您的頭像、個人簡介和顯示名稱。在那裡,您還會發現其他偏好設置。",
"onboarding.page_three.search": "使用搜索欄查找他人與主題標籤,例如 {illustration} 和 {introductions} 。要尋找其他站台的人,請使用他們的完整帳號名稱。",
"onboarding.page_two.compose": "從撰寫欄撰寫帖子。您可以使用下面的圖示上傳圖片、更改隱私設置以及添加內容警告。",
"onboarding.skip": "略過",
"settings.always_show_spoilers_field": "永遠啟用內容警告欄位",
"settings.auto_collapse": "自動折疊",
"settings.auto_collapse_all": "全部",
@@ -83,19 +128,23 @@
"settings.hicolor_privacy_icons.hint": "用明亮且易於區分的顏色顯示隱私圖示",
"settings.image_backgrounds": "圖片背景",
"settings.image_backgrounds_media": "預覽折疊嘟文的媒體檔案",
"settings.image_backgrounds_media_hint": "如果嘟文包含媒體檔案,使用一個作為圖片背景",
"settings.image_backgrounds_media_hint": "如果嘟文包含媒體檔案,使用一個作為圖片背景",
"settings.image_backgrounds_users": "為折疊的嘟文加上圖片背景",
"settings.inline_preview_cards": "針對外部連接顯示內嵌的預覽卡",
"settings.layout_opts": "版面選項",
"settings.media": "媒體",
"settings.media_fullwidth": "在媒體預覽中使用完整寬度",
"settings.media_letterbox": "在媒體預覽加上黑邊",
"settings.media_letterbox_hint": "在媒體預覽中縮小並加上黑邊以取代延展與裁切",
"settings.media_reveal_behind_cw": "預設顯示隱藏在內容警告的敏感媒體檔案",
"settings.notifications.favicon_badge": "未讀通知網站圖示徽章",
"settings.notifications.favicon_badge.hint": "在網站圖示上增加一個未讀通知徽章",
"settings.notifications.tab_badge": "未讀通知徽章",
"settings.notifications.tab_badge.hint": "當通知列未打開時,在導引圖示中顯示未讀通知的徽章",
"settings.notifications_opts": "通知選項",
"settings.pop_in_left": "左邊",
"settings.pop_in_player": "啟用彈出播放器",
"settings.pop_in_position": "彈出播放器位置:",
"settings.pop_in_right": "右邊",
"settings.preferences": "使用者偏好設定",
"settings.prepend_cw_re": "回覆時在內容警告前添加 \"re:\"",
@@ -105,6 +154,7 @@
"settings.rewrite_mentions_acct": "改寫為使用者名稱與網域(當使用者來自外部)",
"settings.rewrite_mentions_no": "不要改寫提及",
"settings.rewrite_mentions_username": "改寫為使用者名稱",
"settings.shared_settings_link": "使用者偏好設定",
"settings.show_action_bar": "在折疊的嘟文顯示操作按鈕",
"settings.show_content_type_choice": "在編寫嘟文時顯示內容類型選擇",
"settings.show_reply_counter": "顯示回覆數量的估計值",
@@ -113,12 +163,14 @@
"settings.side_arm_reply_mode": "當回覆一篇嘟文時,次要發出嘟文按鈕應該設為:",
"settings.side_arm_reply_mode.copy": "複製回覆嘟文的隱私設置",
"settings.side_arm_reply_mode.keep": "保持原本的隱私設定",
"settings.side_arm_reply_mode.restrict": "限制只能使用與回覆嘟文相同的隱私設置",
"settings.status_icons": "嘟文圖示",
"settings.status_icons_language": "語言指示器",
"settings.status_icons_local_only": "僅限本地指示器",
"settings.status_icons_media": "媒體與投票指示器",
"settings.status_icons_reply": "回覆指示器",
"settings.status_icons_visibility": "嘟文隱私指示器",
"settings.swipe_to_change_columns": "允許使用滑動手勢更改顯示欄位(僅限移動裝置)",
"settings.tag_misleading_links": "標記誤導性的連結",
"settings.tag_misleading_links.hint": "在每個未明確提及的連結添加帶有連結目標主機的視覺指示",
"settings.wide_view": "寬廣模式(僅限桌面模式)",
@@ -130,5 +182,17 @@
"status.has_video": "包含視訊檔案",
"status.in_reply_to": "嘟文有回覆",
"status.is_poll": "嘟文有投票",
"status.local_only": "只在此實例可見"
"status.local_only": "只在此實例可見",
"status.sensitive_toggle": "點擊查看",
"status.uncollapse": "展開",
"web_app_crash.change_your_settings": "修改你的 {settings}",
"web_app_crash.content": "您可以嘗試以下任一種方法:",
"web_app_crash.debug_info": "除錯資訊",
"web_app_crash.disable_addons": "禁用瀏覽器插件或內置翻譯工具",
"web_app_crash.issue_tracker": "問題追蹤系統",
"web_app_crash.reload": "重新載入",
"web_app_crash.reload_page": "{reload} 當前頁面",
"web_app_crash.report_issue": "到 {issuetracker} 回報問題",
"web_app_crash.settings": "設定",
"web_app_crash.title": "很抱歉Mastodon 應用程序出現問題。"
}

View File

@@ -14,7 +14,6 @@ import emojify from 'flavours/glitch/features/emoji/emoji';
import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions';
import { loadLocale, getLocale } from 'flavours/glitch/locales';
import { loadPolyfills } from 'flavours/glitch/polyfills';
import ready from 'flavours/glitch/ready';
const messages = defineMessages({
usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' },
@@ -42,159 +41,157 @@ function main() {
};
};
ready(() => {
const locale = document.documentElement.lang;
const locale = document.documentElement.lang;
const dateTimeFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
});
const dateTimeFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
});
const dateFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
timeFormat: false,
});
const dateFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
timeFormat: false,
});
const timeFormat = new Intl.DateTimeFormat(locale, {
timeStyle: 'short',
hour12: false,
});
const timeFormat = new Intl.DateTimeFormat(locale, {
timeStyle: 'short',
hour12: false,
});
const formatMessage = ({ id, defaultMessage }, values) => {
const messageFormat = new IntlMessageFormat(localeData[id] || defaultMessage, locale);
return messageFormat.format(values);
};
const formatMessage = ({ id, defaultMessage }, values) => {
const messageFormat = new IntlMessageFormat(localeData[id] || defaultMessage, locale);
return messageFormat.format(values);
};
[].forEach.call(document.querySelectorAll('.emojify'), (content) => {
content.innerHTML = emojify(content.innerHTML);
});
[].forEach.call(document.querySelectorAll('.emojify'), (content) => {
content.innerHTML = emojify(content.innerHTML);
});
[].forEach.call(document.querySelectorAll('time.formatted'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
const formattedDate = dateTimeFormat.format(datetime);
[].forEach.call(document.querySelectorAll('time.formatted'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
const formattedDate = dateTimeFormat.format(datetime);
content.title = formattedDate;
content.textContent = formattedDate;
});
content.title = formattedDate;
content.textContent = formattedDate;
});
const isToday = date => {
const today = new Date();
const isToday = date => {
const today = new Date();
return date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
};
const todayFormat = new IntlMessageFormat(localeData['relative_format.today'] || 'Today at {time}', locale);
return date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
};
const todayFormat = new IntlMessageFormat(localeData['relative_format.today'] || 'Today at {time}', locale);
[].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
[].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
let formattedContent;
let formattedContent;
if (isToday(datetime)) {
const formattedTime = timeFormat.format(datetime);
if (isToday(datetime)) {
const formattedTime = timeFormat.format(datetime);
formattedContent = todayFormat.format({ time: formattedTime });
} else {
formattedContent = dateFormat.format(datetime);
}
content.title = formattedContent;
content.textContent = formattedContent;
});
[].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
const now = new Date();
const timeGiven = content.getAttribute('datetime').includes('T');
content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime);
content.textContent = timeAgoString({
formatMessage,
formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date),
}, datetime, now, now.getFullYear(), timeGiven);
});
const reactComponents = document.querySelectorAll('[data-component]');
if (reactComponents.length > 0) {
import(/* webpackChunkName: "containers/media_container" */ 'flavours/glitch/containers/media_container')
.then(({ default: MediaContainer }) => {
[].forEach.call(reactComponents, (component) => {
[].forEach.call(component.children, (child) => {
component.removeChild(child);
});
});
const content = document.createElement('div');
const root = createRoot(content);
root.render(<MediaContainer locale={locale} components={reactComponents} />);
document.body.appendChild(content);
scrollToDetailedStatus();
})
.catch(error => {
console.error(error);
scrollToDetailedStatus();
});
formattedContent = todayFormat.format({ time: formattedTime });
} else {
scrollToDetailedStatus();
formattedContent = dateFormat.format(datetime);
}
delegate(document, '#user_account_attributes_username', 'input', throttle(() => {
const username = document.getElementById('user_account_attributes_username');
content.title = formattedContent;
content.textContent = formattedContent;
});
if (username.value && username.value.length > 0) {
axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => {
username.setCustomValidity(formatMessage(messages.usernameTaken));
}).catch(() => {
username.setCustomValidity('');
[].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
const now = new Date();
const timeGiven = content.getAttribute('datetime').includes('T');
content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime);
content.textContent = timeAgoString({
formatMessage,
formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date),
}, datetime, now, now.getFullYear(), timeGiven);
});
const reactComponents = document.querySelectorAll('[data-component]');
if (reactComponents.length > 0) {
import(/* webpackChunkName: "containers/media_container" */ 'flavours/glitch/containers/media_container')
.then(({ default: MediaContainer }) => {
[].forEach.call(reactComponents, (component) => {
[].forEach.call(component.children, (child) => {
component.removeChild(child);
});
});
} else {
const content = document.createElement('div');
const root = createRoot(content);
root.render(<MediaContainer locale={locale} components={reactComponents} />);
document.body.appendChild(content);
scrollToDetailedStatus();
})
.catch(error => {
console.error(error);
scrollToDetailedStatus();
});
} else {
scrollToDetailedStatus();
}
delegate(document, '#user_account_attributes_username', 'input', throttle(() => {
const username = document.getElementById('user_account_attributes_username');
if (username.value && username.value.length > 0) {
axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => {
username.setCustomValidity(formatMessage(messages.usernameTaken));
}).catch(() => {
username.setCustomValidity('');
}
}, 500, { leading: false, trailing: true }));
});
} else {
username.setCustomValidity('');
}
}, 500, { leading: false, trailing: true }));
delegate(document, '#user_password,#user_password_confirmation', 'input', () => {
const password = document.getElementById('user_password');
const confirmation = document.getElementById('user_password_confirmation');
if (!confirmation) return;
delegate(document, '#user_password,#user_password_confirmation', 'input', () => {
const password = document.getElementById('user_password');
const confirmation = document.getElementById('user_password_confirmation');
if (!confirmation) return;
if (confirmation.value && confirmation.value.length > password.maxLength) {
confirmation.setCustomValidity(formatMessage(messages.passwordExceedsLength));
} else if (password.value && password.value !== confirmation.value) {
confirmation.setCustomValidity(formatMessage(messages.passwordDoesNotMatch));
} else {
confirmation.setCustomValidity('');
}
});
if (confirmation.value && confirmation.value.length > password.maxLength) {
confirmation.setCustomValidity(formatMessage(messages.passwordExceedsLength));
} else if (password.value && password.value !== confirmation.value) {
confirmation.setCustomValidity(formatMessage(messages.passwordDoesNotMatch));
} else {
confirmation.setCustomValidity('');
}
});
delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
delegate(document, '.status__content__spoiler-link', 'click', function() {
const statusEl = this.parentNode.parentNode;
delegate(document, '.status__content__spoiler-link', 'click', function() {
const statusEl = this.parentNode.parentNode;
if (statusEl.dataset.spoiler === 'expanded') {
statusEl.dataset.spoiler = 'folded';
this.textContent = (new IntlMessageFormat(localeData['status.show_more'] || 'Show more', locale)).format();
} else {
statusEl.dataset.spoiler = 'expanded';
this.textContent = (new IntlMessageFormat(localeData['status.show_less'] || 'Show less', locale)).format();
}
if (statusEl.dataset.spoiler === 'expanded') {
statusEl.dataset.spoiler = 'folded';
this.textContent = (new IntlMessageFormat(localeData['status.show_more'] || 'Show more', locale)).format();
} else {
statusEl.dataset.spoiler = 'expanded';
this.textContent = (new IntlMessageFormat(localeData['status.show_less'] || 'Show less', locale)).format();
}
return false;
});
return false;
});
[].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
const statusEl = spoilerLink.parentNode.parentNode;
const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more');
spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
});
[].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
const statusEl = spoilerLink.parentNode.parentNode;
const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more');
spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
});
const toggleSidebar = () => {

View File

@@ -13,4 +13,30 @@ ready(() => {
console.error(error);
});
}, 5000);
document.querySelectorAll('.timer-button').forEach(button => {
let counter = 30;
const container = document.createElement('span');
const updateCounter = () => {
container.innerText = ` (${counter})`;
};
updateCounter();
const countdown = setInterval(() => {
counter--;
if (counter === 0) {
button.disabled = false;
button.removeChild(container);
clearInterval(countdown);
} else {
updateCounter();
}
}, 1000);
button.appendChild(container);
});
});

View File

@@ -1038,3 +1038,33 @@ $ui-header-height: 55px;
}
}
}
.hashtag-header {
border-bottom: 1px solid lighten($ui-base-color, 8%);
padding: 15px;
font-size: 17px;
line-height: 22px;
color: $darker-text-color;
strong {
font-weight: 700;
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
gap: 15px;
h1 {
color: $primary-text-color;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 22px;
line-height: 33px;
font-weight: 700;
}
}
}

View File

@@ -147,10 +147,6 @@
display: block;
width: 100%;
}
.layout-multiple-columns &.button--with-bell {
font-size: 12px;
}
}
.icon-button {
@@ -1345,16 +1341,19 @@ button.icon-button.active i.fa-retweet {
display: flex;
align-items: center;
justify-content: center;
background: rgba($black, 0.5);
background: transparent;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
border: 0;
border-radius: 4px;
color: $white;
&__label {
background-color: rgba($black, 0.45);
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
border-radius: 6px;
padding: 10px 15px;
display: flex;
align-items: center;
justify-content: center;
@@ -1368,6 +1367,13 @@ button.icon-button.active i.fa-retweet {
font-weight: 400;
font-size: 13px;
}
&:hover,
&:focus {
.spoiler-button__overlay__label {
background-color: rgba($black, 0.9);
}
}
}
}

View File

@@ -719,15 +719,16 @@
}
.button.button-secondary {
border-color: $ui-button-secondary-border-color;
color: $ui-button-secondary-color;
border-color: $inverted-text-color;
color: $inverted-text-color;
flex: 0 0 auto;
&:hover,
&:focus,
&:active {
border-color: $ui-button-secondary-focus-background-color;
color: $ui-button-secondary-focus-color;
background: transparent;
border-color: $ui-button-background-color;
color: $ui-button-background-color;
}
}
@@ -1412,6 +1413,44 @@ img.modal-warning {
}
}
&__choices {
display: flex;
gap: 40px;
&__choice {
flex: 1;
box-sizing: border-box;
h3 {
margin-bottom: 20px;
}
p {
color: $darker-text-color;
margin-bottom: 20px;
font-size: 15px;
}
.button {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
}
}
@media screen and (max-width: $no-gap-breakpoint - 1px) {
&__choices {
flex-direction: column;
&__choice {
margin-top: 40px;
}
}
}
.link-button {
font-size: inherit;
display: inline;

View File

@@ -159,6 +159,7 @@
&.active {
transform: rotate(90deg);
opacity: 1;
}
&:hover {

View File

@@ -310,9 +310,19 @@ code {
border-radius: 4px;
background: url('images/void.png');
&[src$='missing.png'] {
visibility: hidden;
}
&:last-child {
margin-bottom: 0;
}
&#account_avatar-preview {
width: 90px;
height: 90px;
object-fit: cover;
}
}
}

View File

@@ -420,6 +420,10 @@ html {
border-top: 0;
}
.column-settings__hashtags .column-select__option {
color: $white;
}
.dashboard__quick-access,
.focal-point__preview strong,
.admin-wrapper .content__heading__tabs a.selected {