forked from treehouse/mastodon
Merge branch 'master' into glitch-soc/merge-upstream
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,11 +122,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def poll_options
|
def poll_options
|
||||||
if !object.poll.expired? && object.poll.hide_totals?
|
object.poll.loaded_options
|
||||||
object.poll.unloaded_options
|
|
||||||
else
|
|
||||||
object.poll.loaded_options
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def poll_and_multiple?
|
def poll_and_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
|
||||||
|
|
||||||
poll.update!(
|
begin
|
||||||
last_fetched_at: Time.now.utc,
|
poll.update!(
|
||||||
expires_at: expires_at,
|
last_fetched_at: Time.now.utc,
|
||||||
options: latest_options,
|
expires_at: expires_at,
|
||||||
cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 }
|
options: latest_options,
|
||||||
)
|
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