Merge upstream (#81)

This commit is contained in:
kibigo!
2017-07-15 14:33:15 -07:00
213 changed files with 2714 additions and 1364 deletions

View File

@@ -2,8 +2,6 @@ import api from '../api';
import { updateTimeline } from './timelines';
import * as emojione from 'emojione';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
@@ -74,10 +72,12 @@ export function mentionCompose(account, router) {
export function submitCompose() {
return function (dispatch, getState) {
let status = emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], ''));
const status = getState().getIn(['compose', 'text'], '');
if (!status || !status.length) {
return;
}
dispatch(submitComposeRequest());
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
status = status + ' 👁️';

View File

@@ -0,0 +1,52 @@
import axios from 'axios';
export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
export const ALERTS_CHANGE = 'PUSH_NOTIFICATIONS_ALERTS_CHANGE';
export function setBrowserSupport (value) {
return {
type: SET_BROWSER_SUPPORT,
value,
};
}
export function setSubscription (subscription) {
return {
type: SET_SUBSCRIPTION,
subscription,
};
}
export function clearSubscription () {
return {
type: CLEAR_SUBSCRIPTION,
};
}
export function changeAlerts(key, value) {
return dispatch => {
dispatch({
type: ALERTS_CHANGE,
key,
value,
});
dispatch(saveSettings());
};
}
export function saveSettings() {
return (_, getState) => {
const state = getState().get('push_notifications');
const subscription = state.get('subscription');
const alerts = state.get('alerts');
axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
data: {
alerts,
},
});
};
}

View File

@@ -5,6 +5,8 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number,
time: PropTypes.number,
controls: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired,
@@ -30,7 +32,7 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
render () {
return (
<div className='extended-video-player'>
<div className='extended-video-player' style={{ width: this.props.width, height: this.props.height }}>
<video
ref={this.setRef}
src={this.props.src}

View File

@@ -6,11 +6,18 @@ export default class LoadMore extends React.PureComponent {
static propTypes = {
onClick: PropTypes.func,
visible: PropTypes.bool,
}
static defaultProps = {
visible: true,
}
render() {
const { visible } = this.props;
return (
<button className='load-more' onClick={this.props.onClick}>
<button className='load-more' disabled={!visible} style={{ opacity: visible ? 1 : 0 }} onClick={this.props.onClick}>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
</button>
);

View File

@@ -101,13 +101,9 @@ export default class StatusList extends ImmutablePureComponent {
render () {
const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
let loadMore = null;
const loadMore = <LoadMore visible={!isLoading && statusIds.size > 0 && hasMore} onClick={this.handleLoadMore} />;
let scrollableArea = null;
if (!isLoading && statusIds.size > 0 && hasMore) {
loadMore = <LoadMore onClick={this.handleLoadMore} />;
}
if (isLoading || statusIds.size > 0 || !emptyMessage) {
scrollableArea = (
<div className='scrollable' ref={this.setRef}>

View File

@@ -1,49 +1,28 @@
import emojione from 'emojione';
import { unicodeToFilename } from './emojione_light';
import Trie from 'substring-trie';
const mappedUnicode = emojione.mapUnicodeToShort();
const trie = new Trie(Object.keys(emojione.jsEscapeMap));
const trie = new Trie(Object.keys(unicodeToFilename));
function emojify(str) {
// This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
// and replacing valid shortnames like :smile: and :wink: as well as unicode strings
// and replacing valid unicode strings
// that _aren't_ within tags with an <img> version.
// The goal is to be the same as an emojione.regShortNames/regUnicode replacement, but faster.
// The goal is to be the same as an emojione.regUnicode replacement, but faster.
let i = -1;
let insideTag = false;
let insideShortname = false;
let shortnameStartIndex = -1;
let match;
while (++i < str.length) {
const char = str.charAt(i);
if (insideShortname && char === ':') {
const shortname = str.substring(shortnameStartIndex, i + 1);
if (shortname in emojione.emojioneList) {
const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1];
const alt = emojione.convert(unicode.toUpperCase());
const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`;
str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1);
i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string
} else {
i--; // stray colon, try again
}
insideShortname = false;
} else if (insideTag && char === '>') {
if (insideTag && char === '>') {
insideTag = false;
} else if (char === '<') {
insideTag = true;
insideShortname = false;
} else if (!insideTag && char === ':') {
insideShortname = true;
shortnameStartIndex = i;
} else if (!insideTag && (match = trie.search(str.substring(i)))) {
const unicodeStr = match;
if (unicodeStr in emojione.jsEscapeMap) {
const unicode = emojione.jsEscapeMap[unicodeStr];
const short = mappedUnicode[unicode];
const filename = emojione.emojioneList[short].fname;
const alt = emojione.convert(unicode.toUpperCase());
const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${short}" src="/emoji/${filename}.svg" />`;
if (unicodeStr in unicodeToFilename) {
const filename = unicodeToFilename[unicodeStr];
const alt = unicodeStr;
const replacement = `<img draggable="false" class="emojione" alt="${alt}" src="/emoji/${filename}.svg" />`;
str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length);
i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string
}

View File

@@ -0,0 +1,11 @@
// @preval
// Force tree shaking on emojione by exposing just a subset of its functionality
const emojione = require('emojione');
const mappedUnicode = emojione.mapUnicodeToShort();
module.exports.unicodeToFilename = Object.keys(emojione.jsEscapeMap)
.map(unicodeStr => [unicodeStr, mappedUnicode[emojione.jsEscapeMap[unicodeStr]]])
.map(([unicodeStr, shortCode]) => ({ [unicodeStr]: emojione.emojioneList[shortCode].fname }))
.reduce((x, y) => Object.assign(x, y), { });

View File

@@ -1,2 +1,5 @@
import 'intersection-observer';
import 'requestidlecallback';
import objectFitImages from 'object-fit-images';
objectFitImages();

View File

@@ -140,7 +140,8 @@ export default class ComposeForm extends ImmutablePureComponent {
handleEmojiPick = (data) => {
const position = this.autosuggestTextarea.textarea.selectionStart;
this._restoreCaret = position + data.shortname.length + 1;
const emojiChar = String.fromCodePoint(parseInt(data.unicode, 16));
this._restoreCaret = position + emojiChar.length + 1;
this.props.onPickEmoji(position, data);
}

View File

@@ -109,11 +109,12 @@ export default class EmojiPickerDropdown extends React.PureComponent {
<Dropdown ref={this.setRef} className='emoji-picker__dropdown' onShow={this.onShowDropdown} onHide={this.onHideDropdown}>
<DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)}>
<img
draggable='false'
className={`emojione ${active && loading ? 'pulse-loading' : ''}`}
alt='🙂' src='/emoji/1f602.svg'
alt='🙂'
src='/emoji/1f602.svg'
/>
</DropdownTrigger>
<DropdownContent className='dropdown__left'>
{
this.state.active && !this.state.loading &&

View File

@@ -2,11 +2,11 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites';
import Column from '../ui/components/column';
import ColumnHeader from '../../components/column_header';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import StatusList from '../../components/status_list';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -16,8 +16,6 @@ const messages = defineMessages({
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
loaded: state.getIn(['status_lists', 'favourites', 'loaded']),
me: state.getIn(['meta', 'me']),
});
@connect(mapStateToProps)
@@ -27,34 +25,64 @@ export default class Favourites extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
loaded: PropTypes.bool,
intl: PropTypes.object.isRequired,
me: PropTypes.number.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
};
componentWillMount () {
this.props.dispatch(fetchFavouritedStatuses());
}
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('FAVOURITES', {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
handleScrollToBottom = () => {
this.props.dispatch(expandFavouritedStatuses());
}
render () {
const { loaded, intl } = this.props;
if (!loaded) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
const { intl, statusIds, columnId, multiColumn } = this.props;
const pinned = !!columnId;
return (
<Column icon='star' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim />
<StatusList {...this.props} scrollKey='favourited_statuses' onScrollToBottom={this.handleScrollToBottom} />
<Column ref={this.setRef}>
<ColumnHeader
icon='star'
title={intl.formatMessage(messages.heading)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
/>
<StatusList
trackScroll={!pinned}
statusIds={statusIds}
scrollKey={`favourited_statuses-${columnId}`}
onScrollToBottom={this.handleScrollToBottom}
/>
</Column>
);
}

View File

@@ -9,18 +9,27 @@ export default class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
pushSettings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
};
onPushChange = (key, checked) => {
this.props.onChange(['push', ...key], checked);
}
render () {
const { settings, onChange, onClear } = this.props;
const { settings, pushSettings, onChange, onClear } = this.props;
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
const pushMeta = showPushSettings && <FormattedMessage id='notifications.column_settings.push_meta' defaultMessage='This device' />;
return (
<div>
<div className='column-settings__row'>
@@ -30,7 +39,8 @@ export default class ColumnSettings extends React.PureComponent {
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
</div>
@@ -38,7 +48,8 @@ export default class ColumnSettings extends React.PureComponent {
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
</div>
@@ -46,7 +57,8 @@ export default class ColumnSettings extends React.PureComponent {
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
</div>
@@ -54,7 +66,8 @@ export default class ColumnSettings extends React.PureComponent {
<span className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
<SettingToggle prefix='notifications_desktop' settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingKey={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
</div>

View File

@@ -10,6 +10,7 @@ export default class SettingToggle extends React.PureComponent {
settings: ImmutablePropTypes.map.isRequired,
settingKey: PropTypes.array.isRequired,
label: PropTypes.node.isRequired,
meta: PropTypes.node,
onChange: PropTypes.func.isRequired,
}
@@ -18,13 +19,14 @@ export default class SettingToggle extends React.PureComponent {
}
render () {
const { prefix, settings, settingKey, label } = this.props;
const { prefix, settings, settingKey, label, meta } = this.props;
const id = ['setting-toggle', prefix, ...settingKey].filter(Boolean).join('-');
return (
<div className='setting-toggle'>
<Toggle id={id} checked={settings.getIn(settingKey)} onChange={this.onChange} />
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
{meta && <span className='setting-meta__label'>{meta}</span>}
</div>
);
}

View File

@@ -3,6 +3,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import ColumnSettings from '../components/column_settings';
import { changeSetting, saveSettings } from '../../../actions/settings';
import { clearNotifications } from '../../../actions/notifications';
import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from '../../../actions/push_notifications';
import { openModal } from '../../../actions/modal';
const messages = defineMessages({
@@ -12,16 +13,22 @@ const messages = defineMessages({
const mapStateToProps = state => ({
settings: state.getIn(['settings', 'notifications']),
pushSettings: state.get('push_notifications'),
});
const mapDispatchToProps = (dispatch, { intl }) => ({
onChange (key, checked) {
dispatch(changeSetting(['notifications', ...key], checked));
if (key[0] === 'push') {
dispatch(changePushNotifications(key.slice(1), checked));
} else {
dispatch(changeSetting(['notifications', ...key], checked));
}
},
onSave () {
dispatch(saveSettings());
dispatch(savePushNotificationSettings());
},
onClear () {

View File

@@ -9,7 +9,7 @@ import { links, getIndex, getLink } from './tabs_bar';
import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading';
import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline } from '../../ui/util/async-components';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
const componentMap = {
'COMPOSE': Compose,
@@ -18,6 +18,7 @@ const componentMap = {
'PUBLIC': PublicTimeline,
'COMMUNITY': CommunityTimeline,
'HASHTAG': HashtagTimeline,
'FAVOURITES': FavouritedStatuses,
};
export default class ColumnsArea extends ImmutablePureComponent {
@@ -32,12 +33,33 @@ export default class ColumnsArea extends ImmutablePureComponent {
children: PropTypes.node,
};
state = {
shouldAnimate: false,
}
componentWillReceiveProps() {
this.setState({ shouldAnimate: false });
}
componentDidMount() {
this.lastIndex = getIndex(this.context.router.history.location.pathname);
this.setState({ shouldAnimate: true });
}
componentDidUpdate() {
this.lastIndex = getIndex(this.context.router.history.location.pathname);
this.setState({ shouldAnimate: true });
}
handleSwipe = (index) => {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
this.context.router.history.push(getLink(index));
});
});
this.pendingIndex = index;
}
handleAnimationEnd = () => {
if (typeof this.pendingIndex === 'number') {
this.context.router.history.push(getLink(this.pendingIndex));
this.pendingIndex = null;
}
}
renderView = (link, index) => {
@@ -66,12 +88,14 @@ export default class ColumnsArea extends ImmutablePureComponent {
render () {
const { columns, children, singleColumn } = this.props;
const { shouldAnimate } = this.state;
const columnIndex = getIndex(this.context.router.history.location.pathname);
this.pendingIndex = null;
if (singleColumn) {
return columnIndex !== -1 ? (
<ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} animateTransitions={false} style={{ height: '100%' }}>
<ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
{links.map(this.renderView)}
</ReactSwipeableViews>
) : <div className='columns-area'>{children}</div>;

View File

@@ -65,8 +65,6 @@ export default class MediaModal extends ImmutablePureComponent {
const { media, intl, onClose } = this.props;
const index = this.getIndex();
const attachment = media.get(index);
const url = attachment.get('url');
let leftNav, rightNav, content;
@@ -77,16 +75,18 @@ export default class MediaModal extends ImmutablePureComponent {
rightNav = <div role='button' tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
}
if (attachment.get('type') === 'image') {
content = media.map((image) => {
const width = image.getIn(['meta', 'original', 'width']) || null;
const height = image.getIn(['meta', 'original', 'height']) || null;
content = media.map((image) => {
const width = image.getIn(['meta', 'original', 'width']) || null;
const height = image.getIn(['meta', 'original', 'height']) || null;
if (image.get('type') === 'image') {
return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} key={image.get('preview_url')} />;
}).toArray();
} else if (attachment.get('type') === 'gifv') {
content = <ExtendedVideoPlayer src={url} muted controls={false} />;
}
} else if (image.get('type') === 'gifv') {
return <ExtendedVideoPlayer src={image.get('url')} muted controls={false} width={width} height={height} key={image.get('preview_url')} />;
}
return null;
}).toArray();
return (
<div className='modal-root__modal media-modal'>

View File

@@ -56,12 +56,6 @@ export default class ModalRoot extends React.PureComponent {
return { opacity: spring(0), scale: spring(0.98) };
}
renderModal = (SpecificComponent) => {
const { props, onClose } = this.props;
return <SpecificComponent {...props} onClose={onClose} />;
}
renderLoading = () => {
return <ModalLoading />;
}
@@ -97,7 +91,9 @@ export default class ModalRoot extends React.PureComponent {
<div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} />
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>{this.renderModal}</BundleContainer>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />}
</BundleContainer>
</div>
</div>
))}

View File

@@ -20,11 +20,12 @@ function loadPolyfills() {
);
// Latest version of Firefox and Safari do not have IntersectionObserver.
// Edge does not have requestIdleCallback.
// Edge does not have requestIdleCallback and object-fit CSS property.
// This avoids shipping them all the polyfills.
const needsExtraPolyfills = !(
window.IntersectionObserver &&
window.requestIdleCallback
window.requestIdleCallback &&
'object-fit' in (new Image()).style
);
return Promise.all([

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "المُفَضَّلة :",
"notifications.column_settings.follow": "متابعُون جُدُد :",
"notifications.column_settings.mention": "الإشارات :",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "الترقيّات:",
"notifications.column_settings.show": "إعرِضها في عمود",
"notifications.column_settings.sound": "أصدر صوتا",
@@ -147,6 +149,7 @@
"report.target": "إبلاغ",
"search.placeholder": "ابحث",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "تعذرت ترقية هذا المنشور",
"status.delete": "إحذف",
"status.favourite": "أضف إلى المفضلة",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Предпочитани:",
"notifications.column_settings.follow": "Нови последователи:",
"notifications.column_settings.mention": "Споменавания:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Споделяния:",
"notifications.column_settings.show": "Покажи в колона",
"notifications.column_settings.sound": "Play sound",
@@ -147,6 +149,7 @@
"report.target": "Reporting",
"search.placeholder": "Търсене",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Изтриване",
"status.favourite": "Предпочитани",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favorits:",
"notifications.column_settings.follow": "Nous seguidors:",
"notifications.column_settings.mention": "Mencions:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Mostrar en la columna",
"notifications.column_settings.sound": "Reproduïr so",
@@ -147,6 +149,7 @@
"report.target": "Informes",
"search.placeholder": "Cercar",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "Aquesta publicació no pot ser retootejada",
"status.delete": "Esborrar",
"status.favourite": "Favorit",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favorisierungen:",
"notifications.column_settings.follow": "Neue Folgende:",
"notifications.column_settings.mention": "Erwähnungen:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Geteilte Beiträge:",
"notifications.column_settings.show": "In der Spalte anzeigen",
"notifications.column_settings.sound": "Ton abspielen",
@@ -147,6 +149,7 @@
"report.target": "Melden",
"search.placeholder": "Suche",
"search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Löschen",
"status.favourite": "Favorisieren",

View File

@@ -889,6 +889,14 @@
"defaultMessage": "Play sound",
"id": "notifications.column_settings.sound"
},
{
"defaultMessage": "Push notifications",
"id": "notifications.column_settings.push"
},
{
"defaultMessage": "This device",
"id": "notifications.column_settings.push_meta"
},
{
"defaultMessage": "New followers:",
"id": "notifications.column_settings.follow"
@@ -964,6 +972,15 @@
],
"path": "app/javascript/mastodon/features/public_timeline/index.json"
},
{
"descriptors": [
{
"defaultMessage": "A look inside...",
"id": "standalone.public_title"
}
],
"path": "app/javascript/mastodon/features/standalone/public_timeline/index.json"
},
{
"descriptors": [
{

View File

@@ -114,6 +114,8 @@
"notifications.column_settings.favourite": "Favourites:",
"notifications.column_settings.follow": "New followers:",
"notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Show in column",
"notifications.column_settings.sound": "Play sound",
@@ -170,6 +172,7 @@
"settings.media_fullwidth": "Full-width media previews",
"settings.preferences": "User preferences",
"settings.wide_view": "Wide view (Desktop mode only)",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.collapse": "Collapse",
"status.delete": "Delete",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favoroj:",
"notifications.column_settings.follow": "Novaj sekvantoj:",
"notifications.column_settings.mention": "Mencioj:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Diskonigoj:",
"notifications.column_settings.show": "Montri en kolono",
"notifications.column_settings.sound": "Play sound",
@@ -147,6 +149,7 @@
"report.target": "Reporting",
"search.placeholder": "Serĉi",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Forigi",
"status.favourite": "Favori",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Retoots:",
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Play sound",
@@ -147,6 +149,7 @@
"report.target": "Reporting",
"search.placeholder": "Buscar",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Borrar",
"status.favourite": "Favorito",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "پسندیده‌ها:",
"notifications.column_settings.follow": "پیگیران تازه:",
"notifications.column_settings.mention": "نام‌بردن‌ها:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "بازبوق‌ها:",
"notifications.column_settings.show": "نمایش در ستون",
"notifications.column_settings.sound": "پخش صدا",
@@ -147,6 +149,7 @@
"report.target": "گزارش‌دادن",
"search.placeholder": "جستجو",
"search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید",
"status.delete": "پاک‌کردن",
"status.favourite": "پسندیدن",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Tykkäyksiä:",
"notifications.column_settings.follow": "Uusia seuraajia:",
"notifications.column_settings.mention": "Mainintoja:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Buusteja:",
"notifications.column_settings.show": "Näytä sarakkeessa",
"notifications.column_settings.sound": "Play sound",
@@ -147,6 +149,7 @@
"report.target": "Reporting",
"search.placeholder": "Hae",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Poista",
"status.favourite": "Tykkää",

View File

@@ -29,7 +29,7 @@
"column.favourites": "Favoris",
"column.follow_requests": "Demandes de suivi",
"column.home": "Accueil",
"column.mutes": "Comptes silencés",
"column.mutes": "Comptes masqués",
"column.notifications": "Notifications",
"column.public": "Fil public global",
"column_back_button.label": "Retour",
@@ -52,9 +52,9 @@
"confirmations.delete.confirm": "Supprimer",
"confirmations.delete.message": "Confirmez vous la suppression de ce pouet?",
"confirmations.domain_block.confirm": "Masquer le domaine entier",
"confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier? Dans la plupart des cas, quelques blocages ou silenciations ciblés sont suffisants et préférables.",
"confirmations.mute.confirm": "Silencer",
"confirmations.mute.message": "Confirmez vous la silenciation {name}?",
"confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables.",
"confirmations.mute.confirm": "Masquer",
"confirmations.mute.message": "Confirmez vous le masquage de {name}?",
"emoji_button.activity": "Activités",
"emoji_button.flags": "Drapeaux",
"emoji_button.food": "Boire et manger",
@@ -96,7 +96,7 @@
"navigation_bar.follow_requests": "Demandes de suivi",
"navigation_bar.info": "Plus dinformations",
"navigation_bar.logout": "Déconnexion",
"navigation_bar.mutes": "Comptes silencés",
"navigation_bar.mutes": "Comptes masqués",
"navigation_bar.preferences": "Préférences",
"navigation_bar.public_timeline": "Fil public global",
"notification.favourite": "{name} a ajouté à ses favoris:",
@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favoris:",
"notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s:",
"notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Partages:",
"notifications.column_settings.show": "Afficher dans la colonne",
"notifications.column_settings.sound": "Émettre un son",
@@ -147,6 +149,7 @@
"report.target": "Signalement",
"search.placeholder": "Rechercher",
"search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "Cette publication ne peut être boostée",
"status.delete": "Effacer",
"status.favourite": "Ajouter aux favoris",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "מחובבים:",
"notifications.column_settings.follow": "עוקבים חדשים:",
"notifications.column_settings.mention": "פניות:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "הדהודים:",
"notifications.column_settings.show": "הצגה בטור",
"notifications.column_settings.sound": "שמע מופעל",
@@ -147,6 +149,7 @@
"report.target": "דיווח",
"search.placeholder": "חיפוש",
"search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "לא ניתן להדהד הודעה זו",
"status.delete": "מחיקה",
"status.favourite": "חיבוב",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favoriti:",
"notifications.column_settings.follow": "Novi sljedbenici:",
"notifications.column_settings.mention": "Spominjanja:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Prikaži u stupcu",
"notifications.column_settings.sound": "Sviraj zvuk",
@@ -147,6 +149,7 @@
"report.target": "Prijavljivanje",
"search.placeholder": "Traži",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "Ovaj post ne može biti podignut",
"status.delete": "Obriši",
"status.favourite": "Označi omiljenim",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favourites:",
"notifications.column_settings.follow": "New followers:",
"notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Show in column",
"notifications.column_settings.sound": "Play sound",
@@ -147,6 +149,7 @@
"report.target": "Reporting",
"search.placeholder": "Keresés",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Törlés",
"status.favourite": "Kedvenc",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favorit:",
"notifications.column_settings.follow": "Pengikut baru:",
"notifications.column_settings.mention": "Balasan:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Boost:",
"notifications.column_settings.show": "Tampilkan dalam kolom",
"notifications.column_settings.sound": "Mainkan suara",
@@ -147,6 +149,7 @@
"report.target": "Melaporkan",
"search.placeholder": "Pencarian",
"search_results.total": "{count} {count, plural, one {hasil} other {hasil}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Hapus",
"status.favourite": "Difavoritkan",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favorati:",
"notifications.column_settings.follow": "Nova sequanti:",
"notifications.column_settings.mention": "Mencioni:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Repeti:",
"notifications.column_settings.show": "Montrar en kolumno",
"notifications.column_settings.sound": "Plear sono",
@@ -147,6 +149,7 @@
"report.target": "Denuncante",
"search.placeholder": "Serchez",
"search_results.total": "{count, number} {count, plural, one {rezulto} other {rezulti}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Efacar",
"status.favourite": "Favorizar",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Apprezzati:",
"notifications.column_settings.follow": "Nuovi seguaci:",
"notifications.column_settings.mention": "Menzioni:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Post condivisi:",
"notifications.column_settings.show": "Mostra in colonna",
"notifications.column_settings.sound": "Riproduci suono",
@@ -147,6 +149,7 @@
"report.target": "Invio la segnalazione",
"search.placeholder": "Cerca",
"search_results.total": "{count} {count, plural, one {risultato} other {risultati}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Elimina",
"status.favourite": "Apprezzato",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "お気に入り",
"notifications.column_settings.follow": "新しいフォロワー",
"notifications.column_settings.mention": "返信",
"notifications.column_settings.push": "プッシュ通知",
"notifications.column_settings.push_meta": "このデバイス",
"notifications.column_settings.reblog": "ブースト",
"notifications.column_settings.show": "カラムに表示",
"notifications.column_settings.sound": "通知音を再生",
@@ -147,6 +149,7 @@
"report.target": "問題のユーザー",
"search.placeholder": "検索",
"search_results.total": "{count, number}件の結果",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "この投稿はブーストできません",
"status.delete": "削除",
"status.favourite": "お気に入り",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "즐겨찾기",
"notifications.column_settings.follow": "새 팔로워",
"notifications.column_settings.mention": "답글",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "부스트",
"notifications.column_settings.show": "컬럼에 표시",
"notifications.column_settings.sound": "효과음 재생",
@@ -147,6 +149,7 @@
"report.target": "문제가 된 사용자",
"search.placeholder": "검색",
"search_results.total": "{count, number}건의 결과",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
"status.delete": "삭제",
"status.favourite": "즐겨찾기",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favorieten:",
"notifications.column_settings.follow": "Nieuwe volgers:",
"notifications.column_settings.mention": "Vermeldingen:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "In kolom tonen",
"notifications.column_settings.sound": "Geluid afspelen",
@@ -147,6 +149,7 @@
"report.target": "Rapporteren van",
"search.placeholder": "Zoeken",
"search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "Deze toot kan niet geboost worden",
"status.delete": "Verwijderen",
"status.favourite": "Favoriet",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Likt:",
"notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.mention": "Nevnt:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Fremhevet:",
"notifications.column_settings.show": "Vis i kolonne",
"notifications.column_settings.sound": "Spill lyd",
@@ -147,6 +149,7 @@
"report.target": "Rapporterer",
"search.placeholder": "Søk",
"search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "Denne posten kan ikke fremheves",
"status.delete": "Slett",
"status.favourite": "Lik",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favorits :",
"notifications.column_settings.follow": "Nòus seguidors :",
"notifications.column_settings.mention": "Mencions :",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Partatges :",
"notifications.column_settings.show": "Mostrar dins la colomna",
"notifications.column_settings.sound": "Emetre un son",
@@ -147,6 +149,7 @@
"report.target": "Senhalar {target}",
"search.placeholder": "Recercar",
"search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat",
"status.delete": "Escafar",
"status.favourite": "Apondre als favorits",

View File

@@ -3,10 +3,10 @@
"account.block_domain": "Blokuj wszystko z {domain}",
"account.disclaimer": "Ten użytkownik pochodzi z innej instancji. Ta liczba może być większa.",
"account.edit_profile": "Edytuj profil",
"account.follow": "Obserwuj",
"account.followers": "Obserwujący",
"account.follows": "Obserwacje",
"account.follows_you": "Obserwuje cię",
"account.follow": "Śledź",
"account.followers": "Śledzący",
"account.follows": "Śledzeni",
"account.follows_you": "Śledzi Cię",
"account.media": "Media",
"account.mention": "Wspomnij o @{name}",
"account.mute": "Wycisz @{name}",
@@ -15,7 +15,7 @@
"account.requested": "Oczekująca prośba",
"account.unblock": "Odblokuj @{name}",
"account.unblock_domain": "Odblokuj domenę {domain}",
"account.unfollow": "Przestań obserwować",
"account.unfollow": "Przestań śledzić",
"account.unmute": "Cofnij wyciszenie @{name}",
"boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
"bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.",
@@ -27,7 +27,7 @@
"column.blocks": "Zablokowani użytkownicy",
"column.community": "Lokalna oś czasu",
"column.favourites": "Ulubione",
"column.follow_requests": "Prośby o obserwację",
"column.follow_requests": "Prośby o śledzenie",
"column.home": "Strona główna",
"column.mutes": "Wyciszeni użytkownicy",
"column.notifications": "Powiadomienia",
@@ -37,9 +37,9 @@
"column_header.unpin": "Cofnij przypięcie",
"column_subheading.navigation": "Nawigacja",
"column_subheading.settings": "Ustawienia",
"compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto cobserwuje, może wyświetlać twoje posty przeznaczone tylko dla obserwujących.",
"compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cśledzi, może wyświetlać Twoje posty przeznaczone tylko dla śledzących.",
"compose_form.lock_disclaimer.lock": "zablokowane",
"compose_form.placeholder": "Co ci chodzi po głowie?",
"compose_form.placeholder": "Co Ci chodzi po głowie?",
"compose_form.privacy_disclaimer": "Twój post zostanie dostarczony do użytkowników z {domains}. Czy ufasz {domainsCount, plural, one {temu serwerowi} other {tym serwerom}}? Prywatność postów obowiązuje tylko na instancjach Mastodona. Jeżeli {domains} {domainsCount, plural, one {nie jest instancją Mastodona} other {nie są instancjami Mastodona}}, post może być widoczny dla niewłaściwych osób.",
"compose_form.publish": "Wyślij",
"compose_form.publish_loud": "{publish}!",
@@ -67,7 +67,7 @@
"emoji_button.travel": "Podróże i miejsca",
"empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby odbić piłeczkę!",
"empty_column.hashtag": "Nie ma postów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
"empty_column.home": "Nie obserwujesz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć ciekawych ludzi.",
"empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
"empty_column.home.inactivity": "Strumień jest pusty. Jeżeli nie było Cię tu ostatnio, zostanie on wypełniony wkrótce.",
"empty_column.home.public_timeline": "publiczna oś czasu",
"empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.",
@@ -93,32 +93,34 @@
"navigation_bar.community_timeline": "Lokalna oś czasu",
"navigation_bar.edit_profile": "Edytuj profil",
"navigation_bar.favourites": "Ulubione",
"navigation_bar.follow_requests": "Prośby o obserwację",
"navigation_bar.follow_requests": "Prośby o śledzenie",
"navigation_bar.info": "Szczegółowe informacje",
"navigation_bar.logout": "Wyloguj",
"navigation_bar.mutes": "Wyciszeni użytkownicy",
"navigation_bar.preferences": "Preferencje",
"navigation_bar.public_timeline": "Oś czasu federacji",
"notification.favourite": "{name} dodał twój status do ulubionych",
"notification.follow": "{name} zaczął cobserwować",
"notification.favourite": "{name} dodał Twój status do ulubionych",
"notification.follow": "{name} zaczął Cśledzić",
"notification.mention": "{name} wspomniał o tobie",
"notification.reblog": "{name} podbił twój status",
"notification.reblog": "{name} podbił Twój status",
"notifications.clear": "Wyczyść powiadomienia",
"notifications.clear_confirmation": "Czy na pewno chcesz bezpowrotnie usunąć wszystkie powiadomienia?",
"notifications.column_settings.alert": "Powiadomienia na pulpicie",
"notifications.column_settings.favourite": "Ulubione:",
"notifications.column_settings.follow": "Nowi obserwujący:",
"notifications.column_settings.follow": "Nowi śledzący:",
"notifications.column_settings.mention": "Wspomniali:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Podbili:",
"notifications.column_settings.show": "Pokaż w kolumnie",
"notifications.column_settings.sound": "Odtwarzaj dźwięk",
"onboarding.done": "Gotowe",
"onboarding.next": "Dalej",
"onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Federalna oś czasu wyświetla publiczne wpisy obserwowanych przez członków {domain}. Są to publiczne osie czasu najlepszy sposób na poznanie nowych osób.",
"onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Federalna oś czasu wyświetla publiczne wpisy śledzonych przez członków {domain}. Są to publiczne osie czasu najlepszy sposób na poznanie nowych osób.",
"onboarding.page_four.home": "Główna oś czasu wyświetla publiczne wpisy.",
"onboarding.page_four.notifications": "Kolumna powiadomień wyświetla, gdy ktoś dokonuje interakcji z tobą.",
"onboarding.page_one.federation": "Mastodon jest siecią niezależnych serwerów połączonych w jeden portal społecznościowy. Nazywamy te serwery instancjami.",
"onboarding.page_one.handle": "Jesteś na domenie {domain}, więc twój pełny adres to {handle}",
"onboarding.page_one.handle": "Jesteś na domenie {domain}, więc Twój pełny adres to {handle}",
"onboarding.page_one.welcome": "Witamy w Mastodon!",
"onboarding.page_six.admin": "Administratorem tej instancji jest {admin}.",
"onboarding.page_six.almost_done": "Prawie gotowe...",
@@ -135,8 +137,8 @@
"privacy.change": "Dostosuj widoczność postów",
"privacy.direct.long": "Widoczne tylko dla oznaczonych",
"privacy.direct.short": "Bezpośrednio",
"privacy.private.long": "Widoczne tylko dla obserwujących",
"privacy.private.short": "Tylko obserwujący",
"privacy.private.long": "Widoczne tylko dla śledzących",
"privacy.private.short": "Tylko śledzący",
"privacy.public.long": "Widoczne na publicznych osiach czasu",
"privacy.public.short": "Publiczne",
"privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu",
@@ -147,6 +149,7 @@
"report.target": "Zgłaszanie {target}",
"search.placeholder": "Szukaj",
"search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "Ten post nie może zostać podbity",
"status.delete": "Usuń",
"status.favourite": "Ulubione",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.follow": "Novos seguidores:",
"notifications.column_settings.mention": "Menções:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Partilhas:",
"notifications.column_settings.show": "Mostrar nas colunas",
"notifications.column_settings.sound": "Reproduzir som",
@@ -147,6 +149,7 @@
"report.target": "Denunciar",
"search.placeholder": "Pesquisar",
"search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Eliminar",
"status.favourite": "Adicionar aos favoritos",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.follow": "Novos seguidores:",
"notifications.column_settings.mention": "Menções:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Partilhas:",
"notifications.column_settings.show": "Mostrar nas colunas",
"notifications.column_settings.sound": "Reproduzir som",
@@ -147,6 +149,7 @@
"report.target": "Denunciar",
"search.placeholder": "Pesquisar",
"search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Eliminar",
"status.favourite": "Adicionar aos favoritos",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Нравится:",
"notifications.column_settings.follow": "Новые подписчики:",
"notifications.column_settings.mention": "Упоминания:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Продвижения:",
"notifications.column_settings.show": "Показывать в колонке",
"notifications.column_settings.sound": "Проигрывать звук",
@@ -147,6 +149,7 @@
"report.target": "Жалуемся на",
"search.placeholder": "Поиск",
"search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "Этот статус не может быть продвинут",
"status.delete": "Удалить",
"status.favourite": "Нравится",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favourites:",
"notifications.column_settings.follow": "New followers:",
"notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Show in column",
"notifications.column_settings.sound": "Play sound",
@@ -147,6 +149,7 @@
"report.target": "Reporting",
"search.placeholder": "Search",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Delete",
"status.favourite": "Favourite",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Favoriler:",
"notifications.column_settings.follow": "Yeni takipçiler:",
"notifications.column_settings.mention": "Bahsedilenler:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Boostlar:",
"notifications.column_settings.show": "Bildirimlerde göster",
"notifications.column_settings.sound": "Ses çal",
@@ -147,6 +149,7 @@
"report.target": "Raporlama",
"search.placeholder": "Ara",
"search_results.total": "{count, number} {count, plural, one {sonuç} other {sonuçlar}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "Bu gönderi boost edilemez",
"status.delete": "Sil",
"status.favourite": "Favorilere ekle",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "Вподобане:",
"notifications.column_settings.follow": "Нові підписники:",
"notifications.column_settings.mention": "Сповіщення:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Передмухи:",
"notifications.column_settings.show": "Показати в колонці",
"notifications.column_settings.sound": "Відтворювати звук",
@@ -147,6 +149,7 @@
"report.target": "Скаржимося на",
"search.placeholder": "Пошук",
"search_results.total": "{count, number} {count, plural, one {результат} few {результати} many {результатів} other {результатів}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "Цей допис не може бути передмухнутий",
"status.delete": "Видалити",
"status.favourite": "Подобається",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "你的嘟文被赞:",
"notifications.column_settings.follow": "关注你:",
"notifications.column_settings.mention": "提及你:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "你的嘟文被转嘟:",
"notifications.column_settings.show": "在通知栏显示",
"notifications.column_settings.sound": "播放音效",
@@ -147,6 +149,7 @@
"report.target": "Reporting",
"search.placeholder": "搜索",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "没法转嘟这条嘟文啦……",
"status.delete": "删除",
"status.favourite": "赞",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "喜歡你的文章:",
"notifications.column_settings.follow": "關注你:",
"notifications.column_settings.mention": "提及你:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "轉推你的文章:",
"notifications.column_settings.show": "在通知欄顯示",
"notifications.column_settings.sound": "播放音效",
@@ -147,6 +149,7 @@
"report.target": "舉報",
"search.placeholder": "搜尋",
"search_results.total": "{count, number} 項結果",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "這篇文章無法被轉推",
"status.delete": "刪除",
"status.favourite": "喜歡",

View File

@@ -109,6 +109,8 @@
"notifications.column_settings.favourite": "最愛:",
"notifications.column_settings.follow": "新的關注者:",
"notifications.column_settings.mention": "提到:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "轉推:",
"notifications.column_settings.show": "顯示在欄位中",
"notifications.column_settings.sound": "播放音效",
@@ -147,6 +149,7 @@
"report.target": "通報中",
"search.placeholder": "搜尋",
"search_results.total": "{count, number} 項結果",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "此貼文無法轉推",
"status.delete": "刪除",
"status.favourite": "喜愛",

View File

@@ -1,12 +1,6 @@
const perf = require('./performance');
import ready from './ready';
function onDomContentLoaded(callback) {
if (document.readyState !== 'loading') {
callback();
} else {
document.addEventListener('DOMContentLoaded', callback);
}
}
const perf = require('./performance');
function main() {
perf.start('main()');
@@ -24,11 +18,19 @@ function main() {
}
}
onDomContentLoaded(() => {
ready(() => {
const mountNode = document.getElementById('mastodon');
const props = JSON.parse(mountNode.getAttribute('data-props'));
ReactDOM.render(<Mastodon {...props} />, mountNode);
if (process.env.NODE_ENV === 'production') {
// avoid offline in dev mode because it's harder to debug
const OfflinePluginRuntime = require('offline-plugin/runtime');
const WebPushSubscription = require('./web_push_subscription');
OfflinePluginRuntime.install();
WebPushSubscription.register();
}
perf.stop('main()');
// remember the initial URL

View File

@@ -0,0 +1,7 @@
export default function ready(loaded) {
if (['interactive', 'complete'].includes(document.readyState)) {
loaded();
} else {
document.addEventListener('DOMContentLoaded', loaded);
}
}

View File

@@ -126,7 +126,7 @@ const insertSuggestion = (state, position, token, completion) => {
};
const insertEmoji = (state, position, emojiData) => {
const emoji = emojiData.shortname;
const emoji = String.fromCodePoint(parseInt(emojiData.unicode, 16));
return state.withMutations(map => {
map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`);

View File

@@ -11,6 +11,7 @@ import statuses from './statuses';
import relationships from './relationships';
import settings from './settings';
import local_settings from '../../glitch/reducers/local_settings';
import push_notifications from './push_notifications';
import status_lists from './status_lists';
import cards from './cards';
import reports from './reports';
@@ -33,7 +34,11 @@ const reducers = {
statuses,
relationships,
settings,
<<<<<<< HEAD
local_settings,
=======
push_notifications,
>>>>>>> upstream
cards,
reports,
contexts,

View File

@@ -0,0 +1,51 @@
import { STORE_HYDRATE } from '../actions/store';
import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from '../actions/push_notifications';
import Immutable from 'immutable';
const initialState = Immutable.Map({
subscription: null,
alerts: new Immutable.Map({
follow: false,
favourite: false,
reblog: false,
mention: false,
}),
isSubscribed: false,
browserSupport: false,
});
export default function push_subscriptions(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE: {
const push_subscription = action.state.get('push_subscription');
if (push_subscription) {
return state
.set('subscription', new Immutable.Map({
id: push_subscription.get('id'),
endpoint: push_subscription.get('endpoint'),
}))
.set('alerts', push_subscription.get('alerts') || initialState.get('alerts'))
.set('isSubscribed', true);
}
return state;
}
case SET_SUBSCRIPTION:
return state
.set('subscription', new Immutable.Map({
id: action.subscription.id,
endpoint: action.subscription.endpoint,
}))
.set('alerts', new Immutable.Map(action.subscription.alerts))
.set('isSubscribed', true);
case SET_BROWSER_SUPPORT:
return state.set('browserSupport', action.value);
case CLEAR_SUBSCRIPTION:
return initialState;
case ALERTS_CHANGE:
return state.setIn(action.key, action.value);
default:
return state;
}
};

View File

@@ -0,0 +1 @@
import './web_push_notifications';

View File

@@ -0,0 +1,86 @@
const handlePush = (event) => {
const options = event.data.json();
options.body = options.data.nsfw || options.data.content;
options.image = options.image || undefined; // Null results in a network request (404)
options.timestamp = options.timestamp && new Date(options.timestamp);
const expandAction = options.data.actions.find(action => action.todo === 'expand');
if (expandAction) {
options.actions = [expandAction];
options.hiddenActions = options.data.actions.filter(action => action !== expandAction);
options.data.hiddenImage = options.image;
options.image = undefined;
} else {
options.actions = options.data.actions;
}
event.waitUntil(self.registration.showNotification(options.title, options));
};
const cloneNotification = (notification) => {
const clone = { };
for(var k in notification) {
clone[k] = notification[k];
}
return clone;
};
const expandNotification = (notification) => {
const nextNotification = cloneNotification(notification);
nextNotification.body = notification.data.content;
nextNotification.image = notification.data.hiddenImage;
nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand');
return self.registration.showNotification(nextNotification.title, nextNotification);
};
const makeRequest = (notification, action) =>
fetch(action.action, {
headers: {
'Authorization': `Bearer ${notification.data.access_token}`,
'Content-Type': 'application/json',
},
method: action.method,
credentials: 'include',
});
const removeActionFromNotification = (notification, action) => {
const actions = notification.actions.filter(act => act.action !== action.action);
const nextNotification = cloneNotification(notification);
nextNotification.actions = actions;
return self.registration.showNotification(nextNotification.title, nextNotification);
};
const handleNotificationClick = (event) => {
const reactToNotificationClick = new Promise((resolve, reject) => {
if (event.action) {
const action = event.notification.data.actions.find(({ action }) => action === event.action);
if (action.todo === 'expand') {
resolve(expandNotification(event.notification));
} else if (action.todo === 'request') {
resolve(makeRequest(event.notification, action)
.then(() => removeActionFromNotification(event.notification, action)));
} else {
reject(`Unknown action: ${action.todo}`);
}
} else {
event.notification.close();
resolve(self.clients.openWindow(event.notification.data.url));
}
});
event.waitUntil(reactToNotificationClick);
};
self.addEventListener('push', handlePush);
self.addEventListener('notificationclick', handleNotificationClick);

View File

@@ -0,0 +1,109 @@
import axios from 'axios';
import { store } from './containers/mastodon';
import { setBrowserSupport, setSubscription, clearSubscription } from './actions/push_notifications';
// Taken from https://www.npmjs.com/package/web-push
const urlBase64ToUint8Array = (base64String) => {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
const getRegistration = () => navigator.serviceWorker.ready;
const getPushSubscription = (registration) =>
registration.pushManager.getSubscription()
.then(subscription => ({ registration, subscription }));
const subscribe = (registration) =>
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()),
});
const unsubscribe = ({ registration, subscription }) =>
subscription ? subscription.unsubscribe().then(() => registration) : registration;
const sendSubscriptionToBackend = (subscription) =>
axios.post('/api/web/push_subscriptions', {
data: subscription,
}).then(response => response.data);
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
export function register () {
store.dispatch(setBrowserSupport(supportsPushNotifications));
if (supportsPushNotifications) {
if (!getApplicationServerKey()) {
// eslint-disable-next-line no-console
console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
return;
}
getRegistration()
.then(getPushSubscription)
.then(({ registration, subscription }) => {
if (subscription !== null) {
// We have a subscription, check if it is still valid
const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString();
const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey()).toString();
const serverEndpoint = store.getState().getIn(['push_notifications', 'subscription', 'endpoint']);
// If the VAPID public key did not change and the endpoint corresponds
// to the endpoint saved in the backend, the subscription is valid
if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) {
return subscription;
} else {
// Something went wrong, try to subscribe again
return unsubscribe({ registration, subscription }).then(subscribe).then(sendSubscriptionToBackend);
}
}
// No subscription, try to subscribe
return subscribe(registration).then(sendSubscriptionToBackend);
})
.then(subscription => {
// If we got a PushSubscription (and not a subscription object from the backend)
// it means that the backend subscription is valid (and was set during hydration)
if (!(subscription instanceof PushSubscription)) {
store.dispatch(setSubscription(subscription));
}
})
.catch(error => {
if (error.code === 20 && error.name === 'AbortError') {
// eslint-disable-next-line no-console
console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.');
} else if (error.code === 5 && error.name === 'InvalidCharacterError') {
// eslint-disable-next-line no-console
console.error('The VAPID public key seems to be invalid:', getApplicationServerKey());
}
// Clear alerts and hide UI settings
store.dispatch(clearSubscription());
try {
getRegistration()
.then(getPushSubscription)
.then(unsubscribe);
} catch (e) {
}
});
} else {
// eslint-disable-next-line no-console
console.warn('Your browser does not support Web Push Notifications.');
}
}

View File

@@ -0,0 +1,24 @@
import TimelineContainer from '../mastodon/containers/timeline_container';
import React from 'react';
import ReactDOM from 'react-dom';
import loadPolyfills from '../mastodon/load_polyfills';
import ready from '../mastodon/ready';
require.context('../images/', true);
function loaded() {
const mountNode = document.getElementById('mastodon-timeline');
if (mountNode !== null) {
const props = JSON.parse(mountNode.getAttribute('data-props'));
ReactDOM.render(<TimelineContainer {...props} />, mountNode);
}
}
function main() {
ready(loaded);
}
loadPolyfills().then(main).catch(error => {
console.error(error);
});

View File

@@ -5,9 +5,7 @@ import emojify from '../mastodon/emoji';
import { getLocale } from '../mastodon/locales';
import loadPolyfills from '../mastodon/load_polyfills';
import { processBio } from '../glitch/util/bio_metadata';
import TimelineContainer from '../mastodon/containers/timeline_container';
import React from 'react';
import ReactDOM from 'react-dom';
import ready from '../mastodon/ready';
require.context('../images/', true);
@@ -40,21 +38,10 @@ function loaded() {
const datetime = new Date(content.getAttribute('datetime'));
content.textContent = relativeFormat.format(datetime);;
});
const mountNode = document.getElementById('mastodon-timeline');
if (mountNode !== null) {
const props = JSON.parse(mountNode.getAttribute('data-props'));
ReactDOM.render(<TimelineContainer {...props} />, mountNode);
}
}
function main() {
if (['interactive', 'complete'].includes(document.readyState)) {
loaded();
} else {
document.addEventListener('DOMContentLoaded', loaded);
}
ready(loaded);
delegate(document, '.video-player video', 'click', ({ target }) => {
if (target.paused) {

View File

@@ -1554,6 +1554,9 @@
}
.react-swipeable-view-container > * {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
@@ -2007,6 +2010,7 @@
width: 100%;
margin: 0;
color: $ui-base-color;
background: $simple-background-color;
padding: 10px;
font-family: inherit;
font-size: 14px;
@@ -2029,7 +2033,6 @@
.autosuggest-textarea__textarea {
min-height: 100px;
background: $simple-background-color;
border-radius: 4px 4px 0 0;
padding-bottom: 0;
padding-right: 10px + 22px;
@@ -2620,7 +2623,8 @@ button.icon-button.active i.fa-retweet {
line-height: 24px;
}
.setting-toggle__label {
.setting-toggle__label,
.setting-meta__label {
color: $ui-primary-color;
display: inline-block;
margin-bottom: 14px;
@@ -2628,6 +2632,11 @@ button.icon-button.active i.fa-retweet {
vertical-align: middle;
}
.setting-meta__label {
color: $ui-primary-color;
float: right;
}
.empty-column-indicator,
.error-column {
color: lighten($ui-base-color, 20%);
@@ -2968,6 +2977,7 @@ button.icon-button.active i.fa-retweet {
margin-left: 2px;
width: 24px;
outline: 0;
cursor: pointer;
&:active,
&:focus {
@@ -3297,6 +3307,7 @@ button.icon-button.active i.fa-retweet {
max-height: 80vh;
position: relative;
.extended-video-player,
img,
canvas,
video {
@@ -3306,6 +3317,13 @@ button.icon-button.active i.fa-retweet {
height: auto;
}
.extended-video-player,
video {
display: flex;
width: 80vw;
height: 80vh;
}
img,
canvas {
display: block;

View File

@@ -45,6 +45,10 @@ body.rtl {
margin-right: 8px;
}
.setting-meta__label {
float: left;
}
.status__avatar {
left: auto;
right: 10px;