Merge remote-tracking branch 'origin/styling/add-groups-link' into groups-updates
* origin/styling/add-groups-link: (31 commits) Comment out the "groups" button until ready to reveal. Changed the method of adding main navigation icons. Created a png sprite sized @2x based on largest usage (for retina). This will fix some rendering issues caused by using svg images. It will allow adding depth and more color / shading if we choose later. intents fix federation fix two more federation fixes Removed unused imports Removed unused PublicTimeline component Updated CommunityTimeline to add option for "all federated" content Removed unused import in unauthorized_modal Updated registration legal links Updated compose_form to account for if compose modal open Added empty message to pinned statuses page Updated nextProps withReplies for account timeline Added empty message to account_gallery media page Updated timeline/notification dequeue to be in componentDidMount Added TimelineQueueButtonHeader to status_list Added queue functionality status_list_container for status timelines Updated all Redis.current.publish, PushUpdateWorker.perform_async to work again Added timeline dequeue functionality to onSubmitCompose action Added redux functionality for queueing/dequeueing timelines ...
|
@ -5,7 +5,7 @@ class IntentsController < ApplicationController
|
|||
rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri
|
||||
|
||||
def show
|
||||
if uri.scheme == 'web+gabsocial'
|
||||
if uri.scheme == 'web+mastodon'
|
||||
case uri.host
|
||||
when 'follow'
|
||||
return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].gsub(/\Aacct:/, ''))
|
||||
|
|
|
@ -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());
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
@ -169,6 +169,10 @@ export function submitCompose(routerHistory, group) {
|
|||
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 }));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,15 +57,27 @@ 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
|
||||
|
@ -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();
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,32 +43,47 @@ 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;
|
||||
|
||||
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();
|
||||
|
||||
if (allFediverse) {
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
}
|
||||
else {
|
||||
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
|
@ -69,27 +93,29 @@ class CommunityTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
const { dispatch, onlyMedia, allFediverse } = this.props;
|
||||
|
||||
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>
|
||||
);
|
||||
|
|
|
@ -69,6 +69,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
shouldCondense: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
group: ImmutablePropTypes.map,
|
||||
isModalOpen: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -151,6 +152,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.
|
||||
|
@ -204,7 +207,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('');
|
||||
|
@ -253,7 +256,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}
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -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." />}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
|
@ -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.' />}
|
||||
|
|
|
@ -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.'/>}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
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.' />}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -27,9 +27,6 @@ export const privateLinks = [
|
|||
<NavLink className='tabs-bar__link groups' to='/groups' data-preview-title-id='column.groups' >
|
||||
<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />
|
||||
</NavLink>,
|
||||
// <NavLink className='tabs-bar__link home' to='/groups' data-preview-title-id='column.groups' >
|
||||
// <FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />
|
||||
// </NavLink>,
|
||||
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' >
|
||||
<FormattedMessage id='tabs_bar.search' defaultMessage='Search' />
|
||||
</NavLink>,
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -31,7 +31,6 @@ import SearchPage from 'gabsocial/pages/search_page';
|
|||
import HomePage from 'gabsocial/pages/home_page';
|
||||
|
||||
import {
|
||||
Compose,
|
||||
Status,
|
||||
GettingStarted,
|
||||
CommunityTimeline,
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}র প্রজ্ঞাপন আপনার কাছ থেকে সরিয়ে ফেলুন",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}֊ից",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}さんからの通知を受け取らない",
|
||||
|
|
|
@ -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}-სგან",
|
||||
|
|
|
@ -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} туралы ескертпелерді жасыру",
|
||||
|
|
|
@ -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}의 알림을 뮤트",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
"account.link_verified_on": "La proprietat d’aqueste ligam foguèt verificada lo {date}",
|
||||
"account.locked_info": "L’estatut 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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}నుంచి ప్రకటనలను మ్యూట్ చెయ్యి",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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} 的通知",
|
||||
|
|
|
@ -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} 的通知靜音",
|
||||
|
|
|
@ -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} 的通知",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg id="icon-explore" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><defs><style>.cls-1{fill:#00d177;}</style></defs><title>icon-explore-00d177</title><path id="_00d177" data-name="00d177" class="cls-1" d="M10,0a7,7,0,1,0,7,7A7,7,0,0,0,10,0Zm2.32,10.09-.32.32a.75.75,0,0,0-.17.3,3.79,3.79,0,0,1-.14.48l-.49,1.32a5.26,5.26,0,0,1-1.2.14v-.78a2.11,2.11,0,0,0-.64-1.44.9.9,0,0,1-.26-.64V8.88a.9.9,0,0,0-.47-.79c-.4-.22-1-.53-1.38-.73a4.18,4.18,0,0,1-.89-.62l0,0a3.12,3.12,0,0,1-.51-.58l-1-1.45A5.68,5.68,0,0,1,7.77,1.82l.67.34a.46.46,0,0,0,.66-.41V1.43a5.06,5.06,0,0,1,.69-.06l.79.79a.45.45,0,0,1,0,.64l-.13.14-.29.29a.22.22,0,0,0,0,.32l.13.13a.22.22,0,0,1,0,.32l-.22.22a.21.21,0,0,1-.16.07H9.65a.2.2,0,0,0-.15.06l-.28.28a.21.21,0,0,0-.05.26l.44.88a.22.22,0,0,1-.2.33H9.25A.22.22,0,0,1,9.1,6l-.26-.23a.46.46,0,0,0-.44-.08L7.52,6a.33.33,0,0,0-.23.32.33.33,0,0,0,.19.3l.31.16a2,2,0,0,0,.86.2c.29,0,.63.77.9.9h1.88a.91.91,0,0,1,.64.27l.39.38a.88.88,0,0,1,.25.61A1.29,1.29,0,0,1,12.32,10.09Zm2.45-2.57a.74.74,0,0,1-.4-.29l-.51-.76a.68.68,0,0,1,0-.75l.56-.83a.58.58,0,0,1,.26-.23L15,4.48A5.48,5.48,0,0,1,15.65,7a5.83,5.83,0,0,1-.06.72Z" transform="translate(-3)"/></svg>
|
Before Width: | Height: | Size: 1.2 KiB |
|
@ -1 +0,0 @@
|
|||
<svg id="icon-explore" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><defs><style>.cls-1{fill:#d8d8d8;}</style></defs><title>icon-explore-d8d8d8</title><path id="d8d8d8" class="cls-1" d="M10,0a7,7,0,1,0,7,7A7,7,0,0,0,10,0Zm2.32,10.09-.32.32a.75.75,0,0,0-.17.3,3.79,3.79,0,0,1-.14.48l-.49,1.32a5.26,5.26,0,0,1-1.2.14v-.78a2.11,2.11,0,0,0-.64-1.44.9.9,0,0,1-.26-.64V8.88a.9.9,0,0,0-.47-.79c-.4-.22-1-.53-1.38-.73a4.18,4.18,0,0,1-.89-.62l0,0a3.12,3.12,0,0,1-.51-.58l-1-1.45A5.68,5.68,0,0,1,7.77,1.82l.67.34a.46.46,0,0,0,.66-.41V1.43a5.06,5.06,0,0,1,.69-.06l.79.79a.45.45,0,0,1,0,.64l-.13.14-.29.29a.22.22,0,0,0,0,.32l.13.13a.22.22,0,0,1,0,.32l-.22.22a.21.21,0,0,1-.16.07H9.65a.2.2,0,0,0-.15.06l-.28.28a.21.21,0,0,0-.05.26l.44.88a.22.22,0,0,1-.2.33H9.25A.22.22,0,0,1,9.1,6l-.26-.23a.46.46,0,0,0-.44-.08L7.52,6a.33.33,0,0,0-.23.32.33.33,0,0,0,.19.3l.31.16a2,2,0,0,0,.86.2c.29,0,.63.77.9.9h1.88a.91.91,0,0,1,.64.27l.39.38a.88.88,0,0,1,.25.61A1.29,1.29,0,0,1,12.32,10.09Zm2.45-2.57a.74.74,0,0,1-.4-.29l-.51-.76a.68.68,0,0,1,0-.75l.56-.83a.58.58,0,0,1,.26-.23L15,4.48A5.48,5.48,0,0,1,15.65,7a5.83,5.83,0,0,1-.06.72Z" transform="translate(-3)"/></svg>
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1 +0,0 @@
|
|||
<svg id="icon-explore" xmlns="http://www.w3.org/2000/svg" width="14" height="114" viewBox="0 0 14 114"><defs><style>.cls-1{fill:#d8d8d8;}.cls-2{fill:#00d177;}</style></defs><title>icon-explore-sprite</title><path id="d8d8d8" class="cls-1" d="M10,100a7,7,0,1,0,7,7A7,7,0,0,0,10,100Zm2.32,10.09-.32.32a.75.75,0,0,0-.17.3,3.79,3.79,0,0,1-.14.48l-.49,1.32a5.26,5.26,0,0,1-1.2.14v-.78a2.11,2.11,0,0,0-.64-1.44.9.9,0,0,1-.26-.64v-.91a.9.9,0,0,0-.47-.79c-.4-.22-1-.53-1.38-.73a4.18,4.18,0,0,1-.89-.62l0,0a3.12,3.12,0,0,1-.51-.58l-1-1.45a5.68,5.68,0,0,1,2.92-2.87l.67.34a.46.46,0,0,0,.66-.41v-.32a5.06,5.06,0,0,1,.69-.06l.79.79a.45.45,0,0,1,0,.64l-.13.14-.29.29a.22.22,0,0,0,0,.32l.13.13a.22.22,0,0,1,0,.32l-.22.22a.21.21,0,0,1-.16.07H9.65a.2.2,0,0,0-.15.06l-.28.28a.21.21,0,0,0-.05.26l.44.88a.22.22,0,0,1-.2.33H9.25A.22.22,0,0,1,9.1,106l-.26-.23a.46.46,0,0,0-.44-.08l-.88.29a.33.33,0,0,0-.23.32.33.33,0,0,0,.19.3l.31.16a2,2,0,0,0,.86.2c.29,0,.63.77.9.9h1.88a.91.91,0,0,1,.64.27l.39.38a.88.88,0,0,1,.25.61A1.29,1.29,0,0,1,12.32,110.09Zm2.45-2.57a.74.74,0,0,1-.4-.29l-.51-.76a.68.68,0,0,1,0-.75l.56-.83a.58.58,0,0,1,.26-.23l.36-.18a5.48,5.48,0,0,1,.61,2.52,5.83,5.83,0,0,1-.06.72Z" transform="translate(-3)"/><path id="_00d177" data-name="00d177" class="cls-2" d="M10,0a7,7,0,1,0,7,7A7,7,0,0,0,10,0Zm2.32,10.09-.32.32a.75.75,0,0,0-.17.3,3.79,3.79,0,0,1-.14.48l-.49,1.32a5.26,5.26,0,0,1-1.2.14v-.78a2.11,2.11,0,0,0-.64-1.44.9.9,0,0,1-.26-.64V8.88a.9.9,0,0,0-.47-.79c-.4-.22-1-.53-1.38-.73a4.18,4.18,0,0,1-.89-.62l0,0a3.12,3.12,0,0,1-.51-.58l-1-1.45A5.68,5.68,0,0,1,7.77,1.82l.67.34a.46.46,0,0,0,.66-.41V1.43a5.06,5.06,0,0,1,.69-.06l.79.79a.45.45,0,0,1,0,.64l-.13.14-.29.29a.22.22,0,0,0,0,.32l.13.13a.22.22,0,0,1,0,.32l-.22.22a.21.21,0,0,1-.16.07H9.65a.2.2,0,0,0-.15.06l-.28.28a.21.21,0,0,0-.05.26l.44.88a.22.22,0,0,1-.2.33H9.25A.22.22,0,0,1,9.1,6l-.26-.23a.46.46,0,0,0-.44-.08L7.52,6a.33.33,0,0,0-.23.32.33.33,0,0,0,.19.3l.31.16a2,2,0,0,0,.86.2c.29,0,.63.77.9.9h1.88a.91.91,0,0,1,.64.27l.39.38a.88.88,0,0,1,.25.61A1.29,1.29,0,0,1,12.32,10.09Zm2.45-2.57a.74.74,0,0,1-.4-.29l-.51-.76a.68.68,0,0,1,0-.75l.56-.83a.58.58,0,0,1,.26-.23L15,4.48A5.48,5.48,0,0,1,15.65,7a5.83,5.83,0,0,1-.06.72Z" transform="translate(-3)"/></svg>
|
Before Width: | Height: | Size: 2.2 KiB |
|
@ -1 +0,0 @@
|
|||
<svg id="icon-home" xmlns="http://www.w3.org/2000/svg" width="16.11" height="14" viewBox="0 0 16.11 14"><defs><style>.cls-1{fill:#00d177;}</style></defs><title>navigation-icon-master</title><path id="_00d177" data-name="00d177" class="cls-1" d="M17.69,6.34,15.94,4.81V1.59A.79.79,0,0,0,15.15.8H14.09a.79.79,0,0,0-.79.79v.93L10.7.26A1,1,0,0,0,10.27,0h0l-.16,0H9.91L9.75,0h0A1,1,0,0,0,9.3.26l-7,6.08A1.06,1.06,0,0,0,2.2,7.83,1.12,1.12,0,0,0,3,8.19H4.06v4.75A1.06,1.06,0,0,0,5.11,14h3.6V9.51h2.74V14h3.44a1.06,1.06,0,0,0,1-1.06V8.19H17a1.12,1.12,0,0,0,.78-.36A1.06,1.06,0,0,0,17.69,6.34Z" transform="translate(-1.95)"/></svg>
|
Before Width: | Height: | Size: 622 B |
|
@ -1 +0,0 @@
|
|||
<svg id="icon-home" xmlns="http://www.w3.org/2000/svg" width="16.11" height="14" viewBox="0 0 16.11 14"><defs><style>.cls-1{fill:#d8d8d8;}</style></defs><title>navigation-icon-master</title><path id="d8d8d8" class="cls-1" d="M17.69,6.34,15.94,4.81V1.59A.79.79,0,0,0,15.15.8H14.09a.79.79,0,0,0-.79.79v.93L10.7.26A1,1,0,0,0,10.27,0h0l-.16,0H9.91L9.75,0h0A1,1,0,0,0,9.3.26l-7,6.08A1.06,1.06,0,0,0,2.2,7.83,1.12,1.12,0,0,0,3,8.19H4.06v4.75A1.06,1.06,0,0,0,5.11,14h3.6V9.51h2.74V14h3.44a1.06,1.06,0,0,0,1-1.06V8.19H17a1.12,1.12,0,0,0,.78-.36A1.06,1.06,0,0,0,17.69,6.34Z" transform="translate(-1.95)"/></svg>
|
Before Width: | Height: | Size: 602 B |
|
@ -1 +0,0 @@
|
|||
<svg id="icon-home" xmlns="http://www.w3.org/2000/svg" width="16.11" height="114" viewBox="0 0 16.11 114"><defs><style>.cls-1{fill:#d8d8d8;}.cls-2{fill:#00d177;}</style></defs><title>icon-home-sprite</title><path id="d8d8d8" class="cls-1" d="M17.69,106.34l-1.75-1.53v-3.22a.79.79,0,0,0-.79-.79H14.09a.79.79,0,0,0-.79.79v.93l-2.6-2.26a1,1,0,0,0-.43-.23h0l-.16,0H9.91l-.16,0h0a1,1,0,0,0-.43.23l-7,6.08a1.06,1.06,0,0,0-.11,1.49,1.12,1.12,0,0,0,.78.36H4.06v4.75A1.06,1.06,0,0,0,5.11,114h3.6v-4.49h2.74V114h3.44a1.06,1.06,0,0,0,1-1.06v-4.75H17a1.12,1.12,0,0,0,.78-.36A1.06,1.06,0,0,0,17.69,106.34Z" transform="translate(-1.95)"/><path id="_00d177" data-name="00d177" class="cls-2" d="M17.69,6.34,15.94,4.81V1.59A.79.79,0,0,0,15.15.8H14.09a.79.79,0,0,0-.79.79v.93L10.7.26A1,1,0,0,0,10.27,0h0l-.16,0H9.91L9.75,0h0A1,1,0,0,0,9.3.26l-7,6.08A1.06,1.06,0,0,0,2.2,7.83,1.12,1.12,0,0,0,3,8.19H4.06v4.75A1.06,1.06,0,0,0,5.11,14h3.6V9.51h2.74V14h3.44a1.06,1.06,0,0,0,1-1.06V8.19H17a1.12,1.12,0,0,0,.78-.36A1.06,1.06,0,0,0,17.69,6.34Z" transform="translate(-1.95)"/></svg>
|
Before Width: | Height: | Size: 1.0 KiB |
|
@ -1 +0,0 @@
|
|||
<svg id="icon-messages" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><defs><style>.cls-1{fill:#00d177;}</style></defs><title>icon-messages-00d177</title><path id="_00d177" data-name="00d177" class="cls-1" d="M17,14,15.4,9.21a6.52,6.52,0,1,0-3.19,3.19Z" transform="translate(-3)"/></svg>
|
Before Width: | Height: | Size: 319 B |
|
@ -1 +0,0 @@
|
|||
<svg id="icon-messages" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><defs><style>.cls-1{fill:#d8d8d8;}</style></defs><title>icon-messages-d8d8d8</title><path id="d8d8d8" class="cls-1" d="M17,14,15.4,9.21a6.52,6.52,0,1,0-3.19,3.19Z" transform="translate(-3)"/></svg>
|
Before Width: | Height: | Size: 299 B |
|
@ -1 +0,0 @@
|
|||
<svg id="icon-messages" xmlns="http://www.w3.org/2000/svg" width="14" height="114" viewBox="0 0 14 114"><defs><style>.cls-1{fill:#d8d8d8;}.cls-2{fill:#00d177;}</style></defs><title>icon-messages-sprite</title><path id="d8d8d8" class="cls-1" d="M17,114l-1.6-4.79a6.52,6.52,0,1,0-3.19,3.19Z" transform="translate(-3)"/><path id="_00d177" data-name="00d177" class="cls-2" d="M17,14,15.4,9.21a6.52,6.52,0,1,0-3.19,3.19Z" transform="translate(-3)"/></svg>
|
Before Width: | Height: | Size: 450 B |
|
@ -1 +0,0 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="16" height="14" viewBox="0 0 16 14"><defs><style>.cls-1{fill:#b2f1d5;}.cls-2{fill:#333;}</style></defs><title>icon-new-gab</title><path class="cls-1" d="M8,11.5a8.14,8.14,0,0,1-2.45-.38l-.71-.22-.61.43a6.62,6.62,0,0,1-1.79.9A8.09,8.09,0,0,0,3.06,11l.33-.88-.64-.68A4.28,4.28,0,0,1,1.5,6.5c0-2.76,2.92-5,6.5-5s6.5,2.24,6.5,5S11.59,11.5,8,11.5Z"/><path class="cls-2" d="M4.5,5.5a1,1,0,1,0,1,1A1,1,0,0,0,4.5,5.5ZM8,5.5a1,1,0,1,0,1,1A1,1,0,0,0,8,5.5Zm3.5,0a1,1,0,1,0,1,1A1,1,0,0,0,11.5,5.5ZM8,0C3.58,0,0,2.91,0,6.5a5.72,5.72,0,0,0,1.66,3.95A8,8,0,0,1,.21,12.73.75.75,0,0,0,.75,14,7.55,7.55,0,0,0,5.1,12.55,9.47,9.47,0,0,0,8,13c4.42,0,8-2.91,8-6.5S12.42,0,8,0ZM8,11.5a8.14,8.14,0,0,1-2.45-.38l-.71-.22-.61.43a6.62,6.62,0,0,1-1.79.9A8.09,8.09,0,0,0,3.06,11l.33-.88-.64-.68A4.28,4.28,0,0,1,1.5,6.5c0-2.76,2.92-5,6.5-5s6.5,2.24,6.5,5S11.59,11.5,8,11.5Z"/></svg>
|
Before Width: | Height: | Size: 933 B |
|
@ -1 +0,0 @@
|
|||
<svg id="icon-notifications" xmlns="http://www.w3.org/2000/svg" width="13" height="14" viewBox="0 0 13 14"><defs><style>.cls-1{fill:#00d177;}</style></defs><title>icon-notifications-00d177</title><g id="_00d177" data-name="00d177"><path class="cls-1" d="M10,14a2,2,0,0,0,2-2H8A2,2,0,0,0,10,14Z" transform="translate(-3.5)"/><path class="cls-1" d="M16.29,10.73,15.1,9.4a3.14,3.14,0,0,1-.68-1.79V5.7a4.41,4.41,0,0,0-3.58-4.34V.84a.84.84,0,0,0-1.68,0v.52A4.41,4.41,0,0,0,5.58,5.7V7.61A3.14,3.14,0,0,1,4.9,9.4L3.71,10.73c-.38.42-.22.77.34.77H16C16.51,11.5,16.67,11.15,16.29,10.73Z" transform="translate(-3.5)"/></g></svg>
|
Before Width: | Height: | Size: 617 B |