Fix unfollows not clearing reblogs, fix blocks not clearing reblogs and notifications,
skip ActionCable for follow/unfollow/block events, instead clear UI from blocked account's posts instantly if block request succeeds. Add forgotten i18n for sensitive content
This commit is contained in:
		
					parent
					
						
							
								3373ae02de
							
						
					
				
			
			
				commit
				
					
						7cee27f517
					
				
			
		
					 13 changed files with 86 additions and 27 deletions
				
			
		|  | @ -246,7 +246,8 @@ export function blockAccount(id) { | |||
|     dispatch(blockAccountRequest(id)); | ||||
| 
 | ||||
|     api(getState).post(`/api/v1/accounts/${id}/block`).then(response => { | ||||
|       dispatch(blockAccountSuccess(response.data)); | ||||
|       // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers | ||||
|       dispatch(blockAccountSuccess(response.data, getState().get('statuses'))); | ||||
|     }).catch(error => { | ||||
|       dispatch(blockAccountFail(id, error)); | ||||
|     }); | ||||
|  | @ -272,10 +273,11 @@ export function blockAccountRequest(id) { | |||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function blockAccountSuccess(relationship) { | ||||
| export function blockAccountSuccess(relationship, statuses) { | ||||
|   return { | ||||
|     type: ACCOUNT_BLOCK_SUCCESS, | ||||
|     relationship | ||||
|     relationship, | ||||
|     statuses | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ const Status = React.createClass({ | |||
|     onReblog: React.PropTypes.func, | ||||
|     onDelete: React.PropTypes.func, | ||||
|     onOpenMedia: React.PropTypes.func, | ||||
|     onBlock: React.PropTypes.func, | ||||
|     me: React.PropTypes.number, | ||||
|     muted: React.PropTypes.bool | ||||
|   }, | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl'; | |||
| const messages = defineMessages({ | ||||
|   delete: { id: 'status.delete', defaultMessage: 'Delete' }, | ||||
|   mention: { id: 'status.mention', defaultMessage: 'Mention' }, | ||||
|   block: { id: 'account.block', defaultMessage: 'Block' }, | ||||
|   reply: { id: 'status.reply', defaultMessage: 'Reply' }, | ||||
|   reblog: { id: 'status.reblog', defaultMessage: 'Reblog' }, | ||||
|   favourite: { id: 'status.favourite', defaultMessage: 'Favourite' } | ||||
|  | @ -24,7 +25,8 @@ const StatusActionBar = React.createClass({ | |||
|     onFavourite: React.PropTypes.func, | ||||
|     onReblog: React.PropTypes.func, | ||||
|     onDelete: React.PropTypes.func, | ||||
|     onMention: React.PropTypes.func | ||||
|     onMention: React.PropTypes.func, | ||||
|     onBlock: React.PropTypes.func | ||||
|   }, | ||||
| 
 | ||||
|   mixins: [PureRenderMixin], | ||||
|  | @ -49,6 +51,10 @@ const StatusActionBar = React.createClass({ | |||
|     this.props.onMention(this.props.status.get('account')); | ||||
|   }, | ||||
| 
 | ||||
|   handleBlockClick () { | ||||
|     this.props.onBlock(this.props.status.get('account')); | ||||
|   }, | ||||
| 
 | ||||
|   render () { | ||||
|     const { status, me, intl } = this.props; | ||||
|     let menu = []; | ||||
|  | @ -57,6 +63,7 @@ const StatusActionBar = React.createClass({ | |||
|       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); | ||||
|     } else { | ||||
|       menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick }); | ||||
|       menu.push({ text: intl.formatMessage(messages.block), action: this.handleBlockClick }); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|  |  | |||
|  | @ -75,11 +75,6 @@ const Mastodon = React.createClass({ | |||
|               return store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message))); | ||||
|             case 'delete': | ||||
|               return store.dispatch(deleteFromTimelines(data.id)); | ||||
|             case 'merge': | ||||
|             case 'unmerge': | ||||
|               return store.dispatch(refreshTimeline('home', true)); | ||||
|             case 'block': | ||||
|               return store.dispatch(refreshTimeline('mentions', true)); | ||||
|             case 'notification': | ||||
|               return store.dispatch(updateNotifications(JSON.parse(data.message), getMessagesForLocale(locale), locale)); | ||||
|           } | ||||
|  |  | |||
|  | @ -1,18 +1,19 @@ | |||
| import { connect }       from 'react-redux'; | ||||
| import Status            from '../components/status'; | ||||
| import { connect } from 'react-redux'; | ||||
| import Status from '../components/status'; | ||||
| import { makeGetStatus } from '../selectors'; | ||||
| import { | ||||
|   replyCompose, | ||||
|   mentionCompose | ||||
| }                        from '../actions/compose'; | ||||
| } from '../actions/compose'; | ||||
| import { | ||||
|   reblog, | ||||
|   favourite, | ||||
|   unreblog, | ||||
|   unfavourite | ||||
| }                        from '../actions/interactions'; | ||||
| import { deleteStatus }  from '../actions/statuses'; | ||||
| import { openMedia }     from '../actions/modal'; | ||||
| } from '../actions/interactions'; | ||||
| import { blockAccount } from '../actions/accounts'; | ||||
| import { deleteStatus } from '../actions/statuses'; | ||||
| import { openMedia } from '../actions/modal'; | ||||
| import { createSelector } from 'reselect' | ||||
| 
 | ||||
| const mapStateToProps = (state, props) => ({ | ||||
|  | @ -91,6 +92,10 @@ const mapDispatchToProps = (dispatch) => ({ | |||
| 
 | ||||
|   onOpenMedia (url) { | ||||
|     dispatch(openMedia(url)); | ||||
|   }, | ||||
| 
 | ||||
|   onBlock (account) { | ||||
|     dispatch(blockAccount(account.get('id'))); | ||||
|   } | ||||
| 
 | ||||
| }); | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import Autosuggest from 'react-autosuggest'; | |||
| import AutosuggestAccountContainer from '../../compose/containers/autosuggest_account_container'; | ||||
| import { debounce } from 'react-decoration'; | ||||
| import UploadButtonContainer from '../containers/upload_button_container'; | ||||
| import { defineMessages, injectIntl } from 'react-intl'; | ||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
| import Toggle from 'react-toggle'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|  | @ -188,7 +188,7 @@ const ComposeForm = React.createClass({ | |||
| 
 | ||||
|         <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', marginTop: '10px', borderTop: '1px solid #616b86', paddingTop: '10px' }}> | ||||
|           <Toggle checked={this.props.sensitive} onChange={this.handleChangeSensitivity} /> | ||||
|           <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}>Sensitive content</span> | ||||
|           <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.sensitive' defaultMessage='Mark content as sensitive' /></span> | ||||
|         </label> | ||||
|       </div> | ||||
|     ); | ||||
|  |  | |||
|  | @ -8,6 +8,8 @@ const en = { | |||
|   "status.reblog": "Reblog", | ||||
|   "status.favourite": "Favourite", | ||||
|   "status.reblogged_by": "{name} reblogged", | ||||
|   "status.sensitive_warning": "Sensitive content", | ||||
|   "status.sensitive_toggle": "Click to view", | ||||
|   "video_player.toggle_sound": "Toggle sound", | ||||
|   "account.mention": "Mention", | ||||
|   "account.edit_profile": "Edit profile", | ||||
|  | @ -35,6 +37,7 @@ const en = { | |||
|   "tabs_bar.notifications": "Notifications", | ||||
|   "compose_form.placeholder": "What is on your mind?", | ||||
|   "compose_form.publish": "Toot", | ||||
|   "compose_form.sensitive": "Mark content as sensitive", | ||||
|   "navigation_bar.settings": "Settings", | ||||
|   "navigation_bar.public_timeline": "Public timeline", | ||||
|   "navigation_bar.logout": "Logout", | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ import { | |||
|   NOTIFICATIONS_REFRESH_SUCCESS, | ||||
|   NOTIFICATIONS_EXPAND_SUCCESS | ||||
| } from '../actions/notifications'; | ||||
| import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; | ||||
| import Immutable from 'immutable'; | ||||
| 
 | ||||
| const initialState = Immutable.Map({ | ||||
|  | @ -43,6 +44,10 @@ const appendNormalizedNotifications = (state, notifications, next) => { | |||
|   return state.update('items', list => list.push(...items)).set('next', next); | ||||
| }; | ||||
| 
 | ||||
| const filterNotifications = (state, relationship) => { | ||||
|   return state.update('items', list => list.filterNot(item => item.get('account') === relationship.id)); | ||||
| }; | ||||
| 
 | ||||
| export default function notifications(state = initialState, action) { | ||||
|   switch(action.type) { | ||||
|     case NOTIFICATIONS_UPDATE: | ||||
|  | @ -51,6 +56,8 @@ export default function notifications(state = initialState, action) { | |||
|       return normalizeNotifications(state, action.notifications, action.next); | ||||
|     case NOTIFICATIONS_EXPAND_SUCCESS: | ||||
|       return appendNormalizedNotifications(state, action.notifications, action.next); | ||||
|     case ACCOUNT_BLOCK_SUCCESS: | ||||
|       return filterNotifications(state, action.relationship); | ||||
|     default: | ||||
|       return state; | ||||
|   } | ||||
|  |  | |||
|  | @ -16,7 +16,8 @@ import { | |||
| } from '../actions/timelines'; | ||||
| import { | ||||
|   ACCOUNT_TIMELINE_FETCH_SUCCESS, | ||||
|   ACCOUNT_TIMELINE_EXPAND_SUCCESS | ||||
|   ACCOUNT_TIMELINE_EXPAND_SUCCESS, | ||||
|   ACCOUNT_BLOCK_SUCCESS | ||||
| } from '../actions/accounts'; | ||||
| import { | ||||
|   NOTIFICATIONS_UPDATE, | ||||
|  | @ -56,6 +57,18 @@ const deleteStatus = (state, id, references) => { | |||
|   return state.delete(id); | ||||
| }; | ||||
| 
 | ||||
| const filterStatuses = (state, relationship) => { | ||||
|   state.forEach(status => { | ||||
|     if (status.get('account') !== relationship.id) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     state = deleteStatus(state, status.get('id'), state.filter(item => item.get('reblog') === status.get('id'))); | ||||
|   }); | ||||
| 
 | ||||
|   return state; | ||||
| }; | ||||
| 
 | ||||
| const initialState = Immutable.Map(); | ||||
| 
 | ||||
| export default function statuses(state = initialState, action) { | ||||
|  | @ -79,6 +92,8 @@ export default function statuses(state = initialState, action) { | |||
|       return normalizeStatuses(state, action.statuses); | ||||
|     case TIMELINE_DELETE: | ||||
|       return deleteStatus(state, action.id, action.references); | ||||
|     case ACCOUNT_BLOCK_SUCCESS: | ||||
|       return filterStatuses(state, action.relationship); | ||||
|     default: | ||||
|       return state; | ||||
|   } | ||||
|  |  | |||
|  | @ -13,7 +13,8 @@ import { | |||
| import { | ||||
|   ACCOUNT_FETCH_SUCCESS, | ||||
|   ACCOUNT_TIMELINE_FETCH_SUCCESS, | ||||
|   ACCOUNT_TIMELINE_EXPAND_SUCCESS | ||||
|   ACCOUNT_TIMELINE_EXPAND_SUCCESS, | ||||
|   ACCOUNT_BLOCK_SUCCESS | ||||
| } from '../actions/accounts'; | ||||
| import { | ||||
|   STATUS_FETCH_SUCCESS, | ||||
|  | @ -140,6 +141,21 @@ const deleteStatus = (state, id, accountId, references) => { | |||
|   return state; | ||||
| }; | ||||
| 
 | ||||
| const filterTimelines = (state, relationship, statuses) => { | ||||
|   let references; | ||||
| 
 | ||||
|   statuses.forEach(status => { | ||||
|     if (status.get('account') !== relationship.id) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]); | ||||
|     state = deleteStatus(state, status.get('id'), status.get('account'), references); | ||||
|   }); | ||||
| 
 | ||||
|   return state; | ||||
| }; | ||||
| 
 | ||||
| const normalizeContext = (state, id, ancestors, descendants) => { | ||||
|   const ancestorsIds   = ancestors.map(ancestor => ancestor.get('id')); | ||||
|   const descendantsIds = descendants.map(descendant => descendant.get('id')); | ||||
|  | @ -166,6 +182,8 @@ export default function timelines(state = initialState, action) { | |||
|       return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace); | ||||
|     case ACCOUNT_TIMELINE_EXPAND_SUCCESS: | ||||
|       return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); | ||||
|     case ACCOUNT_BLOCK_SUCCESS: | ||||
|       return filterTimelines(state, action.relationship, action.statuses); | ||||
|     default: | ||||
|       return state; | ||||
|   } | ||||
|  |  | |||
|  | @ -6,19 +6,27 @@ class BlockService < BaseService | |||
| 
 | ||||
|     UnfollowService.new.call(account, target_account) if account.following?(target_account) | ||||
|     account.block!(target_account) | ||||
|     clear_mentions(account, target_account) | ||||
|     clear_timelines(account, target_account) | ||||
|     clear_notifications(account, target_account) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def clear_mentions(account, target_account) | ||||
|     timeline_key = FeedManager.instance.key(:mentions, account.id) | ||||
|   def clear_timelines(account, target_account) | ||||
|     mentions_key = FeedManager.instance.key(:mentions, account.id) | ||||
|     home_key     = FeedManager.instance.key(:home, account.id) | ||||
| 
 | ||||
|     target_account.statuses.select('id').find_each do |status| | ||||
|       redis.zrem(timeline_key, status.id) | ||||
|       redis.zrem(mentions_key, status.id) | ||||
|       redis.zrem(home_key, status.id) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|     FeedManager.instance.broadcast(account.id, type: 'block', id: target_account.id) | ||||
|   def clear_notifications(account, target_account) | ||||
|     Notification.where(account: account).joins(:follow).where(activity_type: 'Follow', follows: { account_id: target_account.id }).destroy_all | ||||
|     Notification.where(account: account).joins(mention: :status).where(activity_type: 'Mention', statuses: { account_id: target_account.id }).destroy_all | ||||
|     Notification.where(account: account).joins(:favourite).where(activity_type: 'Favourite', favourites: { account_id: target_account.id }).destroy_all | ||||
|     Notification.where(account: account).joins(:status).where(activity_type: 'Status', statuses: { account_id: target_account.id }).destroy_all | ||||
|   end | ||||
| 
 | ||||
|   def redis | ||||
|  |  | |||
|  | @ -33,7 +33,6 @@ class FollowService < BaseService | |||
|     end | ||||
| 
 | ||||
|     FeedManager.instance.trim(:home, into_account.id) | ||||
|     FeedManager.instance.broadcast(into_account.id, type: 'merge') | ||||
|   end | ||||
| 
 | ||||
|   def redis | ||||
|  |  | |||
|  | @ -17,9 +17,8 @@ class UnfollowService < BaseService | |||
| 
 | ||||
|     from_account.statuses.select('id').find_each do |status| | ||||
|       redis.zrem(timeline_key, status.id) | ||||
|       redis.zremrangebyscore(timeline_key, status.id, status.id) | ||||
|     end | ||||
| 
 | ||||
|     FeedManager.instance.broadcast(into_account.id, type: 'unmerge') | ||||
|   end | ||||
| 
 | ||||
|   def redis | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue