diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/assets/javascripts/components/actions/timelines.jsx index 91f06511a3..1fabe5d2ce 100644 --- a/app/assets/javascripts/components/actions/timelines.jsx +++ b/app/assets/javascripts/components/actions/timelines.jsx @@ -1,6 +1,6 @@ export const TIMELINE_SET = 'TIMELINE_SET'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; - +export const TIMELINE_DELETE = 'TIMELINE_DELETE'; export function setTimeline(timeline, statuses) { return { @@ -17,3 +17,10 @@ export function updateTimeline(timeline, status) { status: status }; } + +export function deleteFromTimeline(id) { + return { + type: TIMELINE_DELETE, + id: id + }; +} diff --git a/app/assets/javascripts/components/containers/root.jsx b/app/assets/javascripts/components/containers/root.jsx index e1fc31d554..ed53aee80a 100644 --- a/app/assets/javascripts/components/containers/root.jsx +++ b/app/assets/javascripts/components/containers/root.jsx @@ -1,9 +1,9 @@ -import { Provider } from 'react-redux'; -import configureStore from '../store/configureStore'; -import Frontend from '../components/frontend'; -import { setTimeline, updateTimeline } from '../actions/timelines'; -import { setAccessToken } from '../actions/meta'; -import PureRenderMixin from 'react-addons-pure-render-mixin'; +import { Provider } from 'react-redux'; +import configureStore from '../store/configureStore'; +import Frontend from '../components/frontend'; +import { setTimeline, updateTimeline, deleteFromTimelines } from '../actions/timelines'; +import { setAccessToken } from '../actions/meta'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; const store = configureStore(); @@ -32,7 +32,11 @@ const Root = React.createClass({ disconnected: function() {}, received: function(data) { - return store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message))); + if (data.type === 'update') { + return store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message))); + } else if (data.type === 'delete') { + return store.dispatch(deleteFromTimelines(data.id)); + } } }); } diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx index 9900489dfe..fb990ef54e 100644 --- a/app/assets/javascripts/components/reducers/timelines.jsx +++ b/app/assets/javascripts/components/reducers/timelines.jsx @@ -1,10 +1,10 @@ -import { TIMELINE_SET, TIMELINE_UPDATE } from '../actions/timelines'; -import { REBLOG_SUCCESS, FAVOURITE_SUCCESS } from '../actions/interactions'; -import Immutable from 'immutable'; +import { TIMELINE_SET, TIMELINE_UPDATE, TIMELINE_DELETE } from '../actions/timelines'; +import { REBLOG_SUCCESS, FAVOURITE_SUCCESS } from '../actions/interactions'; +import Immutable from 'immutable'; const initialState = Immutable.Map({ - home: Immutable.List(), - mentions: Immutable.List(), + home: Immutable.List([]), + mentions: Immutable.List([]), statuses: Immutable.Map(), accounts: Immutable.Map() }); @@ -44,12 +44,22 @@ function updateTimelineWithMaps(state, timeline, status) { return state; }; +function deleteStatus(state, id) { + ['home', 'mentions'].forEach(function (timeline) { + state = state.update(timeline, list => list.filterNot(item => item === id)); + }); + + return state.deleteIn(['statuses', id]); +}; + export default function timelines(state = initialState, action) { switch(action.type) { case TIMELINE_SET: return timelineToMaps(state, action.timeline, Immutable.fromJS(action.statuses)); case TIMELINE_UPDATE: - return updateTimelineWithMaps(state, action.timeline,Immutable.fromJS(action.status)); + return updateTimelineWithMaps(state, action.timeline, Immutable.fromJS(action.status)); + case TIMELINE_DELETE: + return deleteStatus(state, action.id); case REBLOG_SUCCESS: case FAVOURITE_SUCCESS: return statusToMaps(state, Immutable.fromJS(action.response)); diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 6e96fa75b0..d51681e538 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -31,7 +31,7 @@ class FanOutOnWriteService < BaseService def push(type, receiver, status) redis.zadd(FeedManager.key(type, receiver.id), status.id, status.id) trim(type, receiver) - ActionCable.server.broadcast("timeline:#{receiver.id}", timeline: type, message: inline_render(receiver, status)) + ActionCable.server.broadcast("timeline:#{receiver.id}", type: 'update', timeline: type, message: inline_render(receiver, status)) end def trim(type, receiver) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 1b5da6b2ab..db043df08b 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -1,10 +1,54 @@ class RemoveStatusService < BaseService def call(status) + remove_from_self(status) if status.account.local? + remove_from_followers(status) + remove_from_mentioned(status) + remove_reblogs(status) + status.destroy! + end + + private + + def remove_from_self(status) + unpush(:home, status.account, status) + end + + def remove_from_followers(status) + status.account.followers.each do |follower| + next unless follower.local? + unpush(:home, follower, status) + end + end + + def remove_from_mentioned(status) + status.mentions.each do |mention| + mentioned_account = mention.account + if mentioned_account.local? + unpush(:mentions, mentioned_account, status) + else + send_delete_salmon(mentioned_account, status) + end + end + end + + def send_delete_salmon(account, status) # TODO - # Remove from timelines of self, followers, and mentioned accounts - # For remote mentioned accounts, send delete Salmon - # Push delete event through ActionCable + end + + def remove_reblogs(status) + status.reblogs.each do |reblog| + RemoveStatusService.new.(reblog) + end + end + + def unpush(type, receiver, status) + redis.zremrangebyscore(FeedManager.key(type, receiver.id), status.id, status.id) + ActionCable.server.broadcast("timeline:#{receiver.id}", type: 'delete', id: status.id) + end + + def redis + $redis end end