diff --git a/app/javascript/gabsocial/actions/columns.js b/app/javascript/gabsocial/actions/columns.js deleted file mode 100644 index 36be749d..00000000 --- a/app/javascript/gabsocial/actions/columns.js +++ /dev/null @@ -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()); - }; -} diff --git a/app/javascript/gabsocial/actions/compose.js b/app/javascript/gabsocial/actions/compose.js index 34fd54f6..ec098608 100644 --- a/app/javascript/gabsocial/actions/compose.js +++ b/app/javascript/gabsocial/actions/compose.js @@ -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 })); } }; diff --git a/app/javascript/gabsocial/actions/notifications.js b/app/javascript/gabsocial/actions/notifications.js index 8266ac55..21b1cf3f 100644 --- a/app/javascript/gabsocial/actions/notifications.js +++ b/app/javascript/gabsocial/actions/notifications.js @@ -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, }); diff --git a/app/javascript/gabsocial/actions/streaming.js b/app/javascript/gabsocial/actions/streaming.js index 3876a4f5..e599805e 100644 --- a/app/javascript/gabsocial/actions/streaming.js +++ b/app/javascript/gabsocial/actions/streaming.js @@ -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))); diff --git a/app/javascript/gabsocial/actions/timelines.js b/app/javascript/gabsocial/actions/timelines.js index ee57145e..4214cbde 100644 --- a/app/javascript/gabsocial/actions/timelines.js +++ b/app/javascript/gabsocial/actions/timelines.js @@ -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']); diff --git a/app/javascript/gabsocial/components/scrollable_list.js b/app/javascript/gabsocial/components/scrollable_list.js index 2a94fa04..33662df8 100644 --- a/app/javascript/gabsocial/components/scrollable_list.js +++ b/app/javascript/gabsocial/components/scrollable_list.js @@ -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'; diff --git a/app/javascript/gabsocial/components/status_list.js b/app/javascript/gabsocial/components/status_list.js index 7bbe01cc..262f9635 100644 --- a/app/javascript/gabsocial/components/status_list.js +++ b/app/javascript/gabsocial/components/status_list.js @@ -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 ( - + return [ + , + {scrollableContent} - ); + ]; } } diff --git a/app/javascript/gabsocial/components/timeline_queue_button_header.js b/app/javascript/gabsocial/components/timeline_queue_button_header.js new file mode 100644 index 00000000..e1bfe6d0 --- /dev/null +++ b/app/javascript/gabsocial/components/timeline_queue_button_header.js @@ -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 ( +
+ + + +
+ ); + } +} diff --git a/app/javascript/gabsocial/features/community_timeline/components/column_settings.js b/app/javascript/gabsocial/features/community_timeline/components/column_settings.js index 8250190a..7adeb674 100644 --- a/app/javascript/gabsocial/features/community_timeline/components/column_settings.js +++ b/app/javascript/gabsocial/features/community_timeline/components/column_settings.js @@ -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 () { diff --git a/app/javascript/gabsocial/features/community_timeline/containers/column_settings_container.js b/app/javascript/gabsocial/features/community_timeline/containers/column_settings_container.js index 405064c3..0a756197 100644 --- a/app/javascript/gabsocial/features/community_timeline/containers/column_settings_container.js +++ b/app/javascript/gabsocial/features/community_timeline/containers/column_settings_container.js @@ -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)); }, }; }; diff --git a/app/javascript/gabsocial/features/community_timeline/index.js b/app/javascript/gabsocial/features/community_timeline/index.js index cb8ae3d3..d987d55c 100644 --- a/app/javascript/gabsocial/features/community_timeline/index.js +++ b/app/javascript/gabsocial/features/community_timeline/index.js @@ -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 ( @@ -86,7 +79,7 @@ class CommunityTimeline extends React.PureComponent { } diff --git a/app/javascript/gabsocial/features/direct_timeline/index.js b/app/javascript/gabsocial/features/direct_timeline/index.js index 9eab561e..1b82ae30 100644 --- a/app/javascript/gabsocial/features/direct_timeline/index.js +++ b/app/javascript/gabsocial/features/direct_timeline/index.js @@ -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 ( } diff --git a/app/javascript/gabsocial/features/favourited_statuses/index.js b/app/javascript/gabsocial/features/favourited_statuses/index.js index 81505629..439898aa 100644 --- a/app/javascript/gabsocial/features/favourited_statuses/index.js +++ b/app/javascript/gabsocial/features/favourited_statuses/index.js @@ -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 { } alwaysPrepend - scrollKey={`group_timeline-${columnId}`} + scrollKey='group_timeline' timelineId={`group:${id}`} onLoadMore={this.handleLoadMore} emptyMessage={} diff --git a/app/javascript/gabsocial/features/hashtag_timeline/components/column_settings.js b/app/javascript/gabsocial/features/hashtag_timeline/components/column_settings.js deleted file mode 100644 index cdc138c8..00000000 --- a/app/javascript/gabsocial/features/hashtag_timeline/components/column_settings.js +++ /dev/null @@ -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 ( -
- - {this.modeLabel(mode)} - - - -
- ); - } - - modeLabel (mode) { - switch(mode) { - case 'any': - return ; - case 'all': - return ; - case 'none': - return ; - default: - return ''; - } - }; - - render () { - return ( -
-
-
- - - - - -
-
- - {this.state.open && ( -
- {this.modeSelect('any')} - {this.modeSelect('all')} - {this.modeSelect('none')} -
- )} -
- ); - } - -} diff --git a/app/javascript/gabsocial/features/hashtag_timeline/containers/column_settings_container.js b/app/javascript/gabsocial/features/hashtag_timeline/containers/column_settings_container.js deleted file mode 100644 index c5098052..00000000 --- a/app/javascript/gabsocial/features/hashtag_timeline/containers/column_settings_container.js +++ /dev/null @@ -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); diff --git a/app/javascript/gabsocial/features/hashtag_timeline/index.js b/app/javascript/gabsocial/features/hashtag_timeline/index.js index 5948af79..edca2c1d 100644 --- a/app/javascript/gabsocial/features/hashtag_timeline/index.js +++ b/app/javascript/gabsocial/features/hashtag_timeline/index.js @@ -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 ( - - {columnId && } - - + } diff --git a/app/javascript/gabsocial/features/home_timeline/index.js b/app/javascript/gabsocial/features/home_timeline/index.js index b4a375b8..1448d44f 100644 --- a/app/javascript/gabsocial/features/home_timeline/index.js +++ b/app/javascript/gabsocial/features/home_timeline/index.js @@ -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 ( @@ -78,7 +77,7 @@ class HomeTimeline extends React.PureComponent { } diff --git a/app/javascript/gabsocial/features/list_timeline/index.js b/app/javascript/gabsocial/features/list_timeline/index.js index bd873b21..0dcb7662 100644 --- a/app/javascript/gabsocial/features/list_timeline/index.js +++ b/app/javascript/gabsocial/features/list_timeline/index.js @@ -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 { } diff --git a/app/javascript/gabsocial/features/notifications/index.js b/app/javascript/gabsocial/features/notifications/index.js index 011c6ecb..9f4a7c35 100644 --- a/app/javascript/gabsocial/features/notifications/index.js +++ b/app/javascript/gabsocial/features/notifications/index.js @@ -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 = ; let scrollableContent = null; @@ -150,7 +162,7 @@ class Notifications extends React.PureComponent { const scrollContainer = (
{filterBarContainer} + {scrollContainer}
); diff --git a/app/javascript/gabsocial/features/public_timeline/containers/column_settings_container.js b/app/javascript/gabsocial/features/public_timeline/containers/column_settings_container.js index c56caa59..b8abeb27 100644 --- a/app/javascript/gabsocial/features/public_timeline/containers/column_settings_container.js +++ b/app/javascript/gabsocial/features/public_timeline/containers/column_settings_container.js @@ -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)); }, }; }; diff --git a/app/javascript/gabsocial/features/public_timeline/index.js b/app/javascript/gabsocial/features/public_timeline/index.js index 99949244..da40ca81 100644 --- a/app/javascript/gabsocial/features/public_timeline/index.js +++ b/app/javascript/gabsocial/features/public_timeline/index.js @@ -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 ( - + } /> diff --git a/app/javascript/gabsocial/features/ui/containers/status_list_container.js b/app/javascript/gabsocial/features/ui/containers/status_list_container.js index baa0f901..ee58b9ed 100644 --- a/app/javascript/gabsocial/features/ui/containers/status_list_container.js +++ b/app/javascript/gabsocial/features/ui/containers/status_list_container.js @@ -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); diff --git a/app/javascript/gabsocial/reducers/notifications.js b/app/javascript/gabsocial/reducers/notifications.js index 4d9604de..7531165f 100644 --- a/app/javascript/gabsocial/reducers/notifications.js +++ b/app/javascript/gabsocial/reducers/notifications.js @@ -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: diff --git a/app/javascript/gabsocial/reducers/settings.js b/app/javascript/gabsocial/reducers/settings.js index 558180f8..99f0c718 100644 --- a/app/javascript/gabsocial/reducers/settings.js +++ b/app/javascript/gabsocial/reducers/settings.js @@ -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: diff --git a/app/javascript/gabsocial/reducers/timelines.js b/app/javascript/gabsocial/reducers/timelines.js index 8e20bdf3..b0c1babf 100644 --- a/app/javascript/gabsocial/reducers/timelines.js +++ b/app/javascript/gabsocial/reducers/timelines.js @@ -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: diff --git a/app/javascript/styles/gabsocial/components.scss b/app/javascript/styles/gabsocial/components.scss index bc52cba8..11373a5a 100644 --- a/app/javascript/styles/gabsocial/components.scss +++ b/app/javascript/styles/gabsocial/components.scss @@ -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; + } +} diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 02e9cb64..c903f9d1 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -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 diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index ea5e4788..f62fa66d 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -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) diff --git a/app/workers/push_conversation_worker.rb b/app/workers/push_conversation_worker.rb index f7a1c7a1..77f5895c 100644 --- a/app/workers/push_conversation_worker.rb +++ b/app/workers/push_conversation_worker.rb @@ -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 diff --git a/app/workers/push_update_worker.rb b/app/workers/push_update_worker.rb index 6ed4906b..a491848e 100644 --- a/app/workers/push_update_worker.rb +++ b/app/workers/push_update_worker.rb @@ -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