Merge pull request #1683 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
776e337b8d
8
Gemfile
8
Gemfile
|
@ -6,7 +6,7 @@ ruby '>= 2.5.0', '< 3.1.0'
|
||||||
gem 'pkg-config', '~> 1.4'
|
gem 'pkg-config', '~> 1.4'
|
||||||
gem 'rexml', '~> 3.2'
|
gem 'rexml', '~> 3.2'
|
||||||
|
|
||||||
gem 'puma', '~> 5.5'
|
gem 'puma', '~> 5.6'
|
||||||
gem 'rails', '~> 6.1.4'
|
gem 'rails', '~> 6.1.4'
|
||||||
gem 'sprockets', '~> 3.7.2'
|
gem 'sprockets', '~> 3.7.2'
|
||||||
gem 'thor', '~> 1.2'
|
gem 'thor', '~> 1.2'
|
||||||
|
@ -18,7 +18,7 @@ gem 'makara', '~> 0.5'
|
||||||
gem 'pghero', '~> 2.8'
|
gem 'pghero', '~> 2.8'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.111', require: false
|
gem 'aws-sdk-s3', '~> 1.112', require: false
|
||||||
gem 'fog-core', '<= 2.1.0'
|
gem 'fog-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'kt-paperclip', '~> 7.0'
|
gem 'kt-paperclip', '~> 7.0'
|
||||||
|
@ -26,7 +26,7 @@ gem 'blurhash', '~> 0.1'
|
||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.8'
|
gem 'addressable', '~> 2.8'
|
||||||
gem 'bootsnap', '~> 1.10.2', require: false
|
gem 'bootsnap', '~> 1.10.3', require: false
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.7'
|
gem 'charlock_holmes', '~> 0.7.7'
|
||||||
gem 'chewy', '~> 7.2'
|
gem 'chewy', '~> 7.2'
|
||||||
|
@ -100,7 +100,7 @@ gem 'rdf-normalize', '~> 0.5'
|
||||||
gem 'redcarpet', '~> 3.5'
|
gem 'redcarpet', '~> 3.5'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'fabrication', '~> 2.24'
|
gem 'fabrication', '~> 2.27'
|
||||||
gem 'fuubar', '~> 2.5'
|
gem 'fuubar', '~> 2.5'
|
||||||
gem 'i18n-tasks', '~> 0.9', require: false
|
gem 'i18n-tasks', '~> 0.9', require: false
|
||||||
gem 'pry-byebug', '~> 3.9'
|
gem 'pry-byebug', '~> 3.9'
|
||||||
|
|
34
Gemfile.lock
34
Gemfile.lock
|
@ -79,17 +79,17 @@ GEM
|
||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
awrence (1.1.1)
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.549.0)
|
aws-partitions (1.553.0)
|
||||||
aws-sdk-core (3.125.5)
|
aws-sdk-core (3.126.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.525.0)
|
aws-partitions (~> 1, >= 1.525.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.53.0)
|
aws-sdk-kms (1.54.0)
|
||||||
aws-sdk-core (~> 3, >= 3.125.0)
|
aws-sdk-core (~> 3, >= 3.126.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.111.3)
|
aws-sdk-s3 (1.112.0)
|
||||||
aws-sdk-core (~> 3, >= 3.125.0)
|
aws-sdk-core (~> 3, >= 3.126.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.4.0)
|
aws-sigv4 (1.4.0)
|
||||||
|
@ -104,7 +104,7 @@ GEM
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
blurhash (0.1.5)
|
blurhash (0.1.5)
|
||||||
ffi (~> 1.14)
|
ffi (~> 1.14)
|
||||||
bootsnap (1.10.2)
|
bootsnap (1.10.3)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (5.2.1)
|
brakeman (5.2.1)
|
||||||
browser (4.2.0)
|
browser (4.2.0)
|
||||||
|
@ -209,7 +209,7 @@ GEM
|
||||||
et-orbi (1.2.6)
|
et-orbi (1.2.6)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.76.0)
|
excon (0.76.0)
|
||||||
fabrication (2.24.0)
|
fabrication (2.27.0)
|
||||||
faker (2.19.0)
|
faker (2.19.0)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
faraday (1.8.0)
|
faraday (1.8.0)
|
||||||
|
@ -407,14 +407,14 @@ GEM
|
||||||
openssl (2.2.0)
|
openssl (2.2.0)
|
||||||
openssl-signature_algorithm (0.4.0)
|
openssl-signature_algorithm (0.4.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ox (2.14.6)
|
ox (2.14.7)
|
||||||
parallel (1.21.0)
|
parallel (1.21.0)
|
||||||
parser (3.1.0.0)
|
parser (3.1.0.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.3.0)
|
pg (1.3.1)
|
||||||
pghero (2.8.2)
|
pghero (2.8.2)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
pkg-config (1.4.7)
|
pkg-config (1.4.7)
|
||||||
|
@ -436,7 +436,7 @@ GEM
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
puma (5.5.2)
|
puma (5.6.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.1)
|
pundit (2.1.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
@ -531,7 +531,7 @@ GEM
|
||||||
rspec-support (3.10.3)
|
rspec-support (3.10.3)
|
||||||
rspec_junit_formatter (0.5.1)
|
rspec_junit_formatter (0.5.1)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (1.25.0)
|
rubocop (1.25.1)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.1.0.0)
|
parser (>= 3.1.0.0)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
@ -563,7 +563,7 @@ GEM
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
securecompare (1.0.0)
|
securecompare (1.0.0)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sidekiq (6.4.0)
|
sidekiq (6.4.1)
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
|
@ -682,11 +682,11 @@ DEPENDENCIES
|
||||||
active_record_query_trace (~> 1.8)
|
active_record_query_trace (~> 1.8)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
annotate (~> 3.1)
|
annotate (~> 3.1)
|
||||||
aws-sdk-s3 (~> 1.111)
|
aws-sdk-s3 (~> 1.112)
|
||||||
better_errors (~> 2.9)
|
better_errors (~> 2.9)
|
||||||
binding_of_caller (~> 1.0)
|
binding_of_caller (~> 1.0)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
bootsnap (~> 1.10.2)
|
bootsnap (~> 1.10.3)
|
||||||
brakeman (~> 5.2)
|
brakeman (~> 5.2)
|
||||||
browser
|
browser
|
||||||
bullet (~> 7.0)
|
bullet (~> 7.0)
|
||||||
|
@ -709,7 +709,7 @@ DEPENDENCIES
|
||||||
doorkeeper (~> 5.5)
|
doorkeeper (~> 5.5)
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
ed25519 (~> 1.3)
|
ed25519 (~> 1.3)
|
||||||
fabrication (~> 2.24)
|
fabrication (~> 2.27)
|
||||||
faker (~> 2.19)
|
faker (~> 2.19)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
|
@ -756,7 +756,7 @@ DEPENDENCIES
|
||||||
private_address_check (~> 0.5)
|
private_address_check (~> 0.5)
|
||||||
pry-byebug (~> 3.9)
|
pry-byebug (~> 3.9)
|
||||||
pry-rails (~> 0.3)
|
pry-rails (~> 0.3)
|
||||||
puma (~> 5.5)
|
puma (~> 5.6)
|
||||||
pundit (~> 2.1)
|
pundit (~> 2.1)
|
||||||
rack (~> 2.2.3)
|
rack (~> 2.2.3)
|
||||||
rack-attack (~> 6.5)
|
rack-attack (~> 6.5)
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Api::V1::MediaController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@media_attachment.update!(media_attachment_params)
|
@media_attachment.update!(updateable_media_attachment_params)
|
||||||
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,6 +42,10 @@ class Api::V1::MediaController < Api::BaseController
|
||||||
params.permit(:file, :thumbnail, :description, :focus)
|
params.permit(:file, :thumbnail, :description, :focus)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def updateable_media_attachment_params
|
||||||
|
params.permit(:thumbnail, :description, :focus)
|
||||||
|
end
|
||||||
|
|
||||||
def file_type_error
|
def file_type_error
|
||||||
{ error: 'File type of uploaded media could not be verified' }
|
{ error: 'File type of uploaded media could not be verified' }
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,6 +33,6 @@ class Api::V1::ReportsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def report_params
|
def report_params
|
||||||
params.permit(:account_id, :comment, :forward, status_ids: [])
|
params.permit(:account_id, :comment, :category, :forward, status_ids: [], rule_ids: [])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
class Api::V1::StatusesController < Api::BaseController
|
class Api::V1::StatusesController < Api::BaseController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
|
||||||
before_action :require_user!, except: [:show, :context]
|
before_action :require_user!, except: [:show, :context]
|
||||||
before_action :set_status, only: [:show, :context]
|
before_action :set_status, only: [:show, :context]
|
||||||
before_action :set_thread, only: [:create]
|
before_action :set_thread, only: [:create]
|
||||||
|
@ -35,25 +35,46 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@status = PostStatusService.new.call(current_user.account,
|
@status = PostStatusService.new.call(
|
||||||
|
current_user.account,
|
||||||
text: status_params[:status],
|
text: status_params[:status],
|
||||||
thread: @thread,
|
thread: @thread,
|
||||||
media_ids: status_params[:media_ids],
|
media_ids: status_params[:media_ids],
|
||||||
sensitive: status_params[:sensitive],
|
sensitive: status_params[:sensitive],
|
||||||
spoiler_text: status_params[:spoiler_text],
|
spoiler_text: status_params[:spoiler_text],
|
||||||
visibility: status_params[:visibility],
|
visibility: status_params[:visibility],
|
||||||
|
language: status_params[:language],
|
||||||
scheduled_at: status_params[:scheduled_at],
|
scheduled_at: status_params[:scheduled_at],
|
||||||
application: doorkeeper_token.application,
|
application: doorkeeper_token.application,
|
||||||
poll: status_params[:poll],
|
poll: status_params[:poll],
|
||||||
content_type: status_params[:content_type],
|
content_type: status_params[:content_type],
|
||||||
idempotency: request.headers['Idempotency-Key'],
|
idempotency: request.headers['Idempotency-Key'],
|
||||||
with_rate_limit: true)
|
with_rate_limit: true
|
||||||
|
)
|
||||||
|
|
||||||
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@status = Status.where(account: current_account).find(params[:id])
|
||||||
|
authorize @status, :update?
|
||||||
|
|
||||||
|
UpdateStatusService.new.call(
|
||||||
|
@status,
|
||||||
|
current_account.id,
|
||||||
|
text: status_params[:status],
|
||||||
|
media_ids: status_params[:media_ids],
|
||||||
|
sensitive: status_params[:sensitive],
|
||||||
|
spoiler_text: status_params[:spoiler_text],
|
||||||
|
poll: status_params[:poll],
|
||||||
|
content_type: status_params[:content_type]
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@status = Status.where(account_id: current_user.account).find(params[:id])
|
@status = Status.where(account: current_account).find(params[:id])
|
||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
|
|
||||||
@status.discard
|
@status.discard
|
||||||
|
@ -85,6 +106,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
:sensitive,
|
:sensitive,
|
||||||
:spoiler_text,
|
:spoiler_text,
|
||||||
:visibility,
|
:visibility,
|
||||||
|
:language,
|
||||||
:scheduled_at,
|
:scheduled_at,
|
||||||
:content_type,
|
:content_type,
|
||||||
media_ids: [],
|
media_ids: [],
|
||||||
|
|
|
@ -75,6 +75,8 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
|
||||||
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
|
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
|
||||||
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
|
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
|
||||||
|
|
||||||
|
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
|
@ -88,6 +90,15 @@ export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function setComposeToStatus(status, text, spoiler_text) {
|
||||||
|
return{
|
||||||
|
type: COMPOSE_SET_STATUS,
|
||||||
|
status,
|
||||||
|
text,
|
||||||
|
spoiler_text,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function changeCompose(text) {
|
export function changeCompose(text) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_CHANGE,
|
type: COMPOSE_CHANGE,
|
||||||
|
@ -151,7 +162,8 @@ export function directCompose(account, routerHistory) {
|
||||||
export function submitCompose(routerHistory) {
|
export function submitCompose(routerHistory) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
let status = getState().getIn(['compose', 'text'], '');
|
let status = getState().getIn(['compose', 'text'], '');
|
||||||
let media = getState().getIn(['compose', 'media_attachments']);
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
|
const statusId = getState().getIn(['compose', 'id'], null);
|
||||||
const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
|
const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
|
||||||
let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
|
let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
|
||||||
|
|
||||||
|
@ -159,11 +171,16 @@ export function submitCompose(routerHistory) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(submitComposeRequest());
|
|
||||||
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
|
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
|
||||||
status = status + ' 👁️';
|
status = status + ' 👁️';
|
||||||
}
|
}
|
||||||
api(getState).post('/api/v1/statuses', {
|
|
||||||
|
dispatch(submitComposeRequest());
|
||||||
|
|
||||||
|
api(getState).request({
|
||||||
|
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
|
||||||
|
method: statusId === null ? 'post' : 'put',
|
||||||
|
data: {
|
||||||
status,
|
status,
|
||||||
content_type: getState().getIn(['compose', 'content_type']),
|
content_type: getState().getIn(['compose', 'content_type']),
|
||||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||||
|
@ -172,7 +189,7 @@ export function submitCompose(routerHistory) {
|
||||||
spoiler_text: spoilerText,
|
spoiler_text: spoilerText,
|
||||||
visibility: getState().getIn(['compose', 'privacy']),
|
visibility: getState().getIn(['compose', 'privacy']),
|
||||||
poll: getState().getIn(['compose', 'poll'], null),
|
poll: getState().getIn(['compose', 'poll'], null),
|
||||||
}, {
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||||
},
|
},
|
||||||
|
@ -202,14 +219,16 @@ export function submitCompose(routerHistory) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (statusId === null) {
|
||||||
insertIfOnline('home');
|
insertIfOnline('home');
|
||||||
|
}
|
||||||
|
|
||||||
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
||||||
insertIfOnline('community');
|
insertIfOnline('community');
|
||||||
if (!response.data.local_only) {
|
if (!response.data.local_only) {
|
||||||
insertIfOnline('public');
|
insertIfOnline('public');
|
||||||
}
|
}
|
||||||
} else if (response.data.visibility === 'direct') {
|
} else if (statusId === null && response.data.visibility === 'direct') {
|
||||||
insertIfOnline('direct');
|
insertIfOnline('direct');
|
||||||
}
|
}
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import api from 'flavours/glitch/util/api';
|
||||||
|
|
||||||
import { deleteFromTimelines } from './timelines';
|
import { deleteFromTimelines } from './timelines';
|
||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
import { ensureComposeIsVisible } from './compose';
|
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||||
|
|
||||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||||
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
||||||
|
@ -26,6 +26,10 @@ export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
|
||||||
|
|
||||||
export const REDRAFT = 'REDRAFT';
|
export const REDRAFT = 'REDRAFT';
|
||||||
|
|
||||||
|
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
|
||||||
|
export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
|
||||||
|
export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
|
||||||
|
|
||||||
export function fetchStatusRequest(id, skipLoading) {
|
export function fetchStatusRequest(id, skipLoading) {
|
||||||
return {
|
return {
|
||||||
type: STATUS_FETCH_REQUEST,
|
type: STATUS_FETCH_REQUEST,
|
||||||
|
@ -81,6 +85,37 @@ export function redraft(status, raw_text, content_type) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const editStatus = (id, routerHistory) => (dispatch, getState) => {
|
||||||
|
let status = getState().getIn(['statuses', id]);
|
||||||
|
|
||||||
|
if (status.get('poll')) {
|
||||||
|
status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchStatusSourceRequest());
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||||
|
dispatch(fetchStatusSourceSuccess());
|
||||||
|
ensureComposeIsVisible(getState, routerHistory);
|
||||||
|
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchStatusSourceFail(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchStatusSourceRequest = () => ({
|
||||||
|
type: STATUS_FETCH_SOURCE_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchStatusSourceSuccess = () => ({
|
||||||
|
type: STATUS_FETCH_SOURCE_SUCCESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchStatusSourceFail = error => ({
|
||||||
|
type: STATUS_FETCH_SOURCE_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
let status = getState().getIn(['statuses', id]);
|
let status = getState().getIn(['statuses', id]);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import classNames from 'classnames';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||||
|
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||||
|
@ -126,6 +127,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEditClick = () => {
|
||||||
|
this.props.onEdit(this.props.status, this.context.router.history);
|
||||||
|
}
|
||||||
|
|
||||||
handlePinClick = () => {
|
handlePinClick = () => {
|
||||||
this.props.onPin(this.props.status);
|
this.props.onPin(this.props.status);
|
||||||
}
|
}
|
||||||
|
@ -225,6 +230,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (writtenByMe) {
|
if (writtenByMe) {
|
||||||
|
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
} from 'flavours/glitch/actions/interactions';
|
} from 'flavours/glitch/actions/interactions';
|
||||||
import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
|
import { muteStatus, unmuteStatus, deleteStatus, editStatus } from 'flavours/glitch/actions/statuses';
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||||
import { initReport } from 'flavours/glitch/actions/reports';
|
import { initReport } from 'flavours/glitch/actions/reports';
|
||||||
|
@ -169,6 +169,10 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onEdit (status, history) {
|
||||||
|
dispatch(editStatus(status.get('id'), history));
|
||||||
|
},
|
||||||
|
|
||||||
onDirect (account, router) {
|
onDirect (account, router) {
|
||||||
dispatch(directCompose(account, router));
|
dispatch(directCompose(account, router));
|
||||||
},
|
},
|
||||||
|
|
|
@ -47,6 +47,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
preselectDate: PropTypes.instanceOf(Date),
|
preselectDate: PropTypes.instanceOf(Date),
|
||||||
isSubmitting: PropTypes.bool,
|
isSubmitting: PropTypes.bool,
|
||||||
isChangingUpload: PropTypes.bool,
|
isChangingUpload: PropTypes.bool,
|
||||||
|
isEditing: PropTypes.bool,
|
||||||
isUploading: PropTypes.bool,
|
isUploading: PropTypes.bool,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
onSubmit: PropTypes.func,
|
onSubmit: PropTypes.func,
|
||||||
|
@ -293,6 +294,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
spoilerText,
|
spoilerText,
|
||||||
suggestions,
|
suggestions,
|
||||||
spoilersAlwaysOn,
|
spoilersAlwaysOn,
|
||||||
|
isEditing,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const countText = this.getFulltextForCharacterCounting();
|
const countText = this.getFulltextForCharacterCounting();
|
||||||
|
@ -364,6 +366,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
<Publisher
|
<Publisher
|
||||||
countText={countText}
|
countText={countText}
|
||||||
disabled={!this.canSubmit()}
|
disabled={!this.canSubmit()}
|
||||||
|
isEditing={isEditing}
|
||||||
onSecondarySubmit={handleSecondarySubmit}
|
onSecondarySubmit={handleSecondarySubmit}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
privacy={privacy}
|
privacy={privacy}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ const messages = defineMessages({
|
||||||
defaultMessage: '{publish}!',
|
defaultMessage: '{publish}!',
|
||||||
id: 'compose_form.publish_loud',
|
id: 'compose_form.publish_loud',
|
||||||
},
|
},
|
||||||
|
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
@ -36,6 +37,7 @@ class Publisher extends ImmutablePureComponent {
|
||||||
onSubmit: PropTypes.func,
|
onSubmit: PropTypes.func,
|
||||||
privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
|
privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
|
||||||
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
|
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
|
||||||
|
isEditing: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit = () => {
|
handleSubmit = () => {
|
||||||
|
@ -43,7 +45,7 @@ class Publisher extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm } = this.props;
|
const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props;
|
||||||
|
|
||||||
const diff = maxChars - length(countText || '');
|
const diff = maxChars - length(countText || '');
|
||||||
const computedClass = classNames('composer--publisher', {
|
const computedClass = classNames('composer--publisher', {
|
||||||
|
@ -51,63 +53,37 @@ class Publisher extends ImmutablePureComponent {
|
||||||
over: diff < 0,
|
over: diff < 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const privacyIcons = { direct: 'envelope', private: 'lock', public: 'globe', unlisted: 'unlock' };
|
||||||
|
|
||||||
|
let publishText;
|
||||||
|
if (isEditing) {
|
||||||
|
publishText = intl.formatMessage(messages.saveChanges);
|
||||||
|
} else if (privacy === 'private' || privacy === 'direct') {
|
||||||
|
const iconId = privacyIcons[privacy];
|
||||||
|
publishText = (
|
||||||
|
<span>
|
||||||
|
<Icon id={iconId} /> {intl.formatMessage(messages.publish)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={computedClass}>
|
<div className={computedClass}>
|
||||||
{sideArm && sideArm !== 'none' ? (
|
{sideArm && !isEditing && sideArm !== 'none' ? (
|
||||||
<Button
|
<Button
|
||||||
className='side_arm'
|
className='side_arm'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onSecondarySubmit}
|
onClick={onSecondarySubmit}
|
||||||
style={{ padding: null }}
|
style={{ padding: null }}
|
||||||
text={
|
text={<Icon id={privacyIcons[sideArm]} />}
|
||||||
<span>
|
|
||||||
<Icon
|
|
||||||
id={{
|
|
||||||
public: 'globe',
|
|
||||||
unlisted: 'unlock',
|
|
||||||
private: 'lock',
|
|
||||||
direct: 'envelope',
|
|
||||||
}[sideArm]}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`}
|
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
className='primary'
|
className='primary'
|
||||||
text={function () {
|
text={publishText}
|
||||||
switch (true) {
|
|
||||||
case !!sideArm && sideArm !== 'none':
|
|
||||||
case privacy === 'direct':
|
|
||||||
case privacy === 'private':
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<Icon
|
|
||||||
id={{
|
|
||||||
direct: 'envelope',
|
|
||||||
private: 'lock',
|
|
||||||
public: 'globe',
|
|
||||||
unlisted: 'unlock',
|
|
||||||
}[privacy]}
|
|
||||||
/>
|
|
||||||
{' '}
|
|
||||||
<FormattedMessage {...messages.publish} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case privacy === 'public':
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<FormattedMessage
|
|
||||||
{...messages.publishLoud}
|
|
||||||
values={{ publish: <FormattedMessage {...messages.publish} /> }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <span><FormattedMessage {...messages.publish} /></span>;
|
|
||||||
}
|
|
||||||
}()}
|
|
||||||
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${privacy}.short` })}`}
|
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${privacy}.short` })}`}
|
||||||
onClick={this.handleSubmit}
|
onClick={this.handleSubmit}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -51,6 +51,7 @@ function mapStateToProps (state) {
|
||||||
focusDate: state.getIn(['compose', 'focusDate']),
|
focusDate: state.getIn(['compose', 'focusDate']),
|
||||||
caretPosition: state.getIn(['compose', 'caretPosition']),
|
caretPosition: state.getIn(['compose', 'caretPosition']),
|
||||||
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||||
|
isEditing: state.getIn(['compose', 'id']) !== null,
|
||||||
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
||||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||||
layout: state.getIn(['local_settings', 'layout']),
|
layout: state.getIn(['local_settings', 'layout']),
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { cancelReplyCompose } from 'flavours/glitch/actions/compose';
|
import { cancelReplyCompose } from 'flavours/glitch/actions/compose';
|
||||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
|
||||||
import ReplyIndicator from '../components/reply_indicator';
|
import ReplyIndicator from '../components/reply_indicator';
|
||||||
|
|
||||||
function makeMapStateToProps (state) {
|
const makeMapStateToProps = () => {
|
||||||
const inReplyTo = state.getIn(['compose', 'in_reply_to']);
|
const mapStateToProps = state => {
|
||||||
|
let statusId = state.getIn(['compose', 'id'], null);
|
||||||
|
let editing = true;
|
||||||
|
|
||||||
|
if (statusId === null) {
|
||||||
|
statusId = state.getIn(['compose', 'in_reply_to']);
|
||||||
|
editing = false;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: inReplyTo ? state.getIn(['statuses', inReplyTo]) : null,
|
status: state.getIn(['statuses', statusId]),
|
||||||
|
editing,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -11,6 +11,7 @@ import classNames from 'classnames';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||||
|
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||||
|
@ -52,6 +53,7 @@ class ActionBar extends React.PureComponent {
|
||||||
onMuteConversation: PropTypes.func,
|
onMuteConversation: PropTypes.func,
|
||||||
onBlock: PropTypes.func,
|
onBlock: PropTypes.func,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
|
onEdit: PropTypes.func.isRequired,
|
||||||
onDirect: PropTypes.func.isRequired,
|
onDirect: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
onReport: PropTypes.func,
|
onReport: PropTypes.func,
|
||||||
|
@ -84,6 +86,10 @@ class ActionBar extends React.PureComponent {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEditClick = () => {
|
||||||
|
this.props.onEdit(this.props.status, this.context.router.history);
|
||||||
|
}
|
||||||
|
|
||||||
handleDirectClick = () => {
|
handleDirectClick = () => {
|
||||||
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
||||||
}
|
}
|
||||||
|
@ -166,6 +172,7 @@ class ActionBar extends React.PureComponent {
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
directCompose,
|
directCompose,
|
||||||
} from 'flavours/glitch/actions/compose';
|
} from 'flavours/glitch/actions/compose';
|
||||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
||||||
import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
|
import { muteStatus, unmuteStatus, deleteStatus, editStatus } from 'flavours/glitch/actions/statuses';
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||||
import { initReport } from 'flavours/glitch/actions/reports';
|
import { initReport } from 'flavours/glitch/actions/reports';
|
||||||
|
@ -307,6 +307,10 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEditClick = (status, history) => {
|
||||||
|
this.props.dispatch(editStatus(status.get('id'), history));
|
||||||
|
}
|
||||||
|
|
||||||
handleDirectClick = (account, router) => {
|
handleDirectClick = (account, router) => {
|
||||||
this.props.dispatch(directCompose(account, router));
|
this.props.dispatch(directCompose(account, router));
|
||||||
}
|
}
|
||||||
|
@ -585,6 +589,7 @@ class Status extends ImmutablePureComponent {
|
||||||
onReblog={this.handleReblogClick}
|
onReblog={this.handleReblogClick}
|
||||||
onBookmark={this.handleBookmarkClick}
|
onBookmark={this.handleBookmarkClick}
|
||||||
onDelete={this.handleDeleteClick}
|
onDelete={this.handleDeleteClick}
|
||||||
|
onEdit={this.handleEditClick}
|
||||||
onDirect={this.handleDirectClick}
|
onDirect={this.handleDirectClick}
|
||||||
onMention={this.handleMentionClick}
|
onMention={this.handleMentionClick}
|
||||||
onMute={this.handleMuteClick}
|
onMute={this.handleMuteClick}
|
||||||
|
|
|
@ -46,6 +46,7 @@ import {
|
||||||
INIT_MEDIA_EDIT_MODAL,
|
INIT_MEDIA_EDIT_MODAL,
|
||||||
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
|
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
|
||||||
COMPOSE_CHANGE_MEDIA_FOCUS,
|
COMPOSE_CHANGE_MEDIA_FOCUS,
|
||||||
|
COMPOSE_SET_STATUS,
|
||||||
} from 'flavours/glitch/actions/compose';
|
} from 'flavours/glitch/actions/compose';
|
||||||
import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
|
import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
|
||||||
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
|
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
|
||||||
|
@ -75,6 +76,7 @@ const initialState = ImmutableMap({
|
||||||
spoiler: false,
|
spoiler: false,
|
||||||
spoiler_text: '',
|
spoiler_text: '',
|
||||||
privacy: null,
|
privacy: null,
|
||||||
|
id: null,
|
||||||
content_type: defaultContentType || 'text/plain',
|
content_type: defaultContentType || 'text/plain',
|
||||||
text: '',
|
text: '',
|
||||||
focusDate: null,
|
focusDate: null,
|
||||||
|
@ -160,6 +162,7 @@ function apiStatusToTextHashtags (state, status) {
|
||||||
|
|
||||||
function clearAll(state) {
|
function clearAll(state) {
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
|
map.set('id', null);
|
||||||
map.set('text', '');
|
map.set('text', '');
|
||||||
if (defaultContentType) map.set('content_type', defaultContentType);
|
if (defaultContentType) map.set('content_type', defaultContentType);
|
||||||
map.set('spoiler', false);
|
map.set('spoiler', false);
|
||||||
|
@ -400,6 +403,7 @@ export default function compose(state = initialState, action) {
|
||||||
.set('elefriend', (state.get('elefriend') + 1) % totalElefriends);
|
.set('elefriend', (state.get('elefriend') + 1) % totalElefriends);
|
||||||
case COMPOSE_REPLY:
|
case COMPOSE_REPLY:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
|
map.set('id', null);
|
||||||
map.set('in_reply_to', action.status.get('id'));
|
map.set('in_reply_to', action.status.get('id'));
|
||||||
map.set('text', statusToTextMentions(state, action.status));
|
map.set('text', statusToTextMentions(state, action.status));
|
||||||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||||
|
@ -434,6 +438,7 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('spoiler', false);
|
map.set('spoiler', false);
|
||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
map.set('privacy', state.get('default_privacy'));
|
map.set('privacy', state.get('default_privacy'));
|
||||||
|
map.set('id', null);
|
||||||
map.set('poll', null);
|
map.set('poll', null);
|
||||||
map.update(
|
map.update(
|
||||||
'advanced_options',
|
'advanced_options',
|
||||||
|
@ -565,6 +570,34 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.status.get('poll')) {
|
||||||
|
map.set('poll', ImmutableMap({
|
||||||
|
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
|
||||||
|
multiple: action.status.getIn(['poll', 'multiple']),
|
||||||
|
expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
case COMPOSE_SET_STATUS:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.set('id', action.status.get('id'));
|
||||||
|
map.set('text', action.text);
|
||||||
|
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
||||||
|
map.set('privacy', action.status.get('visibility'));
|
||||||
|
map.set('media_attachments', action.status.get('media_attachments'));
|
||||||
|
map.set('focusDate', new Date());
|
||||||
|
map.set('caretPosition', null);
|
||||||
|
map.set('idempotencyKey', uuid());
|
||||||
|
map.set('sensitive', action.status.get('sensitive'));
|
||||||
|
|
||||||
|
if (action.spoiler_text.length > 0) {
|
||||||
|
map.set('spoiler', true);
|
||||||
|
map.set('spoiler_text', action.spoiler_text);
|
||||||
|
} else {
|
||||||
|
map.set('spoiler', false);
|
||||||
|
map.set('spoiler_text', '');
|
||||||
|
}
|
||||||
|
|
||||||
if (action.status.get('poll')) {
|
if (action.status.get('poll')) {
|
||||||
map.set('poll', ImmutableMap({
|
map.set('poll', ImmutableMap({
|
||||||
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
|
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
|
||||||
|
|
|
@ -70,6 +70,8 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
|
||||||
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
|
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
|
||||||
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
|
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
|
||||||
|
|
||||||
|
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
|
@ -83,6 +85,15 @@ export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function setComposeToStatus(status, text, spoiler_text) {
|
||||||
|
return{
|
||||||
|
type: COMPOSE_SET_STATUS,
|
||||||
|
status,
|
||||||
|
text,
|
||||||
|
spoiler_text,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function changeCompose(text) {
|
export function changeCompose(text) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_CHANGE,
|
type: COMPOSE_CHANGE,
|
||||||
|
@ -139,6 +150,7 @@ export function submitCompose(routerHistory) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
const status = getState().getIn(['compose', 'text'], '');
|
const status = getState().getIn(['compose', 'text'], '');
|
||||||
const media = getState().getIn(['compose', 'media_attachments']);
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
|
const statusId = getState().getIn(['compose', 'id'], null);
|
||||||
|
|
||||||
if ((!status || !status.length) && media.size === 0) {
|
if ((!status || !status.length) && media.size === 0) {
|
||||||
return;
|
return;
|
||||||
|
@ -146,7 +158,10 @@ export function submitCompose(routerHistory) {
|
||||||
|
|
||||||
dispatch(submitComposeRequest());
|
dispatch(submitComposeRequest());
|
||||||
|
|
||||||
api(getState).post('/api/v1/statuses', {
|
api(getState).request({
|
||||||
|
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
|
||||||
|
method: statusId === null ? 'post' : 'put',
|
||||||
|
data: {
|
||||||
status,
|
status,
|
||||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||||
media_ids: media.map(item => item.get('id')),
|
media_ids: media.map(item => item.get('id')),
|
||||||
|
@ -154,7 +169,7 @@ export function submitCompose(routerHistory) {
|
||||||
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
|
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
|
||||||
visibility: getState().getIn(['compose', 'privacy']),
|
visibility: getState().getIn(['compose', 'privacy']),
|
||||||
poll: getState().getIn(['compose', 'poll'], null),
|
poll: getState().getIn(['compose', 'poll'], null),
|
||||||
}, {
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||||
},
|
},
|
||||||
|
@ -176,11 +191,11 @@ export function submitCompose(routerHistory) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (response.data.visibility !== 'direct') {
|
if (statusId === null && response.data.visibility !== 'direct') {
|
||||||
insertIfOnline('home');
|
insertIfOnline('home');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
||||||
insertIfOnline('community');
|
insertIfOnline('community');
|
||||||
if (!response.data.local_only) {
|
if (!response.data.local_only) {
|
||||||
insertIfOnline('public');
|
insertIfOnline('public');
|
||||||
|
|
|
@ -2,7 +2,7 @@ import api from '../api';
|
||||||
|
|
||||||
import { deleteFromTimelines } from './timelines';
|
import { deleteFromTimelines } from './timelines';
|
||||||
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
|
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
|
||||||
import { ensureComposeIsVisible } from './compose';
|
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||||
|
|
||||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||||
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
||||||
|
@ -30,6 +30,10 @@ export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
|
||||||
|
|
||||||
export const REDRAFT = 'REDRAFT';
|
export const REDRAFT = 'REDRAFT';
|
||||||
|
|
||||||
|
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
|
||||||
|
export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
|
||||||
|
export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
|
||||||
|
|
||||||
export function fetchStatusRequest(id, skipLoading) {
|
export function fetchStatusRequest(id, skipLoading) {
|
||||||
return {
|
return {
|
||||||
type: STATUS_FETCH_REQUEST,
|
type: STATUS_FETCH_REQUEST,
|
||||||
|
@ -84,6 +88,37 @@ export function redraft(status, raw_text) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const editStatus = (id, routerHistory) => (dispatch, getState) => {
|
||||||
|
let status = getState().getIn(['statuses', id]);
|
||||||
|
|
||||||
|
if (status.get('poll')) {
|
||||||
|
status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchStatusSourceRequest());
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||||
|
dispatch(fetchStatusSourceSuccess());
|
||||||
|
ensureComposeIsVisible(getState, routerHistory);
|
||||||
|
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchStatusSourceFail(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchStatusSourceRequest = () => ({
|
||||||
|
type: STATUS_FETCH_SOURCE_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchStatusSourceSuccess = () => ({
|
||||||
|
type: STATUS_FETCH_SOURCE_SUCCESS,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchStatusSourceFail = error => ({
|
||||||
|
type: STATUS_FETCH_SOURCE_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
let status = getState().getIn(['statuses', id]);
|
let status = getState().getIn(['statuses', id]);
|
||||||
|
|
|
@ -12,6 +12,7 @@ import classNames from 'classnames';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||||
|
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||||
|
@ -137,6 +138,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEditClick = () => {
|
||||||
|
this.props.onEdit(this.props.status, this.context.router.history);
|
||||||
|
}
|
||||||
|
|
||||||
handlePinClick = () => {
|
handlePinClick = () => {
|
||||||
this.props.onPin(this.props.status);
|
this.props.onPin(this.props.status);
|
||||||
}
|
}
|
||||||
|
@ -255,6 +260,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (writtenByMe) {
|
if (writtenByMe) {
|
||||||
|
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
hideStatus,
|
hideStatus,
|
||||||
revealStatus,
|
revealStatus,
|
||||||
toggleStatusCollapse,
|
toggleStatusCollapse,
|
||||||
|
editStatus,
|
||||||
} from '../actions/statuses';
|
} from '../actions/statuses';
|
||||||
import {
|
import {
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
|
@ -142,6 +143,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onEdit (status, history) {
|
||||||
|
dispatch(editStatus(status.get('id'), history));
|
||||||
|
},
|
||||||
|
|
||||||
onDirect (account, router) {
|
onDirect (account, router) {
|
||||||
dispatch(directCompose(account, router));
|
dispatch(directCompose(account, router));
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,6 +29,7 @@ const messages = defineMessages({
|
||||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
||||||
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
|
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
|
||||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||||
|
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
|
@ -50,6 +51,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
preselectDate: PropTypes.instanceOf(Date),
|
preselectDate: PropTypes.instanceOf(Date),
|
||||||
isSubmitting: PropTypes.bool,
|
isSubmitting: PropTypes.bool,
|
||||||
isChangingUpload: PropTypes.bool,
|
isChangingUpload: PropTypes.bool,
|
||||||
|
isEditing: PropTypes.bool,
|
||||||
isUploading: PropTypes.bool,
|
isUploading: PropTypes.bool,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
@ -200,7 +202,9 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
const disabled = this.props.isSubmitting;
|
const disabled = this.props.isSubmitting;
|
||||||
let publishText = '';
|
let publishText = '';
|
||||||
|
|
||||||
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
if (this.props.isEditing) {
|
||||||
|
publishText = intl.formatMessage(messages.saveChanges);
|
||||||
|
} else if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
||||||
publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
|
publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
|
||||||
} else {
|
} else {
|
||||||
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||||
|
|
|
@ -21,6 +21,7 @@ const mapStateToProps = state => ({
|
||||||
caretPosition: state.getIn(['compose', 'caretPosition']),
|
caretPosition: state.getIn(['compose', 'caretPosition']),
|
||||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||||
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||||
|
isEditing: state.getIn(['compose', 'id']) !== null,
|
||||||
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
||||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||||
|
|
|
@ -6,9 +6,20 @@ import ReplyIndicator from '../components/reply_indicator';
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getStatus = makeGetStatus();
|
const getStatus = makeGetStatus();
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => {
|
||||||
status: getStatus(state, { id: state.getIn(['compose', 'in_reply_to']) }),
|
let statusId = state.getIn(['compose', 'id'], null);
|
||||||
});
|
let editing = true;
|
||||||
|
|
||||||
|
if (statusId === null) {
|
||||||
|
statusId = state.getIn(['compose', 'in_reply_to']);
|
||||||
|
editing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: getStatus(state, { id: statusId }),
|
||||||
|
editing,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,6 +11,7 @@ import classNames from 'classnames';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||||
|
edit: { id: 'status.edit', defaultMessage: 'Edit' },
|
||||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||||
|
@ -59,6 +60,7 @@ class ActionBar extends React.PureComponent {
|
||||||
onFavourite: PropTypes.func.isRequired,
|
onFavourite: PropTypes.func.isRequired,
|
||||||
onBookmark: PropTypes.func.isRequired,
|
onBookmark: PropTypes.func.isRequired,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
|
onEdit: PropTypes.func.isRequired,
|
||||||
onDirect: PropTypes.func.isRequired,
|
onDirect: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
onMute: PropTypes.func,
|
onMute: PropTypes.func,
|
||||||
|
@ -98,6 +100,10 @@ class ActionBar extends React.PureComponent {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEditClick = () => {
|
||||||
|
this.props.onEdit(this.props.status, this.context.router.history);
|
||||||
|
}
|
||||||
|
|
||||||
handleDirectClick = () => {
|
handleDirectClick = () => {
|
||||||
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
||||||
}
|
}
|
||||||
|
@ -209,6 +215,7 @@ class ActionBar extends React.PureComponent {
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
muteStatus,
|
muteStatus,
|
||||||
unmuteStatus,
|
unmuteStatus,
|
||||||
deleteStatus,
|
deleteStatus,
|
||||||
|
editStatus,
|
||||||
hideStatus,
|
hideStatus,
|
||||||
revealStatus,
|
revealStatus,
|
||||||
} from '../../actions/statuses';
|
} from '../../actions/statuses';
|
||||||
|
@ -273,6 +274,10 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEditClick = (status, history) => {
|
||||||
|
this.props.dispatch(editStatus(status.get('id'), history));
|
||||||
|
}
|
||||||
|
|
||||||
handleDirectClick = (account, router) => {
|
handleDirectClick = (account, router) => {
|
||||||
this.props.dispatch(directCompose(account, router));
|
this.props.dispatch(directCompose(account, router));
|
||||||
}
|
}
|
||||||
|
@ -567,6 +572,7 @@ class Status extends ImmutablePureComponent {
|
||||||
onReblog={this.handleReblogClick}
|
onReblog={this.handleReblogClick}
|
||||||
onBookmark={this.handleBookmarkClick}
|
onBookmark={this.handleBookmarkClick}
|
||||||
onDelete={this.handleDeleteClick}
|
onDelete={this.handleDeleteClick}
|
||||||
|
onEdit={this.handleEditClick}
|
||||||
onDirect={this.handleDirectClick}
|
onDirect={this.handleDirectClick}
|
||||||
onMention={this.handleMentionClick}
|
onMention={this.handleMentionClick}
|
||||||
onMute={this.handleMuteClick}
|
onMute={this.handleMuteClick}
|
||||||
|
|
|
@ -43,6 +43,7 @@ import {
|
||||||
INIT_MEDIA_EDIT_MODAL,
|
INIT_MEDIA_EDIT_MODAL,
|
||||||
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
|
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
|
||||||
COMPOSE_CHANGE_MEDIA_FOCUS,
|
COMPOSE_CHANGE_MEDIA_FOCUS,
|
||||||
|
COMPOSE_SET_STATUS,
|
||||||
} from '../actions/compose';
|
} from '../actions/compose';
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
import { STORE_HYDRATE } from '../actions/store';
|
import { STORE_HYDRATE } from '../actions/store';
|
||||||
|
@ -58,6 +59,7 @@ const initialState = ImmutableMap({
|
||||||
spoiler: false,
|
spoiler: false,
|
||||||
spoiler_text: '',
|
spoiler_text: '',
|
||||||
privacy: null,
|
privacy: null,
|
||||||
|
id: null,
|
||||||
text: '',
|
text: '',
|
||||||
focusDate: null,
|
focusDate: null,
|
||||||
caretPosition: null,
|
caretPosition: null,
|
||||||
|
@ -107,6 +109,7 @@ function statusToTextMentions(state, status) {
|
||||||
|
|
||||||
function clearAll(state) {
|
function clearAll(state) {
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
|
map.set('id', null);
|
||||||
map.set('text', '');
|
map.set('text', '');
|
||||||
map.set('spoiler', false);
|
map.set('spoiler', false);
|
||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
|
@ -313,6 +316,7 @@ export default function compose(state = initialState, action) {
|
||||||
return state.set('is_composing', action.value);
|
return state.set('is_composing', action.value);
|
||||||
case COMPOSE_REPLY:
|
case COMPOSE_REPLY:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
|
map.set('id', null);
|
||||||
map.set('in_reply_to', action.status.get('id'));
|
map.set('in_reply_to', action.status.get('id'));
|
||||||
map.set('text', statusToTextMentions(state, action.status));
|
map.set('text', statusToTextMentions(state, action.status));
|
||||||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||||
|
@ -329,21 +333,12 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
case COMPOSE_REPLY_CANCEL:
|
|
||||||
case COMPOSE_RESET:
|
|
||||||
return state.withMutations(map => {
|
|
||||||
map.set('in_reply_to', null);
|
|
||||||
map.set('text', '');
|
|
||||||
map.set('spoiler', false);
|
|
||||||
map.set('spoiler_text', '');
|
|
||||||
map.set('privacy', state.get('default_privacy'));
|
|
||||||
map.set('poll', null);
|
|
||||||
map.set('idempotencyKey', uuid());
|
|
||||||
});
|
|
||||||
case COMPOSE_SUBMIT_REQUEST:
|
case COMPOSE_SUBMIT_REQUEST:
|
||||||
return state.set('is_submitting', true);
|
return state.set('is_submitting', true);
|
||||||
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
case COMPOSE_UPLOAD_CHANGE_REQUEST:
|
||||||
return state.set('is_changing_upload', true);
|
return state.set('is_changing_upload', true);
|
||||||
|
case COMPOSE_REPLY_CANCEL:
|
||||||
|
case COMPOSE_RESET:
|
||||||
case COMPOSE_SUBMIT_SUCCESS:
|
case COMPOSE_SUBMIT_SUCCESS:
|
||||||
return clearAll(state);
|
return clearAll(state);
|
||||||
case COMPOSE_SUBMIT_FAIL:
|
case COMPOSE_SUBMIT_FAIL:
|
||||||
|
@ -454,6 +449,34 @@ export default function compose(state = initialState, action) {
|
||||||
map.set('spoiler_text', '');
|
map.set('spoiler_text', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.status.get('poll')) {
|
||||||
|
map.set('poll', ImmutableMap({
|
||||||
|
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
|
||||||
|
multiple: action.status.getIn(['poll', 'multiple']),
|
||||||
|
expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
case COMPOSE_SET_STATUS:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.set('id', action.status.get('id'));
|
||||||
|
map.set('text', action.text);
|
||||||
|
map.set('in_reply_to', action.status.get('in_reply_to_id'));
|
||||||
|
map.set('privacy', action.status.get('visibility'));
|
||||||
|
map.set('media_attachments', action.status.get('media_attachments'));
|
||||||
|
map.set('focusDate', new Date());
|
||||||
|
map.set('caretPosition', null);
|
||||||
|
map.set('idempotencyKey', uuid());
|
||||||
|
map.set('sensitive', action.status.get('sensitive'));
|
||||||
|
|
||||||
|
if (action.spoiler_text.length > 0) {
|
||||||
|
map.set('spoiler', true);
|
||||||
|
map.set('spoiler_text', action.spoiler_text);
|
||||||
|
} else {
|
||||||
|
map.set('spoiler', false);
|
||||||
|
map.set('spoiler_text', '');
|
||||||
|
}
|
||||||
|
|
||||||
if (action.status.get('poll')) {
|
if (action.status.get('poll')) {
|
||||||
map.set('poll', ImmutableMap({
|
map.set('poll', ImmutableMap({
|
||||||
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
|
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
|
||||||
|
|
|
@ -208,6 +208,10 @@ class MediaAttachment < ApplicationRecord
|
||||||
file.blank? && remote_url.present?
|
file.blank? && remote_url.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def significantly_changed?
|
||||||
|
description_previously_changed? || thumbnail_updated_at_previously_changed? || file_meta_previously_changed?
|
||||||
|
end
|
||||||
|
|
||||||
def larger_media_format?
|
def larger_media_format?
|
||||||
video? || gifv? || audio?
|
video? || gifv? || audio?
|
||||||
end
|
end
|
||||||
|
|
|
@ -83,6 +83,12 @@ class Poll < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reset_votes!
|
||||||
|
self.cached_tallies = options.map { 0 }
|
||||||
|
self.votes_count = 0
|
||||||
|
votes.delete_all unless new_record?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def prepare_cached_tallies
|
def prepare_cached_tallies
|
||||||
|
|
|
@ -39,6 +39,9 @@ class Report < ApplicationRecord
|
||||||
scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with({ user: [:invite_request, :invite] })) }
|
scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with({ user: [:invite_request, :invite] })) }
|
||||||
|
|
||||||
validates :comment, length: { maximum: 1_000 }
|
validates :comment, length: { maximum: 1_000 }
|
||||||
|
validates :rule_ids, absence: true, unless: :violation?
|
||||||
|
|
||||||
|
validate :validate_rule_ids
|
||||||
|
|
||||||
enum category: {
|
enum category: {
|
||||||
other: 0,
|
other: 0,
|
||||||
|
@ -122,4 +125,10 @@ class Report < ApplicationRecord
|
||||||
def set_uri
|
def set_uri
|
||||||
self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? && account.local?
|
self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? && account.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_rule_ids
|
||||||
|
return unless violation?
|
||||||
|
|
||||||
|
errors.add(:rule_ids, I18n.t('reports.errors.invalid_rules')) unless rules.size == rule_ids.size
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -215,6 +215,17 @@ class Status < ApplicationRecord
|
||||||
public_visibility? || unlisted_visibility?
|
public_visibility? || unlisted_visibility?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def snapshot!(media_attachments_changed: false, account_id: nil, at_time: nil)
|
||||||
|
edits.create!(
|
||||||
|
text: text,
|
||||||
|
spoiler_text: spoiler_text,
|
||||||
|
media_attachments_changed: media_attachments_changed,
|
||||||
|
account_id: account_id || self.account_id,
|
||||||
|
content_type: content_type,
|
||||||
|
created_at: at_time || edited_at
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def edited?
|
def edited?
|
||||||
edited_at.present?
|
edited_at.present?
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,7 +39,7 @@ class StatusPolicy < ApplicationPolicy
|
||||||
alias unreblog? destroy?
|
alias unreblog? destroy?
|
||||||
|
|
||||||
def update?
|
def update?
|
||||||
staff?
|
staff? || owned?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -95,10 +95,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
|
|
||||||
# If for some reasons the options were changed, it invalidates all previous
|
# If for some reasons the options were changed, it invalidates all previous
|
||||||
# votes, so we need to remove them
|
# votes, so we need to remove them
|
||||||
if poll_parser.significantly_changes?(poll)
|
@poll_changed = true if poll_parser.significantly_changes?(poll)
|
||||||
@poll_changed = true
|
|
||||||
poll.votes.delete_all unless poll.new_record?
|
|
||||||
end
|
|
||||||
|
|
||||||
poll.last_fetched_at = Time.now.utc
|
poll.last_fetched_at = Time.now.utc
|
||||||
poll.options = poll_parser.options
|
poll.options = poll_parser.options
|
||||||
|
@ -106,6 +103,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
poll.expires_at = poll_parser.expires_at
|
poll.expires_at = poll_parser.expires_at
|
||||||
poll.voters_count = poll_parser.voters_count
|
poll.voters_count = poll_parser.voters_count
|
||||||
poll.cached_tallies = poll_parser.cached_tallies
|
poll.cached_tallies = poll_parser.cached_tallies
|
||||||
|
poll.reset_votes! if @poll_changed
|
||||||
poll.save!
|
poll.save!
|
||||||
|
|
||||||
@status.poll_id = poll.id
|
@status.poll_id = poll.id
|
||||||
|
@ -217,24 +215,18 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
|
|
||||||
return if @status.edits.any?
|
return if @status.edits.any?
|
||||||
|
|
||||||
@status.edits.create(
|
@status.snapshot!(
|
||||||
text: @status.text,
|
|
||||||
spoiler_text: @status.spoiler_text,
|
|
||||||
media_attachments_changed: false,
|
media_attachments_changed: false,
|
||||||
account_id: @account.id,
|
at_time: @status.created_at
|
||||||
created_at: @status.created_at
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_edit!
|
def create_edit!
|
||||||
return unless significant_changes?
|
return unless significant_changes?
|
||||||
|
|
||||||
@status_edit = @status.edits.create(
|
@status.snapshot!(
|
||||||
text: @status.text,
|
|
||||||
spoiler_text: @status.spoiler_text,
|
|
||||||
media_attachments_changed: @media_attachments_changed || @poll_changed,
|
media_attachments_changed: @media_attachments_changed || @poll_changed,
|
||||||
account_id: @account.id,
|
account_id: @account.id
|
||||||
created_at: @status.edited_at
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,40 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ProcessHashtagsService < BaseService
|
class ProcessHashtagsService < BaseService
|
||||||
def call(status, tags = [])
|
def call(status, raw_tags = [])
|
||||||
tags = Extractor.extract_hashtags(status.text) if status.local?
|
@status = status
|
||||||
records = []
|
@account = status.account
|
||||||
|
@raw_tags = status.local? ? Extractor.extract_hashtags(status.text) : raw_tags
|
||||||
|
@previous_tags = status.tags.to_a
|
||||||
|
@current_tags = []
|
||||||
|
|
||||||
Tag.find_or_create_by_names(tags) do |tag|
|
assign_tags!
|
||||||
status.tags << tag
|
update_featured_tags!
|
||||||
records << tag
|
|
||||||
tag.update(last_status_at: status.created_at) if tag.last_status_at.nil? || (tag.last_status_at < status.created_at && tag.last_status_at < 12.hours.ago)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless status.distributable?
|
private
|
||||||
|
|
||||||
status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag|
|
def assign_tags!
|
||||||
featured_tag.increment(status.created_at)
|
@status.tags = @current_tags = Tag.find_or_create_by_names(@raw_tags)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_featured_tags!
|
||||||
|
return unless @status.distributable?
|
||||||
|
|
||||||
|
added_tags = @current_tags - @previous_tags
|
||||||
|
|
||||||
|
unless added_tags.empty?
|
||||||
|
@account.featured_tags.where(tag_id: added_tags.map(&:id)).each do |featured_tag|
|
||||||
|
featured_tag.increment(@status.created_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
removed_tags = @previous_tags - @current_tags
|
||||||
|
|
||||||
|
unless removed_tags.empty?
|
||||||
|
@account.featured_tags.where(tag_id: removed_tags.map(&:id)).each do |featured_tag|
|
||||||
|
featured_tag.decrement(@status.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,13 +8,15 @@ class ReportService < BaseService
|
||||||
@target_account = target_account
|
@target_account = target_account
|
||||||
@status_ids = options.delete(:status_ids) || []
|
@status_ids = options.delete(:status_ids) || []
|
||||||
@comment = options.delete(:comment) || ''
|
@comment = options.delete(:comment) || ''
|
||||||
|
@category = options.delete(:category) || 'other'
|
||||||
|
@rule_ids = options.delete(:rule_ids)
|
||||||
@options = options
|
@options = options
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if @target_account.suspended?
|
raise ActiveRecord::RecordNotFound if @target_account.suspended?
|
||||||
|
|
||||||
create_report!
|
create_report!
|
||||||
notify_staff!
|
notify_staff!
|
||||||
forward_to_origin! if !@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
forward_to_origin! if forward?
|
||||||
|
|
||||||
@report
|
@report
|
||||||
end
|
end
|
||||||
|
@ -27,7 +29,9 @@ class ReportService < BaseService
|
||||||
status_ids: @status_ids,
|
status_ids: @status_ids,
|
||||||
comment: @comment,
|
comment: @comment,
|
||||||
uri: @options[:uri],
|
uri: @options[:uri],
|
||||||
forwarded: ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
forwarded: forward?,
|
||||||
|
category: @category,
|
||||||
|
rule_ids: @rule_ids
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -48,6 +52,10 @@ class ReportService < BaseService
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def forward?
|
||||||
|
!@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
||||||
|
end
|
||||||
|
|
||||||
def payload
|
def payload
|
||||||
Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account))
|
Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account))
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UpdateStatusService < BaseService
|
||||||
|
include Redisable
|
||||||
|
include LanguagesHelper
|
||||||
|
|
||||||
|
# @param [Status] status
|
||||||
|
# @param [Integer] account_id
|
||||||
|
# @param [Hash] options
|
||||||
|
# @option options [Array<Integer>] :media_ids
|
||||||
|
# @option options [Hash] :poll
|
||||||
|
# @option options [String] :text
|
||||||
|
# @option options [String] :spoiler_text
|
||||||
|
# @option options [Boolean] :sensitive
|
||||||
|
# @option options [String] :language
|
||||||
|
# @option options [String] :content_type
|
||||||
|
def call(status, account_id, options = {})
|
||||||
|
@status = status
|
||||||
|
@options = options
|
||||||
|
@account_id = account_id
|
||||||
|
@media_attachments_changed = false
|
||||||
|
@poll_changed = false
|
||||||
|
|
||||||
|
Status.transaction do
|
||||||
|
create_previous_edit!
|
||||||
|
update_media_attachments!
|
||||||
|
update_poll!
|
||||||
|
update_immediate_attributes!
|
||||||
|
create_edit!
|
||||||
|
end
|
||||||
|
|
||||||
|
queue_poll_notifications!
|
||||||
|
reset_preview_card!
|
||||||
|
update_metadata!
|
||||||
|
broadcast_updates!
|
||||||
|
|
||||||
|
@status
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def update_media_attachments!
|
||||||
|
previous_media_attachments = @status.media_attachments.to_a
|
||||||
|
next_media_attachments = validate_media!
|
||||||
|
removed_media_attachments = previous_media_attachments - next_media_attachments
|
||||||
|
added_media_attachments = next_media_attachments - previous_media_attachments
|
||||||
|
|
||||||
|
MediaAttachment.where(id: removed_media_attachments.map(&:id)).update_all(status_id: nil)
|
||||||
|
MediaAttachment.where(id: added_media_attachments.map(&:id)).update_all(status_id: @status.id)
|
||||||
|
|
||||||
|
@status.media_attachments.reload
|
||||||
|
@media_attachments_changed = true if removed_media_attachments.any? || added_media_attachments.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_media!
|
||||||
|
return [] if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable)
|
||||||
|
|
||||||
|
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4 || @options[:poll].present?
|
||||||
|
|
||||||
|
media_attachments = @status.account.media_attachments.where(status_id: [nil, @status.id]).where(scheduled_status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i)).to_a
|
||||||
|
|
||||||
|
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if media_attachments.size > 1 && media_attachments.find(&:audio_or_video?)
|
||||||
|
raise Mastodon::ValidationError, I18n.t('media_attachments.validations.not_ready') if media_attachments.any?(&:not_processed?)
|
||||||
|
|
||||||
|
media_attachments
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_poll!
|
||||||
|
previous_poll = @status.preloadable_poll
|
||||||
|
@previous_expires_at = previous_poll&.expires_at
|
||||||
|
|
||||||
|
if @options[:poll].present?
|
||||||
|
poll = previous_poll || @status.account.polls.new(status: @status, votes_count: 0)
|
||||||
|
|
||||||
|
# If for some reasons the options were changed, it invalidates all previous
|
||||||
|
# votes, so we need to remove them
|
||||||
|
@poll_changed = true if @options[:poll][:options] != poll.options || ActiveModel::Type::Boolean.new.cast(@options[:poll][:multiple]) != poll.multiple
|
||||||
|
|
||||||
|
poll.options = @options[:poll][:options]
|
||||||
|
poll.hide_totals = @options[:poll][:hide_totals] || false
|
||||||
|
poll.multiple = @options[:poll][:multiple] || false
|
||||||
|
poll.expires_in = @options[:poll][:expires_in]
|
||||||
|
poll.reset_votes! if @poll_changed
|
||||||
|
poll.save!
|
||||||
|
|
||||||
|
@status.poll_id = poll.id
|
||||||
|
elsif previous_poll.present?
|
||||||
|
previous_poll.destroy
|
||||||
|
@poll_changed = true
|
||||||
|
@status.poll_id = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_immediate_attributes!
|
||||||
|
@status.text = @options[:text].presence || @options.delete(:spoiler_text) || ''
|
||||||
|
@status.spoiler_text = @options[:spoiler_text] || ''
|
||||||
|
@status.sensitive = @options[:sensitive] || @options[:spoiler_text].present?
|
||||||
|
@status.language = valid_locale_or_nil(@options[:language] || @status.language || @status.account.user&.preferred_posting_language || I18n.default_locale)
|
||||||
|
@status.content_type = @options[:content_type] || @status.content_type
|
||||||
|
@status.edited_at = Time.now.utc
|
||||||
|
|
||||||
|
@status.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_preview_card!
|
||||||
|
return unless @status.text_previously_changed?
|
||||||
|
|
||||||
|
@status.preview_cards.clear
|
||||||
|
LinkCrawlWorker.perform_async(@status.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_metadata!
|
||||||
|
ProcessHashtagsService.new.call(@status)
|
||||||
|
ProcessMentionsService.new.call(@status)
|
||||||
|
end
|
||||||
|
|
||||||
|
def broadcast_updates!
|
||||||
|
DistributionWorker.perform_async(@status.id, { 'update' => true })
|
||||||
|
ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def queue_poll_notifications!
|
||||||
|
poll = @status.preloadable_poll
|
||||||
|
|
||||||
|
# If the poll had no expiration date set but now has, or now has a sooner
|
||||||
|
# expiration date, and people have voted, schedule a notification
|
||||||
|
|
||||||
|
return unless poll.present? && poll.expires_at.present? && poll.votes.exists?
|
||||||
|
|
||||||
|
PollExpirationNotifyWorker.remove_from_scheduled(poll.id) if @previous_expires_at.present? && @previous_expires_at > poll.expires_at
|
||||||
|
PollExpirationNotifyWorker.perform_at(poll.expires_at + 5.minutes, poll.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_previous_edit!
|
||||||
|
# We only need to create a previous edit when no previous edits exist, e.g.
|
||||||
|
# when the status has never been edited. For other cases, we always create
|
||||||
|
# an edit, so the step can be skipped
|
||||||
|
|
||||||
|
return if @status.edits.any?
|
||||||
|
|
||||||
|
@status.snapshot!(
|
||||||
|
media_attachments_changed: false,
|
||||||
|
at_time: @status.created_at
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_edit!
|
||||||
|
@status.snapshot!(
|
||||||
|
media_attachments_changed: @media_attachments_changed || @poll_changed,
|
||||||
|
account_id: @account_id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::StatusUpdateDistributionWorker < ActivityPub::DistributionWorker
|
||||||
|
# Distribute an profile update to servers that might have a copy
|
||||||
|
# of the account in question
|
||||||
|
def perform(status_id, options = {})
|
||||||
|
@options = options.with_indifferent_access
|
||||||
|
@status = Status.find(status_id)
|
||||||
|
@account = @status.account
|
||||||
|
|
||||||
|
distribute!
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def activity
|
||||||
|
ActivityPub::ActivityPresenter.new(
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(@status), '#updates/', @status.edited_at.to_i].join,
|
||||||
|
type: 'Update',
|
||||||
|
actor: ActivityPub::TagManager.instance.uri_for(@status.account),
|
||||||
|
published: @status.edited_at,
|
||||||
|
to: ActivityPub::TagManager.instance.to(@status),
|
||||||
|
cc: ActivityPub::TagManager.instance.cc(@status),
|
||||||
|
virtual_object: @status
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,9 @@
|
||||||
{{- $fullName := include "mastodon.fullname" . -}}
|
{{- $fullName := include "mastodon.fullname" . -}}
|
||||||
{{- $webPort := .Values.mastodon.web.port -}}
|
{{- $webPort := .Values.mastodon.web.port -}}
|
||||||
{{- $streamingPort := .Values.mastodon.streaming.port -}}
|
{{- $streamingPort := .Values.mastodon.streaming.port -}}
|
||||||
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
{{- if or (.Capabilities.APIVersions.Has "networking.k8s.io/v1/Ingress") (not (.Capabilities.APIVersions.Has "networking.k8s.io/v1beta1/Ingress")) }}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
apiVersion: networking.k8s.io/v1beta1
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
apiVersion: extensions/v1beta1
|
apiVersion: extensions/v1beta1
|
||||||
|
@ -35,12 +37,32 @@ spec:
|
||||||
{{- range .paths }}
|
{{- range .paths }}
|
||||||
- path: {{ .path }}
|
- path: {{ .path }}
|
||||||
backend:
|
backend:
|
||||||
|
{{- if or ($.Capabilities.APIVersions.Has "networking.k8s.io/v1/Ingress") (not ($.Capabilities.APIVersions.Has "networking.k8s.io/v1beta1/Ingress")) }}
|
||||||
|
service:
|
||||||
|
name: {{ $fullName }}-web
|
||||||
|
port:
|
||||||
|
number: {{ $webPort }}
|
||||||
|
{{- else }}
|
||||||
serviceName: {{ $fullName }}-web
|
serviceName: {{ $fullName }}-web
|
||||||
servicePort: {{ $webPort }}
|
servicePort: {{ $webPort }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if or ($.Capabilities.APIVersions.Has "networking.k8s.io/v1/Ingress") (not ($.Capabilities.APIVersions.Has "networking.k8s.io/v1beta1/Ingress")) }}
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
{{- end }}
|
||||||
- path: {{ .path }}api/v1/streaming
|
- path: {{ .path }}api/v1/streaming
|
||||||
backend:
|
backend:
|
||||||
|
{{- if or ($.Capabilities.APIVersions.Has "networking.k8s.io/v1/Ingress") (not ($.Capabilities.APIVersions.Has "networking.k8s.io/v1beta1/Ingress")) }}
|
||||||
|
service:
|
||||||
|
name: {{ $fullName }}-streaming
|
||||||
|
port:
|
||||||
|
number: {{ $streamingPort }}
|
||||||
|
{{- else }}
|
||||||
serviceName: {{ $fullName }}-streaming
|
serviceName: {{ $fullName }}-streaming
|
||||||
servicePort: {{ $streamingPort }}
|
servicePort: {{ $streamingPort }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- if or ($.Capabilities.APIVersions.Has "networking.k8s.io/v1/Ingress") (not ($.Capabilities.APIVersions.Has "networking.k8s.io/v1beta1/Ingress")) }}
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
@ -1225,6 +1225,9 @@ en:
|
||||||
reply:
|
reply:
|
||||||
proceed: Proceed to reply
|
proceed: Proceed to reply
|
||||||
prompt: 'You want to reply to this post:'
|
prompt: 'You want to reply to this post:'
|
||||||
|
reports:
|
||||||
|
errors:
|
||||||
|
invalid_rules: does not reference valid rules
|
||||||
scheduled_statuses:
|
scheduled_statuses:
|
||||||
over_daily_limit: You have exceeded the limit of %{limit} scheduled posts for today
|
over_daily_limit: You have exceeded the limit of %{limit} scheduled posts for today
|
||||||
over_total_limit: You have exceeded the limit of %{limit} scheduled posts
|
over_total_limit: You have exceeded the limit of %{limit} scheduled posts
|
||||||
|
|
|
@ -335,7 +335,7 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
# JSON / REST API
|
# JSON / REST API
|
||||||
namespace :v1 do
|
namespace :v1 do
|
||||||
resources :statuses, only: [:create, :show, :destroy] do
|
resources :statuses, only: [:create, :show, :update, :destroy] do
|
||||||
scope module: :statuses do
|
scope module: :statuses do
|
||||||
resources :reblogged_by, controller: :reblogged_by_accounts, only: :index
|
resources :reblogged_by, controller: :reblogged_by_accounts, only: :index
|
||||||
resources :favourited_by, controller: :favourited_by_accounts, only: :index
|
resources :favourited_by, controller: :favourited_by_accounts, only: :index
|
||||||
|
|
|
@ -110,22 +110,25 @@ RSpec.describe Api::V1::MediaController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when not attached to a status' do
|
context 'when the author \'s' do
|
||||||
let(:media) { Fabricate(:media_attachment, status: nil, account: user.account) }
|
let(:status) { nil }
|
||||||
|
let(:media) { Fabricate(:media_attachment, status: status, account: user.account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
put :update, params: { id: media.id, description: 'Lorem ipsum!!!' }
|
||||||
|
end
|
||||||
|
|
||||||
it 'updates the description' do
|
it 'updates the description' do
|
||||||
put :update, params: { id: media.id, description: 'Lorem ipsum!!!' }
|
|
||||||
expect(media.reload.description).to eq 'Lorem ipsum!!!'
|
expect(media.reload.description).to eq 'Lorem ipsum!!!'
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
context 'when attached to a status' do
|
context 'when already attached to a status' do
|
||||||
let(:media) { Fabricate(:media_attachment, status: Fabricate(:status), account: user.account) }
|
let(:status) { Fabricate(:status, account: user.account) }
|
||||||
|
|
||||||
it 'returns http not found' do
|
it 'returns http not found' do
|
||||||
put :update, params: { id: media.id, description: 'Lorem ipsum!!!' }
|
|
||||||
expect(response).to have_http_status(:not_found)
|
expect(response).to have_http_status(:not_found)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -102,6 +102,23 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
|
||||||
expect(Status.find_by(id: status.id)).to be nil
|
expect(Status.find_by(id: status.id)).to be nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'PUT #update' do
|
||||||
|
let(:scopes) { 'write:statuses' }
|
||||||
|
let(:status) { Fabricate(:status, account: user.account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
put :update, params: { id: status.id, status: 'I am updated' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the status' do
|
||||||
|
expect(status.reload.text).to eq 'I am updated'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'without an oauth token' do
|
context 'without an oauth token' do
|
||||||
|
|
|
@ -137,7 +137,7 @@ RSpec.describe StatusPolicy, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
permissions :index?, :update? do
|
permissions :index? do
|
||||||
it 'grants access if staff' do
|
it 'grants access if staff' do
|
||||||
expect(subject).to permit(admin.account)
|
expect(subject).to permit(admin.account)
|
||||||
end
|
end
|
||||||
|
@ -146,4 +146,18 @@ RSpec.describe StatusPolicy, type: :model do
|
||||||
expect(subject).to_not permit(alice)
|
expect(subject).to_not permit(alice)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
permissions :update? do
|
||||||
|
it 'grants access if staff' do
|
||||||
|
expect(subject).to permit(admin.account, status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'grants access if owner' do
|
||||||
|
expect(subject).to permit(status.account, status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'denies access unless staff' do
|
||||||
|
expect(subject).to_not permit(bob, status)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,248 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
|
||||||
|
let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) }
|
||||||
|
|
||||||
|
let(:alice) { Fabricate(:account) }
|
||||||
|
let(:bob) { Fabricate(:account) }
|
||||||
|
|
||||||
|
let(:mentions) { [] }
|
||||||
|
let(:tags) { [] }
|
||||||
|
let(:media_attachments) { [] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
mentions.each { |a| Fabricate(:mention, status: status, account: a) }
|
||||||
|
tags.each { |t| status.tags << t }
|
||||||
|
media_attachments.each { |m| status.media_attachments << m }
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
summary: 'Show more',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
tag: [
|
||||||
|
{ type: 'Hashtag', name: 'hoge' },
|
||||||
|
{ type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:json) { Oj.load(Oj.dump(payload)) }
|
||||||
|
|
||||||
|
subject { described_class.new }
|
||||||
|
|
||||||
|
describe '#call' do
|
||||||
|
it 'updates text' do
|
||||||
|
subject.call(status, json)
|
||||||
|
expect(status.reload.text).to eq 'Hello universe'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates content warning' do
|
||||||
|
subject.call(status, json)
|
||||||
|
expect(status.reload.spoiler_text).to eq 'Show more'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'originally without tags' do
|
||||||
|
before do
|
||||||
|
subject.call(status, json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates tags' do
|
||||||
|
expect(status.tags.reload.map(&:name)).to eq %w(hoge)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'originally with tags' do
|
||||||
|
let(:tags) { [Fabricate(:tag, name: 'test'), Fabricate(:tag, name: 'foo')] }
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
summary: 'Show more',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
tag: [
|
||||||
|
{ type: 'Hashtag', name: 'foo' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.call(status, json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates tags' do
|
||||||
|
expect(status.tags.reload.map(&:name)).to eq %w(foo)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'originally without mentions' do
|
||||||
|
before do
|
||||||
|
subject.call(status, json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates mentions' do
|
||||||
|
expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'originally with mentions' do
|
||||||
|
let(:mentions) { [alice, bob] }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.call(status, json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates mentions' do
|
||||||
|
expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'originally without media attachments' do
|
||||||
|
before do
|
||||||
|
allow(RedownloadMediaWorker).to receive(:perform_async)
|
||||||
|
subject.call(status, json)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
attachment: [
|
||||||
|
{ type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates media attachments' do
|
||||||
|
media_attachment = status.media_attachments.reload.first
|
||||||
|
|
||||||
|
expect(media_attachment).to_not be_nil
|
||||||
|
expect(media_attachment.remote_url).to eq 'https://example.com/foo.png'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'queues download of media attachments' do
|
||||||
|
expect(RedownloadMediaWorker).to have_received(:perform_async)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'records media change in edit' do
|
||||||
|
expect(status.edits.reload.last.media_attachments_changed).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'originally with media attachments' do
|
||||||
|
let(:media_attachments) { [Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png'), Fabricate(:media_attachment, remote_url: 'https://example.com/unused.png')] }
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
attachment: [
|
||||||
|
{ type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png', name: 'A picture' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(RedownloadMediaWorker).to receive(:perform_async)
|
||||||
|
subject.call(status, json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the existing media attachment in-place' do
|
||||||
|
media_attachment = status.media_attachments.reload.first
|
||||||
|
|
||||||
|
expect(media_attachment).to_not be_nil
|
||||||
|
expect(media_attachment.remote_url).to eq 'https://example.com/foo.png'
|
||||||
|
expect(media_attachment.description).to eq 'A picture'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not queue redownload for the existing media attachment' do
|
||||||
|
expect(RedownloadMediaWorker).to_not have_received(:perform_async)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates media attachments' do
|
||||||
|
expect(status.media_attachments.reload.map(&:remote_url)).to eq %w(https://example.com/foo.png)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'records media change in edit' do
|
||||||
|
expect(status.edits.reload.last.media_attachments_changed).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'originally with a poll' do
|
||||||
|
before do
|
||||||
|
poll = Fabricate(:poll, status: status)
|
||||||
|
status.update(preloadable_poll: poll)
|
||||||
|
subject.call(status, json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes poll' do
|
||||||
|
expect(status.reload.poll).to eq nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'records media change in edit' do
|
||||||
|
expect(status.edits.reload.last.media_attachments_changed).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'originally without a poll' do
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'foo',
|
||||||
|
type: 'Question',
|
||||||
|
content: 'Hello universe',
|
||||||
|
updated: '2021-09-08T22:39:25Z',
|
||||||
|
closed: true,
|
||||||
|
oneOf: [
|
||||||
|
{ type: 'Note', name: 'Foo' },
|
||||||
|
{ type: 'Note', name: 'Bar' },
|
||||||
|
{ type: 'Note', name: 'Baz' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.call(status, json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a poll' do
|
||||||
|
poll = status.reload.poll
|
||||||
|
|
||||||
|
expect(poll).to_not be_nil
|
||||||
|
expect(poll.options).to eq %w(Foo Bar Baz)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'records media change in edit' do
|
||||||
|
expect(status.edits.reload.last.media_attachments_changed).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates edit history' do
|
||||||
|
subject.call(status, json)
|
||||||
|
expect(status.edits.reload.map(&:text)).to eq ['Hello world', 'Hello universe']
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets edited timestamp' do
|
||||||
|
subject.call(status, json)
|
||||||
|
expect(status.reload.edited_at.to_s).to eq '2021-09-08 22:39:25 UTC'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'records that no media has been changed in edit' do
|
||||||
|
subject.call(status, json)
|
||||||
|
expect(status.edits.reload.last.media_attachments_changed).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,140 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe UpdateStatusService, type: :service do
|
||||||
|
subject { described_class.new }
|
||||||
|
|
||||||
|
context 'when text changes' do
|
||||||
|
let!(:status) { Fabricate(:status, text: 'Foo') }
|
||||||
|
let(:preview_card) { Fabricate(:preview_card) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
status.preview_cards << preview_card
|
||||||
|
subject.call(status, status.account_id, text: 'Bar')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates text' do
|
||||||
|
expect(status.reload.text).to eq 'Bar'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'resets preview card' do
|
||||||
|
expect(status.reload.preview_card).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'saves edit history' do
|
||||||
|
expect(status.edits.pluck(:text, :media_attachments_changed)).to eq [['Foo', false], ['Bar', false]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when content warning changes' do
|
||||||
|
let!(:status) { Fabricate(:status, text: 'Foo', spoiler_text: '') }
|
||||||
|
let(:preview_card) { Fabricate(:preview_card) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
status.preview_cards << preview_card
|
||||||
|
subject.call(status, status.account_id, text: 'Foo', spoiler_text: 'Bar')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates content warning' do
|
||||||
|
expect(status.reload.spoiler_text).to eq 'Bar'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'saves edit history' do
|
||||||
|
expect(status.edits.pluck(:text, :spoiler_text, :media_attachments_changed)).to eq [['Foo', '', false], ['Foo', 'Bar', false]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when media attachments change' do
|
||||||
|
let!(:status) { Fabricate(:status, text: 'Foo') }
|
||||||
|
let!(:detached_media_attachment) { Fabricate(:media_attachment, account: status.account) }
|
||||||
|
let!(:attached_media_attachment) { Fabricate(:media_attachment, account: status.account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
status.media_attachments << detached_media_attachment
|
||||||
|
subject.call(status, status.account_id, text: 'Foo', media_ids: [attached_media_attachment.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates media attachments' do
|
||||||
|
expect(status.media_attachments.to_a).to eq [attached_media_attachment]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'detaches detached media attachments' do
|
||||||
|
expect(detached_media_attachment.reload.status_id).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'attaches attached media attachments' do
|
||||||
|
expect(attached_media_attachment.reload.status_id).to eq status.id
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'saves edit history' do
|
||||||
|
expect(status.edits.pluck(:text, :media_attachments_changed)).to eq [['Foo', false], ['Foo', true]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when poll changes' do
|
||||||
|
let(:account) { Fabricate(:account) }
|
||||||
|
let!(:status) { Fabricate(:status, text: 'Foo', account: account, poll_attributes: {options: %w(Foo Bar), account: account, multiple: false, hide_totals: false, expires_at: 7.days.from_now }) }
|
||||||
|
let!(:poll) { status.poll }
|
||||||
|
let!(:voter) { Fabricate(:account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
status.update(poll: poll)
|
||||||
|
VoteService.new.call(voter, poll, [0])
|
||||||
|
subject.call(status, status.account_id, text: 'Foo', poll: { options: %w(Bar Baz Foo), expires_in: 5.days.to_i })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates poll' do
|
||||||
|
poll = status.poll.reload
|
||||||
|
expect(poll.options).to eq %w(Bar Baz Foo)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'resets votes' do
|
||||||
|
poll = status.poll.reload
|
||||||
|
expect(poll.votes_count).to eq 0
|
||||||
|
expect(poll.votes.count).to eq 0
|
||||||
|
expect(poll.cached_tallies).to eq [0, 0, 0]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'saves edit history' do
|
||||||
|
expect(status.edits.pluck(:text, :media_attachments_changed)).to eq [['Foo', false], ['Foo', true]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when mentions in text change' do
|
||||||
|
let!(:account) { Fabricate(:account) }
|
||||||
|
let!(:alice) { Fabricate(:account, username: 'alice') }
|
||||||
|
let!(:bob) { Fabricate(:account, username: 'bob') }
|
||||||
|
let!(:status) { PostStatusService.new.call(account, text: 'Hello @alice') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.call(status, status.account_id, text: 'Hello @bob')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'changes mentions' do
|
||||||
|
expect(status.active_mentions.pluck(:account_id)).to eq [bob.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'keeps old mentions as silent mentions' do
|
||||||
|
expect(status.mentions.pluck(:account_id)).to eq [alice.id, bob.id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when hashtags in text change' do
|
||||||
|
let!(:account) { Fabricate(:account) }
|
||||||
|
let!(:status) { PostStatusService.new.call(account, text: 'Hello #foo') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.call(status, status.account_id, text: 'Hello #bar')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'changes tags' do
|
||||||
|
expect(status.tags.pluck(:name)).to eq %w(bar)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'notifies ActivityPub about the update' do
|
||||||
|
status = Fabricate(:status, text: 'Foo')
|
||||||
|
allow(ActivityPub::DistributionWorker).to receive(:perform_async)
|
||||||
|
subject.call(status, status.account_id, text: 'Bar')
|
||||||
|
expect(ActivityPub::DistributionWorker).to have_received(:perform_async)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,48 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe ActivityPub::StatusUpdateDistributionWorker do
|
||||||
|
subject { described_class.new }
|
||||||
|
|
||||||
|
let(:status) { Fabricate(:status, text: 'foo') }
|
||||||
|
let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com') }
|
||||||
|
|
||||||
|
describe '#perform' do
|
||||||
|
before do
|
||||||
|
follower.follow!(status.account)
|
||||||
|
|
||||||
|
status.snapshot!
|
||||||
|
status.text = 'bar'
|
||||||
|
status.edited_at = Time.now.utc
|
||||||
|
status.snapshot!
|
||||||
|
status.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with public status' do
|
||||||
|
before do
|
||||||
|
status.update(visibility: :public)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'delivers to followers' do
|
||||||
|
expect(ActivityPub::DeliveryWorker).to receive(:push_bulk) do |items, &block|
|
||||||
|
expect(items.map(&block)).to match([[kind_of(String), status.account.id, 'http://example.com', anything]])
|
||||||
|
end
|
||||||
|
|
||||||
|
subject.perform(status.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with private status' do
|
||||||
|
before do
|
||||||
|
status.update(visibility: :private)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'delivers to followers' do
|
||||||
|
expect(ActivityPub::DeliveryWorker).to receive(:push_bulk) do |items, &block|
|
||||||
|
expect(items.map(&block)).to match([[kind_of(String), status.account.id, 'http://example.com', anything]])
|
||||||
|
end
|
||||||
|
|
||||||
|
subject.perform(status.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
18
yarn.lock
18
yarn.lock
|
@ -2911,20 +2911,10 @@ caniuse-api@^3.0.0:
|
||||||
lodash.memoize "^4.1.2"
|
lodash.memoize "^4.1.2"
|
||||||
lodash.uniq "^4.5.0"
|
lodash.uniq "^4.5.0"
|
||||||
|
|
||||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219:
|
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001271, caniuse-lite@^1.0.30001286:
|
||||||
version "1.0.30001228"
|
version "1.0.30001310"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz#bfdc5942cd3326fa51ee0b42fbef4da9d492a7fa"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001310.tgz"
|
||||||
integrity sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==
|
integrity sha512-cb9xTV8k9HTIUA3GnPUJCk0meUnrHL5gy5QePfDjxHyNBcnzPzrHFv5GqfP7ue5b1ZyzZL0RJboD6hQlPXjhjg==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001271:
|
|
||||||
version "1.0.30001274"
|
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001274.tgz#26ca36204d15b17601ba6fc35dbdad950a647cc7"
|
|
||||||
integrity sha512-+Nkvv0fHyhISkiMIjnyjmf5YJcQ1IQHZN6U9TLUMroWR38FNwpsC51Gb68yueafX1V6ifOisInSgP9WJFS13ew==
|
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001286:
|
|
||||||
version "1.0.30001300"
|
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001300.tgz#11ab6c57d3eb6f964cba950401fd00a146786468"
|
|
||||||
integrity sha512-cVjiJHWGcNlJi8TZVKNMnvMid3Z3TTdDHmLDzlOdIiZq138Exvo0G+G0wTdVYolxKb4AYwC+38pxodiInVtJSA==
|
|
||||||
|
|
||||||
chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
|
chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
|
|
Loading…
Reference in New Issue