Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
		
						commit
						5528719bc9
					
				|  | @ -158,7 +158,9 @@ export function submitCompose(routerHistory) { | ||||||
|       // into the columns
 |       // into the columns
 | ||||||
| 
 | 
 | ||||||
|       const insertIfOnline = timelineId => { |       const insertIfOnline = timelineId => { | ||||||
|         if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) { |         const timeline = getState().getIn(['timelines', timelineId]); | ||||||
|  | 
 | ||||||
|  |         if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) { | ||||||
|           dispatch(updateTimeline(timelineId, { ...response.data })); |           dispatch(updateTimeline(timelineId, { ...response.data })); | ||||||
|         } |         } | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ import { | ||||||
|   updateTimeline, |   updateTimeline, | ||||||
|   deleteFromTimelines, |   deleteFromTimelines, | ||||||
|   expandHomeTimeline, |   expandHomeTimeline, | ||||||
|  |   connectTimeline, | ||||||
|   disconnectTimeline, |   disconnectTimeline, | ||||||
| } from './timelines'; | } from './timelines'; | ||||||
| import { updateNotifications, expandNotifications } from './notifications'; | import { updateNotifications, expandNotifications } from './notifications'; | ||||||
|  | @ -16,7 +17,12 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null, | ||||||
| 
 | 
 | ||||||
|   return connectStream (path, pollingRefresh, (dispatch, getState) => { |   return connectStream (path, pollingRefresh, (dispatch, getState) => { | ||||||
|     const locale = getState().getIn(['meta', 'locale']); |     const locale = getState().getIn(['meta', 'locale']); | ||||||
|  | 
 | ||||||
|     return { |     return { | ||||||
|  |       onConnect() { | ||||||
|  |         dispatch(connectTimeline(timelineId)); | ||||||
|  |       }, | ||||||
|  | 
 | ||||||
|       onDisconnect() { |       onDisconnect() { | ||||||
|         dispatch(disconnectTimeline(timelineId)); |         dispatch(disconnectTimeline(timelineId)); | ||||||
|       }, |       }, | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ export const TIMELINE_EXPAND_FAIL    = 'TIMELINE_EXPAND_FAIL'; | ||||||
| 
 | 
 | ||||||
| export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; | export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; | ||||||
| 
 | 
 | ||||||
|  | export const TIMELINE_CONNECT    = 'TIMELINE_CONNECT'; | ||||||
| export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; | export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; | ||||||
| 
 | 
 | ||||||
| export function updateTimeline(timeline, status, accept) { | export function updateTimeline(timeline, status, accept) { | ||||||
|  | @ -143,6 +144,13 @@ export function scrollTopTimeline(timeline, top) { | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export function connectTimeline(timeline) { | ||||||
|  |   return { | ||||||
|  |     type: TIMELINE_CONNECT, | ||||||
|  |     timeline, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export function disconnectTimeline(timeline) { | export function disconnectTimeline(timeline) { | ||||||
|   return { |   return { | ||||||
|     type: TIMELINE_DISCONNECT, |     type: TIMELINE_DISCONNECT, | ||||||
|  |  | ||||||
|  | @ -94,7 +94,7 @@ class Poll extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|   renderOption (option, optionIndex) { |   renderOption (option, optionIndex) { | ||||||
|     const { poll, disabled } = this.props; |     const { poll, disabled } = this.props; | ||||||
|     const percent            = (option.get('votes_count') / poll.get('votes_count')) * 100; |     const percent            = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100; | ||||||
|     const leading            = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); |     const leading            = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); | ||||||
|     const active             = !!this.state.selected[`${optionIndex}`]; |     const active             = !!this.state.selected[`${optionIndex}`]; | ||||||
|     const showResults        = poll.get('voted') || poll.get('expired'); |     const showResults        = poll.get('voted') || poll.get('expired'); | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import { | ||||||
|   TIMELINE_EXPAND_REQUEST, |   TIMELINE_EXPAND_REQUEST, | ||||||
|   TIMELINE_EXPAND_FAIL, |   TIMELINE_EXPAND_FAIL, | ||||||
|   TIMELINE_SCROLL_TOP, |   TIMELINE_SCROLL_TOP, | ||||||
|  |   TIMELINE_CONNECT, | ||||||
|   TIMELINE_DISCONNECT, |   TIMELINE_DISCONNECT, | ||||||
| } from '../actions/timelines'; | } from '../actions/timelines'; | ||||||
| import { | import { | ||||||
|  | @ -20,6 +21,7 @@ const initialState = ImmutableMap(); | ||||||
| 
 | 
 | ||||||
| const initialTimeline = ImmutableMap({ | const initialTimeline = ImmutableMap({ | ||||||
|   unread: 0, |   unread: 0, | ||||||
|  |   online: false, | ||||||
|   top: true, |   top: true, | ||||||
|   isLoading: false, |   isLoading: false, | ||||||
|   hasMore: true, |   hasMore: true, | ||||||
|  | @ -142,14 +144,13 @@ export default function timelines(state = initialState, action) { | ||||||
|     return filterTimeline('home', state, action.relationship, action.statuses); |     return filterTimeline('home', state, action.relationship, action.statuses); | ||||||
|   case TIMELINE_SCROLL_TOP: |   case TIMELINE_SCROLL_TOP: | ||||||
|     return updateTop(state, action.timeline, action.top); |     return updateTop(state, action.timeline, action.top); | ||||||
|  |   case TIMELINE_CONNECT: | ||||||
|  |     return state.update(action.timeline, initialTimeline, map => map.set('online', true)); | ||||||
|   case TIMELINE_DISCONNECT: |   case TIMELINE_DISCONNECT: | ||||||
|     return state.update( |     return state.update( | ||||||
|       action.timeline, |       action.timeline, | ||||||
|       initialTimeline, |       initialTimeline, | ||||||
|       map => map.update( |       map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items) | ||||||
|         'items', |  | ||||||
|         items => items.first() ? items.unshift(null) : items |  | ||||||
|       ) |  | ||||||
|     ); |     ); | ||||||
|   default: |   default: | ||||||
|     return state; |     return state; | ||||||
|  |  | ||||||
|  | @ -2,11 +2,11 @@ import WebSocketClient from 'websocket.js'; | ||||||
| 
 | 
 | ||||||
| const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max)); | const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max)); | ||||||
| 
 | 
 | ||||||
| export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onDisconnect() {}, onReceive() {} })) { | export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} })) { | ||||||
|   return (dispatch, getState) => { |   return (dispatch, getState) => { | ||||||
|     const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); |     const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); | ||||||
|     const accessToken = getState().getIn(['meta', 'access_token']); |     const accessToken = getState().getIn(['meta', 'access_token']); | ||||||
|     const { onDisconnect, onReceive } = callbacks(dispatch, getState); |     const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState); | ||||||
| 
 | 
 | ||||||
|     let polling = null; |     let polling = null; | ||||||
| 
 | 
 | ||||||
|  | @ -28,6 +28,8 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ | ||||||
|         if (pollingRefresh) { |         if (pollingRefresh) { | ||||||
|           clearPolling(); |           clearPolling(); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         onConnect(); | ||||||
|       }, |       }, | ||||||
| 
 | 
 | ||||||
|       disconnected () { |       disconnected () { | ||||||
|  | @ -47,6 +49,8 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ | ||||||
|           clearPolling(); |           clearPolling(); | ||||||
|           pollingRefresh(dispatch); |           pollingRefresh(dispatch); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         onConnect(); | ||||||
|       }, |       }, | ||||||
| 
 | 
 | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -241,6 +241,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity | ||||||
| 
 | 
 | ||||||
|   def poll_vote? |   def poll_vote? | ||||||
|     return false if replied_to_status.nil? || replied_to_status.poll.nil? || !replied_to_status.local? || !replied_to_status.poll.options.include?(@object['name']) |     return false if replied_to_status.nil? || replied_to_status.poll.nil? || !replied_to_status.local? || !replied_to_status.poll.options.include?(@object['name']) | ||||||
|  |     return true if replied_to_status.poll.expired? | ||||||
|     replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name']), uri: @object['id']) |     replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name']), uri: @object['id']) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -28,7 +28,7 @@ class Poll < ApplicationRecord | ||||||
| 
 | 
 | ||||||
|   validates :options, presence: true |   validates :options, presence: true | ||||||
|   validates :expires_at, presence: true, if: :local? |   validates :expires_at, presence: true, if: :local? | ||||||
|   validates_with PollValidator, if: :local? |   validates_with PollValidator, on: :create, if: :local? | ||||||
| 
 | 
 | ||||||
|   scope :attached, -> { where.not(status_id: nil) } |   scope :attached, -> { where.not(status_id: nil) } | ||||||
|   scope :unattached, -> { where(status_id: nil) } |   scope :unattached, -> { where(status_id: nil) } | ||||||
|  | @ -41,17 +41,17 @@ class Poll < ApplicationRecord | ||||||
|   after_commit :reset_parent_cache, on: :update |   after_commit :reset_parent_cache, on: :update | ||||||
| 
 | 
 | ||||||
|   def loaded_options |   def loaded_options | ||||||
|     options.map.with_index { |title, key| Option.new(self, key.to_s, title, cached_tallies[key]) } |     options.map.with_index { |title, key| Option.new(self, key.to_s, title, show_totals_now? ? cached_tallies[key] : nil) } | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def unloaded_options |  | ||||||
|     options.map.with_index { |title, key| Option.new(self, key.to_s, title, nil) } |  | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def possibly_stale? |   def possibly_stale? | ||||||
|     remote? && last_fetched_before_expiration? && time_passed_since_last_fetch? |     remote? && last_fetched_before_expiration? && time_passed_since_last_fetch? | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def voted?(account) | ||||||
|  |     account.id == account_id || votes.where(account: account).exists? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   delegate :local?, to: :account |   delegate :local?, to: :account | ||||||
| 
 | 
 | ||||||
|   def remote? |   def remote? | ||||||
|  | @ -95,4 +95,8 @@ class Poll < ApplicationRecord | ||||||
|   def time_passed_since_last_fetch? |   def time_passed_since_last_fetch? | ||||||
|     last_fetched_at.nil? || last_fetched_at < 1.minute.ago |     last_fetched_at.nil? || last_fetched_at < 1.minute.ago | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def show_totals_now? | ||||||
|  |     expired? || !hide_totals? | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -122,12 +122,8 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def poll_options |   def poll_options | ||||||
|     if !object.poll.expired? && object.poll.hide_totals? |  | ||||||
|       object.poll.unloaded_options |  | ||||||
|     else |  | ||||||
|     object.poll.loaded_options |     object.poll.loaded_options | ||||||
|   end |   end | ||||||
|   end |  | ||||||
| 
 | 
 | ||||||
|   def poll_and_multiple? |   def poll_and_multiple? | ||||||
|     object.poll&.multiple? |     object.poll&.multiple? | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ class REST::PollSerializer < ActiveModel::Serializer | ||||||
|   attributes :id, :expires_at, :expired, |   attributes :id, :expires_at, :expired, | ||||||
|              :multiple, :votes_count |              :multiple, :votes_count | ||||||
| 
 | 
 | ||||||
|   has_many :dynamic_options, key: :options |   has_many :loaded_options, key: :options | ||||||
| 
 | 
 | ||||||
|   attribute :voted, if: :current_user? |   attribute :voted, if: :current_user? | ||||||
| 
 | 
 | ||||||
|  | @ -12,20 +12,12 @@ class REST::PollSerializer < ActiveModel::Serializer | ||||||
|     object.id.to_s |     object.id.to_s | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def dynamic_options |  | ||||||
|     if !object.expired? && object.hide_totals? |  | ||||||
|       object.unloaded_options |  | ||||||
|     else |  | ||||||
|       object.loaded_options |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def expired |   def expired | ||||||
|     object.expired? |     object.expired? | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def voted |   def voted | ||||||
|     object.votes.where(account: current_user.account).exists? |     object.voted?(current_user.account) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def current_user? |   def current_user? | ||||||
|  |  | ||||||
|  | @ -32,12 +32,17 @@ class ActivityPub::FetchRemotePollService < BaseService | ||||||
|     # votes, so we need to remove them |     # votes, so we need to remove them | ||||||
|     poll.votes.delete_all if latest_options != poll.options |     poll.votes.delete_all if latest_options != poll.options | ||||||
| 
 | 
 | ||||||
|  |     begin | ||||||
|       poll.update!( |       poll.update!( | ||||||
|         last_fetched_at: Time.now.utc, |         last_fetched_at: Time.now.utc, | ||||||
|         expires_at: expires_at, |         expires_at: expires_at, | ||||||
|         options: latest_options, |         options: latest_options, | ||||||
|         cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 } |         cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 } | ||||||
|       ) |       ) | ||||||
|  |     rescue ActiveRecord::StaleObjectError | ||||||
|  |       poll.reload | ||||||
|  |       retry | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
|  |  | ||||||
|  | @ -11,6 +11,8 @@ class VoteService < BaseService | ||||||
|     @choices = choices |     @choices = choices | ||||||
|     @votes   = [] |     @votes   = [] | ||||||
| 
 | 
 | ||||||
|  |     return if @poll.expired? | ||||||
|  | 
 | ||||||
|     ApplicationRecord.transaction do |     ApplicationRecord.transaction do | ||||||
|       @choices.each do |choice| |       @choices.each do |choice| | ||||||
|         @votes << @poll.votes.create!(account: @account, choice: choice) |         @votes << @poll.votes.create!(account: @account, choice: choice) | ||||||
|  |  | ||||||
|  | @ -1,10 +1,8 @@ | ||||||
| - options      = (!poll.expired? && poll.hide_totals?) ? poll.unloaded_options : poll.loaded_options | - show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired? | ||||||
| - voted        = user_signed_in? && poll.votes.where(account: current_account).exists? |  | ||||||
| - show_results = voted || poll.expired? |  | ||||||
| 
 | 
 | ||||||
| .poll | .poll | ||||||
|   %ul |   %ul | ||||||
|     - options.each do |option| |     - poll.loaded_options.each do |option| | ||||||
|       %li |       %li | ||||||
|         - if show_results |         - if show_results | ||||||
|           - percent = poll.votes_count > 0 ? 100 * option.votes_count / poll.votes_count : 0 |           - percent = poll.votes_count > 0 ? 100 * option.votes_count / poll.votes_count : 0 | ||||||
|  |  | ||||||
|  | @ -482,6 +482,28 @@ RSpec.describe ActivityPub::Activity::Create do | ||||||
|           expect(poll.reload.cached_tallies).to eq [1, 0] |           expect(poll.reload.cached_tallies).to eq [1, 0] | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  | 
 | ||||||
|  |       context 'when a vote to an expired local poll' do | ||||||
|  |         let(:poll) do | ||||||
|  |           poll = Fabricate.build(:poll, options: %w(Yellow Blue), expires_at: 1.day.ago) | ||||||
|  |           poll.save(validate: false) | ||||||
|  |           poll | ||||||
|  |         end | ||||||
|  |         let!(:local_status) { Fabricate(:status, owned_poll: poll) } | ||||||
|  | 
 | ||||||
|  |         let(:object_json) do | ||||||
|  |           { | ||||||
|  |             id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, | ||||||
|  |             type: 'Note', | ||||||
|  |             name: 'Yellow', | ||||||
|  |             inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status) | ||||||
|  |           } | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'does not add a vote to the poll' do | ||||||
|  |           expect(poll.votes.first).to be_nil | ||||||
|  |         end | ||||||
|  |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when sender is followed by local users' do |     context 'when sender is followed by local users' do | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue