Merge branch 'develop' of https://code.gab.com/gab/social/gab-social into develop

This commit is contained in:
Rob Colbert 2019-07-14 03:03:37 -04:00
commit 37be893d12
115 changed files with 514 additions and 539 deletions

View File

@ -1,16 +0,0 @@
import { saveSettings } from './settings';
export const COLUMN_PARAMS_CHANGE = 'COLUMN_PARAMS_CHANGE';
export function changeColumnParams(uuid, path, value) {
return dispatch => {
dispatch({
type: COLUMN_PARAMS_CHANGE,
uuid,
path,
value,
});
dispatch(saveSettings());
};
}

View File

@ -6,7 +6,7 @@ import { tagHistory } from '../settings';
import { useEmoji } from './emojis';
import resizeImage from '../utils/resize_image';
import { importFetchedAccounts } from './importer';
import { updateTimeline } from './timelines';
import { updateTimeline, dequeueTimeline } from './timelines';
import { showAlertForError } from './alerts';
import { showAlert } from './alerts';
import { defineMessages } from 'react-intl';
@ -168,6 +168,10 @@ export function submitCompose(routerHistory) {
const timeline = getState().getIn(['timelines', timelineId]);
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
let dequeueArgs = {};
if (timelineId === 'community') dequeueArgs.onlyMedia = getState().getIn(['settings', 'community', 'other', 'onlyMedia']),
dispatch(dequeueTimeline(timelineId, null, dequeueArgs));
dispatch(updateTimeline(timelineId, { ...response.data }));
}
};

View File

@ -16,6 +16,8 @@ import { me } from 'gabsocial/initial_state';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
export const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE';
export const NOTIFICATIONS_DEQUEUE = 'NOTIFICATIONS_DEQUEUE';
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
@ -26,6 +28,8 @@ export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
export const MAX_QUEUED_NOTIFICATIONS = 40;
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
@ -42,18 +46,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => {
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
const filters = getFilters(getState(), { contextType: 'notifications' });
let filtered = false;
if (notification.type === 'mention') {
const regex = regexFromFilters(filters);
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
filtered = regex && regex.test(searchIndex);
}
if (showInColumn) {
dispatch(importFetchedAccount(notification.account));
@ -65,21 +57,33 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
dispatch({
type: NOTIFICATIONS_UPDATE,
notification,
meta: (playSound && !filtered) ? { sound: 'ribbit' } : undefined,
});
fetchRelatedRelationships(dispatch, [notification]);
} else if (playSound && !filtered) {
dispatch({
type: NOTIFICATIONS_UPDATE_NOOP,
meta: { sound: 'ribbit' },
});
}
};
};
export function updateNotificationsQueue(notification, intlMessages, intlLocale, curPath) {
return (dispatch, getState) => {
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
const filters = getFilters(getState(), { contextType: 'notifications' });
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
let filtered = false;
const isOnNotificationsPage = curPath === '/notifications';
if (notification.type === 'mention') {
const regex = regexFromFilters(filters);
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
filtered = regex && regex.test(searchIndex);
}
// Desktop notifications
if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
@ -88,7 +92,49 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
notify.close();
});
}
};
if (playSound && !filtered) {
dispatch({
type: NOTIFICATIONS_UPDATE_NOOP,
meta: { sound: 'ribbit' },
});
}
if (isOnNotificationsPage) {
dispatch({
type: NOTIFICATIONS_UPDATE_QUEUE,
notification,
intlMessages,
intlLocale,
});
}
else {
dispatch(updateNotifications(notification, intlMessages, intlLocale));
}
}
};
export function dequeueNotifications() {
return (dispatch, getState) => {
const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableList());
const totalQueuedNotificationsCount = getState().getIn(['notifications', 'totalQueuedNotificationsCount'], 0);
if (totalQueuedNotificationsCount == 0) {
return;
}
else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
queuedNotifications.forEach(block => {
dispatch(updateNotifications(block.notification, block.intlMessages, block.intlLocale));
});
}
else {
dispatch(expandNotifications());
}
dispatch({
type: NOTIFICATIONS_DEQUEUE,
});
}
};
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
@ -169,7 +215,7 @@ export function expandNotificationsFail(error, isLoadingMore) {
export function clearNotifications() {
return (dispatch, getState) => {
if (!me) return;
dispatch({
type: NOTIFICATIONS_CLEAR,
});

View File

@ -1,12 +1,12 @@
import { connectStream } from '../stream';
import {
updateTimeline,
deleteFromTimelines,
expandHomeTimeline,
connectTimeline,
disconnectTimeline,
updateTimelineQueue,
} from './timelines';
import { updateNotifications, expandNotifications } from './notifications';
import { updateNotificationsQueue, expandNotifications } from './notifications';
import { updateConversations } from './conversations';
import { fetchFilters } from './filters';
import { getLocale } from '../locales';
@ -30,13 +30,13 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
onReceive (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), accept));
dispatch(updateTimelineQueue(timelineId, JSON.parse(data.payload), accept));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
dispatch(updateNotificationsQueue(JSON.parse(data.payload), messages, locale, window.location.pathname));
break;
case 'conversation':
dispatch(updateConversations(JSON.parse(data.payload)));

View File

@ -1,10 +1,12 @@
import { importFetchedStatus, importFetchedStatuses } from './importer';
import api, { getLinks } from '../api';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { Map as ImmutableMap, List as ImmutableList, toJS } from 'immutable';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
export const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE';
export const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE';
export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
@ -13,6 +15,8 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const MAX_QUEUED_ITEMS = 40;
export function updateTimeline(timeline, status, accept) {
return dispatch => {
if (typeof accept === 'function' && !accept(status)) {
@ -29,6 +33,64 @@ export function updateTimeline(timeline, status, accept) {
};
};
export function updateTimelineQueue(timeline, status, accept) {
return dispatch => {
if (typeof accept === 'function' && !accept(status)) {
return;
}
dispatch({
type: TIMELINE_UPDATE_QUEUE,
timeline,
status,
});
}
};
export function dequeueTimeline(timeline, expandFunc, optionalExpandArgs) {
return (dispatch, getState) => {
const queuedItems = getState().getIn(['timelines', timeline, 'queuedItems'], ImmutableList());
const totalQueuedItemsCount = getState().getIn(['timelines', timeline, 'totalQueuedItemsCount'], 0);
let shouldDispatchDequeue = true;
if (totalQueuedItemsCount == 0) {
return;
}
else if (totalQueuedItemsCount > 0 && totalQueuedItemsCount <= MAX_QUEUED_ITEMS) {
queuedItems.forEach(status => {
dispatch(updateTimeline(timeline, status.toJS(), null));
});
}
else {
if (typeof expandFunc === 'function') {
dispatch(clearTimeline(timeline));
expandFunc();
}
else {
if (timeline === 'home') {
dispatch(clearTimeline(timeline));
dispatch(expandHomeTimeline(optionalExpandArgs));
}
else if (timeline === 'community') {
dispatch(clearTimeline(timeline));
dispatch(expandCommunityTimeline(optionalExpandArgs));
}
else {
shouldDispatchDequeue = false;
}
}
}
if (!shouldDispatchDequeue) return;
dispatch({
type: TIMELINE_DEQUEUE,
timeline,
});
}
};
export function deleteFromTimelines(id) {
return (dispatch, getState) => {
const accountId = getState().getIn(['statuses', id, 'account']);

View File

@ -1,5 +1,4 @@
import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll-4';
import PropTypes from 'prop-types';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import LoadMore from './load_more';

View File

@ -7,6 +7,7 @@ import StatusContainer from '../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import LoadGap from './load_gap';
import ScrollableList from './scrollable_list';
import TimelineQueueButtonHeader from './timeline_queue_button_header';
export default class StatusList extends ImmutablePureComponent {
@ -22,6 +23,12 @@ export default class StatusList extends ImmutablePureComponent {
emptyMessage: PropTypes.node,
alwaysPrepend: PropTypes.bool,
timelineId: PropTypes.string,
queuedItemSize: PropTypes.number,
onDequeueTimeline: PropTypes.func,
};
componentDidMount() {
this.handleDequeueTimeline();
};
getFeaturedStatusCount = () => {
@ -64,13 +71,18 @@ export default class StatusList extends ImmutablePureComponent {
}
}
handleDequeueTimeline = () => {
const { onDequeueTimeline, timelineId } = this.props;
if (!onDequeueTimeline || !timelineId) return;
onDequeueTimeline(timelineId);
}
setRef = c => {
this.node = c;
}
render () {
const { statusIds, featuredStatusIds, onLoadMore, timelineId, ...other } = this.props;
const { isLoading, isPartial } = other;
const { statusIds, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, ...other } = this.props;
if (isPartial) {
return (
@ -119,11 +131,12 @@ export default class StatusList extends ImmutablePureComponent {
)).concat(scrollableContent);
}
return (
<ScrollableList {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
return [
<TimelineQueueButtonHeader key='timeline-queue-button-header' onClick={this.handleDequeueTimeline} count={totalQueuedItemsCount} itemType='gab' />,
<ScrollableList key='scrollable-list' {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
{scrollableContent}
</ScrollableList>
);
];
}
}

View File

@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { shortNumberFormat } from '../utils/numbers';
export default class TimelineQueueButtonHeader extends React.PureComponent {
static propTypes = {
onClick: PropTypes.func.isRequired,
count: PropTypes.number,
itemType: PropTypes.string,
};
static defaultProps = {
count: 0,
itemType: 'item',
};
render () {
const { count, itemType, onClick } = this.props;
if (count <= 0) return null;
return (
<div className='timeline-queue-header'>
<a className='timeline-queue-header__btn' onClick={onClick}>
<FormattedMessage
id='timeline_queue.label'
defaultMessage='Click to see {count} new {type}'
values={{
count: shortNumberFormat(count),
type: count == 1 ? itemType : `${itemType}s`,
}}
/>
</a>
</div>
);
}
}

View File

@ -24,7 +24,7 @@ const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
mention: { id: 'account.mention', defaultMessage: 'Mention' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
@ -311,7 +311,7 @@ class Header extends ImmutablePureComponent {
{actionBtn}
{account.get('id') !== me &&
<Button className='button button-alternative-2' onClick={this.props.onMention}>
<FormattedMessage id='account.mention' defaultMessage='Mention @{name}' values={{
<FormattedMessage id='account.mention' defaultMessage='Mention' values={{
name: account.get('acct')
}} />
</Button>

View File

@ -212,6 +212,13 @@ class AccountGallery extends ImmutablePureComponent {
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
))}
{
attachments.size == 0 &&
<div className='empty-column-indicator'>
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.'/>
</div>
}
{loadOlder}
</div>

View File

@ -93,7 +93,7 @@ class AccountTimeline extends ImmutablePureComponent {
this.props.dispatch(expandAccountFeaturedTimeline(nextProps.accountId));
}
this.props.dispatch(expandAccountTimeline(nextProps.accountId, { withReplies: nextProps.params.withReplies }));
this.props.dispatch(expandAccountTimeline(nextProps.accountId, { withReplies: nextProps.withReplies }));
}
}

View File

@ -11,7 +11,6 @@ class ColumnSettings extends React.PureComponent {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
};
render () {
@ -21,6 +20,7 @@ class ColumnSettings extends React.PureComponent {
<div>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
<SettingToggle settings={settings} settingPath={['other', 'allFediverse']} onChange={onChange} label={<FormattedMessage id='community.column_settings.all_fediverse' defaultMessage='All Fediverse' />} />
</div>
</div>
);

View File

@ -1,26 +1,15 @@
import { connect } from 'react-redux';
import ColumnSettings from '../components/column_settings';
import { changeSetting } from '../../../actions/settings';
import { changeColumnParams } from '../../../actions/columns';
const mapStateToProps = (state, { columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
const mapStateToProps = state => ({
settings: state.getIn(['settings', 'community']),
});
return {
settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'community']),
};
};
const mapDispatchToProps = (dispatch, { columnId }) => {
const mapDispatchToProps = (dispatch) => {
return {
onChange (key, checked) {
if (columnId) {
dispatch(changeColumnParams(columnId, key, checked));
} else {
dispatch(changeSetting(['community', ...key], checked));
}
dispatch(changeSetting(['community', ...key], checked));
},
};
};

View File

@ -4,23 +4,32 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import { expandCommunityTimeline } from '../../actions/timelines';
import ColumnSettingsContainer from './containers/column_settings_container';
import { connectCommunityStream } from '../../actions/streaming';
import HomeColumnHeader from '../../components/home_column_header';
import {
expandCommunityTimeline,
expandPublicTimeline,
} from '../../actions/timelines';
import {
connectCommunityStream,
connectPublicStream,
} from '../../actions/streaming';
const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' },
title: { id: 'column.community', defaultMessage: 'Community timeline' },
});
const mapStateToProps = (state, { onlyMedia, columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
const mapStateToProps = state => {
const allFediverse = state.getIn(['settings', 'community', 'other', 'allFediverse']);
const onlyMedia = state.getIn(['settings', 'community', 'other', 'onlyMedia']);
const timelineId = allFediverse ? 'public' : 'community';
return {
hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']),
timelineId,
allFediverse,
onlyMedia,
hasUnread: state.getIn(['timelines', `${timelineId}${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
};
};
@ -34,30 +43,45 @@ class CommunityTimeline extends React.PureComponent {
static defaultProps = {
onlyMedia: false,
allFediverse: false,
};
static propTypes = {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
onlyMedia: PropTypes.bool,
allFediverse: PropTypes.bool,
timelineId: PropTypes.string,
};
componentDidMount () {
const { dispatch, onlyMedia } = this.props;
const { dispatch, onlyMedia, allFediverse } = this.props;
dispatch(expandCommunityTimeline({ onlyMedia }));
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
if (allFediverse) {
dispatch(expandPublicTimeline({ onlyMedia }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
}
else {
dispatch(expandCommunityTimeline({ onlyMedia }));
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
}
}
componentDidUpdate (prevProps) {
if (prevProps.onlyMedia !== this.props.onlyMedia) {
const { dispatch, onlyMedia } = this.props;
if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.allFediverse !== this.props.allFediverse) {
const { dispatch, onlyMedia, allFediverse } = this.props;
this.disconnect();
dispatch(expandCommunityTimeline({ onlyMedia }));
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
if (allFediverse) {
dispatch(expandPublicTimeline({ onlyMedia }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
}
else {
dispatch(expandCommunityTimeline({ onlyMedia }));
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
}
}
}
@ -69,27 +93,29 @@ class CommunityTimeline extends React.PureComponent {
}
handleLoadMore = maxId => {
const { dispatch, onlyMedia } = this.props;
const { dispatch, onlyMedia, allFediverse } = this.props;
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
if (allFediverse) {
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
}
else {
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
}
}
render () {
const { intl, hasUnread, columnId, onlyMedia } = this.props;
const { intl, hasUnread, onlyMedia, timelineId, allFediverse } = this.props;
return (
<Column label={intl.formatMessage(messages.title)}>
<HomeColumnHeader
activeItem='all'
active={hasUnread}
>
<HomeColumnHeader activeItem='all' active={hasUnread} >
<ColumnSettingsContainer />
</HomeColumnHeader>
<StatusListContainer
scrollKey={`community_timeline-${columnId}`}
timelineId={`community${onlyMedia ? ':media' : ''}`}
scrollKey={`${timelineId}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The community timeline is empty. Write something publicly to get the ball rolling!' />}
/>
</Column>
);

View File

@ -68,6 +68,7 @@ class ComposeForm extends ImmutablePureComponent {
anyMedia: PropTypes.bool,
shouldCondense: PropTypes.bool,
autoFocus: PropTypes.bool,
isModalOpen: PropTypes.bool,
};
static defaultProps = {
@ -150,6 +151,8 @@ class ComposeForm extends ImmutablePureComponent {
}
componentDidUpdate (prevProps) {
if (!this.autosuggestTextarea) return;
// This statement does several things:
// - If we're beginning a reply, and,
// - Replying to zero or one users, places the cursor at the end of the textbox.
@ -203,7 +206,7 @@ class ComposeForm extends ImmutablePureComponent {
}
render () {
const { intl, onPaste, showSearch, anyMedia, shouldCondense, autoFocus } = this.props;
const { intl, onPaste, showSearch, anyMedia, shouldCondense, autoFocus, isModalOpen } = this.props;
const condensed = shouldCondense && !this.props.text && !this.state.composeFocused;
const disabled = this.props.isSubmitting;
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
@ -252,7 +255,7 @@ class ComposeForm extends ImmutablePureComponent {
</div>
<AutosuggestTextarea
ref={this.setAutosuggestTextarea}
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={disabled}
value={this.props.text}

View File

@ -25,6 +25,7 @@ const mapStateToProps = state => ({
isUploading: state.getIn(['compose', 'is_uploading']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
isModalOpen: state.get('modal').modalType === 'COMPOSE',
});
const mapDispatchToProps = (dispatch) => ({

View File

@ -18,7 +18,6 @@ class DirectTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
};
@ -45,14 +44,14 @@ class DirectTimeline extends React.PureComponent {
}
render () {
const { intl, hasUnread, columnId } = this.props;
const { intl, hasUnread } = this.props;
return (
<Column label={intl.formatMessage(messages.title)}>
<ColumnHeader icon='envelope' active={hasUnread} title={intl.formatMessage(messages.title)} />
<ConversationsListContainer
scrollKey={`direct_timeline-${columnId}`}
scrollKey='direct_timeline'
timelineId='direct'
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}

View File

@ -29,7 +29,6 @@ class Favourites extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
isMyAccount: PropTypes.bool.isRequired,
@ -44,7 +43,7 @@ class Favourites extends ImmutablePureComponent {
}, 300, { leading: true })
render () {
const { intl, statusIds, columnId, hasMore, isLoading, isMyAccount } = this.props;
const { intl, statusIds, hasMore, isLoading, isMyAccount } = this.props;
if (!isMyAccount) {
return (
@ -60,7 +59,7 @@ class Favourites extends ImmutablePureComponent {
<Column>
<StatusList
statusIds={statusIds}
scrollKey={`favourited_statuses-${columnId}`}
scrollKey='favourited_statuses'
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}

View File

@ -30,7 +30,6 @@ class GroupTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
hasUnread: PropTypes.bool,
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
intl: PropTypes.object.isRequired,
@ -59,7 +58,7 @@ class GroupTimeline extends React.PureComponent {
}
render () {
const { hasUnread, columnId, group } = this.props;
const { hasUnread, group } = this.props;
const { id } = this.props.params;
const title = group ? group.get('title') : id;
@ -93,7 +92,7 @@ class GroupTimeline extends React.PureComponent {
<StatusListContainer
prepend={<HeaderContainer groupId={id} />}
alwaysPrepend
scrollKey={`group_timeline-${columnId}`}
scrollKey='group_timeline'
timelineId={`group:${id}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group post new statuses, they will appear here.' />}

View File

@ -1,113 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import AsyncSelect from 'react-select/lib/Async';
const messages = defineMessages({
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
noOptions: { id: 'hashtag.column_settings.select.no_options_message', defaultMessage: 'No suggestions found' },
});
export default @injectIntl
class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
onLoad: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
state = {
open: this.hasTags(),
};
hasTags () {
return ['all', 'any', 'none'].map(mode => this.tags(mode).length > 0).includes(true);
}
tags (mode) {
let tags = this.props.settings.getIn(['tags', mode]) || [];
if (tags.toJSON) {
return tags.toJSON();
} else {
return tags;
}
};
onSelect = mode => value => this.props.onChange(['tags', mode], value);
onToggle = () => {
if (this.state.open && this.hasTags()) {
this.props.onChange('tags', {});
}
this.setState({ open: !this.state.open });
};
noOptionsMessage = () => this.props.intl.formatMessage(messages.noOptions);
modeSelect (mode) {
return (
<div className='column-settings__row'>
<span className='column-settings__section'>
{this.modeLabel(mode)}
</span>
<AsyncSelect
isMulti
autoFocus
value={this.tags(mode)}
onChange={this.onSelect(mode)}
loadOptions={this.props.onLoad}
className='column-select__container'
classNamePrefix='column-select'
name='tags'
placeholder={this.props.intl.formatMessage(messages.placeholder)}
noOptionsMessage={this.noOptionsMessage}
/>
</div>
);
}
modeLabel (mode) {
switch(mode) {
case 'any':
return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
case 'all':
return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
case 'none':
return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
default:
return '';
}
};
render () {
return (
<div>
<div className='column-settings__row'>
<div className='setting-toggle'>
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
<span className='setting-toggle__label'>
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
</span>
</div>
</div>
{this.state.open && (
<div className='column-settings__hashtags'>
{this.modeSelect('any')}
{this.modeSelect('all')}
{this.modeSelect('none')}
</div>
)}
</div>
);
}
}

View File

@ -1,31 +0,0 @@
import { connect } from 'react-redux';
import ColumnSettings from '../components/column_settings';
import { changeColumnParams } from '../../../actions/columns';
import api from '../../../api';
const mapStateToProps = (state, { columnId }) => {
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === columnId);
if (!(columnId && index >= 0)) {
return {};
}
return { settings: columns.get(index).get('params') };
};
const mapDispatchToProps = (dispatch, { columnId }) => ({
onChange (key, value) {
dispatch(changeColumnParams(columnId, key, value));
},
onLoad (value) {
return api().get('/api/v2/search', { params: { q: value } }).then(response => {
return (response.data.hashtags || []).map((tag) => {
return { value: tag.name, label: `#${tag.name}` };
});
});
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);

View File

@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import ColumnSettingsContainer from './containers/column_settings_container';
import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines';
import { FormattedMessage } from 'react-intl';
import { connectHashtagStream } from '../../actions/streaming';
@ -21,7 +20,6 @@ class HashtagTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
columnId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
hasUnread: PropTypes.bool,
};
@ -104,17 +102,14 @@ class HashtagTimeline extends React.PureComponent {
}
render () {
const { hasUnread, columnId } = this.props;
const { hasUnread } = this.props;
const { id } = this.props.params;
return (
<Column label={`#${id}`}>
<ColumnHeader icon='hashtag' active={hasUnread} title={this.title()}>
{columnId && <ColumnSettingsContainer columnId={columnId} />}
</ColumnHeader>
<ColumnHeader icon='hashtag' active={hasUnread} title={this.title()} />
<StatusListContainer
scrollKey={`hashtag_timeline-${columnId}`}
scrollKey='hashtag_timeline'
timelineId={`hashtag:${id}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}

View File

@ -26,7 +26,6 @@ class HomeTimeline extends React.PureComponent {
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
isPartial: PropTypes.bool,
columnId: PropTypes.string,
};
handleLoadMore = maxId => {
@ -67,7 +66,7 @@ class HomeTimeline extends React.PureComponent {
}
render () {
const { intl, hasUnread, columnId } = this.props;
const { intl, hasUnread } = this.props;
return (
<Column label={intl.formatMessage(messages.title)}>
@ -78,7 +77,7 @@ class HomeTimeline extends React.PureComponent {
<ColumnSettingsContainer />
</HomeColumnHeader>
<StatusListContainer
scrollKey={`home_timeline-${columnId}`}
scrollKey='home_timeline'
onLoadMore={this.handleLoadMore}
timelineId='home'
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty. Start following other users to recieve their content here.'/>}

View File

@ -36,7 +36,6 @@ class ListTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
hasUnread: PropTypes.bool,
list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
intl: PropTypes.object.isRequired,
@ -69,7 +68,7 @@ class ListTimeline extends React.PureComponent {
}
handleDeleteClick = () => {
const { dispatch, columnId, intl } = this.props;
const { dispatch, intl } = this.props;
const { id } = this.props.params;
dispatch(openModal('CONFIRM', {
@ -77,18 +76,13 @@ class ListTimeline extends React.PureComponent {
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => {
dispatch(deleteList(id));
if (!!columnId) {
//
} else {
this.context.router.history.push('/lists');
}
this.context.router.history.push('/lists');
},
}));
}
render () {
const { hasUnread, columnId, list } = this.props;
const { hasUnread, list } = this.props;
const { id } = this.props.params;
const title = list ? list.get('title') : id;
@ -126,7 +120,7 @@ class ListTimeline extends React.PureComponent {
</ColumnHeader>
<StatusListContainer
scrollKey={`list_timeline-${columnId}`}
scrollKey='list_timeline'
timelineId={`list:${id}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}

View File

@ -4,7 +4,11 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import { expandNotifications, scrollTopNotifications } from '../../actions/notifications';
import {
expandNotifications,
scrollTopNotifications,
dequeueNotifications,
} from '../../actions/notifications';
import NotificationContainer from './containers/notification_container';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
@ -14,6 +18,7 @@ import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import ScrollableList from '../../components/scrollable_list';
import LoadGap from '../../components/load_gap';
import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header';
const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
@ -40,6 +45,7 @@ const mapStateToProps = state => ({
isLoading: state.getIn(['notifications', 'isLoading'], true),
isUnread: state.getIn(['notifications', 'unread']) > 0,
hasMore: state.getIn(['notifications', 'hasMore']),
totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
});
export default @connect(mapStateToProps)
@ -47,7 +53,6 @@ export default @connect(mapStateToProps)
class Notifications extends React.PureComponent {
static propTypes = {
columnId: PropTypes.string,
notifications: ImmutablePropTypes.list.isRequired,
showFilterBar: PropTypes.bool.isRequired,
dispatch: PropTypes.func.isRequired,
@ -55,6 +60,8 @@ class Notifications extends React.PureComponent {
isLoading: PropTypes.bool,
isUnread: PropTypes.bool,
hasMore: PropTypes.bool,
dequeueNotifications: PropTypes.func,
totalQueuedNotificationsCount: PropTypes.number,
};
componentWillUnmount () {
@ -65,6 +72,7 @@ class Notifications extends React.PureComponent {
}
componentDidMount() {
this.handleDequeueNotifications();
this.props.dispatch(scrollTopNotifications(true));
}
@ -113,8 +121,12 @@ class Notifications extends React.PureComponent {
}
}
handleDequeueNotifications = () => {
this.props.dispatch(dequeueNotifications());
};
render () {
const { intl, notifications, isLoading, isUnread, columnId, hasMore, showFilterBar } = this.props;
const { intl, notifications, isLoading, isUnread, hasMore, showFilterBar, totalQueuedNotificationsCount } = this.props;
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
let scrollableContent = null;
@ -150,7 +162,7 @@ class Notifications extends React.PureComponent {
const scrollContainer = (
<ScrollableList
scrollKey={`notifications-${columnId}`}
scrollKey='notifications'
isLoading={isLoading}
showLoading={isLoading && notifications.size === 0}
hasMore={hasMore}
@ -169,6 +181,7 @@ class Notifications extends React.PureComponent {
<ColumnSettingsContainer />
</ColumnHeader>
{filterBarContainer}
<TimelineQueueButtonHeader onClick={this.handleDequeueNotifications} count={totalQueuedNotificationsCount} itemType='notification' />
{scrollContainer}
</Column>
);

View File

@ -5,7 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
import Column from '../ui/components/column';
import StatusList from '../../components/status_list';
import { injectIntl } from 'react-intl';
import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { meUsername } from 'gabsocial/initial_state';
import MissingIndicator from 'gabsocial/components/missing_indicator';
@ -51,6 +51,7 @@ class PinnedStatuses extends ImmutablePureComponent {
statusIds={statusIds}
scrollKey='pinned_statuses'
hasMore={hasMore}
emptyMessage={<FormattedMessage id='pinned_statuses.none' defaultMessage='No pins to show.'/>}
/>
</Column>
);

View File

@ -1,28 +0,0 @@
import { connect } from 'react-redux';
import ColumnSettings from '../../community_timeline/components/column_settings';
import { changeSetting } from '../../../actions/settings';
import { changeColumnParams } from '../../../actions/columns';
const mapStateToProps = (state, { columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
return {
settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'public']),
};
};
const mapDispatchToProps = (dispatch, { columnId }) => {
return {
onChange (key, checked) {
if (columnId) {
dispatch(changeColumnParams(columnId, key, checked));
} else {
dispatch(changeSetting(['public', ...key], checked));
}
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);

View File

@ -1,96 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import { expandPublicTimeline } from '../../actions/timelines';
import ColumnSettingsContainer from './containers/column_settings_container';
import { connectPublicStream } from '../../actions/streaming';
const messages = defineMessages({
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
});
const mapStateToProps = (state, { onlyMedia, columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
return {
hasUnread: state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']),
};
};
export default @connect(mapStateToProps)
@injectIntl
class PublicTimeline extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static defaultProps = {
onlyMedia: false,
};
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
hasUnread: PropTypes.bool,
onlyMedia: PropTypes.bool,
};
componentDidMount () {
const { dispatch, onlyMedia } = this.props;
dispatch(expandPublicTimeline({ onlyMedia }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
}
componentDidUpdate (prevProps) {
if (prevProps.onlyMedia !== this.props.onlyMedia) {
const { dispatch, onlyMedia } = this.props;
this.disconnect();
dispatch(expandPublicTimeline({ onlyMedia }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
}
}
componentWillUnmount () {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}
handleLoadMore = maxId => {
const { dispatch, onlyMedia } = this.props;
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
}
render () {
const { intl, hasUnread, onlyMedia } = this.props;
return (
<Column label={intl.formatMessage(messages.title)}>
<ColumnHeader icon='globe' active={hasUnread} title={intl.formatMessage(messages.title)}>
<ColumnSettingsContainer columnId={columnId} />
</ColumnHeader>
<StatusListContainer
timelineId={`public${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore}
scrollKey={`public_timeline-${columnId}`}
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
/>
</Column>
);
}
}

View File

@ -12,7 +12,7 @@ import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components';
import Icon from 'gabsocial/components/icon';
const messages = defineMessages({

View File

@ -6,7 +6,6 @@ import { me } from '../../../initial_state';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Avatar from '../../../components/avatar';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ComposeFormContainer from '../../compose/containers/compose_form_container';
import IconButton from 'gabsocial/components/icon_button';
const messages = defineMessages({

View File

@ -4,6 +4,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { createSelector } from 'reselect';
import { debounce } from 'lodash';
import { me } from '../../../initial_state';
import { dequeueTimeline } from 'gabsocial/actions/timelines';
const makeGetStatusIds = () => createSelector([
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
@ -28,17 +29,22 @@ const makeGetStatusIds = () => createSelector([
});
});
const makeMapStateToProps = () => {
const mapStateToProps = (state, {timelineId}) => {
const getStatusIds = makeGetStatusIds();
const mapStateToProps = (state, { timelineId }) => ({
return {
statusIds: getStatusIds(state, { type: timelineId }),
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
});
return mapStateToProps;
totalQueuedItemsCount: state.getIn(['timelines', timelineId, 'totalQueuedItemsCount']),
};
};
export default connect(makeMapStateToProps)(StatusList);
const mapDispatchToProps = (dispatch, ownProps) => ({
onDequeueTimeline(timelineId) {
dispatch(dequeueTimeline(timelineId, ownProps.onLoadMore));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(StatusList);

View File

@ -29,7 +29,6 @@ import SearchPage from 'gabsocial/pages/search_page';
import HomePage from 'gabsocial/pages/home_page';
import {
Compose,
Status,
GettingStarted,
CommunityTimeline,

View File

@ -14,10 +14,6 @@ export function HomeTimeline () {
return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline');
}
export function PublicTimeline () {
return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline');
}
export function CommunityTimeline () {
return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline');
}

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "تم التحقق مِن مِلْكية هذا الرابط بتاريخ {date}",
"account.locked_info": "تم تأمين خصوصية هذا الحساب عبر قفل. صاحب الحساب يُراجِع يدويا طلبات المتابَعة و الاشتراك بحسابه.",
"account.media": "وسائط",
"account.mention": "أُذكُر/ي @{name}",
"account.mention": "أُذكُر/ي",
"account.moved_to": "{name} إنتقل إلى :",
"account.mute": "كتم @{name}",
"account.mute_notifications": "كتم الإخطارات من @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
"account.mention": "Mentar a @{name}",
"account.mention": "Mentar",
"account.moved_to": "{name} has moved to:",
"account.mute": "Silenciar a @{name}",
"account.mute_notifications": "Mute notifications from @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "এই লিংকের মালিকানা চেক করা হয়েছে {date} তারিকে",
"account.locked_info": "এই নিবন্ধনের গোপনীয়তার ক্ষেত্র তালা দেওয়া আছে। নিবন্ধনকারী অনুসরণ করার অনুমতি যাদেরকে দেবেন, শুধু তারাই অনুসরণ করতে পারবেন।",
"account.media": "ছবি বা ভিডিও",
"account.mention": "@{name} কে উল্লেখ করুন",
"account.mention": "কে উল্লেখ করুন",
"account.moved_to": "{name} চলে গেছে এখানে:",
"account.mute": "@{name}র কার্যক্রম সরিয়ে ফেলুন",
"account.mute_notifications": "@{name}র প্রজ্ঞাপন আপনার কাছ থেকে সরিয়ে ফেলুন",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "La propietat d'aquest enllaç es va verificar el dia {date}",
"account.locked_info": "Aquest estat de privadesa del compte està definit com a bloquejat. El propietari revisa manualment qui pot seguir-lo.",
"account.media": "Mèdia",
"account.mention": "Esmentar @{name}",
"account.mention": "Esmentar",
"account.moved_to": "{name} s'ha mogut a:",
"account.mute": "Silencia @{name}",
"account.mute_notifications": "Notificacions desactivades de @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "A prupietà di stu ligame hè stata verificata u {date}",
"account.locked_info": "U statutu di vita privata di u contu hè chjosu. U pruprietariu esamina manualmente e dumande d'abbunamentu.",
"account.media": "Media",
"account.mention": "Mintuvà @{name}",
"account.mention": "Mintuvà",
"account.moved_to": "{name} hè partutu nant'à:",
"account.mute": "Piattà @{name}",
"account.mute_notifications": "Piattà nutificazione da @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Vlastnictví tohoto odkazu bylo zkontrolováno {date}",
"account.locked_info": "Stav soukromí tohoto účtu je nastaven na zamčeno. Jeho vlastník ručně posuzuje, kdo ho může sledovat.",
"account.media": "Média",
"account.mention": "Zmínit uživatele @{name}",
"account.mention": "Zmínit uživatele",
"account.moved_to": "{name} se přesunul/a na:",
"account.mute": "Skrýt uživatele @{name}",
"account.mute_notifications": "Skrýt oznámení od uživatele @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Gwiriwyd perchnogaeth y ddolen yma ar {date}",
"account.locked_info": "Mae'r statws preifatrwydd cyfrif hwn wedi'i osod i gloi. Mae'r perchennog yn adolygu'r sawl sy'n gallu eu dilyn.",
"account.media": "Cyfryngau",
"account.mention": "Crybwyll @{name}",
"account.mention": "Crybwyll",
"account.moved_to": "Mae @{name} wedi symud i:",
"account.mute": "Tawelu @{name}",
"account.mute_notifications": "Cuddio hysbysiadau o @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ejerskabet af dette link blev tjekket den %{date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Medie",
"account.mention": "Nævn @{name}",
"account.mention": "Nævn",
"account.moved_to": "{name} er flyttet til:",
"account.mute": "Dæmp @{name}",
"account.mute_notifications": "Dæmp notifikationer fra @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Besitz dieses Links wurde geprüft am {date}",
"account.locked_info": "Der Privatsphärenstatus dieses Accounts wurde auf gesperrt gesetzt. Die Person bestimmt manuell wer ihm/ihr folgen darf.",
"account.media": "Medien",
"account.mention": "@{name} erwähnen",
"account.mention": "erwähnen",
"account.moved_to": "{name} ist umgezogen auf:",
"account.mute": "@{name} stummschalten",
"account.mute_notifications": "Benachrichtigungen von @{name} verbergen",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Η ιδιοκτησία αυτού του συνδέσμου εκλέχθηκε την {date}",
"account.locked_info": "Η κατάσταση απορρήτου αυτού του λογαριασμού είναι κλειδωμένη. Ο ιδιοκτήτης επιβεβαιώνει χειροκίνητα ποιος μπορεί να τον ακολουθήσει.",
"account.media": "Πολυμέσα",
"account.mention": "Ανάφερε @{name}",
"account.mention": "Ανάφερε",
"account.moved_to": "{name} μεταφέρθηκε στο:",
"account.mute": "Σώπασε τον/την @{name}",
"account.mute_notifications": "Σώπασε τις ειδοποιήσεις από τον/την @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
"account.mention": "Mention @{name}",
"account.mention": "Mention",
"account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
"account.mute_notifications": "Mute notifications from @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "La posedanto de tiu ligilo estis kontrolita je {date}",
"account.locked_info": "La privateco de tiu konto estas elektita kiel fermita. La posedanto povas mane akcepti tiun, kiu povas sekvi rin.",
"account.media": "Aŭdovidaĵoj",
"account.mention": "Mencii @{name}",
"account.mention": "Mencii",
"account.moved_to": "{name} moviĝis al:",
"account.mute": "Silentigi @{name}",
"account.mute_notifications": "Silentigi sciigojn el @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
"account.mention": "Mencionar a @{name}",
"account.mention": "Mencionar",
"account.moved_to": "{name} se ha mudado a:",
"account.mute": "Silenciar a @{name}",
"account.mute_notifications": "Silenciar notificaciones de @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Esteka honen jabetzaren egiaztaketa data: {date}",
"account.locked_info": "Kontu honen pribatutasun egoera blokeatuta gisa ezarri da. Jabeak eskuz erabakitzen du nork jarraitu diezaioken.",
"account.media": "Media",
"account.mention": "Aipatu @{name}",
"account.mention": "Aipatu",
"account.moved_to": "{name} hona lekualdatu da:",
"account.mute": "Mututu @{name}",
"account.mute_notifications": "Mututu @{name}(r)en jakinarazpenak",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "مالکیت این نشانی در تایخ {date} بررسی شد",
"account.locked_info": "این حساب خصوصی است. صاحب این حساب تصمیم می‌گیرد که چه کسی می‌تواند پیگیرش باشد.",
"account.media": "عکس و ویدیو",
"account.mention": "نام‌بردن از @{name}",
"account.mention": "نام‌بردن از",
"account.moved_to": "{name} منتقل شده است به:",
"account.mute": "بی‌صدا کردن @{name}",
"account.mute_notifications": "بی‌صداکردن اعلان‌ها از طرف @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Tämän linkin omistaja tarkistettiin {date}",
"account.locked_info": "Tämän tili on yksityinen. Käyttäjä vahvistaa itse kuka voi seurata häntä.",
"account.media": "Media",
"account.mention": "Mainitse @{name}",
"account.mention": "Mainitse",
"account.moved_to": "{name} on muuttanut instanssiin:",
"account.mute": "Mykistä @{name}",
"account.mute_notifications": "Mykistä ilmoitukset käyttäjältä @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "La propriété de ce lien a été vérifiée le {date}",
"account.locked_info": "Ce compte est verrouillé. Son propriétaire approuve manuellement qui peut le ou la suivre.",
"account.media": "Média",
"account.mention": "Mentionner @{name}",
"account.mention": "Mentionner",
"account.moved_to": "{name} a déménagé vers:",
"account.mute": "Masquer @{name}",
"account.mute_notifications": "Ignorer les notifications de @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "A propiedade de esta ligazón foi comprobada en {date}",
"account.locked_info": "O estado da intimidade de esta conta estableceuse en pechado. A persoa dona da conta revisa quen pode seguila.",
"account.media": "Medios",
"account.mention": "Mencionar @{name}",
"account.mention": "Mencionar",
"account.moved_to": "{name} marchou a:",
"account.mute": "Acalar @{name}",
"account.mute_notifications": "Acalar as notificacións de @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "מדיה",
"account.mention": "אזכור של @{name}",
"account.mention": "אזכור של",
"account.moved_to": "החשבון {name} הועבר אל:",
"account.mute": "להשתיק את @{name}",
"account.mute_notifications": "להסתיר התראות מאת @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
"account.mention": "Mention @{name}",
"account.mention": "Mention",
"account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
"account.mute_notifications": "Mute notifications from @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
"account.mention": "Spomeni @{name}",
"account.mention": "Spomeni",
"account.moved_to": "{name} has moved to:",
"account.mute": "Utišaj @{name}",
"account.mute_notifications": "Mute notifications from @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Média",
"account.mention": "@{name} említése",
"account.mention": "említése",
"account.moved_to": "{name} átköltözött:",
"account.mute": "@{name} némítása",
"account.mute_notifications": "@{name} értesítések némítása",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Մեդիա",
"account.mention": "Նշել @{name}֊ին",
"account.mention": "Նշել",
"account.moved_to": "{name}֊ը տեղափոխվել է՝",
"account.mute": "Լռեցնել @{name}֊ին",
"account.mute_notifications": "Անջատել ծանուցումները @{name}֊ից",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
"account.mention": "Balasan @{name}",
"account.mention": "Balasan",
"account.moved_to": "{name} telah pindah ke:",
"account.mute": "Bisukan @{name}",
"account.mute_notifications": "Sembunyikan notifikasi dari @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
"account.mention": "Mencionar @{name}",
"account.mention": "Mencionar",
"account.moved_to": "{name} has moved to:",
"account.mute": "Celar @{name}",
"account.mute_notifications": "Mute notifications from @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "La proprietà di questo link è stata controllata il {date}",
"account.locked_info": "Il livello di privacy di questo account è impostato a \"bloccato\". Il proprietario esamina manualmente le richieste di seguirlo.",
"account.media": "Media",
"account.mention": "Menziona @{name}",
"account.mention": "Menziona",
"account.moved_to": "{name} si è trasferito su:",
"account.mute": "Silenzia @{name}",
"account.mute_notifications": "Silenzia notifiche da @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "このリンクの所有権は{date}に確認されました",
"account.locked_info": "このアカウントは承認制アカウントです。相手が承認するまでフォローは完了しません。",
"account.media": "メディア",
"account.mention": "@{name}さんにトゥート",
"account.mention": "さんにトゥート",
"account.moved_to": "{name}さんは引っ越しました:",
"account.mute": "@{name}さんをミュート",
"account.mute_notifications": "@{name}さんからの通知を受け取らない",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "მედია",
"account.mention": "ასახელეთ @{name}",
"account.mention": "ასახელეთ",
"account.moved_to": "{name} გადავიდა:",
"account.mute": "გააჩუმე @{name}",
"account.mute_notifications": "გააჩუმე შეტყობინებები @{name}-სგან",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Сілтеме меншігі расталған күн {date}",
"account.locked_info": "Бұл қолданушы өзі туралы мәліметтерді жасырған. Тек жазылғандар ғана көре алады.",
"account.media": "Медиа",
"account.mention": "Аталым @{name}",
"account.mention": "Аталым",
"account.moved_to": "{name} көшіп кетті:",
"account.mute": "Үнсіз қылу @{name}",
"account.mute_notifications": "@{name} туралы ескертпелерді жасыру",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "{date}에 이 링크의 소유권이 확인 됨",
"account.locked_info": "이 계정의 프라이버시 설정은 잠금으로 설정되어 있습니다. 계정 소유자가 수동으로 팔로어를 승인합니다.",
"account.media": "미디어",
"account.mention": "@{name}에게 글쓰기",
"account.mention": "에게 글쓰기",
"account.moved_to": "{name}는 계정을 이동했습니다:",
"account.mute": "@{name} 뮤트",
"account.mute_notifications": "@{name}의 알림을 뮤트",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Šīs saites piederība ir pārbaudīta {date}",
"account.locked_info": "Šī konta privātuma status ir iestatīts slēgts. Īpašnieks izskatīs un izvēlēsies kas viņam drīkst sekot.",
"account.media": "Mēdiji",
"account.mention": "Piemin @{name}",
"account.mention": "Piemin",
"account.moved_to": "{name} ir pārvācies uz:",
"account.mute": "Apklusināt @{name}",
"account.mute_notifications": "Nerādīt paziņojumus no @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
"account.mention": "Mention @{name}",
"account.mention": "Mention",
"account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
"account.mute_notifications": "Mute notifications from @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Eigendom van deze link is gecontroleerd op {date}",
"account.locked_info": "De privacystatus van dit account is op besloten gezet. De eigenaar bepaalt handmatig wie hen kan volgen.",
"account.media": "Media",
"account.mention": "Vermeld @{name}",
"account.mention": "Vermeld",
"account.moved_to": "{name} is verhuisd naar:",
"account.mute": "Negeer @{name}",
"account.mute_notifications": "Negeer meldingen van @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
"account.mention": "Nevn @{name}",
"account.mention": "Nevn",
"account.moved_to": "{name} har flyttet til:",
"account.mute": "Demp @{name}",
"account.mute_notifications": "Ignorer varsler fra @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "La proprietat daqueste ligam foguèt verificada lo {date}",
"account.locked_info": "Lestatut de privacitat del compte es configurat sus clavat. Lo proprietari causís qual pòt sègre son compte.",
"account.media": "Mèdias",
"account.mention": "Mencionar @{name}",
"account.mention": "Mencionar",
"account.moved_to": "{name} a mudat los catons a:",
"account.mute": "Rescondre @{name}",
"account.mute_notifications": "Rescondre las notificacions de @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Własność tego odnośnika została potwierdzona {date}",
"account.locked_info": "To konto jest prywatne. Właściciel ręcznie wybiera kto może go śledzić.",
"account.media": "Zawartość multimedialna",
"account.mention": "Wspomnij o @{name}",
"account.mention": "Wspomnij",
"account.moved_to": "{name} przeniósł(-osła) się do:",
"account.mute": "Wycisz @{name}",
"account.mute_notifications": "Wycisz powiadomienia o @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "A posse desse link foi verificada em {date}",
"account.locked_info": "Essa conta está trancada. Se você a seguir sua solicitação será revisada manualmente.",
"account.media": "Mídia",
"account.mention": "Mencionar @{name}",
"account.mention": "Mencionar",
"account.moved_to": "{name} se mudou para:",
"account.mute": "Silenciar @{name}",
"account.mute_notifications": "Silenciar notificações de @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "A posse deste link foi verificada em {date}",
"account.locked_info": "O estatuto de privacidade desta conta é fechado. O dono revê manualmente que a pode seguir.",
"account.media": "Media",
"account.mention": "Mencionar @{name}",
"account.mention": "Mencionar",
"account.moved_to": "{name} mudou a sua conta para:",
"account.mute": "Silenciar @{name}",
"account.mute_notifications": "Silenciar notificações de @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Deținerea acestui link a fost verificată la {date}",
"account.locked_info": "Acest profil este privat. Această persoană gestioneaz manual cine o urmărește.",
"account.media": "Media",
"account.mention": "Menționează @{name}",
"account.mention": "Menționează",
"account.moved_to": "{name} a fost mutat la:",
"account.mute": "Oprește @{name}",
"account.mute_notifications": "Oprește notificările de la @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Vlastníctvo tohto odkazu bolo skontrolované {date}",
"account.locked_info": "Stav súkromia pre tento účet je nastavený na zamknutý. Jeho vlastník sám prehodnocuje, kto ho môže sledovať.",
"account.media": "Médiá",
"account.mention": "Spomeň @{name}",
"account.mention": "Spomeň",
"account.moved_to": "{name} sa presunul/a na:",
"account.mute": "Ignorovať @{name}",
"account.mute_notifications": "Stĺm oboznámenia od @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Lastništvo te povezave je bilo preverjeno {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Mediji",
"account.mention": "Omeni @{name}",
"account.mention": "Omeni",
"account.moved_to": "{name} se je premaknil na:",
"account.mute": "Utišaj @{name}",
"account.mute_notifications": "Utišaj obvestila od @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Pronësia e kësaj lidhjeje qe kontrolluar më {date}",
"account.locked_info": "Gjendja e privatësisë së kësaj llogarie është caktuar si e kyçur. I zoti merr dorazi në shqyrtim cilët mund ta ndjekin.",
"account.media": "Media",
"account.mention": "Përmendni @{name}",
"account.mention": "Përmendni",
"account.moved_to": "{name} ka kaluar te:",
"account.mute": "Heshtoni @{name}",
"account.mute_notifications": "Heshtoji njoftimet prej @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Медији",
"account.mention": "Помени корисника @{name}",
"account.mention": "Помени корисника",
"account.moved_to": "{name} се померио на:",
"account.mute": "Ућуткај корисника @{name}",
"account.mute_notifications": "Искључи обавештења од корисника @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
"account.mention": "Nämna @{name}",
"account.mention": "Nämna",
"account.moved_to": "{name} har flyttat till:",
"account.mute": "Tysta @{name}",
"account.mute_notifications": "Stäng av notifieringar från @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
"account.mention": "Mention @{name}",
"account.mention": "Mention",
"account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
"account.mute_notifications": "Mute notifications from @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "ఈ లంకె యొక్క యాజమాన్యం {date}న పరీక్షించబడింది",
"account.locked_info": "ఈ ఖాతా యొక్క గోప్యత స్థితి లాక్ చేయబడి వుంది. ఈ ఖాతాను ఎవరు అనుసరించవచ్చో యజమానే నిర్ణయం తీసుకుంటారు.",
"account.media": "మీడియా",
"account.mention": "@{name}ను ప్రస్తావించు",
"account.mention": "ప్రస్తావించు",
"account.moved_to": "{name} ఇక్కడికి మారారు:",
"account.mute": "@{name}ను మ్యూట్ చెయ్యి",
"account.mute_notifications": "@{name}నుంచి ప్రకటనలను మ్యూట్ చెయ్యి",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "ตรวจสอบความเป็นเจ้าของของลิงก์นี้เมื่อ {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "สื่อ",
"account.mention": "กล่าวถึง @{name}",
"account.mention": "กล่าวถึง",
"account.moved_to": "{name} ได้ย้ายไปยัง:",
"account.mute": "ปิดเสียง @{name}",
"account.mute_notifications": "ปิดเสียงการแจ้งเตือนจาก @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Bu bağlantının mülkiyeti {date} tarihinde kontrol edildi",
"account.locked_info": "Bu hesabın gizlilik durumu kilitli olarak ayarlanmış. Sahibi, onu kimin takip edebileceğini elle inceler.",
"account.media": "Medya",
"account.mention": "@{name} kullanıcısından bahset",
"account.mention": "kullanıcısından bahset",
"account.moved_to": "{name} şuraya taşındı:",
"account.mute": "@{name} kullanıcısını sessize al",
"account.mute_notifications": "@{name} kullanıcısının bildirimlerini kapat",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Медіа",
"account.mention": "Згадати @{name}",
"account.mention": "Згадати",
"account.moved_to": "{name} переїхав на:",
"account.mute": "Заглушити @{name}",
"account.mute_notifications": "Не показувати сповіщення від @{name}",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "媒体",
"account.mention": "提及 @{name}",
"account.mention": "提及",
"account.moved_to": "{name} 已经迁移到:",
"account.mute": "隐藏 @{name}",
"account.mute_notifications": "隐藏来自 @{name} 的通知",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "媒體",
"account.mention": "提及 @{name}",
"account.mention": "提及",
"account.moved_to": "{name} 已經遷移到:",
"account.mute": "將 @{name} 靜音",
"account.mute_notifications": "將來自 @{name} 的通知靜音",

View File

@ -18,7 +18,7 @@
"account.link_verified_on": "此連結的所有權已在 {date} 檢查",
"account.locked_info": "此帳號的隱私狀態被設為鎖定,擁有者將手動審核可關注此帳號的人。",
"account.media": "媒體",
"account.mention": "提及 @{name}",
"account.mention": "提及",
"account.moved_to": "{name} 已遷移至:",
"account.mute": "靜音 @{name}",
"account.mute_notifications": "靜音來自 @{name} 的通知",

View File

@ -6,6 +6,9 @@ import {
NOTIFICATIONS_FILTER_SET,
NOTIFICATIONS_CLEAR,
NOTIFICATIONS_SCROLL_TOP,
NOTIFICATIONS_UPDATE_QUEUE,
NOTIFICATIONS_DEQUEUE,
MAX_QUEUED_NOTIFICATIONS,
} from '../actions/notifications';
import {
ACCOUNT_BLOCK_SUCCESS,
@ -21,6 +24,8 @@ const initialState = ImmutableMap({
top: false,
unread: 0,
isLoading: false,
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
});
const notificationToMap = notification => ImmutableMap({
@ -93,6 +98,32 @@ const deleteByStatus = (state, statusId) => {
return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId));
};
const updateNotificationsQueue = (state, notification, intlMessages, intlLocale) => {
const queuedNotifications = state.getIn(['queuedNotifications'], ImmutableList());
const listedNotifications = state.getIn(['items'], ImmutableList());
const totalQueuedNotificationsCount = state.getIn(['totalQueuedNotificationsCount'], 0);
let alreadyExists = queuedNotifications.find(existingQueuedNotification => existingQueuedNotification.id === notification.id);
if (!alreadyExists) alreadyExists = listedNotifications.find(existingListedNotification => existingListedNotification.get('id') === notification.id);
if (alreadyExists) {
return state;
}
let newQueuedNotifications = queuedNotifications;
return state.withMutations(mutable => {
if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
mutable.set('queuedNotifications', newQueuedNotifications.push({
notification,
intlMessages,
intlLocale,
}));
}
mutable.set('totalQueuedNotificationsCount', totalQueuedNotificationsCount + 1);
});
};
export default function notifications(state = initialState, action) {
switch(action.type) {
case NOTIFICATIONS_EXPAND_REQUEST:
@ -105,6 +136,13 @@ export default function notifications(state = initialState, action) {
return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE:
return normalizeNotification(state, action.notification);
case NOTIFICATIONS_UPDATE_QUEUE:
return updateNotificationsQueue(state, action.notification, action.intlMessages, action.intlLocale);
case NOTIFICATIONS_DEQUEUE:
return state.withMutations(mutable => {
mutable.set('queuedNotifications', ImmutableList())
mutable.set('totalQueuedNotificationsCount', 0)
});
case NOTIFICATIONS_EXPAND_SUCCESS:
return expandNormalizedNotifications(state, action.notifications, action.next);
case ACCOUNT_BLOCK_SUCCESS:

View File

@ -1,6 +1,5 @@
import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings';
import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
import { COLUMN_PARAMS_CHANGE } from '../actions/columns';
import { STORE_HYDRATE } from '../actions/store';
import { EMOJI_USE } from '../actions/emojis';
import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
@ -88,17 +87,6 @@ const defaultColumns = fromJS([
const hydrate = (state, settings) => state.mergeDeep(settings).update('columns', (val = defaultColumns) => val);
const changeColumnParams = (state, uuid, path, value) => {
const columns = state.get('columns');
const index = columns.findIndex(item => item.get('uuid') === uuid);
const newColumns = columns.update(index, column => column.updateIn(['params', ...path], () => value));
return state
.set('columns', newColumns)
.set('saved', false);
};
const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false);
const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId));
@ -112,8 +100,6 @@ export default function settings(state = initialState, action) {
return state
.setIn(action.path, action.value)
.set('saved', false);
case COLUMN_PARAMS_CHANGE:
return changeColumnParams(state, action.uuid, action.path, action.value);
case EMOJI_USE:
return updateFrequentEmojis(state, action.emoji);
case SETTING_SAVE:

View File

@ -7,6 +7,9 @@ import {
TIMELINE_EXPAND_FAIL,
TIMELINE_CONNECT,
TIMELINE_DISCONNECT,
TIMELINE_UPDATE_QUEUE,
TIMELINE_DEQUEUE,
MAX_QUEUED_ITEMS,
} from '../actions/timelines';
import {
ACCOUNT_BLOCK_SUCCESS,
@ -25,6 +28,8 @@ const initialTimeline = ImmutableMap({
isLoading: false,
hasMore: true,
items: ImmutableList(),
queuedItems: ImmutableList(), //max= MAX_QUEUED_ITEMS
totalQueuedItemsCount: 0, //used for queuedItems overflow for MAX_QUEUED_ITEMS+
});
const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => {
@ -77,6 +82,28 @@ const updateTimeline = (state, timeline, status) => {
}));
};
const updateTimelineQueue = (state, timeline, status) => {
const queuedStatuses = state.getIn([timeline, 'queuedItems'], ImmutableList());
const listedStatuses = state.getIn([timeline, 'items'], ImmutableList());
const totalQueuedItemsCount = state.getIn([timeline, 'totalQueuedItemsCount'], 0);
let alreadyExists = queuedStatuses.find(existingQueuedStatus => existingQueuedStatus.get('id') === status.get('id'));
if (!alreadyExists) alreadyExists = listedStatuses.find(existingListedStatusId => existingListedStatusId === status.get('id'));
if (alreadyExists) {
return state;
}
let newQueuedStatuses = queuedStatuses;
return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
if (totalQueuedItemsCount <= MAX_QUEUED_ITEMS) {
mMap.set('queuedItems', newQueuedStatuses.push(status));
}
mMap.set('totalQueuedItemsCount', totalQueuedItemsCount + 1);
}));
};
const deleteStatus = (state, id, accountId, references, exclude_account = null) => {
state.keySeq().forEach(timeline => {
if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`)))
@ -126,6 +153,13 @@ export default function timelines(state = initialState, action) {
return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent);
case TIMELINE_UPDATE:
return updateTimeline(state, action.timeline, fromJS(action.status));
case TIMELINE_UPDATE_QUEUE:
return updateTimelineQueue(state, action.timeline, fromJS(action.status));
case TIMELINE_DEQUEUE:
return state.update(action.timeline, initialTimeline, map => map.withMutations(mMap => {
mMap.set('queuedItems', ImmutableList())
mMap.set('totalQueuedItemsCount', 0)
}));
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
case TIMELINE_CLEAR:

View File

@ -1380,6 +1380,7 @@ a.account__display-name {
.ui {
display: block;
width: 100%;
padding: 0 0 100px 0;
.page {
display: flex;
@ -5103,3 +5104,25 @@ noscript {
}
}
}
.timeline-queue-header {
display: block;
width: 100%;
height: 52px;
position: relative;
background-color: darken($ui-base-color, 8%);
border-bottom: 1px solid;
border-top: 1px solid;
border-color: darken($ui-base-color, 4%);
&__btn {
display: block;
width: 100%;
height: 100%;
text-align: center;
line-height: 52px;
font-size: 14px;
cursor: pointer;
color: $secondary-text-color;
}
}

View File

@ -15,11 +15,8 @@
height: 100%;
display: inline-block;
border-radius: 4px;
background: darken($ui-primary-color, 14%);
&.leading {
background: $ui-highlight-color;
}
background: rgba($gab-placeholder-accent, .3);
&.leading {background: rgba($gab-placeholder-accent, .6);}
}
&__text {
@ -31,7 +28,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #fff;
input[type=radio],
input[type=checkbox] {
display: none;

View File

@ -2,41 +2,35 @@
// Content containers are meant to behave much differently than the mastodon default UI
// For now linking default gab colors to replace the base UI colors and formulas
html {
scrollbar-color: lighten($gab-background-container, 4%) $gab-background-container;
}
html {scrollbar-color: lighten($gab-background-container, 4%) $gab-background-container;}
::-webkit-scrollbar {
width: 12px;
height: 12px;
width: 12px;
height: 12px;
}
::-webkit-scrollbar-thumb {
background: lighten($gab-background-container, 4%);
border: none;
border: none;
background: rgba($gab-placeholder-accent, .5);
@at-root body.theme-gabsocial-light#{&} {background: rgba($gab-background-container-light, .3);}
}
::-webkit-scrollbar-thumb:hover {
background: lighten($gab-background-container, 6%);
background: rgba($gab-placeholder-accent, .75);
@at-root body.theme-gabsocial-light#{&} {background: rgba($gab-background-container-light, .4);}
}
::-webkit-scrollbar-thumb:active {
background: lighten($gab-background-container, 4%);
background: $gab-placeholder-accent;
@at-root body.theme-gabsocial-light#{&} {background: rgba($gab-background-container-light, .5);}
}
::-webkit-scrollbar-track {
border: none;
background: $gab-background-container;
border: none;
background: rgba($gab-background-container, .5);
@at-root body.theme-gabsocial-light#{&} {background: rgba($gab-background-base, .3);}
}
::-webkit-scrollbar-track:hover {
background: lighten($gab-background-container, 4%);
background: rgba($gab-background-container, .75);
@at-root body.theme-gabsocial-light#{&} {background: rgba($gab-background-base, .4);}
}
::-webkit-scrollbar-track:active {
background: $gab-background-container;
background: $gab-background-container;
@at-root body.theme-gabsocial-light#{&} {background: rgba($gab-background-base, .5);}
}
::-webkit-scrollbar-corner {
background: transparent;
}
::-webkit-scrollbar-corner {background: transparent;}

View File

@ -30,7 +30,7 @@ class FeedManager
def push_to_home(account, status)
return false unless add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
trim(:home, account.id)
#PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}") if push_update_required?("timeline:#{account.id}")
PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}") if push_update_required?("timeline:#{account.id}")
true
end
@ -48,7 +48,7 @@ class FeedManager
end
return false unless add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
trim(:list, list.id)
#PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
true
end

View File

@ -61,7 +61,7 @@ class FanOutOnWriteService < BaseService
Rails.logger.debug "Delivering status #{status.id} to group"
# Redis.current.publish("timeline:group:#{status.group_id}", @payload)
Redis.current.publish("timeline:group:#{status.group_id}", @payload)
end
def deliver_to_mentioned_followers(status)
@ -89,15 +89,15 @@ class FanOutOnWriteService < BaseService
def deliver_to_public(status)
Rails.logger.debug "Delivering status #{status.id} to public timeline"
# Redis.current.publish('timeline:public', @payload)
# Redis.current.publish('timeline:public:local', @payload) if status.local?
Redis.current.publish('timeline:public', @payload)
Redis.current.publish('timeline:public:local', @payload) if status.local?
end
def deliver_to_media(status)
Rails.logger.debug "Delivering status #{status.id} to media timeline"
# Redis.current.publish('timeline:public:media', @payload)
# Redis.current.publish('timeline:public:local:media', @payload) if status.local?
Redis.current.publish('timeline:public:media', @payload)
Redis.current.publish('timeline:public:local:media', @payload) if status.local?
end
def deliver_to_own_conversation(status)

View File

@ -16,7 +16,7 @@
= invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: false
.fields-group
= f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: homepage_path, about_tos_path: about_tos_path), disabled: closed_registrations?
= f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', about_tos_path: about_tos_path), disabled: closed_registrations?
.actions
= f.button :button, sign_up_message, type: :submit, class: 'button button-primary', disabled: closed_registrations?

View File

@ -33,7 +33,7 @@
= f.input :invite_code, as: :hidden
.fields-group
= f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_privacy_path, about_tos_path: about_tos_path)
= f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', about_tos_path: about_tos_path)
.actions
= f.button :button, @invite.present? ? t('auth.register') : sign_up_message, type: :submit

View File

@ -7,7 +7,7 @@ class PushConversationWorker
conversation = AccountConversation.find(conversation_account_id)
message = InlineRenderer.render(conversation, conversation.account, :conversation)
timeline_id = "timeline:direct:#{conversation.account_id}"
# Redis.current.publish(timeline_id, Oj.dump(event: :conversation, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
Redis.current.publish(timeline_id, Oj.dump(event: :conversation, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
true
rescue ActiveRecord::RecordNotFound
true

View File

@ -9,7 +9,7 @@ class PushUpdateWorker
message = InlineRenderer.render(status, account, :status)
timeline_id = "timeline:#{account.id}" if timeline_id.nil?
# Redis.current.publish(timeline_id, Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
Redis.current.publish(timeline_id, Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
true
rescue ActiveRecord::RecordNotFound
true

View File

@ -515,7 +515,7 @@ ca:
auth:
apply_for_account: Demana una invitació
change_password: Contrasenya
checkbox_agreement_html: Estic d'acord amb les <a href="%{rules_path}" target="_blank">normes del servidor</a> i <a href="%{about_tos_path}" target="_blank"> els termes del servei</a>
checkbox_agreement_html: Estic dacord amb els <a href="%{about_tos_path}" target="_blank">termes del servei</a>.
confirm_email: Confirmar correu electrònic
delete_account: Suprimeix el compte
delete_account_html: Si vols suprimir el compte pots <a href="%{path}">fer-ho aquí</a>. Se't demanarà confirmació.

View File

@ -515,7 +515,7 @@ co:
auth:
apply_for_account: Dumandà un'invitazione
change_password: Chjave daccessu
checkbox_agreement_html: d'accunsentu cù e <a href="%{rules_path}" target="_blank">regule di u servore</a> è i <a href="%{about_tos_path}" target="_blank">termini di u serviziu</a>
checkbox_agreement_html: in conformità à a <a href="%{about_tos_path}" target="_blank">termina di u serviziu</a>
confirm_email: Cunfirmà le-mail
delete_account: Sguassà u contu
delete_account_html: Sè voi vulete toglie u vostru contu <a href="%{path}">ghjè quì</a>. Duverete cunfirmà a vostra scelta.

View File

@ -521,7 +521,7 @@ cs:
auth:
apply_for_account: Vyžádat si pozvánku
change_password: Heslo
checkbox_agreement_html: Souhlasím s <a href="%{rules_path}" target="_blank">pravidly serveru</a> a <a href="%{about_tos_path}" target="_blank">podmínkami používání</a>
checkbox_agreement_html: Souhlasím s <a href="%{about_tos_path}" target="_blank">Smluvními podmínkami</a>
confirm_email: Potvrdit e-mail
delete_account: Odstranit účet
delete_account_html: Chcete-li odstranit svůj účet, <a href="%{path}">pokračujte zde</a>. Budete požádán/a o potvrzení.

Some files were not shown because too many files have changed in this diff Show More