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 resizeImage from '../utils/resize_image';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { updateTimeline } from './timelines';
|
||||
import { updateTimeline, dequeueTimeline } from './timelines';
|
||||
import { showAlertForError } from './alerts';
|
||||
import { showAlert } from './alerts';
|
||||
import { defineMessages } from 'react-intl';
|
||||
@ -168,6 +168,10 @@ export function submitCompose(routerHistory) {
|
||||
const timeline = getState().getIn(['timelines', timelineId]);
|
||||
|
||||
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
|
||||
let dequeueArgs = {};
|
||||
if (timelineId === 'community') dequeueArgs.onlyMedia = getState().getIn(['settings', 'community', 'other', 'onlyMedia']),
|
||||
|
||||
dispatch(dequeueTimeline(timelineId, null, dequeueArgs));
|
||||
dispatch(updateTimeline(timelineId, { ...response.data }));
|
||||
}
|
||||
};
|
||||
|
@ -16,6 +16,8 @@ import { me } from 'gabsocial/initial_state';
|
||||
|
||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
||||
export const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE';
|
||||
export const NOTIFICATIONS_DEQUEUE = 'NOTIFICATIONS_DEQUEUE';
|
||||
|
||||
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
|
||||
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
|
||||
@ -26,6 +28,8 @@ export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
|
||||
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
||||
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||
|
||||
export const MAX_QUEUED_NOTIFICATIONS = 40;
|
||||
|
||||
defineMessages({
|
||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
||||
@ -42,18 +46,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
|
||||
export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||
return (dispatch, getState) => {
|
||||
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
|
||||
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
||||
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
||||
const filters = getFilters(getState(), { contextType: 'notifications' });
|
||||
|
||||
let filtered = false;
|
||||
|
||||
if (notification.type === 'mention') {
|
||||
const regex = regexFromFilters(filters);
|
||||
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
|
||||
|
||||
filtered = regex && regex.test(searchIndex);
|
||||
}
|
||||
|
||||
if (showInColumn) {
|
||||
dispatch(importFetchedAccount(notification.account));
|
||||
@ -65,21 +57,33 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_UPDATE,
|
||||
notification,
|
||||
meta: (playSound && !filtered) ? { sound: 'ribbit' } : undefined,
|
||||
});
|
||||
|
||||
fetchRelatedRelationships(dispatch, [notification]);
|
||||
} else if (playSound && !filtered) {
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_UPDATE_NOOP,
|
||||
meta: { sound: 'ribbit' },
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export function updateNotificationsQueue(notification, intlMessages, intlLocale, curPath) {
|
||||
return (dispatch, getState) => {
|
||||
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
|
||||
const filters = getFilters(getState(), { contextType: 'notifications' });
|
||||
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
|
||||
|
||||
let filtered = false;
|
||||
|
||||
const isOnNotificationsPage = curPath === '/notifications';
|
||||
|
||||
if (notification.type === 'mention') {
|
||||
const regex = regexFromFilters(filters);
|
||||
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
|
||||
filtered = regex && regex.test(searchIndex);
|
||||
}
|
||||
|
||||
// Desktop notifications
|
||||
if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
|
||||
const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
|
||||
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
|
||||
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
|
||||
|
||||
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
|
||||
|
||||
@ -88,7 +92,49 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||
notify.close();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (playSound && !filtered) {
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_UPDATE_NOOP,
|
||||
meta: { sound: 'ribbit' },
|
||||
});
|
||||
}
|
||||
|
||||
if (isOnNotificationsPage) {
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_UPDATE_QUEUE,
|
||||
notification,
|
||||
intlMessages,
|
||||
intlLocale,
|
||||
});
|
||||
}
|
||||
else {
|
||||
dispatch(updateNotifications(notification, intlMessages, intlLocale));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function dequeueNotifications() {
|
||||
return (dispatch, getState) => {
|
||||
const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableList());
|
||||
const totalQueuedNotificationsCount = getState().getIn(['notifications', 'totalQueuedNotificationsCount'], 0);
|
||||
|
||||
if (totalQueuedNotificationsCount == 0) {
|
||||
return;
|
||||
}
|
||||
else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
|
||||
queuedNotifications.forEach(block => {
|
||||
dispatch(updateNotifications(block.notification, block.intlMessages, block.intlLocale));
|
||||
});
|
||||
}
|
||||
else {
|
||||
dispatch(expandNotifications());
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_DEQUEUE,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||
@ -169,7 +215,7 @@ export function expandNotificationsFail(error, isLoadingMore) {
|
||||
export function clearNotifications() {
|
||||
return (dispatch, getState) => {
|
||||
if (!me) return;
|
||||
|
||||
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_CLEAR,
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this.handleDequeueTimeline();
|
||||
};
|
||||
|
||||
getFeaturedStatusCount = () => {
|
||||
@ -64,13 +71,17 @@ export default class StatusList extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleDequeueTimeline = () => {
|
||||
const { onDequeueTimeline, timelineId } = this.props;
|
||||
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 +130,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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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 () {
|
||||
|
@ -1,26 +1,15 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
import { changeSetting } from '../../../actions/settings';
|
||||
import { changeColumnParams } from '../../../actions/columns';
|
||||
|
||||
const mapStateToProps = (state, { columnId }) => {
|
||||
const uuid = columnId;
|
||||
const columns = state.getIn(['settings', 'columns']);
|
||||
const index = columns.findIndex(c => c.get('uuid') === uuid);
|
||||
const mapStateToProps = state => ({
|
||||
settings: state.getIn(['settings', 'community']),
|
||||
});
|
||||
|
||||
return {
|
||||
settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'community']),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { columnId }) => {
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
onChange (key, checked) {
|
||||
if (columnId) {
|
||||
dispatch(changeColumnParams(columnId, key, checked));
|
||||
} else {
|
||||
dispatch(changeSetting(['community', ...key], checked));
|
||||
}
|
||||
dispatch(changeSetting(['community', ...key], checked));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -13,16 +13,10 @@ const messages = defineMessages({
|
||||
title: { id: 'column.community', defaultMessage: 'Local 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', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
|
||||
onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']),
|
||||
};
|
||||
};
|
||||
const mapStateToProps = (state, { onlyMedia }) => ({
|
||||
hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
|
||||
onlyMedia: state.getIn(['settings', 'community', 'other', 'onlyMedia']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
@ -38,7 +32,6 @@ class CommunityTimeline extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
onlyMedia: PropTypes.bool,
|
||||
@ -75,7 +68,7 @@ class CommunityTimeline extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, hasUnread, columnId, onlyMedia } = this.props;
|
||||
const { intl, hasUnread, onlyMedia } = this.props;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
@ -86,7 +79,7 @@ class CommunityTimeline extends React.PureComponent {
|
||||
<ColumnSettingsContainer />
|
||||
</HomeColumnHeader>
|
||||
<StatusListContainer
|
||||
scrollKey={`community_timeline-${columnId}`}
|
||||
scrollKey='community_timeline'
|
||||
timelineId={`community${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!' />}
|
||||
|
@ -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}
|
||||
|
@ -30,7 +30,6 @@ class GroupTimeline extends React.PureComponent {
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
hasUnread: PropTypes.bool,
|
||||
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
intl: PropTypes.object.isRequired,
|
||||
@ -59,7 +58,7 @@ class GroupTimeline extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { hasUnread, columnId, group } = this.props;
|
||||
const { hasUnread, group } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const title = group ? group.get('title') : id;
|
||||
|
||||
@ -93,7 +92,7 @@ class GroupTimeline extends React.PureComponent {
|
||||
<StatusListContainer
|
||||
prepend={<HeaderContainer groupId={id} />}
|
||||
alwaysPrepend
|
||||
scrollKey={`group_timeline-${columnId}`}
|
||||
scrollKey='group_timeline'
|
||||
timelineId={`group:${id}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group post new statuses, they will appear here.' />}
|
||||
|
@ -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');
|
||||
}
|
||||
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 () {
|
||||
@ -62,6 +69,7 @@ class Notifications extends React.PureComponent {
|
||||
this.handleScrollToTop.cancel();
|
||||
this.handleScroll.cancel();
|
||||
this.props.dispatch(scrollTopNotifications(false));
|
||||
this.handleDequeueNotifications();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -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>
|
||||
);
|
||||
|
@ -1,26 +1,15 @@
|
||||
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);
|
||||
const mapStateToProps = state => ({
|
||||
settings: state.getIn(['settings', 'public']),
|
||||
});
|
||||
|
||||
return {
|
||||
settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'public']),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { columnId }) => {
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onChange (key, checked) {
|
||||
if (columnId) {
|
||||
dispatch(changeColumnParams(columnId, key, checked));
|
||||
} else {
|
||||
dispatch(changeSetting(['public', ...key], checked));
|
||||
}
|
||||
dispatch(changeSetting(['public', ...key], checked));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -13,14 +13,12 @@ const messages = defineMessages({
|
||||
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { onlyMedia, columnId }) => {
|
||||
const uuid = columnId;
|
||||
const mapStateToProps = (state, { onlyMedia }) => {
|
||||
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']),
|
||||
onlyMedia: state.getIn(['settings', 'public', 'other', 'onlyMedia']),
|
||||
};
|
||||
};
|
||||
|
||||
@ -39,7 +37,6 @@ class PublicTimeline extends React.PureComponent {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
hasUnread: PropTypes.bool,
|
||||
onlyMedia: PropTypes.bool,
|
||||
};
|
||||
@ -80,13 +77,13 @@ class PublicTimeline extends React.PureComponent {
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader icon='globe' active={hasUnread} title={intl.formatMessage(messages.title)}>
|
||||
<ColumnSettingsContainer columnId={columnId} />
|
||||
<ColumnSettingsContainer/>
|
||||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
timelineId={`public${onlyMedia ? ':media' : ''}`}
|
||||
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' />}
|
||||
/>
|
||||
</Column>
|
||||
|
@ -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);
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
return false unless add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
|
||||
trim(:home, account.id)
|
||||
#PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}") if push_update_required?("timeline:#{account.id}")
|
||||
PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}") if push_update_required?("timeline:#{account.id}")
|
||||
true
|
||||
end
|
||||
|
||||
@ -48,7 +48,7 @@ class FeedManager
|
||||
end
|
||||
return false unless add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||
trim(:list, list.id)
|
||||
#PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
|
||||
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
|
||||
true
|
||||
end
|
||||
|
||||
|
@ -61,7 +61,7 @@ class FanOutOnWriteService < BaseService
|
||||
|
||||
Rails.logger.debug "Delivering status #{status.id} to group"
|
||||
|
||||
# Redis.current.publish("timeline:group:#{status.group_id}", @payload)
|
||||
Redis.current.publish("timeline:group:#{status.group_id}", @payload)
|
||||
end
|
||||
|
||||
def deliver_to_mentioned_followers(status)
|
||||
@ -89,15 +89,15 @@ class FanOutOnWriteService < BaseService
|
||||
def deliver_to_public(status)
|
||||
Rails.logger.debug "Delivering status #{status.id} to public timeline"
|
||||
|
||||
# Redis.current.publish('timeline:public', @payload)
|
||||
# Redis.current.publish('timeline:public:local', @payload) if status.local?
|
||||
Redis.current.publish('timeline:public', @payload)
|
||||
Redis.current.publish('timeline:public:local', @payload) if status.local?
|
||||
end
|
||||
|
||||
def deliver_to_media(status)
|
||||
Rails.logger.debug "Delivering status #{status.id} to media timeline"
|
||||
|
||||
# Redis.current.publish('timeline:public:media', @payload)
|
||||
# Redis.current.publish('timeline:public:local:media', @payload) if status.local?
|
||||
Redis.current.publish('timeline:public:media', @payload)
|
||||
Redis.current.publish('timeline:public:local:media', @payload) if status.local?
|
||||
end
|
||||
|
||||
def deliver_to_own_conversation(status)
|
||||
|
@ -7,7 +7,7 @@ class PushConversationWorker
|
||||
conversation = AccountConversation.find(conversation_account_id)
|
||||
message = InlineRenderer.render(conversation, conversation.account, :conversation)
|
||||
timeline_id = "timeline:direct:#{conversation.account_id}"
|
||||
# Redis.current.publish(timeline_id, Oj.dump(event: :conversation, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
||||
Redis.current.publish(timeline_id, Oj.dump(event: :conversation, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
||||
true
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
|
@ -9,7 +9,7 @@ class PushUpdateWorker
|
||||
message = InlineRenderer.render(status, account, :status)
|
||||
timeline_id = "timeline:#{account.id}" if timeline_id.nil?
|
||||
|
||||
# Redis.current.publish(timeline_id, Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
||||
Redis.current.publish(timeline_id, Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
||||
true
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
|
Loading…
x
Reference in New Issue
Block a user