Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
commit
9201398507
26 changed files with 191 additions and 97 deletions
2
Gemfile
2
Gemfile
|
@ -123,7 +123,7 @@ group :development do
|
||||||
gem 'annotate', '~> 2.7'
|
gem 'annotate', '~> 2.7'
|
||||||
gem 'better_errors', '~> 2.5'
|
gem 'better_errors', '~> 2.5'
|
||||||
gem 'binding_of_caller', '~> 0.7'
|
gem 'binding_of_caller', '~> 0.7'
|
||||||
gem 'bullet', '~> 5.7'
|
gem 'bullet', '~> 5.8'
|
||||||
gem 'letter_opener', '~> 1.4'
|
gem 'letter_opener', '~> 1.4'
|
||||||
gem 'letter_opener_web', '~> 1.3'
|
gem 'letter_opener_web', '~> 1.3'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
|
|
36
Gemfile.lock
36
Gemfile.lock
|
@ -76,8 +76,8 @@ GEM
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
aws-eventstream (1.0.1)
|
aws-eventstream (1.0.1)
|
||||||
aws-partitions (1.106.0)
|
aws-partitions (1.107.0)
|
||||||
aws-sdk-core (3.35.0)
|
aws-sdk-core (3.36.0)
|
||||||
aws-eventstream (~> 1.0)
|
aws-eventstream (~> 1.0)
|
||||||
aws-partitions (~> 1.0)
|
aws-partitions (~> 1.0)
|
||||||
aws-sigv4 (~> 1.0)
|
aws-sigv4 (~> 1.0)
|
||||||
|
@ -85,7 +85,7 @@ GEM
|
||||||
aws-sdk-kms (1.11.0)
|
aws-sdk-kms (1.11.0)
|
||||||
aws-sdk-core (~> 3, >= 3.26.0)
|
aws-sdk-core (~> 3, >= 3.26.0)
|
||||||
aws-sigv4 (~> 1.0)
|
aws-sigv4 (~> 1.0)
|
||||||
aws-sdk-s3 (1.23.0)
|
aws-sdk-s3 (1.23.1)
|
||||||
aws-sdk-core (~> 3, >= 3.26.0)
|
aws-sdk-core (~> 3, >= 3.26.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.0)
|
aws-sigv4 (~> 1.0)
|
||||||
|
@ -103,9 +103,9 @@ GEM
|
||||||
brakeman (4.3.1)
|
brakeman (4.3.1)
|
||||||
browser (2.5.3)
|
browser (2.5.3)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
bullet (5.7.6)
|
bullet (5.8.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11.0)
|
uniform_notifier (~> 1.11)
|
||||||
bundler-audit (0.6.0)
|
bundler-audit (0.6.0)
|
||||||
bundler (~> 1.2)
|
bundler (~> 1.2)
|
||||||
thor (~> 0.18)
|
thor (~> 0.18)
|
||||||
|
@ -126,7 +126,7 @@ GEM
|
||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano-yarn (2.0.2)
|
capistrano-yarn (2.0.2)
|
||||||
capistrano (~> 3.0)
|
capistrano (~> 3.0)
|
||||||
capybara (3.10.0)
|
capybara (3.10.1)
|
||||||
addressable
|
addressable
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
|
@ -254,8 +254,7 @@ GEM
|
||||||
hashie (3.5.7)
|
hashie (3.5.7)
|
||||||
heapy (0.1.4)
|
heapy (0.1.4)
|
||||||
highline (2.0.0)
|
highline (2.0.0)
|
||||||
hiredis (0.6.1)
|
hiredis (0.6.3)
|
||||||
hitimes (1.3.0)
|
|
||||||
hkdf (0.3.0)
|
hkdf (0.3.0)
|
||||||
html2text (0.2.1)
|
html2text (0.2.1)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
|
@ -333,7 +332,7 @@ GEM
|
||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
memory_profiler (0.9.12)
|
memory_profiler (0.9.12)
|
||||||
method_source (0.9.0)
|
method_source (0.9.1)
|
||||||
microformats (4.0.7)
|
microformats (4.0.7)
|
||||||
json
|
json
|
||||||
nokogiri
|
nokogiri
|
||||||
|
@ -389,7 +388,7 @@ GEM
|
||||||
av (~> 0.9.0)
|
av (~> 0.9.0)
|
||||||
paperclip (>= 2.5.2)
|
paperclip (>= 2.5.2)
|
||||||
parallel (1.12.1)
|
parallel (1.12.1)
|
||||||
parallel_tests (2.26.0)
|
parallel_tests (2.26.2)
|
||||||
parallel
|
parallel
|
||||||
parser (2.5.3.0)
|
parser (2.5.3.0)
|
||||||
ast (~> 2.4.0)
|
ast (~> 2.4.0)
|
||||||
|
@ -399,7 +398,7 @@ GEM
|
||||||
pg (1.1.3)
|
pg (1.1.3)
|
||||||
pghero (2.2.0)
|
pghero (2.2.0)
|
||||||
activerecord
|
activerecord
|
||||||
pkg-config (1.3.1)
|
pkg-config (1.3.2)
|
||||||
powerpack (0.1.2)
|
powerpack (0.1.2)
|
||||||
premailer (1.11.1)
|
premailer (1.11.1)
|
||||||
addressable
|
addressable
|
||||||
|
@ -409,13 +408,13 @@ GEM
|
||||||
actionmailer (>= 3, < 6)
|
actionmailer (>= 3, < 6)
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
private_address_check (0.5.0)
|
private_address_check (0.5.0)
|
||||||
pry (0.11.3)
|
pry (0.12.0)
|
||||||
coderay (~> 1.1.0)
|
coderay (~> 1.1.0)
|
||||||
method_source (~> 0.9.0)
|
method_source (~> 0.9.0)
|
||||||
pry-byebug (3.6.0)
|
pry-byebug (3.6.0)
|
||||||
byebug (~> 10.0)
|
byebug (~> 10.0)
|
||||||
pry (~> 0.10)
|
pry (~> 0.10)
|
||||||
pry-rails (0.3.6)
|
pry-rails (0.3.7)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (3.0.3)
|
public_suffix (3.0.3)
|
||||||
puma (3.12.0)
|
puma (3.12.0)
|
||||||
|
@ -550,7 +549,7 @@ GEM
|
||||||
scss_lint (0.57.1)
|
scss_lint (0.57.1)
|
||||||
rake (>= 0.9, < 13)
|
rake (>= 0.9, < 13)
|
||||||
sass (~> 3.5, >= 3.5.5)
|
sass (~> 3.5, >= 3.5.5)
|
||||||
sidekiq (5.2.2)
|
sidekiq (5.2.3)
|
||||||
connection_pool (~> 2.2, >= 2.2.2)
|
connection_pool (~> 2.2, >= 2.2.2)
|
||||||
rack-protection (>= 1.5.0)
|
rack-protection (>= 1.5.0)
|
||||||
redis (>= 3.3.5, < 5)
|
redis (>= 3.3.5, < 5)
|
||||||
|
@ -600,13 +599,12 @@ GEM
|
||||||
thor (0.20.0)
|
thor (0.20.0)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tilt (2.0.8)
|
tilt (2.0.8)
|
||||||
timers (4.1.2)
|
timers (4.2.0)
|
||||||
hitimes
|
|
||||||
tty-color (0.4.3)
|
tty-color (0.4.3)
|
||||||
tty-command (0.8.2)
|
tty-command (0.8.2)
|
||||||
pastel (~> 0.7.0)
|
pastel (~> 0.7.0)
|
||||||
tty-cursor (0.6.0)
|
tty-cursor (0.6.0)
|
||||||
tty-prompt (0.17.1)
|
tty-prompt (0.17.2)
|
||||||
necromancer (~> 0.4.0)
|
necromancer (~> 0.4.0)
|
||||||
pastel (~> 0.7.0)
|
pastel (~> 0.7.0)
|
||||||
timers (~> 4.0)
|
timers (~> 4.0)
|
||||||
|
@ -627,7 +625,7 @@ GEM
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.5)
|
unf_ext (0.0.7.5)
|
||||||
unicode-display_width (1.4.0)
|
unicode-display_width (1.4.0)
|
||||||
uniform_notifier (1.11.0)
|
uniform_notifier (1.12.1)
|
||||||
warden (1.2.7)
|
warden (1.2.7)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
webmock (3.4.2)
|
webmock (3.4.2)
|
||||||
|
@ -662,7 +660,7 @@ DEPENDENCIES
|
||||||
bootsnap (~> 1.3)
|
bootsnap (~> 1.3)
|
||||||
brakeman (~> 4.3)
|
brakeman (~> 4.3)
|
||||||
browser
|
browser
|
||||||
bullet (~> 5.7)
|
bullet (~> 5.8)
|
||||||
bundler-audit (~> 0.6)
|
bundler-audit (~> 0.6)
|
||||||
capistrano (~> 3.11)
|
capistrano (~> 3.11)
|
||||||
capistrano-rails (~> 1.4)
|
capistrano-rails (~> 1.4)
|
||||||
|
|
|
@ -17,7 +17,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def follow
|
def follow
|
||||||
FollowService.new.call(current_user.account, @account.acct, reblogs: truthy_param?(:reblogs))
|
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
|
||||||
|
|
||||||
options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
|
options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,12 @@ module SignatureVerification
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
account = account_from_key_id(signature_params['keyId'])
|
account_stoplight = Stoplight("source:#{request.ip}") { account_from_key_id(signature_params['keyId']) }
|
||||||
|
.with_fallback { nil }
|
||||||
|
.with_threshold(1)
|
||||||
|
.with_cool_off_time(5.minutes.seconds)
|
||||||
|
|
||||||
|
account = account_stoplight.run
|
||||||
|
|
||||||
if account.nil?
|
if account.nil?
|
||||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
||||||
|
|
|
@ -145,12 +145,14 @@ export function fetchAccountFail(id, error) {
|
||||||
export function followAccount(id, reblogs = true) {
|
export function followAccount(id, reblogs = true) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
||||||
dispatch(followAccountRequest(id));
|
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
||||||
|
|
||||||
|
dispatch(followAccountRequest(id, locked));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
|
||||||
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(followAccountFail(error));
|
dispatch(followAccountFail(error, locked));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -167,10 +169,12 @@ export function unfollowAccount(id) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function followAccountRequest(id) {
|
export function followAccountRequest(id, locked) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_FOLLOW_REQUEST,
|
type: ACCOUNT_FOLLOW_REQUEST,
|
||||||
id,
|
id,
|
||||||
|
locked,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -179,13 +183,16 @@ export function followAccountSuccess(relationship, alreadyFollowing) {
|
||||||
type: ACCOUNT_FOLLOW_SUCCESS,
|
type: ACCOUNT_FOLLOW_SUCCESS,
|
||||||
relationship,
|
relationship,
|
||||||
alreadyFollowing,
|
alreadyFollowing,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function followAccountFail(error) {
|
export function followAccountFail(error, locked) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_FOLLOW_FAIL,
|
type: ACCOUNT_FOLLOW_FAIL,
|
||||||
error,
|
error,
|
||||||
|
locked,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -193,6 +200,7 @@ export function unfollowAccountRequest(id) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_UNFOLLOW_REQUEST,
|
type: ACCOUNT_UNFOLLOW_REQUEST,
|
||||||
id,
|
id,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -201,6 +209,7 @@ export function unfollowAccountSuccess(relationship, statuses) {
|
||||||
type: ACCOUNT_UNFOLLOW_SUCCESS,
|
type: ACCOUNT_UNFOLLOW_SUCCESS,
|
||||||
relationship,
|
relationship,
|
||||||
statuses,
|
statuses,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -208,6 +217,7 @@ export function unfollowAccountFail(error) {
|
||||||
return {
|
return {
|
||||||
type: ACCOUNT_UNFOLLOW_FAIL,
|
type: ACCOUNT_UNFOLLOW_FAIL,
|
||||||
error,
|
error,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,6 @@ export const expandAccountTimeline = (accountId, { maxId, withReplies }
|
||||||
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
||||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
|
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
|
||||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||||
|
|
||||||
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
|
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
|
||||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||||
max_id: maxId,
|
max_id: maxId,
|
||||||
|
@ -111,6 +110,7 @@ export function expandTimelineRequest(timeline) {
|
||||||
return {
|
return {
|
||||||
type: TIMELINE_EXPAND_REQUEST,
|
type: TIMELINE_EXPAND_REQUEST,
|
||||||
timeline,
|
timeline,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -121,6 +121,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial) {
|
||||||
statuses,
|
statuses,
|
||||||
next,
|
next,
|
||||||
partial,
|
partial,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -129,6 +130,7 @@ export function expandTimelineFail(timeline, error) {
|
||||||
type: TIMELINE_EXPAND_FAIL,
|
type: TIMELINE_EXPAND_FAIL,
|
||||||
timeline,
|
timeline,
|
||||||
error,
|
error,
|
||||||
|
skipLoading: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { throttle } from 'lodash';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
|
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
|
||||||
|
import LoadingIndicator from './loading_indicator';
|
||||||
|
|
||||||
const MOUSE_IDLE_DELAY = 300;
|
const MOUSE_IDLE_DELAY = 300;
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ export default class ScrollableList extends PureComponent {
|
||||||
trackScroll: PropTypes.bool,
|
trackScroll: PropTypes.bool,
|
||||||
shouldUpdateScroll: PropTypes.func,
|
shouldUpdateScroll: PropTypes.func,
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
|
showLoading: PropTypes.bool,
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
prepend: PropTypes.node,
|
prepend: PropTypes.node,
|
||||||
alwaysPrepend: PropTypes.bool,
|
alwaysPrepend: PropTypes.bool,
|
||||||
|
@ -39,8 +41,6 @@ export default class ScrollableList extends PureComponent {
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
fullscreen: null,
|
fullscreen: null,
|
||||||
mouseMovedRecently: false,
|
|
||||||
scrollToTopOnMouseIdle: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
intersectionObserverWrapper = new IntersectionObserverWrapper();
|
intersectionObserverWrapper = new IntersectionObserverWrapper();
|
||||||
|
@ -65,11 +65,14 @@ export default class ScrollableList extends PureComponent {
|
||||||
});
|
});
|
||||||
|
|
||||||
mouseIdleTimer = null;
|
mouseIdleTimer = null;
|
||||||
|
mouseMovedRecently = false;
|
||||||
|
scrollToTopOnMouseIdle = false;
|
||||||
|
|
||||||
clearMouseIdleTimer = () => {
|
clearMouseIdleTimer = () => {
|
||||||
if (this.mouseIdleTimer === null) {
|
if (this.mouseIdleTimer === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearTimeout(this.mouseIdleTimer);
|
clearTimeout(this.mouseIdleTimer);
|
||||||
this.mouseIdleTimer = null;
|
this.mouseIdleTimer = null;
|
||||||
};
|
};
|
||||||
|
@ -77,37 +80,36 @@ export default class ScrollableList extends PureComponent {
|
||||||
handleMouseMove = throttle(() => {
|
handleMouseMove = throttle(() => {
|
||||||
// As long as the mouse keeps moving, clear and restart the idle timer.
|
// As long as the mouse keeps moving, clear and restart the idle timer.
|
||||||
this.clearMouseIdleTimer();
|
this.clearMouseIdleTimer();
|
||||||
this.mouseIdleTimer =
|
this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
|
||||||
setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
|
|
||||||
|
|
||||||
this.setState(({
|
if (!this.mouseMovedRecently && this.node.scrollTop === 0) {
|
||||||
mouseMovedRecently,
|
// Only set if we just started moving and are scrolled to the top.
|
||||||
scrollToTopOnMouseIdle,
|
this.scrollToTopOnMouseIdle = true;
|
||||||
}) => ({
|
}
|
||||||
mouseMovedRecently: true,
|
|
||||||
// Only set scrollToTopOnMouseIdle if we just started moving and were
|
// Save setting this flag for last, so we can do the comparison above.
|
||||||
// scrolled to the top. Otherwise, just retain the previous state.
|
this.mouseMovedRecently = true;
|
||||||
scrollToTopOnMouseIdle:
|
|
||||||
mouseMovedRecently
|
|
||||||
? scrollToTopOnMouseIdle
|
|
||||||
: (this.node.scrollTop === 0),
|
|
||||||
}));
|
|
||||||
}, MOUSE_IDLE_DELAY / 2);
|
}, MOUSE_IDLE_DELAY / 2);
|
||||||
|
|
||||||
handleMouseIdle = () => {
|
handleWheel = throttle(() => {
|
||||||
if (this.state.scrollToTopOnMouseIdle) {
|
this.scrollToTopOnMouseIdle = false;
|
||||||
this.node.scrollTop = 0;
|
}, 150, {
|
||||||
this.props.onScrollToTop();
|
trailing: true,
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
mouseMovedRecently: false,
|
|
||||||
scrollToTopOnMouseIdle: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
handleMouseIdle = () => {
|
||||||
|
if (this.scrollToTopOnMouseIdle) {
|
||||||
|
this.node.scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mouseMovedRecently = false;
|
||||||
|
this.scrollToTopOnMouseIdle = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.attachScrollListener();
|
this.attachScrollListener();
|
||||||
this.attachIntersectionObserver();
|
this.attachIntersectionObserver();
|
||||||
|
|
||||||
attachFullscreenListener(this.onFullScreenChange);
|
attachFullscreenListener(this.onFullScreenChange);
|
||||||
|
|
||||||
// Handle initial scroll posiiton
|
// Handle initial scroll posiiton
|
||||||
|
@ -118,7 +120,8 @@ export default class ScrollableList extends PureComponent {
|
||||||
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
|
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
|
||||||
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
|
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
|
||||||
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
|
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
|
||||||
if ((someItemInserted && this.node.scrollTop > 0) || this.state.mouseMovedRecently) {
|
|
||||||
|
if ((someItemInserted && this.node.scrollTop > 0) || this.mouseMovedRecently) {
|
||||||
return this.node.scrollHeight - this.node.scrollTop;
|
return this.node.scrollHeight - this.node.scrollTop;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
@ -161,20 +164,24 @@ export default class ScrollableList extends PureComponent {
|
||||||
|
|
||||||
attachScrollListener () {
|
attachScrollListener () {
|
||||||
this.node.addEventListener('scroll', this.handleScroll);
|
this.node.addEventListener('scroll', this.handleScroll);
|
||||||
|
this.node.addEventListener('wheel', this.handleWheel);
|
||||||
}
|
}
|
||||||
|
|
||||||
detachScrollListener () {
|
detachScrollListener () {
|
||||||
this.node.removeEventListener('scroll', this.handleScroll);
|
this.node.removeEventListener('scroll', this.handleScroll);
|
||||||
|
this.node.removeEventListener('wheel', this.handleWheel);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFirstChildKey (props) {
|
getFirstChildKey (props) {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
let firstChild = children;
|
let firstChild = children;
|
||||||
|
|
||||||
if (children instanceof ImmutableList) {
|
if (children instanceof ImmutableList) {
|
||||||
firstChild = children.get(0);
|
firstChild = children.get(0);
|
||||||
} else if (Array.isArray(children)) {
|
} else if (Array.isArray(children)) {
|
||||||
firstChild = children[0];
|
firstChild = children[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return firstChild && firstChild.key;
|
return firstChild && firstChild.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,20 +189,32 @@ export default class ScrollableList extends PureComponent {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = (e) => {
|
handleLoadMore = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onLoadMore();
|
this.props.onLoadMore();
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, alwaysPrepend, alwaysShowScrollbar, emptyMessage, onLoadMore } = this.props;
|
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, alwaysShowScrollbar, emptyMessage, onLoadMore } = this.props;
|
||||||
const { fullscreen } = this.state;
|
const { fullscreen } = this.state;
|
||||||
const childrenCount = React.Children.count(children);
|
const childrenCount = React.Children.count(children);
|
||||||
|
|
||||||
const loadMore = (hasMore && childrenCount > 0 && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
const loadMore = (hasMore && childrenCount > 0 && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
||||||
let scrollableArea = null;
|
let scrollableArea = null;
|
||||||
|
|
||||||
if (isLoading || childrenCount > 0 || !emptyMessage) {
|
if (showLoading) {
|
||||||
|
scrollableArea = (
|
||||||
|
<div className='scrollable scrollable--flex' ref={this.setRef}>
|
||||||
|
<div role='feed' className='item-list'>
|
||||||
|
{prepend}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='scrollable__append'>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (isLoading || childrenCount > 0 || !emptyMessage) {
|
||||||
scrollableArea = (
|
scrollableArea = (
|
||||||
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||||
<div role='feed' className='item-list'>
|
<div role='feed' className='item-list'>
|
||||||
|
|
|
@ -67,6 +67,7 @@ class Status extends ImmutablePureComponent {
|
||||||
unread: PropTypes.bool,
|
unread: PropTypes.bool,
|
||||||
onMoveUp: PropTypes.func,
|
onMoveUp: PropTypes.func,
|
||||||
onMoveDown: PropTypes.func,
|
onMoveDown: PropTypes.func,
|
||||||
|
showThread: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Avoid checking props that are functions (and whose equality will always
|
// Avoid checking props that are functions (and whose equality will always
|
||||||
|
@ -168,7 +169,7 @@ class Status extends ImmutablePureComponent {
|
||||||
let media = null;
|
let media = null;
|
||||||
let statusAvatar, prepend, rebloggedByText;
|
let statusAvatar, prepend, rebloggedByText;
|
||||||
|
|
||||||
const { intl, hidden, featured, otherAccounts, unread } = this.props;
|
const { intl, hidden, featured, otherAccounts, unread, showThread } = this.props;
|
||||||
|
|
||||||
let { status, account, ...other } = this.props;
|
let { status, account, ...other } = this.props;
|
||||||
|
|
||||||
|
@ -309,6 +310,12 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
{media}
|
{media}
|
||||||
|
|
||||||
|
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
|
||||||
|
<button className='status__content__read-more-button' onClick={this.handleClick}>
|
||||||
|
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<StatusActionBar status={status} account={account} {...other} />
|
<StatusActionBar status={status} account={account} {...other} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -148,7 +148,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
let reblogIcon = 'retweet';
|
let reblogIcon = 'retweet';
|
||||||
let replyIcon;
|
|
||||||
let replyTitle;
|
let replyTitle;
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
||||||
|
@ -191,10 +190,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('in_reply_to_id', null) === null) {
|
if (status.get('in_reply_to_id', null) === null) {
|
||||||
replyIcon = 'reply';
|
|
||||||
replyTitle = intl.formatMessage(messages.reply);
|
replyTitle = intl.formatMessage(messages.reply);
|
||||||
} else {
|
} else {
|
||||||
replyIcon = 'reply-all';
|
|
||||||
replyTitle = intl.formatMessage(messages.replyAll);
|
replyTitle = intl.formatMessage(messages.replyAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,7 +201,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
|
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon='reply' onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
|
||||||
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
<IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
||||||
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
prepend: PropTypes.node,
|
prepend: PropTypes.node,
|
||||||
emptyMessage: PropTypes.node,
|
emptyMessage: PropTypes.node,
|
||||||
alwaysPrepend: PropTypes.bool,
|
alwaysPrepend: PropTypes.bool,
|
||||||
timelineId: PropTypes.string.isRequired,
|
timelineId: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -104,6 +104,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
onMoveUp={this.handleMoveUp}
|
onMoveUp={this.handleMoveUp}
|
||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
contextType={timelineId}
|
contextType={timelineId}
|
||||||
|
showThread
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : null;
|
) : null;
|
||||||
|
@ -117,12 +118,13 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
onMoveUp={this.handleMoveUp}
|
onMoveUp={this.handleMoveUp}
|
||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
contextType={timelineId}
|
contextType={timelineId}
|
||||||
|
showThread
|
||||||
/>
|
/>
|
||||||
)).concat(scrollableContent);
|
)).concat(scrollableContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} shouldUpdateScroll={shouldUpdateScroll} ref={this.setRef}>
|
<ScrollableList {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} shouldUpdateScroll={shouldUpdateScroll} ref={this.setRef}>
|
||||||
{scrollableContent}
|
{scrollableContent}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import HeaderContainer from './containers/header_container';
|
||||||
import ColumnBackButton from '../../components/column_back_button';
|
import ColumnBackButton from '../../components/column_back_button';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
|
const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
|
||||||
const path = withReplies ? `${accountId}:with_replies` : accountId;
|
const path = withReplies ? `${accountId}:with_replies` : accountId;
|
||||||
|
@ -78,6 +79,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
|
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
|
||||||
|
alwaysPrepend
|
||||||
scrollKey='account_timeline'
|
scrollKey='account_timeline'
|
||||||
statusIds={statusIds}
|
statusIds={statusIds}
|
||||||
featuredStatusIds={featuredStatusIds}
|
featuredStatusIds={featuredStatusIds}
|
||||||
|
@ -85,6 +87,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
shouldUpdateScroll={shouldUpdateScroll}
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />}
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
@ -159,7 +159,7 @@ class ActionBar extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='detailed-status__action-bar'>
|
<div className='detailed-status__action-bar'>
|
||||||
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
|
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
|
||||||
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
|
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
|
||||||
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import {
|
import {
|
||||||
ACCOUNT_FOLLOW_SUCCESS,
|
ACCOUNT_FOLLOW_SUCCESS,
|
||||||
|
ACCOUNT_FOLLOW_REQUEST,
|
||||||
|
ACCOUNT_FOLLOW_FAIL,
|
||||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
ACCOUNT_UNFOLLOW_SUCCESS,
|
||||||
|
ACCOUNT_UNFOLLOW_REQUEST,
|
||||||
|
ACCOUNT_UNFOLLOW_FAIL,
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
ACCOUNT_UNBLOCK_SUCCESS,
|
ACCOUNT_UNBLOCK_SUCCESS,
|
||||||
ACCOUNT_MUTE_SUCCESS,
|
ACCOUNT_MUTE_SUCCESS,
|
||||||
|
@ -37,6 +41,14 @@ const initialState = ImmutableMap();
|
||||||
|
|
||||||
export default function relationships(state = initialState, action) {
|
export default function relationships(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
|
case ACCOUNT_FOLLOW_REQUEST:
|
||||||
|
return state.setIn([action.id, action.locked ? 'requested' : 'following'], true);
|
||||||
|
case ACCOUNT_FOLLOW_FAIL:
|
||||||
|
return state.setIn([action.id, action.locked ? 'requested' : 'following'], false);
|
||||||
|
case ACCOUNT_UNFOLLOW_REQUEST:
|
||||||
|
return state.setIn([action.id, 'following'], false);
|
||||||
|
case ACCOUNT_UNFOLLOW_FAIL:
|
||||||
|
return state.setIn([action.id, 'following'], true);
|
||||||
case ACCOUNT_FOLLOW_SUCCESS:
|
case ACCOUNT_FOLLOW_SUCCESS:
|
||||||
case ACCOUNT_UNFOLLOW_SUCCESS:
|
case ACCOUNT_UNFOLLOW_SUCCESS:
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case ACCOUNT_BLOCK_SUCCESS:
|
||||||
|
|
|
@ -1847,7 +1847,7 @@ a.account__display-name {
|
||||||
}
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
width: 330px;
|
width: 350px;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -2092,6 +2092,16 @@ a.account__display-name {
|
||||||
@supports(display: grid) { // hack to fix Chrome <57
|
@supports(display: grid) { // hack to fix Chrome <57
|
||||||
contain: strict;
|
contain: strict;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--flex {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__append {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable.fullscreen {
|
.scrollable.fullscreen {
|
||||||
|
|
|
@ -330,9 +330,12 @@ code {
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=text],
|
input[type=text],
|
||||||
|
input[type=number],
|
||||||
input[type=email],
|
input[type=email],
|
||||||
input[type=password] {
|
input[type=password],
|
||||||
border-bottom-color: $valid-value-color;
|
textarea,
|
||||||
|
select {
|
||||||
|
border-color: lighten($error-red, 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
|
|
|
@ -94,7 +94,7 @@ class Request
|
||||||
end
|
end
|
||||||
|
|
||||||
def timeout
|
def timeout
|
||||||
{ write: 10, connect: 10, read: 10 }
|
{ connect: 1, read: 10, write: 10 }
|
||||||
end
|
end
|
||||||
|
|
||||||
def http_client
|
def http_client
|
||||||
|
|
|
@ -18,6 +18,6 @@ module AuthorExtractor
|
||||||
acct = "#{username}@#{domain}"
|
acct = "#{username}@#{domain}"
|
||||||
end
|
end
|
||||||
|
|
||||||
ResolveAccountService.new.call(acct, update_profile)
|
ResolveAccountService.new.call(acct, update_profile: update_profile)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,9 +7,9 @@ class FollowService < BaseService
|
||||||
# @param [Account] source_account From which to follow
|
# @param [Account] source_account From which to follow
|
||||||
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
|
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
|
||||||
# @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true
|
# @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true
|
||||||
def call(source_account, uri, reblogs: nil)
|
def call(source_account, target_account, reblogs: nil)
|
||||||
reblogs = true if reblogs.nil?
|
reblogs = true if reblogs.nil?
|
||||||
target_account = uri.is_a?(Account) ? uri : ResolveAccountService.new.call(uri)
|
target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
|
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
|
||||||
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account)
|
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account)
|
||||||
|
@ -42,7 +42,7 @@ class FollowService < BaseService
|
||||||
follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs)
|
follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs)
|
||||||
|
|
||||||
if target_account.local?
|
if target_account.local?
|
||||||
NotifyService.new.call(target_account, follow_request)
|
LocalNotificationWorker.perform_async(target_account.id, follow_request.id, follow_request.class.name)
|
||||||
elsif target_account.ostatus?
|
elsif target_account.ostatus?
|
||||||
NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id)
|
NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id)
|
||||||
AfterRemoteFollowRequestWorker.perform_async(follow_request.id)
|
AfterRemoteFollowRequestWorker.perform_async(follow_request.id)
|
||||||
|
@ -57,7 +57,7 @@ class FollowService < BaseService
|
||||||
follow = source_account.follow!(target_account, reblogs: reblogs)
|
follow = source_account.follow!(target_account, reblogs: reblogs)
|
||||||
|
|
||||||
if target_account.local?
|
if target_account.local?
|
||||||
NotifyService.new.call(target_account, follow)
|
LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name)
|
||||||
else
|
else
|
||||||
Pubsubhubbub::SubscribeWorker.perform_async(target_account.id) unless target_account.subscribed?
|
Pubsubhubbub::SubscribeWorker.perform_async(target_account.id) unless target_account.subscribed?
|
||||||
NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id)
|
NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id)
|
||||||
|
|
|
@ -47,7 +47,7 @@ class ProcessMentionsService < BaseService
|
||||||
mentioned_account = mention.account
|
mentioned_account = mention.account
|
||||||
|
|
||||||
if mentioned_account.local?
|
if mentioned_account.local?
|
||||||
LocalNotificationWorker.perform_async(mention.id)
|
LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name)
|
||||||
elsif mentioned_account.ostatus? && !@status.stream_entry.hidden?
|
elsif mentioned_account.ostatus? && !@status.stream_entry.hidden?
|
||||||
NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id)
|
NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id)
|
||||||
elsif mentioned_account.activitypub?
|
elsif mentioned_account.activitypub?
|
||||||
|
|
|
@ -9,17 +9,27 @@ class ResolveAccountService < BaseService
|
||||||
# Find or create a local account for a remote user.
|
# Find or create a local account for a remote user.
|
||||||
# When creating, look up the user's webfinger and fetch all
|
# When creating, look up the user's webfinger and fetch all
|
||||||
# important information from their feed
|
# important information from their feed
|
||||||
# @param [String] uri User URI in the form of username@domain
|
# @param [String, Account] uri User URI in the form of username@domain
|
||||||
|
# @param [Hash] options
|
||||||
# @return [Account]
|
# @return [Account]
|
||||||
def call(uri, update_profile = true, redirected = nil)
|
def call(uri, options = {})
|
||||||
|
@options = options
|
||||||
|
|
||||||
|
if uri.is_a?(Account)
|
||||||
|
@account = uri
|
||||||
|
@username = @account.username
|
||||||
|
@domain = @account.domain
|
||||||
|
|
||||||
|
return @account if @account.local? || !webfinger_update_due?
|
||||||
|
else
|
||||||
@username, @domain = uri.split('@')
|
@username, @domain = uri.split('@')
|
||||||
@update_profile = update_profile
|
|
||||||
|
|
||||||
return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
|
return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
|
||||||
|
|
||||||
@account = Account.find_remote(@username, @domain)
|
@account = Account.find_remote(@username, @domain)
|
||||||
|
|
||||||
return @account unless webfinger_update_due?
|
return @account unless webfinger_update_due?
|
||||||
|
end
|
||||||
|
|
||||||
Rails.logger.debug "Looking up webfinger for #{uri}"
|
Rails.logger.debug "Looking up webfinger for #{uri}"
|
||||||
|
|
||||||
|
@ -30,8 +40,8 @@ class ResolveAccountService < BaseService
|
||||||
if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
|
if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
|
||||||
@username = confirmed_username
|
@username = confirmed_username
|
||||||
@domain = confirmed_domain
|
@domain = confirmed_domain
|
||||||
elsif redirected.nil?
|
elsif options[:redirected].nil?
|
||||||
return call("#{confirmed_username}@#{confirmed_domain}", update_profile, true)
|
return call("#{confirmed_username}@#{confirmed_domain}", options.merge(redirected: true))
|
||||||
else
|
else
|
||||||
Rails.logger.debug 'Requested and returned acct URIs do not match'
|
Rails.logger.debug 'Requested and returned acct URIs do not match'
|
||||||
return
|
return
|
||||||
|
@ -76,7 +86,7 @@ class ResolveAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def webfinger_update_due?
|
def webfinger_update_due?
|
||||||
@account.nil? || @account.possibly_stale?
|
@account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def activitypub_ready?
|
def activitypub_ready?
|
||||||
|
@ -93,7 +103,7 @@ class ResolveAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_profile?
|
def update_profile?
|
||||||
@update_profile
|
@options[:update_profile]
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_activitypub
|
def handle_activitypub
|
||||||
|
|
|
@ -14,7 +14,7 @@ class FollowLimitValidator < ActiveModel::Validator
|
||||||
if account.following_count < LIMIT
|
if account.following_count < LIMIT
|
||||||
LIMIT
|
LIMIT
|
||||||
else
|
else
|
||||||
account.followers_count * RATIO
|
[(account.followers_count * RATIO).round, LIMIT].max
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
- if object.errors.any?
|
- if object.errors.any?
|
||||||
.flash-message#error_explanation
|
.flash-message.alert#error_explanation
|
||||||
%strong= t('generic.validation_errors', count: object.errors.count)
|
%strong= t('generic.validation_errors', count: object.errors.count)
|
||||||
|
|
|
@ -3,9 +3,16 @@
|
||||||
class LocalNotificationWorker
|
class LocalNotificationWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
def perform(mention_id)
|
def perform(receiver_account_id, activity_id = nil, activity_class_name = nil)
|
||||||
mention = Mention.find(mention_id)
|
if activity_id.nil? && activity_class_name.nil?
|
||||||
NotifyService.new.call(mention.account, mention)
|
activity = Mention.find(receiver_account_id)
|
||||||
|
receiver = activity.account
|
||||||
|
else
|
||||||
|
receiver = Account.find(receiver_account_id)
|
||||||
|
activity = activity_class_name.constantize.find(activity_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
NotifyService.new.call(receiver, activity)
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,9 @@ workers ENV.fetch('WEB_CONCURRENCY') { 2 }
|
||||||
preload_app!
|
preload_app!
|
||||||
|
|
||||||
on_worker_boot do
|
on_worker_boot do
|
||||||
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
|
ActiveSupport.on_load(:active_record) do
|
||||||
|
ActiveRecord::Base.establish_connection
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
plugin :tmp_restart
|
plugin :tmp_restart
|
||||||
|
|
|
@ -6,6 +6,8 @@ require_relative 'cli_helper'
|
||||||
|
|
||||||
module Mastodon
|
module Mastodon
|
||||||
class MediaCLI < Thor
|
class MediaCLI < Thor
|
||||||
|
include ActionView::Helpers::NumberHelper
|
||||||
|
|
||||||
def self.exit_on_failure?
|
def self.exit_on_failure?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -36,11 +38,13 @@ module Mastodon
|
||||||
time_ago = options[:days].days.ago
|
time_ago = options[:days].days.ago
|
||||||
queued = 0
|
queued = 0
|
||||||
processed = 0
|
processed = 0
|
||||||
|
size = 0
|
||||||
dry_run = options[:dry_run] ? '(DRY RUN)' : ''
|
dry_run = options[:dry_run] ? '(DRY RUN)' : ''
|
||||||
|
|
||||||
if options[:background]
|
if options[:background]
|
||||||
MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).select(:id).reorder(nil).find_in_batches do |media_attachments|
|
MediaAttachment.where.not(remote_url: '').where.not(file_file_name: nil).where('created_at < ?', time_ago).select(:id, :file_file_size).reorder(nil).find_in_batches do |media_attachments|
|
||||||
queued += media_attachments.size
|
queued += media_attachments.size
|
||||||
|
size += media_attachments.reduce(0) { |sum, m| sum + (m.file_file_size || 0) }
|
||||||
Maintenance::UncacheMediaWorker.push_bulk(media_attachments.map(&:id)) unless options[:dry_run]
|
Maintenance::UncacheMediaWorker.push_bulk(media_attachments.map(&:id)) unless options[:dry_run]
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
@ -49,6 +53,7 @@ module Mastodon
|
||||||
Maintenance::UncacheMediaWorker.new.perform(m) unless options[:dry_run]
|
Maintenance::UncacheMediaWorker.new.perform(m) unless options[:dry_run]
|
||||||
options[:verbose] ? say(m.id) : say('.', :green, false)
|
options[:verbose] ? say(m.id) : say('.', :green, false)
|
||||||
processed += 1
|
processed += 1
|
||||||
|
size += m.file_file_size || 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -56,9 +61,9 @@ module Mastodon
|
||||||
say
|
say
|
||||||
|
|
||||||
if options[:background]
|
if options[:background]
|
||||||
say("Scheduled the deletion of #{queued} media attachments #{dry_run}", :green, true)
|
say("Scheduled the deletion of #{queued} media attachments (approx. #{number_to_human_size(size)}) #{dry_run}", :green, true)
|
||||||
else
|
else
|
||||||
say("Removed #{processed} media attachments #{dry_run}", :green, true)
|
say("Removed #{processed} media attachments (approx. #{number_to_human_size(size)}) #{dry_run}", :green, true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -99,10 +99,12 @@ describe AuthorizeInteractionsController do
|
||||||
|
|
||||||
allow(ResolveAccountService).to receive(:new).and_return(service)
|
allow(ResolveAccountService).to receive(:new).and_return(service)
|
||||||
allow(service).to receive(:call).with('user@hostname').and_return(target_account)
|
allow(service).to receive(:call).with('user@hostname').and_return(target_account)
|
||||||
|
allow(service).to receive(:call).with(target_account, skip_webfinger: true).and_return(target_account)
|
||||||
|
|
||||||
|
|
||||||
post :create, params: { acct: 'acct:user@hostname' }
|
post :create, params: { acct: 'acct:user@hostname' }
|
||||||
|
|
||||||
expect(service).to have_received(:call).with('user@hostname')
|
expect(service).to have_received(:call).with(target_account, skip_webfinger: true)
|
||||||
expect(account.following?(target_account)).to be true
|
expect(account.following?(target_account)).to be true
|
||||||
expect(response).to render_template(:success)
|
expect(response).to render_template(:success)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue