Merge upstream (#81)
This commit is contained in:
@@ -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 + ' 👁️';
|
||||
|
52
app/javascript/mastodon/actions/push_notifications.js
Normal file
52
app/javascript/mastodon/actions/push_notifications.js
Normal 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,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
@@ -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}
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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}>
|
||||
|
@@ -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
|
||||
}
|
||||
|
11
app/javascript/mastodon/emojione_light.js
Normal file
11
app/javascript/mastodon/emojione_light.js
Normal 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), { });
|
@@ -1,2 +1,5 @@
|
||||
import 'intersection-observer';
|
||||
import 'requestidlecallback';
|
||||
import objectFitImages from 'object-fit-images';
|
||||
|
||||
objectFitImages();
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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 &&
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
||||
|
@@ -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 () {
|
||||
|
@@ -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>;
|
||||
|
@@ -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'>
|
||||
|
@@ -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>
|
||||
))}
|
||||
|
@@ -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([
|
||||
|
@@ -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": "أضف إلى المفضلة",
|
||||
|
@@ -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": "Предпочитани",
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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": [
|
||||
{
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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": "پسندیدن",
|
||||
|
@@ -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ää",
|
||||
|
@@ -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 d’informations",
|
||||
"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",
|
||||
|
@@ -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": "חיבוב",
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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": "お気に入り",
|
||||
|
@@ -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": "즐겨찾기",
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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 cię obserwuje, może wyświetlać twoje posty przeznaczone tylko dla obserwujących.",
|
||||
"compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię ś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ął cię obserwować",
|
||||
"notification.favourite": "{name} dodał Twój status do ulubionych",
|
||||
"notification.follow": "{name} zaczął Cię ś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",
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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": "Нравится",
|
||||
|
@@ -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",
|
||||
|
@@ -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": "Boost’lar:",
|
||||
"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",
|
||||
|
@@ -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": "Подобається",
|
||||
|
@@ -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": "赞",
|
||||
|
@@ -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": "喜歡",
|
||||
|
@@ -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": "喜愛",
|
||||
|
@@ -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
|
||||
|
7
app/javascript/mastodon/ready.js
Normal file
7
app/javascript/mastodon/ready.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function ready(loaded) {
|
||||
if (['interactive', 'complete'].includes(document.readyState)) {
|
||||
loaded();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', loaded);
|
||||
}
|
||||
}
|
@@ -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)}`);
|
||||
|
@@ -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,
|
||||
|
51
app/javascript/mastodon/reducers/push_notifications.js
Normal file
51
app/javascript/mastodon/reducers/push_notifications.js
Normal 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;
|
||||
}
|
||||
};
|
1
app/javascript/mastodon/service_worker/entry.js
Normal file
1
app/javascript/mastodon/service_worker/entry.js
Normal file
@@ -0,0 +1 @@
|
||||
import './web_push_notifications';
|
@@ -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);
|
109
app/javascript/mastodon/web_push_subscription.js
Normal file
109
app/javascript/mastodon/web_push_subscription.js
Normal 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.');
|
||||
}
|
||||
}
|
24
app/javascript/packs/about.js
Normal file
24
app/javascript/packs/about.js
Normal 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);
|
||||
});
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
|
@@ -45,6 +45,10 @@ body.rtl {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.setting-meta__label {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.status__avatar {
|
||||
left: auto;
|
||||
right: 10px;
|
||||
|
Reference in New Issue
Block a user