Merge branch 'feature/adding_queued_timeline' of https://code.gab.com/gab/social/gab-social into develop
This commit is contained in:
commit
4043355b01
@ -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 { useEmoji } from './emojis';
|
||||||
import resizeImage from '../utils/resize_image';
|
import resizeImage from '../utils/resize_image';
|
||||||
import { importFetchedAccounts } from './importer';
|
import { importFetchedAccounts } from './importer';
|
||||||
import { updateTimeline } from './timelines';
|
import { updateTimeline, dequeueTimeline } from './timelines';
|
||||||
import { showAlertForError } from './alerts';
|
import { showAlertForError } from './alerts';
|
||||||
import { showAlert } from './alerts';
|
import { showAlert } from './alerts';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
@ -168,6 +168,10 @@ export function submitCompose(routerHistory) {
|
|||||||
const timeline = getState().getIn(['timelines', timelineId]);
|
const timeline = getState().getIn(['timelines', timelineId]);
|
||||||
|
|
||||||
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
|
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 }));
|
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 = 'NOTIFICATIONS_UPDATE';
|
||||||
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
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_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
|
||||||
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
|
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_CLEAR = 'NOTIFICATIONS_CLEAR';
|
||||||
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||||
|
|
||||||
|
export const MAX_QUEUED_NOTIFICATIONS = 40;
|
||||||
|
|
||||||
defineMessages({
|
defineMessages({
|
||||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||||
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
||||||
@ -42,18 +46,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
|
|||||||
export function updateNotifications(notification, intlMessages, intlLocale) {
|
export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
|
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) {
|
if (showInColumn) {
|
||||||
dispatch(importFetchedAccount(notification.account));
|
dispatch(importFetchedAccount(notification.account));
|
||||||
@ -65,21 +57,33 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: NOTIFICATIONS_UPDATE,
|
type: NOTIFICATIONS_UPDATE,
|
||||||
notification,
|
notification,
|
||||||
meta: (playSound && !filtered) ? { sound: 'ribbit' } : undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchRelatedRelationships(dispatch, [notification]);
|
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
|
// Desktop notifications
|
||||||
if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
|
if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
|
||||||
const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
|
const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
|
||||||
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
|
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
|
||||||
|
|
||||||
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
|
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
|
||||||
|
|
||||||
@ -88,7 +92,49 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||||||
notify.close();
|
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();
|
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||||
@ -169,7 +215,7 @@ export function expandNotificationsFail(error, isLoadingMore) {
|
|||||||
export function clearNotifications() {
|
export function clearNotifications() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: NOTIFICATIONS_CLEAR,
|
type: NOTIFICATIONS_CLEAR,
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { connectStream } from '../stream';
|
import { connectStream } from '../stream';
|
||||||
import {
|
import {
|
||||||
updateTimeline,
|
|
||||||
deleteFromTimelines,
|
deleteFromTimelines,
|
||||||
expandHomeTimeline,
|
expandHomeTimeline,
|
||||||
connectTimeline,
|
connectTimeline,
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
|
updateTimelineQueue,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotificationsQueue, expandNotifications } from './notifications';
|
||||||
import { updateConversations } from './conversations';
|
import { updateConversations } from './conversations';
|
||||||
import { fetchFilters } from './filters';
|
import { fetchFilters } from './filters';
|
||||||
import { getLocale } from '../locales';
|
import { getLocale } from '../locales';
|
||||||
@ -30,13 +30,13 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
|
|||||||
onReceive (data) {
|
onReceive (data) {
|
||||||
switch(data.event) {
|
switch(data.event) {
|
||||||
case 'update':
|
case 'update':
|
||||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), accept));
|
dispatch(updateTimelineQueue(timelineId, JSON.parse(data.payload), accept));
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
dispatch(deleteFromTimelines(data.payload));
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
break;
|
break;
|
||||||
case 'notification':
|
case 'notification':
|
||||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
dispatch(updateNotificationsQueue(JSON.parse(data.payload), messages, locale, window.location.pathname));
|
||||||
break;
|
break;
|
||||||
case 'conversation':
|
case 'conversation':
|
||||||
dispatch(updateConversations(JSON.parse(data.payload)));
|
dispatch(updateConversations(JSON.parse(data.payload)));
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
import api, { getLinks } from '../api';
|
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_UPDATE = 'TIMELINE_UPDATE';
|
||||||
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
||||||
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
|
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_REQUEST = 'TIMELINE_EXPAND_REQUEST';
|
||||||
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
|
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_CONNECT = 'TIMELINE_CONNECT';
|
||||||
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
||||||
|
|
||||||
|
export const MAX_QUEUED_ITEMS = 40;
|
||||||
|
|
||||||
export function updateTimeline(timeline, status, accept) {
|
export function updateTimeline(timeline, status, accept) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
if (typeof accept === 'function' && !accept(status)) {
|
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) {
|
export function deleteFromTimelines(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const accountId = getState().getIn(['statuses', id, 'account']);
|
const accountId = getState().getIn(['statuses', id, 'account']);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { ScrollContainer } from 'react-router-scroll-4';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
||||||
import LoadMore from './load_more';
|
import LoadMore from './load_more';
|
||||||
|
@ -7,6 +7,7 @@ import StatusContainer from '../containers/status_container';
|
|||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import LoadGap from './load_gap';
|
import LoadGap from './load_gap';
|
||||||
import ScrollableList from './scrollable_list';
|
import ScrollableList from './scrollable_list';
|
||||||
|
import TimelineQueueButtonHeader from './timeline_queue_button_header';
|
||||||
|
|
||||||
export default class StatusList extends ImmutablePureComponent {
|
export default class StatusList extends ImmutablePureComponent {
|
||||||
|
|
||||||
@ -22,6 +23,12 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
emptyMessage: PropTypes.node,
|
emptyMessage: PropTypes.node,
|
||||||
alwaysPrepend: PropTypes.bool,
|
alwaysPrepend: PropTypes.bool,
|
||||||
timelineId: PropTypes.string,
|
timelineId: PropTypes.string,
|
||||||
|
queuedItemSize: PropTypes.number,
|
||||||
|
onDequeueTimeline: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.handleDequeueTimeline();
|
||||||
};
|
};
|
||||||
|
|
||||||
getFeaturedStatusCount = () => {
|
getFeaturedStatusCount = () => {
|
||||||
@ -64,13 +71,17 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDequeueTimeline = () => {
|
||||||
|
const { onDequeueTimeline, timelineId } = this.props;
|
||||||
|
onDequeueTimeline(timelineId);
|
||||||
|
}
|
||||||
|
|
||||||
setRef = c => {
|
setRef = c => {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { statusIds, featuredStatusIds, onLoadMore, timelineId, ...other } = this.props;
|
const { statusIds, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, ...other } = this.props;
|
||||||
const { isLoading, isPartial } = other;
|
|
||||||
|
|
||||||
if (isPartial) {
|
if (isPartial) {
|
||||||
return (
|
return (
|
||||||
@ -119,11 +130,12 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
)).concat(scrollableContent);
|
)).concat(scrollableContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return [
|
||||||
<ScrollableList {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
|
<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}
|
{scrollableContent}
|
||||||
</ScrollableList>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,6 @@ class ColumnSettings extends React.PureComponent {
|
|||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
columnId: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -1,26 +1,15 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ColumnSettings from '../components/column_settings';
|
import ColumnSettings from '../components/column_settings';
|
||||||
import { changeSetting } from '../../../actions/settings';
|
import { changeSetting } from '../../../actions/settings';
|
||||||
import { changeColumnParams } from '../../../actions/columns';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { columnId }) => {
|
const mapStateToProps = state => ({
|
||||||
const uuid = columnId;
|
settings: state.getIn(['settings', 'community']),
|
||||||
const columns = state.getIn(['settings', 'columns']);
|
});
|
||||||
const index = columns.findIndex(c => c.get('uuid') === uuid);
|
|
||||||
|
|
||||||
return {
|
const mapDispatchToProps = (dispatch) => {
|
||||||
settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'community']),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { columnId }) => {
|
|
||||||
return {
|
return {
|
||||||
onChange (key, checked) {
|
onChange (key, checked) {
|
||||||
if (columnId) {
|
dispatch(changeSetting(['community', ...key], checked));
|
||||||
dispatch(changeColumnParams(columnId, key, checked));
|
|
||||||
} else {
|
|
||||||
dispatch(changeSetting(['community', ...key], checked));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -13,16 +13,10 @@ const messages = defineMessages({
|
|||||||
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, { onlyMedia, columnId }) => {
|
const mapStateToProps = (state, { onlyMedia }) => ({
|
||||||
const uuid = columnId;
|
hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
|
||||||
const columns = state.getIn(['settings', 'columns']);
|
onlyMedia: state.getIn(['settings', 'community', 'other', 'onlyMedia']),
|
||||||
const index = columns.findIndex(c => c.get('uuid') === uuid);
|
});
|
||||||
|
|
||||||
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']),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
@ -38,7 +32,6 @@ class CommunityTimeline extends React.PureComponent {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
columnId: PropTypes.string,
|
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
onlyMedia: PropTypes.bool,
|
onlyMedia: PropTypes.bool,
|
||||||
@ -75,7 +68,7 @@ class CommunityTimeline extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hasUnread, columnId, onlyMedia } = this.props;
|
const { intl, hasUnread, onlyMedia } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)}>
|
<Column label={intl.formatMessage(messages.title)}>
|
||||||
@ -86,7 +79,7 @@ class CommunityTimeline extends React.PureComponent {
|
|||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
</HomeColumnHeader>
|
</HomeColumnHeader>
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
scrollKey={`community_timeline-${columnId}`}
|
scrollKey='community_timeline'
|
||||||
timelineId={`community${onlyMedia ? ':media' : ''}`}
|
timelineId={`community${onlyMedia ? ':media' : ''}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
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 local timeline is empty. Write something publicly to get the ball rolling!' />}
|
||||||
|
@ -18,7 +18,6 @@ class DirectTimeline extends React.PureComponent {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
columnId: PropTypes.string,
|
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
};
|
};
|
||||||
@ -45,14 +44,14 @@ class DirectTimeline extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hasUnread, columnId } = this.props;
|
const { intl, hasUnread } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)}>
|
<Column label={intl.formatMessage(messages.title)}>
|
||||||
<ColumnHeader icon='envelope' active={hasUnread} title={intl.formatMessage(messages.title)} />
|
<ColumnHeader icon='envelope' active={hasUnread} title={intl.formatMessage(messages.title)} />
|
||||||
|
|
||||||
<ConversationsListContainer
|
<ConversationsListContainer
|
||||||
scrollKey={`direct_timeline-${columnId}`}
|
scrollKey='direct_timeline'
|
||||||
timelineId='direct'
|
timelineId='direct'
|
||||||
onLoadMore={this.handleLoadMore}
|
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." />}
|
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,
|
dispatch: PropTypes.func.isRequired,
|
||||||
statusIds: ImmutablePropTypes.list.isRequired,
|
statusIds: ImmutablePropTypes.list.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
columnId: PropTypes.string,
|
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
isMyAccount: PropTypes.bool.isRequired,
|
isMyAccount: PropTypes.bool.isRequired,
|
||||||
@ -44,7 +43,7 @@ class Favourites extends ImmutablePureComponent {
|
|||||||
}, 300, { leading: true })
|
}, 300, { leading: true })
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, statusIds, columnId, hasMore, isLoading, isMyAccount } = this.props;
|
const { intl, statusIds, hasMore, isLoading, isMyAccount } = this.props;
|
||||||
|
|
||||||
if (!isMyAccount) {
|
if (!isMyAccount) {
|
||||||
return (
|
return (
|
||||||
@ -60,7 +59,7 @@ class Favourites extends ImmutablePureComponent {
|
|||||||
<Column>
|
<Column>
|
||||||
<StatusList
|
<StatusList
|
||||||
statusIds={statusIds}
|
statusIds={statusIds}
|
||||||
scrollKey={`favourited_statuses-${columnId}`}
|
scrollKey='favourited_statuses'
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
@ -30,7 +30,6 @@ class GroupTimeline extends React.PureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
columnId: PropTypes.string,
|
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
@ -59,7 +58,7 @@ class GroupTimeline extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { hasUnread, columnId, group } = this.props;
|
const { hasUnread, group } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
const title = group ? group.get('title') : id;
|
const title = group ? group.get('title') : id;
|
||||||
|
|
||||||
@ -93,7 +92,7 @@ class GroupTimeline extends React.PureComponent {
|
|||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
prepend={<HeaderContainer groupId={id} />}
|
prepend={<HeaderContainer groupId={id} />}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
scrollKey={`group_timeline-${columnId}`}
|
scrollKey='group_timeline'
|
||||||
timelineId={`group:${id}`}
|
timelineId={`group:${id}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group post new statuses, they will appear here.' />}
|
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group post new statuses, they will appear here.' />}
|
||||||
|
@ -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 StatusListContainer from '../ui/containers/status_list_container';
|
||||||
import Column from '../../components/column';
|
import Column from '../../components/column';
|
||||||
import ColumnHeader from '../../components/column_header';
|
import ColumnHeader from '../../components/column_header';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
|
||||||
import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines';
|
import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connectHashtagStream } from '../../actions/streaming';
|
import { connectHashtagStream } from '../../actions/streaming';
|
||||||
@ -21,7 +20,6 @@ class HashtagTimeline extends React.PureComponent {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
columnId: PropTypes.string,
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
};
|
};
|
||||||
@ -104,17 +102,14 @@ class HashtagTimeline extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { hasUnread, columnId } = this.props;
|
const { hasUnread } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={`#${id}`}>
|
<Column label={`#${id}`}>
|
||||||
<ColumnHeader icon='hashtag' active={hasUnread} title={this.title()}>
|
<ColumnHeader icon='hashtag' active={hasUnread} title={this.title()} />
|
||||||
{columnId && <ColumnSettingsContainer columnId={columnId} />}
|
|
||||||
</ColumnHeader>
|
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
scrollKey={`hashtag_timeline-${columnId}`}
|
scrollKey='hashtag_timeline'
|
||||||
timelineId={`hashtag:${id}`}
|
timelineId={`hashtag:${id}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
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,
|
intl: PropTypes.object.isRequired,
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
isPartial: PropTypes.bool,
|
isPartial: PropTypes.bool,
|
||||||
columnId: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleLoadMore = maxId => {
|
handleLoadMore = maxId => {
|
||||||
@ -67,7 +66,7 @@ class HomeTimeline extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hasUnread, columnId } = this.props;
|
const { intl, hasUnread } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)}>
|
<Column label={intl.formatMessage(messages.title)}>
|
||||||
@ -78,7 +77,7 @@ class HomeTimeline extends React.PureComponent {
|
|||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
</HomeColumnHeader>
|
</HomeColumnHeader>
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
scrollKey={`home_timeline-${columnId}`}
|
scrollKey='home_timeline'
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
timelineId='home'
|
timelineId='home'
|
||||||
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty. Start following other users to recieve their content here.'/>}
|
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 = {
|
static propTypes = {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
columnId: PropTypes.string,
|
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
@ -69,7 +68,7 @@ class ListTimeline extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteClick = () => {
|
handleDeleteClick = () => {
|
||||||
const { dispatch, columnId, intl } = this.props;
|
const { dispatch, intl } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
|
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
@ -77,18 +76,13 @@ class ListTimeline extends React.PureComponent {
|
|||||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
dispatch(deleteList(id));
|
dispatch(deleteList(id));
|
||||||
|
this.context.router.history.push('/lists');
|
||||||
if (!!columnId) {
|
|
||||||
//
|
|
||||||
} else {
|
|
||||||
this.context.router.history.push('/lists');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { hasUnread, columnId, list } = this.props;
|
const { hasUnread, list } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
const title = list ? list.get('title') : id;
|
const title = list ? list.get('title') : id;
|
||||||
|
|
||||||
@ -126,7 +120,7 @@ class ListTimeline extends React.PureComponent {
|
|||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
scrollKey={`list_timeline-${columnId}`}
|
scrollKey='list_timeline'
|
||||||
timelineId={`list:${id}`}
|
timelineId={`list:${id}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
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.' />}
|
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 ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Column from '../../components/column';
|
import Column from '../../components/column';
|
||||||
import ColumnHeader from '../../components/column_header';
|
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 NotificationContainer from './containers/notification_container';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
@ -14,6 +18,7 @@ import { List as ImmutableList } from 'immutable';
|
|||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import LoadGap from '../../components/load_gap';
|
import LoadGap from '../../components/load_gap';
|
||||||
|
import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||||
@ -40,6 +45,7 @@ const mapStateToProps = state => ({
|
|||||||
isLoading: state.getIn(['notifications', 'isLoading'], true),
|
isLoading: state.getIn(['notifications', 'isLoading'], true),
|
||||||
isUnread: state.getIn(['notifications', 'unread']) > 0,
|
isUnread: state.getIn(['notifications', 'unread']) > 0,
|
||||||
hasMore: state.getIn(['notifications', 'hasMore']),
|
hasMore: state.getIn(['notifications', 'hasMore']),
|
||||||
|
totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
@ -47,7 +53,6 @@ export default @connect(mapStateToProps)
|
|||||||
class Notifications extends React.PureComponent {
|
class Notifications extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
columnId: PropTypes.string,
|
|
||||||
notifications: ImmutablePropTypes.list.isRequired,
|
notifications: ImmutablePropTypes.list.isRequired,
|
||||||
showFilterBar: PropTypes.bool.isRequired,
|
showFilterBar: PropTypes.bool.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
@ -55,6 +60,8 @@ class Notifications extends React.PureComponent {
|
|||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
isUnread: PropTypes.bool,
|
isUnread: PropTypes.bool,
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
|
dequeueNotifications: PropTypes.func,
|
||||||
|
totalQueuedNotificationsCount: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
@ -62,6 +69,7 @@ class Notifications extends React.PureComponent {
|
|||||||
this.handleScrollToTop.cancel();
|
this.handleScrollToTop.cancel();
|
||||||
this.handleScroll.cancel();
|
this.handleScroll.cancel();
|
||||||
this.props.dispatch(scrollTopNotifications(false));
|
this.props.dispatch(scrollTopNotifications(false));
|
||||||
|
this.handleDequeueNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -113,8 +121,12 @@ class Notifications extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDequeueNotifications = () => {
|
||||||
|
this.props.dispatch(dequeueNotifications());
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
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." />;
|
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;
|
let scrollableContent = null;
|
||||||
@ -150,7 +162,7 @@ class Notifications extends React.PureComponent {
|
|||||||
|
|
||||||
const scrollContainer = (
|
const scrollContainer = (
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey={`notifications-${columnId}`}
|
scrollKey='notifications'
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
showLoading={isLoading && notifications.size === 0}
|
showLoading={isLoading && notifications.size === 0}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
@ -169,6 +181,7 @@ class Notifications extends React.PureComponent {
|
|||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
{filterBarContainer}
|
{filterBarContainer}
|
||||||
|
<TimelineQueueButtonHeader onClick={this.handleDequeueNotifications} count={totalQueuedNotificationsCount} itemType='notification' />
|
||||||
{scrollContainer}
|
{scrollContainer}
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
@ -1,26 +1,15 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ColumnSettings from '../../community_timeline/components/column_settings';
|
import ColumnSettings from '../../community_timeline/components/column_settings';
|
||||||
import { changeSetting } from '../../../actions/settings';
|
import { changeSetting } from '../../../actions/settings';
|
||||||
import { changeColumnParams } from '../../../actions/columns';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { columnId }) => {
|
const mapStateToProps = state => ({
|
||||||
const uuid = columnId;
|
settings: state.getIn(['settings', 'public']),
|
||||||
const columns = state.getIn(['settings', 'columns']);
|
});
|
||||||
const index = columns.findIndex(c => c.get('uuid') === uuid);
|
|
||||||
|
|
||||||
return {
|
const mapDispatchToProps = dispatch => {
|
||||||
settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'public']),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { columnId }) => {
|
|
||||||
return {
|
return {
|
||||||
onChange (key, checked) {
|
onChange (key, checked) {
|
||||||
if (columnId) {
|
dispatch(changeSetting(['public', ...key], checked));
|
||||||
dispatch(changeColumnParams(columnId, key, checked));
|
|
||||||
} else {
|
|
||||||
dispatch(changeSetting(['public', ...key], checked));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -13,14 +13,12 @@ const messages = defineMessages({
|
|||||||
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
|
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, { onlyMedia, columnId }) => {
|
const mapStateToProps = (state, { onlyMedia }) => {
|
||||||
const uuid = columnId;
|
|
||||||
const columns = state.getIn(['settings', 'columns']);
|
const columns = state.getIn(['settings', 'columns']);
|
||||||
const index = columns.findIndex(c => c.get('uuid') === uuid);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasUnread: state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
|
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']),
|
onlyMedia: state.getIn(['settings', 'public', 'other', 'onlyMedia']),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -39,7 +37,6 @@ class PublicTimeline extends React.PureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
columnId: PropTypes.string,
|
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
onlyMedia: PropTypes.bool,
|
onlyMedia: PropTypes.bool,
|
||||||
};
|
};
|
||||||
@ -80,13 +77,13 @@ class PublicTimeline extends React.PureComponent {
|
|||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.title)}>
|
<Column label={intl.formatMessage(messages.title)}>
|
||||||
<ColumnHeader icon='globe' active={hasUnread} title={intl.formatMessage(messages.title)}>
|
<ColumnHeader icon='globe' active={hasUnread} title={intl.formatMessage(messages.title)}>
|
||||||
<ColumnSettingsContainer columnId={columnId} />
|
<ColumnSettingsContainer/>
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
timelineId={`public${onlyMedia ? ':media' : ''}`}
|
timelineId={`public${onlyMedia ? ':media' : ''}`}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
scrollKey={`public_timeline-${columnId}`}
|
scrollKey='public_timeline'
|
||||||
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' />}
|
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>
|
</Column>
|
||||||
|
@ -4,6 +4,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { me } from '../../../initial_state';
|
import { me } from '../../../initial_state';
|
||||||
|
import { dequeueTimeline } from 'gabsocial/actions/timelines';
|
||||||
|
|
||||||
const makeGetStatusIds = () => createSelector([
|
const makeGetStatusIds = () => createSelector([
|
||||||
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
||||||
@ -28,17 +29,22 @@ const makeGetStatusIds = () => createSelector([
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const mapStateToProps = (state, {timelineId}) => {
|
||||||
const getStatusIds = makeGetStatusIds();
|
const getStatusIds = makeGetStatusIds();
|
||||||
|
|
||||||
const mapStateToProps = (state, { timelineId }) => ({
|
return {
|
||||||
statusIds: getStatusIds(state, { type: timelineId }),
|
statusIds: getStatusIds(state, { type: timelineId }),
|
||||||
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
|
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
|
||||||
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
||||||
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
|
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
|
||||||
});
|
totalQueuedItemsCount: state.getIn(['timelines', timelineId, 'totalQueuedItemsCount']),
|
||||||
|
};
|
||||||
return mapStateToProps;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(makeMapStateToProps)(StatusList);
|
const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||||
|
onDequeueTimeline(timelineId) {
|
||||||
|
dispatch(dequeueTimeline(timelineId, ownProps.onLoadMore));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(StatusList);
|
||||||
|
@ -6,6 +6,9 @@ import {
|
|||||||
NOTIFICATIONS_FILTER_SET,
|
NOTIFICATIONS_FILTER_SET,
|
||||||
NOTIFICATIONS_CLEAR,
|
NOTIFICATIONS_CLEAR,
|
||||||
NOTIFICATIONS_SCROLL_TOP,
|
NOTIFICATIONS_SCROLL_TOP,
|
||||||
|
NOTIFICATIONS_UPDATE_QUEUE,
|
||||||
|
NOTIFICATIONS_DEQUEUE,
|
||||||
|
MAX_QUEUED_NOTIFICATIONS,
|
||||||
} from '../actions/notifications';
|
} from '../actions/notifications';
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
@ -21,6 +24,8 @@ const initialState = ImmutableMap({
|
|||||||
top: false,
|
top: false,
|
||||||
unread: 0,
|
unread: 0,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
|
||||||
|
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationToMap = notification => ImmutableMap({
|
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));
|
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) {
|
export default function notifications(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case NOTIFICATIONS_EXPAND_REQUEST:
|
case NOTIFICATIONS_EXPAND_REQUEST:
|
||||||
@ -105,6 +136,13 @@ export default function notifications(state = initialState, action) {
|
|||||||
return updateTop(state, action.top);
|
return updateTop(state, action.top);
|
||||||
case NOTIFICATIONS_UPDATE:
|
case NOTIFICATIONS_UPDATE:
|
||||||
return normalizeNotification(state, action.notification);
|
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:
|
case NOTIFICATIONS_EXPAND_SUCCESS:
|
||||||
return expandNormalizedNotifications(state, action.notifications, action.next);
|
return expandNormalizedNotifications(state, action.notifications, action.next);
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case ACCOUNT_BLOCK_SUCCESS:
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings';
|
import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings';
|
||||||
import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
|
import { NOTIFICATIONS_FILTER_SET } from '../actions/notifications';
|
||||||
import { COLUMN_PARAMS_CHANGE } from '../actions/columns';
|
|
||||||
import { STORE_HYDRATE } from '../actions/store';
|
import { STORE_HYDRATE } from '../actions/store';
|
||||||
import { EMOJI_USE } from '../actions/emojis';
|
import { EMOJI_USE } from '../actions/emojis';
|
||||||
import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
|
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 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 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));
|
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
|
return state
|
||||||
.setIn(action.path, action.value)
|
.setIn(action.path, action.value)
|
||||||
.set('saved', false);
|
.set('saved', false);
|
||||||
case COLUMN_PARAMS_CHANGE:
|
|
||||||
return changeColumnParams(state, action.uuid, action.path, action.value);
|
|
||||||
case EMOJI_USE:
|
case EMOJI_USE:
|
||||||
return updateFrequentEmojis(state, action.emoji);
|
return updateFrequentEmojis(state, action.emoji);
|
||||||
case SETTING_SAVE:
|
case SETTING_SAVE:
|
||||||
|
@ -7,6 +7,9 @@ import {
|
|||||||
TIMELINE_EXPAND_FAIL,
|
TIMELINE_EXPAND_FAIL,
|
||||||
TIMELINE_CONNECT,
|
TIMELINE_CONNECT,
|
||||||
TIMELINE_DISCONNECT,
|
TIMELINE_DISCONNECT,
|
||||||
|
TIMELINE_UPDATE_QUEUE,
|
||||||
|
TIMELINE_DEQUEUE,
|
||||||
|
MAX_QUEUED_ITEMS,
|
||||||
} from '../actions/timelines';
|
} from '../actions/timelines';
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
@ -25,6 +28,8 @@ const initialTimeline = ImmutableMap({
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
items: ImmutableList(),
|
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) => {
|
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) => {
|
const deleteStatus = (state, id, accountId, references, exclude_account = null) => {
|
||||||
state.keySeq().forEach(timeline => {
|
state.keySeq().forEach(timeline => {
|
||||||
if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`)))
|
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);
|
return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent);
|
||||||
case TIMELINE_UPDATE:
|
case TIMELINE_UPDATE:
|
||||||
return updateTimeline(state, action.timeline, fromJS(action.status));
|
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:
|
case TIMELINE_DELETE:
|
||||||
return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
|
return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
|
||||||
case TIMELINE_CLEAR:
|
case TIMELINE_CLEAR:
|
||||||
|
@ -5104,3 +5104,25 @@ noscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeline-queue-header {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 52px;
|
||||||
|
position: relative;
|
||||||
|
background-color: darken($ui-base-color, 8%);
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
border-top: 1px solid;
|
||||||
|
border-color: darken($ui-base-color, 4%);
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 52px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $secondary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -30,7 +30,7 @@ class FeedManager
|
|||||||
def push_to_home(account, status)
|
def push_to_home(account, status)
|
||||||
return false unless add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
|
return false unless add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
|
||||||
trim(:home, account.id)
|
trim(:home, account.id)
|
||||||
#PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}") if push_update_required?("timeline:#{account.id}")
|
PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}") if push_update_required?("timeline:#{account.id}")
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ class FeedManager
|
|||||||
end
|
end
|
||||||
return false unless add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
return false unless add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||||
trim(:list, list.id)
|
trim(:list, list.id)
|
||||||
#PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
|
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ class FanOutOnWriteService < BaseService
|
|||||||
|
|
||||||
Rails.logger.debug "Delivering status #{status.id} to group"
|
Rails.logger.debug "Delivering status #{status.id} to group"
|
||||||
|
|
||||||
# Redis.current.publish("timeline:group:#{status.group_id}", @payload)
|
Redis.current.publish("timeline:group:#{status.group_id}", @payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
def deliver_to_mentioned_followers(status)
|
def deliver_to_mentioned_followers(status)
|
||||||
@ -89,15 +89,15 @@ class FanOutOnWriteService < BaseService
|
|||||||
def deliver_to_public(status)
|
def deliver_to_public(status)
|
||||||
Rails.logger.debug "Delivering status #{status.id} to public timeline"
|
Rails.logger.debug "Delivering status #{status.id} to public timeline"
|
||||||
|
|
||||||
# Redis.current.publish('timeline:public', @payload)
|
Redis.current.publish('timeline:public', @payload)
|
||||||
# Redis.current.publish('timeline:public:local', @payload) if status.local?
|
Redis.current.publish('timeline:public:local', @payload) if status.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def deliver_to_media(status)
|
def deliver_to_media(status)
|
||||||
Rails.logger.debug "Delivering status #{status.id} to media timeline"
|
Rails.logger.debug "Delivering status #{status.id} to media timeline"
|
||||||
|
|
||||||
# Redis.current.publish('timeline:public:media', @payload)
|
Redis.current.publish('timeline:public:media', @payload)
|
||||||
# Redis.current.publish('timeline:public:local:media', @payload) if status.local?
|
Redis.current.publish('timeline:public:local:media', @payload) if status.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
def deliver_to_own_conversation(status)
|
def deliver_to_own_conversation(status)
|
||||||
|
@ -7,7 +7,7 @@ class PushConversationWorker
|
|||||||
conversation = AccountConversation.find(conversation_account_id)
|
conversation = AccountConversation.find(conversation_account_id)
|
||||||
message = InlineRenderer.render(conversation, conversation.account, :conversation)
|
message = InlineRenderer.render(conversation, conversation.account, :conversation)
|
||||||
timeline_id = "timeline:direct:#{conversation.account_id}"
|
timeline_id = "timeline:direct:#{conversation.account_id}"
|
||||||
# Redis.current.publish(timeline_id, Oj.dump(event: :conversation, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
Redis.current.publish(timeline_id, Oj.dump(event: :conversation, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
||||||
true
|
true
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
|
@ -9,7 +9,7 @@ class PushUpdateWorker
|
|||||||
message = InlineRenderer.render(status, account, :status)
|
message = InlineRenderer.render(status, account, :status)
|
||||||
timeline_id = "timeline:#{account.id}" if timeline_id.nil?
|
timeline_id = "timeline:#{account.id}" if timeline_id.nil?
|
||||||
|
|
||||||
# Redis.current.publish(timeline_id, Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
Redis.current.publish(timeline_id, Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
||||||
true
|
true
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
|
Loading…
x
Reference in New Issue
Block a user