Compare commits

..

1 Commits

Author SHA1 Message Date
David Yip
307a8dfad0 When the mute list changes, queue up a home feed regeneration. #199. 2017-11-19 05:49:39 -06:00
905 changed files with 7203 additions and 23414 deletions

View File

@@ -1,36 +1,21 @@
version: "2"
checks:
argument-count:
enabled: false
complex-logic:
enabled: false
file-lines:
enabled: false
method-complexity:
enabled: false
method-count:
enabled: false
method-lines:
enabled: false
nested-control-flow:
enabled: false
return-statements:
enabled: false
similar-code:
enabled: false
identical-code:
enabled: false
plugins:
engines:
brakeman:
enabled: true
bundler-audit:
enabled: true
duplication:
enabled: false
eslint:
enabled: true
rubocop:
enabled: true
scss-lint:
enabled: true
exclude_patterns:
ratings:
paths:
- "**.rb"
- "**.js"
- "**.scss"
exclude_paths:
- spec/
- vendor/asset

View File

@@ -11,11 +11,10 @@ DB_PASS=
DB_PORT=5432
# Federation
# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
# Note: Changing LOCAL_DOMAIN or LOCAL_HTTPS at a later time will cause unwanted side effects.
# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
LOCAL_DOMAIN=example.com
# Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links)
LOCAL_HTTPS=true
# Use this only if you need to run mastodon on a different domain than the one used for federation.
# You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md

3
.gitmodules vendored
View File

@@ -0,0 +1,3 @@
[submodule "app/javascript/themes/mastodon-go"]
path = app/javascript/themes/mastodon-go
url = https://github.com/marrus-sh/mastodon-go

View File

@@ -27,14 +27,11 @@ addons:
apt:
sources:
- trusty-media
- sourceline: deb https://dl.yarnpkg.com/debian/ stable main
key_url: https://dl.yarnpkg.com/debian/pubkey.gpg
packages:
- ffmpeg
- libicu-dev
- libprotobuf-dev
- protobuf-compiler
- yarn
- libicu-dev
rvm:
- 2.3.4
@@ -45,6 +42,7 @@ services:
install:
- nvm install
- npm install -g yarn
- bundle install --path=vendor/bundle --without development production --retry=3 --jobs=16
- yarn install

View File

@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beatrix.bitrot@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eugen@zeonfederated.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

View File

@@ -7,8 +7,8 @@ ENV UID=991 GID=991 \
RAILS_SERVE_STATIC_FILES=true \
RAILS_ENV=production NODE_ENV=production
ARG YARN_VERSION=1.3.2
ARG YARN_DOWNLOAD_SHA256=6cfe82e530ef0837212f13e45c1565ba53f5199eec2527b85ecbcd88bf26821d
ARG YARN_VERSION=1.1.0
ARG YARN_DOWNLOAD_SHA256=171c1f9ee93c488c0d774ac6e9c72649047c3f896277d88d0f805266519430f3
ARG LIBICONV_VERSION=1.15
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178

View File

@@ -14,13 +14,12 @@ gem 'pg', '~> 0.20'
gem 'pghero', '~> 1.7'
gem 'dotenv-rails', '~> 2.2'
gem 'aws-sdk', '~> 2.10', require: false
gem 'fog-aws', '~> 1.4', require: false
gem 'fog-core', '~> 1.45'
gem 'fog-local', '~> 0.4', require: false
gem 'fog-openstack', '~> 0.1', require: false
gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder', '~> 0.6'
gem 'posix-spawn'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.5'
@@ -29,7 +28,7 @@ gem 'browser'
gem 'charlock_holmes', '~> 0.7.5'
gem 'iso-639'
gem 'cld3', '~> 3.2.0'
gem 'devise', '~> 4.3'
gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0'
gem 'doorkeeper', '~> 4.2'
gem 'fast_blank', '~> 1.0'
@@ -50,6 +49,7 @@ gem 'oj', '~> 3.3'
gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.8'
gem 'pundit', '~> 1.1'
gem 'rabl', '~> 0.13'
gem 'rack-attack', '~> 5.0'
gem 'rack-cors', '~> 0.4', require: 'rack/cors'
gem 'rack-timeout', '~> 0.4'
@@ -59,7 +59,6 @@ gem 'redis', '~> 3.3', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 0.10'
gem 'ruby-oembed', '~> 0.12', require: 'oembed'
gem 'ruby-progressbar', '~> 1.4'
gem 'sanitize', '~> 4.4'
gem 'sidekiq', '~> 5.0'
gem 'sidekiq-scheduler', '~> 2.1'

View File

@@ -24,11 +24,11 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.10.7)
active_model_serializers (0.10.6)
actionpack (>= 4.1, < 6)
activemodel (>= 4.1, < 6)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
active_record_query_trace (1.5.4)
activejob (5.1.4)
activesupport (= 5.1.4)
@@ -57,14 +57,6 @@ GEM
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
aws-sdk (2.10.100)
aws-sdk-resources (= 2.10.100)
aws-sdk-core (2.10.100)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.10.100)
aws-sdk-core (= 2.10.100)
aws-sigv4 (1.0.2)
bcrypt (3.1.11)
better_errors (2.4.0)
coderay (>= 1.0.0)
@@ -91,15 +83,15 @@ GEM
capistrano-bundler (1.3.0)
capistrano (~> 3.1)
sshkit (~> 1.2)
capistrano-rails (1.3.1)
capistrano-rails (1.3.0)
capistrano (~> 3.1)
capistrano-bundler (~> 1.1)
capistrano-rbenv (2.1.3)
capistrano-rbenv (2.1.2)
capistrano (~> 3.1)
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (2.16.1)
capybara (2.15.4)
addressable
mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3)
@@ -121,7 +113,7 @@ GEM
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.3)
crass (1.0.2)
debug_inspector (0.0.3)
devise (4.3.0)
bcrypt (~> 3.0)
@@ -129,11 +121,11 @@ GEM
railties (>= 4.1.0, < 5.2)
responders
warden (~> 1.2.3)
devise-two-factor (3.0.2)
activesupport (< 5.2)
devise-two-factor (3.0.0)
activesupport
attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0)
railties (< 5.2)
railties
rotp (~> 2.0)
diff-lcs (1.3)
docile (1.1.5)
@@ -160,6 +152,11 @@ GEM
i18n (~> 0.5)
fast_blank (1.0.0)
ffi (1.9.18)
fog-aws (1.4.1)
fog-core (~> 1.38)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
fog-core (1.45.0)
builder
excon (~> 0.58)
@@ -173,6 +170,9 @@ GEM
fog-core (>= 1.40)
fog-json (>= 1.0)
ipaddress (>= 0.8)
fog-xml (0.1.3)
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
formatador (0.2.5)
fuubar (2.2.0)
rspec-core (~> 3.0)
@@ -184,7 +184,7 @@ GEM
http (~> 2.2)
nokogiri (~> 1.8)
oj (~> 3.0)
hamlit (2.8.5)
hamlit (2.8.4)
temple (>= 0.8.0)
thor
tilt
@@ -196,7 +196,7 @@ GEM
hamster (3.0.0)
concurrent-ruby (~> 1.0)
hashdiff (0.3.7)
highline (1.7.10)
highline (1.7.8)
hiredis (0.6.1)
hkdf (0.3.0)
htmlentities (4.3.4)
@@ -213,9 +213,9 @@ GEM
httplog (0.99.7)
colorize
rack
i18n (0.9.1)
i18n (0.9.0)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.19)
i18n-tasks (0.9.18)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
easy_translate (>= 0.5.0)
@@ -228,7 +228,6 @@ GEM
idn-ruby (0.1.0)
ipaddress (0.8.3)
iso-639 (0.2.8)
jmespath (1.3.1)
json (2.1.0)
json-ld (2.1.7)
multi_json (~> 1.12)
@@ -237,8 +236,8 @@ GEM
json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11)
rdf (~> 2.2)
jsonapi-renderer (0.2.0)
jwt (2.1.0)
jsonapi-renderer (0.1.3)
jwt (1.5.6)
kaminari (1.1.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.1.1)
@@ -268,8 +267,8 @@ GEM
loofah (2.1.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.0)
mini_mime (>= 0.1.1)
mail (2.6.6)
mime-types (>= 1.16, < 4)
mario-redis-lock (1.2.0)
redis (~> 3, >= 3.0.5)
method_source (0.9.0)
@@ -280,7 +279,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.2)
mini_mime (1.0.0)
mini_mime (0.1.4)
mini_portile2 (2.3.0)
minitest (5.10.3)
msgpack (1.1.0)
@@ -306,7 +305,7 @@ GEM
http (~> 2.0)
nokogiri (~> 1.6)
openssl (~> 2.0)
ox (2.8.2)
ox (2.8.1)
paperclip (5.1.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@@ -317,25 +316,26 @@ GEM
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.12.0)
parallel_tests (2.19.0)
parallel_tests (2.17.0)
parallel
parser (2.4.0.2)
ast (~> 2.3)
parser (2.4.0.0)
ast (~> 2.2)
pg (0.21.0)
pghero (1.7.0)
activerecord
pkg-config (1.2.8)
posix-spawn (0.3.13)
powerpack (0.1.1)
pry (0.11.3)
pry (0.11.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.1)
puma (3.11.0)
public_suffix (3.0.0)
puma (3.10.0)
pundit (1.1.0)
activesupport (>= 3.0.0)
rabl (0.13.1)
activesupport (>= 2.3.14)
rack (2.0.3)
rack-attack (5.0.1)
rack
@@ -344,7 +344,7 @@ GEM
rack
rack-proxy (0.6.2)
rack
rack-test (0.8.2)
rack-test (0.7.0)
rack (>= 1.0, < 3)
rack-timeout (0.4.2)
rails (5.1.4)
@@ -381,11 +381,8 @@ GEM
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
rake (12.3.0)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rdf (2.2.12)
rake (12.2.1)
rdf (2.2.11)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2)
@@ -398,8 +395,8 @@ GEM
redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2)
redis-namespace (1.6.0)
redis (>= 3.0.4)
redis-namespace (1.5.3)
redis (~> 3.0, >= 3.0.4)
redis-rack (2.0.3)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
@@ -424,7 +421,7 @@ GEM
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-rails (3.7.2)
rspec-rails (3.7.1)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
@@ -452,14 +449,10 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
sass (3.5.3)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
scss_lint (0.56.0)
sass (3.4.25)
scss_lint (0.55.0)
rake (>= 0.9, < 13)
sass (~> 3.5.3)
sass (~> 3.4.20)
sidekiq (5.0.5)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
@@ -493,7 +486,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkit (1.15.1)
sshkit (1.14.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
statsd-ruby (1.2.1)
@@ -521,7 +514,7 @@ GEM
uniform_notifier (1.10.0)
warden (1.2.7)
rack (>= 1.0)
webmock (3.1.1)
webmock (3.1.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
@@ -529,12 +522,12 @@ GEM
activesupport (>= 4.2)
rack-proxy (>= 0.6.1)
railties (>= 4.2)
webpush (0.3.3)
webpush (0.3.2)
hkdf (~> 0.2)
jwt (~> 2.0)
jwt
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
websocket-extensions (0.1.2)
xpath (2.1.0)
nokogiri (~> 1.3)
@@ -546,7 +539,6 @@ DEPENDENCIES
active_record_query_trace (~> 1.5)
addressable (~> 2.5)
annotate (~> 2.7)
aws-sdk (~> 2.10)
better_errors (~> 2.4)
binding_of_caller (~> 0.7)
bootsnap
@@ -562,13 +554,14 @@ DEPENDENCIES
charlock_holmes (~> 0.7.5)
cld3 (~> 3.2.0)
climate_control (~> 0.2)
devise (~> 4.3)
devise (~> 4.2)
devise-two-factor (~> 3.0)
doorkeeper (~> 4.2)
dotenv-rails (~> 2.2)
fabrication (~> 2.18)
faker (~> 1.7)
fast_blank (~> 1.0)
fog-aws (~> 1.4)
fog-core (~> 1.45)
fog-local (~> 0.4)
fog-openstack (~> 0.1)
@@ -603,10 +596,10 @@ DEPENDENCIES
pg (~> 0.20)
pghero (~> 1.7)
pkg-config (~> 1.2)
posix-spawn
pry-rails (~> 0.3)
puma (~> 3.10)
pundit (~> 1.1)
rabl (~> 0.13)
rack-attack (~> 5.0)
rack-cors (~> 0.4)
rack-timeout (~> 0.4)
@@ -623,7 +616,6 @@ DEPENDENCIES
rspec-sidekiq (~> 3.0)
rubocop
ruby-oembed (~> 0.12)
ruby-progressbar (~> 1.4)
sanitize (~> 4.4)
scss_lint (~> 0.55)
sidekiq (~> 5.0)
@@ -646,4 +638,4 @@ RUBY VERSION
ruby 2.4.2p198
BUNDLED WITH
1.16.1
1.15.4

View File

@@ -1,7 +1,6 @@
# frozen_string_literal: true
class AboutController < ApplicationController
before_action :set_pack
before_action :set_body_classes
before_action :set_instance_presenter, only: [:show, :more, :terms]
@@ -22,10 +21,6 @@ class AboutController < ApplicationController
helper_method :new_user
def set_pack
use_pack action_name == 'show' ? 'about' : 'common'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end

View File

@@ -7,7 +7,6 @@ class AccountsController < ApplicationController
def show
respond_to do |format|
format.html do
use_pack 'public'
@pinned_statuses = []
if current_account && @account.blocking?(current_account)
@@ -49,7 +48,7 @@ class AccountsController < ApplicationController
end
def default_statuses
@account.statuses.not_local_only.where(visibility: [:public, :unlisted])
@account.statuses.where(visibility: [:public, :unlisted])
end
def only_media_scope

View File

@@ -1,18 +0,0 @@
# frozen_string_literal: true
class ActivityPub::FollowsController < Api::BaseController
include SignatureVerification
def show
render(
json: FollowRequest.includes(:account).references(:account).find_by!(
id: params.require(:id),
accounts: { domain: nil, username: params.require(:account_username) },
target_account: signed_request_account
),
serializer: ActivityPub::FollowSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
)
end
end

View File

@@ -21,7 +21,7 @@ module Admin
def destroy
authorize @account_moderation_note, :destroy?
@account_moderation_note.destroy!
@account_moderation_note.destroy
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
end

View File

@@ -32,21 +32,18 @@ module Admin
def memorialize
authorize @account, :memorialize?
@account.memorialize!
log_action :memorialize, @account
redirect_to admin_account_path(@account.id)
end
def enable
authorize @account.user, :enable?
@account.user.enable!
log_action :enable, @account.user
redirect_to admin_account_path(@account.id)
end
def disable
authorize @account.user, :disable?
@account.user.disable!
log_action :disable, @account.user
redirect_to admin_account_path(@account.id)
end
@@ -89,8 +86,7 @@ module Admin
:username,
:display_name,
:email,
:ip,
:staff
:ip
)
end
end

View File

@@ -1,9 +0,0 @@
# frozen_string_literal: true
module Admin
class ActionLogsController < BaseController
def index
@action_logs = Admin::ActionLog.page(params[:page])
end
end
end

View File

@@ -3,15 +3,9 @@
module Admin
class BaseController < ApplicationController
include Authorization
include AccountableConcern
layout 'admin'
before_action :require_staff!
before_action :set_pack
def set_pack
use_pack 'admin'
end
layout 'admin'
end
end

View File

@@ -7,7 +7,6 @@ module Admin
def create
authorize @user, :confirm?
@user.confirm!
log_action :confirm, @user
redirect_to admin_accounts_path
end

View File

@@ -3,7 +3,6 @@
module Admin
class CustomEmojisController < BaseController
before_action :set_custom_emoji, except: [:index, :new, :create]
before_action :set_filter_params
def index
authorize :custom_emoji, :index?
@@ -21,7 +20,6 @@ module Admin
@custom_emoji = CustomEmoji.new(resource_params)
if @custom_emoji.save
log_action :create, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
else
render :new
@@ -32,53 +30,43 @@ module Admin
authorize @custom_emoji, :update?
if @custom_emoji.update(resource_params)
log_action :update, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.updated_msg')
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
else
flash[:alert] = I18n.t('admin.custom_emojis.update_failed_msg')
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
end
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
end
def destroy
authorize @custom_emoji, :destroy?
@custom_emoji.destroy!
log_action :destroy, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.destroyed_msg')
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
@custom_emoji.destroy
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
end
def copy
authorize @custom_emoji, :copy?
emoji = CustomEmoji.find_or_initialize_by(domain: nil,
shortcode: @custom_emoji.shortcode)
emoji = CustomEmoji.find_or_initialize_by(domain: nil, shortcode: @custom_emoji.shortcode)
emoji.image = @custom_emoji.image
if emoji.save
log_action :create, emoji
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
else
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
end
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
redirect_to admin_custom_emojis_path(page: params[:page])
end
def enable
authorize @custom_emoji, :enable?
@custom_emoji.update!(disabled: false)
log_action :enable, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.enabled_msg')
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
end
def disable
authorize @custom_emoji, :disable?
@custom_emoji.update!(disabled: true)
log_action :disable, @custom_emoji
flash[:notice] = I18n.t('admin.custom_emojis.disabled_msg')
redirect_to admin_custom_emojis_path(page: params[:page], **@filter_params)
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
end
private
@@ -87,10 +75,6 @@ module Admin
@custom_emoji = CustomEmoji.find(params[:id])
end
def set_filter_params
@filter_params = filter_params.to_hash.symbolize_keys
end
def resource_params
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
end
@@ -102,9 +86,7 @@ module Admin
def filter_params
params.permit(
:local,
:remote,
:by_domain,
:shortcode
:remote
)
end
end

View File

@@ -21,7 +21,6 @@ module Admin
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
else
render :new
@@ -35,7 +34,6 @@ module Admin
def destroy
authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
log_action :destroy, @domain_block
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
end

View File

@@ -20,7 +20,6 @@ module Admin
@email_domain_block = EmailDomainBlock.new(resource_params)
if @email_domain_block.save
log_action :create, @email_domain_block
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
else
render :new
@@ -29,8 +28,7 @@ module Admin
def destroy
authorize @email_domain_block, :destroy?
@email_domain_block.destroy!
log_action :destroy, @email_domain_block
@email_domain_block.destroy
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
end

View File

@@ -1,47 +0,0 @@
# frozen_string_literal: true
module Admin
class InvitesController < BaseController
def index
authorize :invite, :index?
@invites = filtered_invites.includes(user: :account).page(params[:page])
@invite = Invite.new
end
def create
authorize :invite, :create?
@invite = Invite.new(resource_params)
@invite.user = current_user
if @invite.save
redirect_to admin_invites_path
else
@invites = Invite.page(params[:page])
render :index
end
end
def destroy
@invite = Invite.find(params[:id])
authorize @invite, :destroy?
@invite.expire!
redirect_to admin_invites_path
end
private
def resource_params
params.require(:invite).permit(:max_uses, :expires_in)
end
def filtered_invites
InviteFilter.new(filter_params).results
end
def filter_params
params.permit(:available, :expired)
end
end
end

View File

@@ -8,7 +8,7 @@ module Admin
def create
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
@form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report)
@@ -16,15 +16,13 @@ module Admin
def update
authorize @status, :update?
@status.update!(status_params)
log_action :update, @status
@status.update(status_params)
redirect_to admin_report_path(@report)
end
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
log_action :destroy, @status
render json: @status
end

View File

@@ -25,17 +25,12 @@ module Admin
def process_report
case params[:outcome].to_s
when 'resolve'
@report.update!(action_taken_by_current_attributes)
log_action :resolve, @report
@report.update(action_taken_by_current_attributes)
when 'suspend'
Admin::SuspensionWorker.perform_async(@report.target_account.id)
log_action :resolve, @report
log_action :suspend, @report.target_account
resolve_all_target_account_reports
when 'silence'
@report.target_account.update!(silenced: true)
log_action :resolve, @report
log_action :silence, @report.target_account
@report.target_account.update(silenced: true)
resolve_all_target_account_reports
else
raise ActiveRecord::RecordNotFound

View File

@@ -7,7 +7,6 @@ module Admin
def create
authorize @user, :reset_password?
@user.send_reset_password_instructions
log_action :reset_password, @user
redirect_to admin_accounts_path
end

View File

@@ -7,14 +7,12 @@ module Admin
def promote
authorize @user, :promote?
@user.promote!
log_action :promote, @user
redirect_to admin_account_path(@user.account_id)
end
def demote
authorize @user, :demote?
@user.demote!
log_action :demote, @user
redirect_to admin_account_path(@user.account_id)
end

View File

@@ -16,9 +16,6 @@ module Admin
show_staff_badge
bootstrap_timeline_accounts
thumbnail
min_invite_role
activity_api_enabled
peers_api_enabled
).freeze
BOOLEAN_SETTINGS = %w(
@@ -26,8 +23,6 @@ module Admin
open_deletion
timeline_preview
show_staff_badge
activity_api_enabled
peers_api_enabled
).freeze
UPLOAD_SETTINGS = %w(

View File

@@ -6,15 +6,13 @@ module Admin
def create
authorize @account, :silence?
@account.update!(silenced: true)
log_action :silence, @account
@account.update(silenced: true)
redirect_to admin_accounts_path
end
def destroy
authorize @account, :unsilence?
@account.update!(silenced: false)
log_action :unsilence, @account
@account.update(silenced: false)
redirect_to admin_accounts_path
end

View File

@@ -26,7 +26,7 @@ module Admin
def create
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
@form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params)
@@ -34,15 +34,13 @@ module Admin
def update
authorize @status, :update?
@status.update!(status_params)
log_action :update, @status
@status.update(status_params)
redirect_to admin_account_statuses_path(@account.id, current_params)
end
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
log_action :destroy, @status
render json: @status
end

View File

@@ -7,14 +7,12 @@ module Admin
def create
authorize @account, :suspend?
Admin::SuspensionWorker.perform_async(@account.id)
log_action :suspend, @account
redirect_to admin_accounts_path
end
def destroy
authorize @account, :unsuspend?
@account.unsuspend!
log_action :unsuspend, @account
redirect_to admin_accounts_path
end

View File

@@ -7,7 +7,6 @@ module Admin
def destroy
authorize @user, :disable_2fa?
@user.disable_two_factor!
log_action :disable_2fa, @user
redirect_to admin_accounts_path
end

View File

@@ -72,4 +72,19 @@ class Api::BaseController < ApplicationController
def render_empty
render json: {}, status: 200
end
def set_maps(statuses) # rubocop:disable Style/AccessorMethodName
if current_account.nil?
@reblogs_map = {}
@favourites_map = {}
@mutes_map = {}
return
end
status_ids = statuses.compact.flat_map { |s| [s.id, s.reblog_of_id] }.uniq
conversation_ids = statuses.compact.map(&:conversation_id).compact.uniq
@reblogs_map = Status.reblogs_map(status_ids, current_account)
@favourites_map = Status.favourites_map(status_ids, current_account)
@mutes_map = Status.mutes_map(conversation_ids, current_account)
end
end

View File

@@ -1,20 +0,0 @@
# frozen_string_literal: true
class Api::V1::Accounts::ListsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
before_action :set_account
respond_to :json
def index
@lists = @account.lists.where(account: current_account)
render json: @lists, each_serializer: REST::ListSerializer
end
private
def set_account
@account = Account.find(params[:account_id])
end
end

View File

@@ -17,13 +17,12 @@ class Api::V1::Accounts::SearchController < Api::BaseController
AccountSearchService.new.call(
params[:q],
limit_param(DEFAULT_ACCOUNTS_LIMIT),
current_account,
resolve: truthy_param?(:resolve),
following: truthy_param?(:following)
resolving_search?,
current_account
)
end
def truthy_param?(key)
params[key] == 'true'
def resolving_search?
params[:resolve] == 'true'
end
end

View File

@@ -13,9 +13,11 @@ class Api::V1::AccountsController < Api::BaseController
end
def follow
FollowService.new.call(current_user.account, @account.acct, reblogs: params[:reblogs])
reblogs_arg = { reblogs: params[:reblogs] }
FollowService.new.call(current_user.account, @account.acct, reblogs_arg)
options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: params[:reblogs] } }, requested_map: { @account.id => false } }
options = @account.locked? ? {} : { following_map: { @account.id => reblogs_arg }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
end
@@ -51,7 +53,7 @@ class Api::V1::AccountsController < Api::BaseController
@account = Account.find(params[:id])
end
def relationships(**options)
def relationships(options = {})
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
end
end

View File

@@ -1,36 +0,0 @@
# frozen_string_literal: true
class Api::V1::Instances::ActivityController < Api::BaseController
before_action :require_enabled_api!
respond_to :json
def show
render_cached_json('api:v1:instances:activity:show', expires_in: 1.day) { activity }
end
private
def activity
weeks = []
12.times do |i|
day = i.weeks.ago.to_date
week_id = day.cweek
week = Date.commercial(day.cwyear, week_id)
weeks << {
week: week.to_time.to_i.to_s,
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || 0,
logins: Redis.current.pfcount("activity:logins:#{week_id}"),
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || 0,
}
end
weeks
end
def require_enabled_api!
head 404 unless Setting.activity_api_enabled
end
end

View File

@@ -1,17 +0,0 @@
# frozen_string_literal: true
class Api::V1::Instances::PeersController < Api::BaseController
before_action :require_enabled_api!
respond_to :json
def index
render_cached_json('api:v1:instances:peers:index', expires_in: 1.day) { Account.remote.domains }
end
private
def require_enabled_api!
head 404 unless Setting.peers_api_enabled
end
end

View File

@@ -10,7 +10,7 @@ class Api::V1::Lists::AccountsController < Api::BaseController
after_action :insert_pagination_headers, only: :show
def show
@accounts = load_accounts
@accounts = @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
render json: @accounts, each_serializer: REST::AccountSerializer
end
@@ -35,14 +35,6 @@ class Api::V1::Lists::AccountsController < Api::BaseController
@list = List.where(account: current_account).find(params[:list_id])
end
def load_accounts
if unlimited?
@list.accounts.all
else
@list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
end
end
def list_accounts
Account.find(account_ids)
end
@@ -60,16 +52,12 @@ class Api::V1::Lists::AccountsController < Api::BaseController
end
def next_path
return if unlimited?
if records_continue?
api_v1_list_accounts_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
return if unlimited?
unless @accounts.empty?
api_v1_list_accounts_url pagination_params(since_id: pagination_since_id)
end
@@ -90,8 +78,4 @@ class Api::V1::Lists::AccountsController < Api::BaseController
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end
def unlimited?
params[:limit] == '0'
end
end

View File

@@ -1,14 +1,18 @@
# frozen_string_literal: true
class Api::V1::ListsController < Api::BaseController
LISTS_LIMIT = 50
before_action -> { doorkeeper_authorize! :read }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write }, except: [:index, :show]
before_action :require_user!
before_action :set_list, except: [:index, :create]
after_action :insert_pagination_headers, only: :index
def index
@lists = List.where(account: current_account).all
@lists = List.where(account: current_account).paginate_by_max_id(limit_param(LISTS_LIMIT), params[:max_id], params[:since_id])
render json: @lists, each_serializer: REST::ListSerializer
end
@@ -40,4 +44,36 @@ class Api::V1::ListsController < Api::BaseController
def list_params
params.permit(:title)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
if records_continue?
api_v1_lists_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
unless @lists.empty?
api_v1_lists_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@lists.last.id
end
def pagination_since_id
@lists.first.id
end
def records_continue?
@lists.size == limit_param(LISTS_LIMIT)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end
end

View File

@@ -28,8 +28,6 @@ class Api::Web::PushSubscriptionsController < Api::BaseController
},
}
data.deep_merge!(params[:data]) if params[:data]
web_subscription = ::Web::PushSubscription.create!(
endpoint: params[:subscription][:endpoint],
key_p256dh: params[:subscription][:keys][:p256dh],

View File

@@ -12,8 +12,8 @@ class ApplicationController < ActionController::Base
helper_method :current_account
helper_method :current_session
helper_method :current_flavour
helper_method :current_skin
helper_method :current_theme
helper_method :theme_data
helper_method :single_user_mode?
rescue_from ActionController::RoutingError, with: :not_found
@@ -54,75 +54,6 @@ class ApplicationController < ActionController::Base
new_user_session_path
end
def pack(data, pack_name, skin = 'default')
return nil unless pack?(data, pack_name)
pack_data = {
common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.flavour(current_flavour) : Themes.instance.core, 'common', skin),
flavour: data['name'],
pack: pack_name,
preload: nil,
skin: nil,
supported_locales: data['locales'],
}
if data['pack'][pack_name].is_a?(Hash)
pack_data[:common] = nil if data['pack'][pack_name]['use_common'] == false
pack_data[:pack] = nil unless data['pack'][pack_name]['filename']
if data['pack'][pack_name]['preload']
pack_data[:preload] = [data['pack'][pack_name]['preload']] if data['pack'][pack_name]['preload'].is_a?(String)
pack_data[:preload] = data['pack'][pack_name]['preload'] if data['pack'][pack_name]['preload'].is_a?(Array)
end
if skin != 'default' && data['skin'][skin]
pack_data[:skin] = skin if data['skin'][skin].include?(pack_name)
else # default skin
pack_data[:skin] = 'default' if data['pack'][pack_name]['stylesheet']
end
end
pack_data
end
def pack?(data, pack_name)
if data['pack'].is_a?(Hash) && data['pack'].key?(pack_name)
return true if data['pack'][pack_name].is_a?(String) || data['pack'][pack_name].is_a?(Hash)
end
false
end
def nil_pack(data, pack_name, skin = 'default')
{
common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.flavour(current_flavour) : Themes.instance.core, 'common', skin),
flavour: data['name'],
pack: nil,
preload: nil,
skin: nil,
supported_locales: data['locales'],
}
end
def resolve_pack(data, pack_name, skin = 'default')
result = pack(data, pack_name, skin)
unless result
if data['name'] && data.key?('fallback')
if data['fallback'].nil?
return nil_pack(data, pack_name, skin)
elsif data['fallback'].is_a?(String) && Themes.instance.flavour(data['fallback'])
return resolve_pack(Themes.instance.flavour(data['fallback']), pack_name)
elsif data['fallback'].is_a?(Array)
data['fallback'].each do |fallback|
return resolve_pack(Themes.instance.flavour(fallback), pack_name) if Themes.instance.flavour(fallback)
end
end
return nil_pack(data, pack_name, skin)
end
return data.key?('name') && data['name'] != Setting.default_settings['flavour'] ? resolve_pack(Themes.instance.flavour(Setting.default_settings['flavour']), pack_name) : nil_pack(data, pack_name, skin)
end
result
end
def use_pack(pack_name)
@core = resolve_pack(Themes.instance.core, pack_name)
@theme = resolve_pack(Themes.instance.flavour(current_flavour), pack_name, current_skin)
end
protected
def forbidden
@@ -153,14 +84,13 @@ class ApplicationController < ActionController::Base
@current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id'])
end
def current_flavour
return Setting.default_settings['flavour'] unless Themes.instance.flavours.include? current_user&.setting_flavour
current_user.setting_flavour
def current_theme
return Setting.default_settings['theme'] unless Themes.instance.names.include? current_user&.setting_theme
current_user.setting_theme
end
def current_skin
return 'default' unless Themes.instance.skins_for(current_flavour).include? current_user&.setting_skin
current_user.setting_skin
def theme_data
Themes.instance.get(current_theme)
end
def cache_collection(raw, klass)
@@ -196,13 +126,4 @@ class ApplicationController < ActionController::Base
end
end
end
def render_cached_json(cache_key, **options)
data = Rails.cache.fetch(cache_key, { raw: true }.merge(options)) do
yield.to_json
end
expires_in options[:expires_in], public: true
render json: data
end
end

View File

@@ -3,11 +3,9 @@
class Auth::ConfirmationsController < Devise::ConfirmationsController
layout 'auth'
before_action :set_pack
private
def set_pack
use_pack 'auth'
def show
super do |user|
BootstrapTimelineWorker.perform_async(user.account_id) if user.errors.empty?
end
end
end

View File

@@ -2,7 +2,6 @@
class Auth::PasswordsController < Devise::PasswordsController
before_action :check_validity_of_reset_password_token, only: :edit
before_action :set_pack
layout 'auth'
@@ -18,8 +17,4 @@ class Auth::PasswordsController < Devise::PasswordsController
def reset_password_token_is_valid?
resource_class.with_reset_password_token(params[:reset_password_token]).present?
end
def set_pack
use_pack 'auth'
end
end

View File

@@ -5,7 +5,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create]
before_action :set_pack
before_action :set_sessions, only: [:edit, :update]
before_action :set_instance_presenter, only: [:new, :create, :update]
@@ -17,16 +16,13 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def build_resource(hash = nil)
super(hash)
resource.locale = I18n.locale
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
resource.locale = I18n.locale
resource.build_account if resource.account.nil?
end
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up) do |u|
u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code)
u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation)
end
end
@@ -38,32 +34,12 @@ class Auth::RegistrationsController < Devise::RegistrationsController
new_user_session_path
end
def after_update_path_for(_resource)
edit_user_registration_path
end
def check_enabled_registrations
redirect_to root_path if single_user_mode? || !allowed_registrations?
end
def allowed_registrations?
Setting.open_registrations || (invite_code.present? && Invite.find_by(code: invite_code)&.valid_for_use?)
end
def invite_code
if params[:user]
params[:user][:invite_code]
else
params[:invite_code]
end
redirect_to root_path if single_user_mode? || !Setting.open_registrations
end
private
def set_pack
use_pack %w(edit update).include?(action_name) ? 'admin' : 'auth'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end

View File

@@ -8,7 +8,6 @@ class Auth::SessionsController < Devise::SessionsController
skip_before_action :require_no_authentication, only: [:create]
skip_before_action :check_suspension, only: [:destroy]
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
prepend_before_action :set_pack
before_action :set_instance_presenter, only: [:new]
def create
@@ -86,10 +85,6 @@ class Auth::SessionsController < Devise::SessionsController
private
def set_pack
use_pack 'auth'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end

View File

@@ -4,8 +4,6 @@ class AuthorizeFollowsController < ApplicationController
layout 'modal'
before_action :authenticate_user!
before_action :set_pack
before_action :set_body_classes
def show
@account = located_account || render(:error)
@@ -25,10 +23,6 @@ class AuthorizeFollowsController < ApplicationController
private
def set_pack
use_pack 'modal'
end
def follow_attempt
FollowService.new.call(current_account, acct_without_prefix)
end
@@ -64,8 +58,4 @@ class AuthorizeFollowsController < ApplicationController
def acct_params
params.fetch(:acct, '')
end
def set_body_classes
@body_classes = 'modal-layout'
end
end

View File

@@ -1,9 +0,0 @@
# frozen_string_literal: true
module AccountableConcern
extend ActiveSupport::Concern
def log_action(action, target)
Admin::ActionLog.create(account: current_account, action: action, target: target)
end
end

View File

@@ -44,8 +44,7 @@ module RateLimitHeaders
end
def api_throttle_data
most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] }
request.env['rack.attack.throttle_data'][most_limited_type]
request.env['rack.attack.throttle_data']['api']
end
def request_time

View File

@@ -17,7 +17,6 @@ module UserTrackingConcern
# Mark as signed-in today
current_user.update_tracked_fields!(request)
ActivityTracker.record('activity:logins', current_user.id)
# Regenerate feed if needed
regenerate_feed! if user_needs_feed_update?

View File

@@ -7,9 +7,7 @@ class FollowerAccountsController < ApplicationController
@follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
respond_to do |format|
format.html do
use_pack 'public'
end
format.html
format.json do
render json: collection_presenter,

View File

@@ -7,9 +7,7 @@ class FollowingAccountsController < ApplicationController
@follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
respond_to do |format|
format.html do
use_pack 'public'
end
format.html
format.json do
render json: collection_presenter,

View File

@@ -2,7 +2,6 @@
class HomeController < ApplicationController
before_action :authenticate_user!
before_action :set_pack
before_action :set_initial_state_json
def index
@@ -38,10 +37,6 @@ class HomeController < ApplicationController
redirect_to(default_redirect_path)
end
def set_pack
use_pack 'home'
end
def set_initial_state_json
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json

View File

@@ -1,48 +0,0 @@
# frozen_string_literal: true
class InvitesController < ApplicationController
include Authorization
layout 'admin'
before_action :authenticate_user!
before_action :set_pack
def index
authorize :invite, :create?
@invites = Invite.where(user: current_user)
@invite = Invite.new(expires_in: 1.day.to_i)
end
def create
authorize :invite, :create?
@invite = Invite.new(resource_params)
@invite.user = current_user
if @invite.save
redirect_to invites_path
else
@invites = Invite.where(user: current_user)
render :index
end
end
def destroy
@invite = Invite.where(user: current_user).find(params[:id])
authorize @invite, :destroy?
@invite.expire!
redirect_to invites_path
end
private
def set_pack
use_pack 'settings'
end
def resource_params
params.require(:invite).permit(:max_uses, :expires_in)
end
end

View File

@@ -5,7 +5,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
before_action :store_current_location
before_action :authenticate_resource_owner!
before_action :set_pack
include Localized
@@ -14,8 +13,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
def store_current_location
store_location_for(:user, request.url)
end
def set_pack
use_pack 'auth'
end
end

View File

@@ -5,7 +5,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :store_current_location
before_action :authenticate_resource_owner!
before_action :set_pack
include Localized
@@ -14,8 +13,4 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
def store_current_location
store_location_for(:user, request.url)
end
def set_pack
use_pack 'settings'
end
end

View File

@@ -4,7 +4,6 @@ class RemoteFollowController < ApplicationController
layout 'modal'
before_action :set_account
before_action :set_pack
before_action :gone, if: :suspended_account?
def new
@@ -32,10 +31,6 @@ class RemoteFollowController < ApplicationController
{ acct: session[:remote_follow] }
end
def set_pack
use_pack 'modal'
end
def set_account
@account = Account.find_local!(params[:account_username])
end
@@ -43,8 +38,4 @@ class RemoteFollowController < ApplicationController
def suspended_account?
@account.suspended?
end
def set_body_classes
@body_classes = 'modal-layout'
end
end

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
class Settings::ApplicationsController < Settings::BaseController
class Settings::ApplicationsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_application, only: [:show, :update, :destroy, :regenerate]
before_action :prepare_scopes, only: [:create, :update]

View File

@@ -1,12 +0,0 @@
# frozen_string_literal: true
class Settings::BaseController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_pack
def set_pack
use_pack 'settings'
end
end

View File

@@ -1,8 +1,10 @@
# frozen_string_literal: true
class Settings::DeletesController < Settings::BaseController
class Settings::DeletesController < ApplicationController
layout 'admin'
prepend_before_action :check_enabled_deletion
before_action :check_enabled_deletion
before_action :authenticate_user!
def show
@confirmation = Form::DeleteConfirmation.new

View File

@@ -1,6 +1,10 @@
# frozen_string_literal: true
class Settings::ExportsController < Settings::BaseController
class Settings::ExportsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
def show
@export = Export.new(current_account)
end

View File

@@ -1,35 +0,0 @@
# frozen_string_literal: true
class Settings::FlavoursController < Settings::BaseController
def index
redirect_to action: 'show', flavour: current_flavour
end
def show
unless Themes.instance.flavours.include?(params[:flavour]) or params[:flavour] == current_flavour
redirect_to action: 'show', flavour: current_flavour
end
@listing = Themes.instance.flavours
@selected = params[:flavour]
end
def update
user_settings.update(user_settings_params(params[:flavour]).to_h)
redirect_to action: 'show', flavour: params[:flavour]
end
private
def user_settings
UserSettingsDecorator.new(current_user)
end
def user_settings_params(flavour)
params.require(:user).merge({ setting_flavour: flavour }).permit(
:setting_flavour,
:setting_skin
)
end
end

View File

@@ -2,7 +2,11 @@
require 'sidekiq-bulk'
class Settings::FollowerDomainsController < Settings::BaseController
class Settings::FollowerDomainsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
def show
@account = current_account
@domains = current_account.followers.reorder('MIN(follows.id) DESC').group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)

View File

@@ -1,6 +1,9 @@
# frozen_string_literal: true
class Settings::ImportsController < Settings::BaseController
class Settings::ImportsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_account
def show

View File

@@ -1,7 +1,13 @@
# frozen_string_literal: true
class Settings::KeywordMutesController < Settings::BaseController
class Settings::KeywordMutesController < ApplicationController
include UserTrackingConcern
layout 'admin'
before_action :authenticate_user!
before_action :load_keyword_mute, only: [:edit, :update, :destroy]
after_action :bust_feed_caches, only: [:create, :update, :destroy]
def index
@keyword_mutes = paginated_keyword_mutes_for_account
@@ -51,6 +57,12 @@ class Settings::KeywordMutesController < Settings::BaseController
@keyword_mute = keyword_mutes_for_account.find(params[:id])
end
def bust_feed_caches
# FIXME: this key is really meant to be an implementation detail
Redis.current.del("account:#{current_user.account_id}:regeneration")
regenerate_feed!
end
def keyword_mute_params
params.require(:keyword_mute).permit(:keyword, :whole_word)
end

View File

@@ -1,34 +0,0 @@
# frozen_string_literal: true
class Settings::MigrationsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
def show
@migration = Form::Migration.new(account: current_account.moved_to_account)
end
def update
@migration = Form::Migration.new(resource_params)
if @migration.valid? && migration_account_changed?
current_account.update!(moved_to_account: @migration.account)
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg')
else
render :show
end
end
private
def resource_params
params.require(:migration).permit(:acct)
end
def migration_account_changed?
current_account.moved_to_account_id != @migration.account&.id &&
current_account.id != @migration.account&.id
end
end

View File

@@ -1,6 +1,10 @@
# frozen_string_literal: true
class Settings::NotificationsController < Settings::BaseController
class Settings::NotificationsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
def show; end
def update

View File

@@ -1,6 +1,10 @@
# frozen_string_literal: true
class Settings::PreferencesController < Settings::BaseController
class Settings::PreferencesController < ApplicationController
layout 'admin'
before_action :authenticate_user!
def show; end
def update
@@ -33,12 +37,12 @@ class Settings::PreferencesController < Settings::BaseController
:setting_default_sensitive,
:setting_unfollow_modal,
:setting_boost_modal,
:setting_favourite_modal,
:setting_delete_modal,
:setting_auto_play_gif,
:setting_reduce_motion,
:setting_system_font_ui,
:setting_noindex,
:setting_theme,
notification_emails: %i(follow follow_request reblog favourite mention digest),
interactions: %i(must_be_follower must_be_following)
)

View File

@@ -1,8 +1,11 @@
# frozen_string_literal: true
class Settings::ProfilesController < Settings::BaseController
class Settings::ProfilesController < ApplicationController
include ObfuscateFilename
layout 'admin'
before_action :authenticate_user!
before_action :set_account
obfuscate_filename [:account, :avatar]

View File

@@ -1,6 +1,5 @@
# frozen_string_literal: true
# Intentionally does not inherit from BaseController
class Settings::SessionsController < ApplicationController
before_action :set_session, only: :destroy

View File

@@ -2,7 +2,11 @@
module Settings
module TwoFactorAuthentication
class ConfirmationsController < BaseController
class ConfirmationsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
def new
prepare_two_factor_form
end

View File

@@ -2,7 +2,11 @@
module Settings
module TwoFactorAuthentication
class RecoveryCodesController < BaseController
class RecoveryCodesController < ApplicationController
layout 'admin'
before_action :authenticate_user!
def create
@recovery_codes = current_user.generate_otp_backup_codes!
current_user.save!

View File

@@ -1,7 +1,10 @@
# frozen_string_literal: true
module Settings
class TwoFactorAuthenticationsController < BaseController
class TwoFactorAuthenticationsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :verify_otp_required, only: [:create]
def show

View File

@@ -4,7 +4,6 @@ class SharesController < ApplicationController
layout 'modal'
before_action :authenticate_user!
before_action :set_pack
before_action :set_body_classes
def show
@@ -25,11 +24,7 @@ class SharesController < ApplicationController
}
end
def set_pack
use_pack 'share'
end
def set_body_classes
@body_classes = 'modal-layout compose-standalone'
@body_classes = 'compose-standalone'
end
end

View File

@@ -10,12 +10,10 @@ class StatusesController < ApplicationController
before_action :set_link_headers
before_action :check_account_suspension
before_action :redirect_to_original, only: [:show]
before_action { response.headers['Vary'] = 'Accept' }
def show
respond_to do |format|
format.html do
use_pack 'public'
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
@descendants = cache_collection(@status.descendants(current_account), Status)
@@ -27,12 +25,6 @@ class StatusesController < ApplicationController
serializer: ActivityPub::NoteSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
# Allow HTTP caching for 3 minutes if the status is public
unless @stream_entry.hidden?
request.session_options[:skip] = true
expires_in(3.minutes, public: true)
end
end
end
end
@@ -45,7 +37,6 @@ class StatusesController < ApplicationController
end
def embed
use_pack 'embed'
response.headers['X-Frame-Options'] = 'ALLOWALL'
render 'stream_entries/embed', layout: 'embedded'
end

View File

@@ -14,7 +14,6 @@ class StreamEntriesController < ApplicationController
def show
respond_to do |format|
format.html do
use_pack 'public'
@ancestors = @stream_entry.activity.reply? ? cache_collection(@stream_entry.activity.ancestors(current_account), Status) : []
@descendants = cache_collection(@stream_entry.activity.descendants(current_account), Status)
end

View File

@@ -9,7 +9,6 @@ class TagsController < ApplicationController
respond_to do |format|
format.html do
use_pack 'about'
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end

View File

@@ -1,19 +1,15 @@
# frozen_string_literal: true
module WellKnown
class HostMetaController < ActionController::Base
class HostMetaController < ApplicationController
include RoutingHelper
before_action { response.headers['Vary'] = 'Accept' }
def show
@webfinger_template = "#{webfinger_url}?resource={uri}"
respond_to do |format|
format.xml { render content_type: 'application/xrd+xml' }
end
expires_in(3.days, public: true)
end
end
end

View File

@@ -1,25 +1,23 @@
# frozen_string_literal: true
module WellKnown
class WebfingerController < ActionController::Base
class WebfingerController < ApplicationController
include RoutingHelper
before_action { response.headers['Vary'] = 'Accept' }
def show
@account = Account.find_local!(username_from_resource)
@canonical_account_uri = @account.to_webfinger_s
@magic_key = pem_to_magic_key(@account.keypair.public_key)
respond_to do |format|
format.any(:json, :html) do
render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json'
render formats: :json, content_type: 'application/jrd+json'
end
format.xml do
render content_type: 'application/xrd+xml'
end
end
expires_in(3.days, public: true)
rescue ActiveRecord::RecordNotFound
head 404
end
@@ -37,6 +35,21 @@ module WellKnown
WebfingerResource.new(resource_user).username
end
def pem_to_magic_key(public_key)
modulus, exponent = [public_key.n, public_key.e].map do |component|
result = []
until component.zero?
result << [component % 256].pack('C')
component >>= 8
end
result.reverse.join
end
(['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
end
def resource_param
params.require(:resource)
end

View File

@@ -1,103 +0,0 @@
# frozen_string_literal: true
module Admin::ActionLogsHelper
def log_target(log)
if log.target
linkable_log_target(log.target)
else
log_target_from_history(log.target_type, log.recorded_changes)
end
end
def linkable_log_target(record)
case record.class.name
when 'Account'
link_to record.acct, admin_account_path(record.id)
when 'User'
link_to record.account.acct, admin_account_path(record.account_id)
when 'CustomEmoji'
record.shortcode
when 'Report'
link_to "##{record.id}", admin_report_path(record)
when 'DomainBlock', 'EmailDomainBlock'
link_to record.domain, "https://#{record.domain}"
when 'Status'
link_to record.account.acct, TagManager.instance.url_for(record)
end
end
def log_target_from_history(type, attributes)
case type
when 'CustomEmoji'
attributes['shortcode']
when 'DomainBlock', 'EmailDomainBlock'
link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status'
tmp_status = Status.new(attributes)
link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
end
end
def relevant_log_changes(log)
if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
log.recorded_changes.slice('domain')
elsif log.target_type == 'CustomEmoji' && log.action == :update
log.recorded_changes.slice('domain', 'visible_in_picker')
elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
log.recorded_changes.slice('moderator', 'admin')
elsif log.target_type == 'DomainBlock'
log.recorded_changes.slice('severity', 'reject_media')
elsif log.target_type == 'Status' && log.action == :update
log.recorded_changes.slice('sensitive')
end
end
def log_extra_attributes(hash)
safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ')
end
def log_change(val)
return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array)
safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→')
end
def icon_for_log(log)
case log.target_type
when 'Account', 'User'
'user'
when 'CustomEmoji'
'file'
when 'Report'
'flag'
when 'DomainBlock'
'lock'
when 'EmailDomainBlock'
'envelope'
when 'Status'
'pencil'
end
end
def class_for_log_icon(log)
case log.action
when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
'positive'
when :create
opposite_verbs?(log) ? 'negative' : 'positive'
when :update, :reset_password, :disable_2fa, :memorialize
'neutral'
when :demote, :silence, :disable, :suspend
'negative'
when :destroy
opposite_verbs?(log) ? 'positive' : 'negative'
else
''
end
end
private
def opposite_verbs?(log)
%w(DomainBlock EmailDomainBlock).include?(log.target_type)
end
end

View File

@@ -1,12 +1,10 @@
# frozen_string_literal: true
module Admin::FilterHelper
ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip staff).freeze
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
INVITE_FILTER = %i(available expired).freeze
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
new_url = filtered_url_for(link_to_params)
@@ -14,7 +12,7 @@ module Admin::FilterHelper
link_to text, new_url, class: filter_link_class(new_class)
end
def table_link_to(icon, text, path, **options)
def table_link_to(icon, text, path, options = {})
link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link')
end

View File

@@ -5,7 +5,7 @@ module ApplicationHelper
current_page?(path) ? 'active' : ''
end
def active_link_to(label, path, **options)
def active_link_to(label, path, options = {})
link_to label, path, options.merge(class: active_nav_class(path))
end

View File

@@ -9,24 +9,6 @@ module JsonLdHelper
value.is_a?(Array) ? value.first : value
end
# The url attribute can be a string, an array of strings, or an array of objects.
# The objects could include a mimeType. Not-included mimeType means it's text/html.
def url_to_href(value, preferred_type = nil)
single_value = if value.is_a?(Array) && !value.first.is_a?(String)
value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
elsif value.is_a?(Array)
value.first
else
value
end
if single_value.nil? || single_value.is_a?(String)
single_value
else
single_value['href']
end
end
def as_array(value)
value.is_a?(Array) ? value : [value]
end

View File

@@ -11,7 +11,7 @@ module RoutingHelper
end
end
def full_asset_url(source, **options)
def full_asset_url(source, options = {})
source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
URI.join(root_url, source).to_s

View File

@@ -10,7 +10,6 @@ module SettingsHelper
eo: 'Esperanto',
es: 'Español',
fa: 'فارسی',
gl: 'Galego',
fi: 'Suomi',
fr: 'Français',
he: 'עברית',
@@ -28,9 +27,6 @@ module SettingsHelper
pt: 'Português',
'pt-BR': 'Português do Brasil',
ru: 'Русский',
sk: 'Slovensky',
sr: 'Српски',
'sr-Latn': 'Srpski (latinica)',
sv: 'Svenska',
th: 'ภาษาไทย',
tr: 'Türkçe',

View File

@@ -1,8 +0,0 @@
// This file will be loaded on all pages, regardless of theme.
import { start } from 'rails-ujs';
import 'font-awesome/css/font-awesome.css';
require.context('../images/', true);
start();

View File

@@ -1,23 +0,0 @@
// This file will be loaded on embed pages, regardless of theme.
window.addEventListener('message', e => {
const data = e.data || {};
if (!window.parent || data.type !== 'setHeight') {
return;
}
function setEmbedHeight () {
window.parent.postMessage({
type: 'setHeight',
id: data.id,
height: document.getElementsByTagName('html')[0].scrollHeight,
}, '*');
};
if (['interactive', 'complete'].includes(document.readyState)) {
setEmbedHeight();
} else {
document.addEventListener('DOMContentLoaded', setEmbedHeight);
}
});

View File

@@ -1,25 +0,0 @@
// This file will be loaded on public pages, regardless of theme.
const { delegate } = require('rails-ujs');
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
if (button !== 0) {
return true;
}
window.location.href = target.href;
return false;
});
delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
const contentEl = target.parentNode.parentNode.querySelector('.e-content');
if (contentEl.style.display === 'block') {
contentEl.style.display = 'none';
target.parentNode.style.marginBottom = 0;
} else {
contentEl.style.display = 'block';
target.parentNode.style.marginBottom = null;
}
return false;
});

View File

@@ -1,39 +0,0 @@
// This file will be loaded on settings pages, regardless of theme.
const { length } = require('stringz');
const { delegate } = require('rails-ujs');
import { processBio } from 'flavours/glitch/util/bio_metadata';
delegate(document, '.account_display_name', 'input', ({ target }) => {
const nameCounter = document.querySelector('.name-counter');
if (nameCounter) {
nameCounter.textContent = 30 - length(target.value);
}
});
delegate(document, '.account_note', 'input', ({ target }) => {
const noteCounter = document.querySelector('.note-counter');
if (noteCounter) {
const noteWithoutMetadata = processBio(target.value).text;
noteCounter.textContent = 500 - length(noteWithoutMetadata);
}
});
delegate(document, '#account_avatar', 'change', ({ target }) => {
const avatar = document.querySelector('.card.compact .avatar img');
const [file] = target.files || [];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
avatar.src = url;
});
delegate(document, '#account_header', 'change', ({ target }) => {
const header = document.querySelector('.card.compact');
const [file] = target.files || [];
const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
header.style.backgroundImage = `url(${url})`;
});

View File

@@ -1,16 +0,0 @@
# These packs will be loaded on every appropriate page, regardless of
# theme.
pack:
about:
admin: admin.js
auth:
common:
filename: common.js
stylesheet: true
embed: embed.js
error:
home:
modal:
public: public.js
settings: settings.js
share:

View File

@@ -1,313 +0,0 @@
import api from 'flavours/glitch/util/api';
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET';
export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP';
export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL';
export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL';
export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL';
export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY';
export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL';
export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
export const fetchList = id => (dispatch, getState) => {
if (getState().getIn(['lists', id])) {
return;
}
dispatch(fetchListRequest(id));
api(getState).get(`/api/v1/lists/${id}`)
.then(({ data }) => dispatch(fetchListSuccess(data)))
.catch(err => dispatch(fetchListFail(id, err)));
};
export const fetchListRequest = id => ({
type: LIST_FETCH_REQUEST,
id,
});
export const fetchListSuccess = list => ({
type: LIST_FETCH_SUCCESS,
list,
});
export const fetchListFail = (id, error) => ({
type: LIST_FETCH_FAIL,
id,
error,
});
export const fetchLists = () => (dispatch, getState) => {
dispatch(fetchListsRequest());
api(getState).get('/api/v1/lists')
.then(({ data }) => dispatch(fetchListsSuccess(data)))
.catch(err => dispatch(fetchListsFail(err)));
};
export const fetchListsRequest = () => ({
type: LISTS_FETCH_REQUEST,
});
export const fetchListsSuccess = lists => ({
type: LISTS_FETCH_SUCCESS,
lists,
});
export const fetchListsFail = error => ({
type: LISTS_FETCH_FAIL,
error,
});
export const submitListEditor = shouldReset => (dispatch, getState) => {
const listId = getState().getIn(['listEditor', 'listId']);
const title = getState().getIn(['listEditor', 'title']);
if (listId === null) {
dispatch(createList(title, shouldReset));
} else {
dispatch(updateList(listId, title, shouldReset));
}
};
export const setupListEditor = listId => (dispatch, getState) => {
dispatch({
type: LIST_EDITOR_SETUP,
list: getState().getIn(['lists', listId]),
});
dispatch(fetchListAccounts(listId));
};
export const changeListEditorTitle = value => ({
type: LIST_EDITOR_TITLE_CHANGE,
value,
});
export const createList = (title, shouldReset) => (dispatch, getState) => {
dispatch(createListRequest());
api(getState).post('/api/v1/lists', { title }).then(({ data }) => {
dispatch(createListSuccess(data));
if (shouldReset) {
dispatch(resetListEditor());
}
}).catch(err => dispatch(createListFail(err)));
};
export const createListRequest = () => ({
type: LIST_CREATE_REQUEST,
});
export const createListSuccess = list => ({
type: LIST_CREATE_SUCCESS,
list,
});
export const createListFail = error => ({
type: LIST_CREATE_FAIL,
error,
});
export const updateList = (id, title, shouldReset) => (dispatch, getState) => {
dispatch(updateListRequest(id));
api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => {
dispatch(updateListSuccess(data));
if (shouldReset) {
dispatch(resetListEditor());
}
}).catch(err => dispatch(updateListFail(id, err)));
};
export const updateListRequest = id => ({
type: LIST_UPDATE_REQUEST,
id,
});
export const updateListSuccess = list => ({
type: LIST_UPDATE_SUCCESS,
list,
});
export const updateListFail = (id, error) => ({
type: LIST_UPDATE_FAIL,
id,
error,
});
export const resetListEditor = () => ({
type: LIST_EDITOR_RESET,
});
export const deleteList = id => (dispatch, getState) => {
dispatch(deleteListRequest(id));
api(getState).delete(`/api/v1/lists/${id}`)
.then(() => dispatch(deleteListSuccess(id)))
.catch(err => dispatch(deleteListFail(id, err)));
};
export const deleteListRequest = id => ({
type: LIST_DELETE_REQUEST,
id,
});
export const deleteListSuccess = id => ({
type: LIST_DELETE_SUCCESS,
id,
});
export const deleteListFail = (id, error) => ({
type: LIST_DELETE_FAIL,
id,
error,
});
export const fetchListAccounts = listId => (dispatch, getState) => {
dispatch(fetchListAccountsRequest(listId));
api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } })
.then(({ data }) => dispatch(fetchListAccountsSuccess(listId, data)))
.catch(err => dispatch(fetchListAccountsFail(listId, err)));
};
export const fetchListAccountsRequest = id => ({
type: LIST_ACCOUNTS_FETCH_REQUEST,
id,
});
export const fetchListAccountsSuccess = (id, accounts, next) => ({
type: LIST_ACCOUNTS_FETCH_SUCCESS,
id,
accounts,
next,
});
export const fetchListAccountsFail = (id, error) => ({
type: LIST_ACCOUNTS_FETCH_FAIL,
id,
error,
});
export const fetchListSuggestions = q => (dispatch, getState) => {
const params = {
q,
resolve: false,
limit: 4,
following: true,
};
api(getState).get('/api/v1/accounts/search', { params })
.then(({ data }) => dispatch(fetchListSuggestionsReady(q, data)));
};
export const fetchListSuggestionsReady = (query, accounts) => ({
type: LIST_EDITOR_SUGGESTIONS_READY,
query,
accounts,
});
export const clearListSuggestions = () => ({
type: LIST_EDITOR_SUGGESTIONS_CLEAR,
});
export const changeListSuggestions = value => ({
type: LIST_EDITOR_SUGGESTIONS_CHANGE,
value,
});
export const addToListEditor = accountId => (dispatch, getState) => {
dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
};
export const addToList = (listId, accountId) => (dispatch, getState) => {
dispatch(addToListRequest(listId, accountId));
api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
.then(() => dispatch(addToListSuccess(listId, accountId)))
.catch(err => dispatch(addToListFail(listId, accountId, err)));
};
export const addToListRequest = (listId, accountId) => ({
type: LIST_EDITOR_ADD_REQUEST,
listId,
accountId,
});
export const addToListSuccess = (listId, accountId) => ({
type: LIST_EDITOR_ADD_SUCCESS,
listId,
accountId,
});
export const addToListFail = (listId, accountId, error) => ({
type: LIST_EDITOR_ADD_FAIL,
listId,
accountId,
error,
});
export const removeFromListEditor = accountId => (dispatch, getState) => {
dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
};
export const removeFromList = (listId, accountId) => (dispatch, getState) => {
dispatch(removeFromListRequest(listId, accountId));
api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
.then(() => dispatch(removeFromListSuccess(listId, accountId)))
.catch(err => dispatch(removeFromListFail(listId, accountId, err)));
};
export const removeFromListRequest = (listId, accountId) => ({
type: LIST_EDITOR_REMOVE_REQUEST,
listId,
accountId,
});
export const removeFromListSuccess = (listId, accountId) => ({
type: LIST_EDITOR_REMOVE_SUCCESS,
listId,
accountId,
});
export const removeFromListFail = (listId, accountId, error) => ({
type: LIST_EDITOR_REMOVE_FAIL,
listId,
accountId,
error,
});

View File

@@ -1,26 +0,0 @@
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
// This just renders a FontAwesome icon.
export default function Icon ({
className,
fullwidth,
icon,
}) {
const computedClass = classNames('icon', 'fa', { 'fa-fw': fullwidth }, `fa-${icon}`, className);
return icon ? (
<span
aria-hidden='true'
className={computedClass}
/>
) : null;
}
// Props.
Icon.propTypes = {
className: PropTypes.string,
fullwidth: PropTypes.bool,
icon: PropTypes.string,
};

View File

@@ -1,97 +0,0 @@
// Inspired by <CommonLink> from Mastodon GO!
// ~ 😘 kibi!
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
// Utils.
import { assignHandlers } from 'flavours/glitch/util/react_helpers';
// Handlers.
const handlers = {
// We don't handle clicks that are made with modifiers, since these
// often have special browser meanings (eg, "open in new tab").
click (e) {
const { onClick } = this.props;
if (!onClick || e.button || e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
return;
}
onClick(e);
e.preventDefault(); // Prevents following of the link
},
};
// The component.
export default class Link extends React.PureComponent {
// Constructor.
constructor (props) {
super(props);
assignHandlers(this, handlers);
}
// Rendering.
render () {
const { click } = this.handlers;
const {
children,
className,
href,
onClick,
role,
title,
...rest
} = this.props;
const computedClass = classNames('link', className, `role-${role}`);
// We assume that our `onClick` is a routing function and give it
// the qualities of a link even if no `href` is provided. However,
// if we have neither an `onClick` or an `href`, our link is
// purely presentational.
const conditionalProps = {};
if (href) {
conditionalProps.href = href;
conditionalProps.onClick = click;
} else if (onClick) {
conditionalProps.onClick = click;
conditionalProps.role = 'link';
conditionalProps.tabIndex = 0;
} else {
conditionalProps.role = 'presentation';
}
// If we were provided a `role` it overwrites any that we may have
// set above. This can be used for "links" which are actually
// buttons.
if (role) {
conditionalProps.role = role;
}
// Rendering. We set `rel='noopener'` for user privacy, and our
// `target` as `'_blank'`.
return (
<a
className={computedClass}
{...conditionalProps}
rel='noopener'
target='_blank'
title={title}
{...rest}
>{children}</a>
);
}
}
// Props.
Link.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
href: PropTypes.string, // The link destination
onClick: PropTypes.func, // A function to call instead of opening the link
role: PropTypes.string, // An ARIA role for the link
title: PropTypes.string, // A title for the link
};

View File

@@ -1,423 +0,0 @@
// Package imports.
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
// Actions.
import {
cancelReplyCompose,
changeCompose,
changeComposeSensitivity,
changeComposeSpoilerText,
changeComposeSpoilerness,
changeComposeVisibility,
changeUploadCompose,
clearComposeSuggestions,
fetchComposeSuggestions,
insertEmojiCompose,
selectComposeSuggestion,
submitCompose,
toggleComposeAdvancedOption,
undoUploadCompose,
uploadCompose,
} from 'flavours/glitch/actions/compose';
import {
closeModal,
openModal,
} from 'flavours/glitch/actions/modal';
// Components.
import ComposerOptions from './options';
import ComposerPublisher from './publisher';
import ComposerReply from './reply';
import ComposerSpoiler from './spoiler';
import ComposerTextarea from './textarea';
import ComposerUploadForm from './upload_form';
import ComposerWarning from './warning';
// Utils.
import { countableText } from 'flavours/glitch/util/counter';
import { me } from 'flavours/glitch/util/initial_state';
import { isMobile } from 'flavours/glitch/util/is_mobile';
import { assignHandlers } from 'flavours/glitch/util/react_helpers';
import { wrap } from 'flavours/glitch/util/redux_helpers';
// State mapping.
function mapStateToProps (state) {
const inReplyTo = state.getIn(['compose', 'in_reply_to']);
return {
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
amUnlocked: !state.getIn(['accounts', me, 'locked']),
doNotFederate: state.getIn(['compose', 'advanced_options', 'do_not_federate']),
focusDate: state.getIn(['compose', 'focusDate']),
isSubmitting: state.getIn(['compose', 'is_submitting']),
isUploading: state.getIn(['compose', 'is_uploading']),
layout: state.getIn(['local_settings', 'layout']),
media: state.getIn(['compose', 'media_attachments']),
preselectDate: state.getIn(['compose', 'preselectDate']),
privacy: state.getIn(['compose', 'privacy']),
progress: state.getIn(['compose', 'progress']),
replyAccount: inReplyTo ? state.getIn(['accounts', state.getIn(['statuses', inReplyTo, 'account'])]) : null,
replyContent: inReplyTo ? state.getIn(['statuses', inReplyTo, 'contentHtml']) : null,
resetFileKey: state.getIn(['compose', 'resetFileKey']),
sideArm: state.getIn(['local_settings', 'side_arm']),
sensitive: state.getIn(['compose', 'sensitive']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
spoiler: state.getIn(['compose', 'spoiler']),
spoilerText: state.getIn(['compose', 'spoiler_text']),
suggestionToken: state.getIn(['compose', 'suggestion_token']),
suggestions: state.getIn(['compose', 'suggestions']),
text: state.getIn(['compose', 'text']),
};
};
// Dispatch mapping.
const mapDispatchToProps = {
onCancelReply: cancelReplyCompose,
onChangeDescription: changeUploadCompose,
onChangeSensitivity: changeComposeSensitivity,
onChangeSpoilerText: changeComposeSpoilerText,
onChangeSpoilerness: changeComposeSpoilerness,
onChangeText: changeCompose,
onChangeVisibility: changeComposeVisibility,
onClearSuggestions: clearComposeSuggestions,
onCloseModal: closeModal,
onFetchSuggestions: fetchComposeSuggestions,
onInsertEmoji: insertEmojiCompose,
onOpenActionsModal: openModal.bind(null, 'ACTIONS'),
onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }),
onSelectSuggestion: selectComposeSuggestion,
onSubmit: submitCompose,
onToggleAdvancedOption: toggleComposeAdvancedOption,
onUndoUpload: undoUploadCompose,
onUpload: uploadCompose,
};
// Handlers.
const handlers = {
// Changes the text value of the spoiler.
handleChangeSpoiler ({ target: { value } }) {
const { onChangeSpoilerText } = this.props;
if (onChangeSpoilerText) {
onChangeSpoilerText(value);
}
},
// Inserts an emoji at the caret.
handleEmoji (data) {
const { textarea: { selectionStart } } = this;
const { onInsertEmoji } = this.props;
this.caretPos = selectionStart + data.native.length + 1;
if (onInsertEmoji) {
onInsertEmoji(selectionStart, data);
}
},
// Handles the secondary submit button.
handleSecondarySubmit () {
const { handleSubmit } = this.handlers;
const {
onChangeVisibility,
sideArm,
} = this.props;
if (sideArm !== 'none' && onChangeVisibility) {
onChangeVisibility(sideArm);
}
handleSubmit();
},
// Selects a suggestion from the autofill.
handleSelect (tokenStart, token, value) {
const { onSelectSuggestion } = this.props;
this.caretPos = null;
if (onSelectSuggestion) {
onSelectSuggestion(tokenStart, token, value);
}
},
// Submits the status.
handleSubmit () {
const { textarea: { value } } = this;
const {
onChangeText,
onSubmit,
text,
} = this.props;
// If something changes inside the textarea, then we update the
// state before submitting.
if (onChangeText && text !== value) {
onChangeText(value);
}
// Submits the status.
if (onSubmit) {
onSubmit();
}
},
// Sets a reference to the textarea.
handleRefTextarea (textareaComponent) {
if (textareaComponent) {
this.textarea = textareaComponent.textarea;
}
},
};
// The component.
class Composer extends React.Component {
// Constructor.
constructor (props) {
super(props);
assignHandlers(this, handlers);
// Instance variables.
this.caretPos = null;
this.textarea = null;
}
// If this is the update where we've finished uploading,
// save the last caret position so we can restore it below!
componentWillReceiveProps (nextProps) {
const { textarea } = this;
const { isUploading } = this.props;
if (textarea && isUploading && !nextProps.isUploading) {
this.caretPos = textarea.selectionStart;
}
}
// This statement does several things:
// - If we're beginning a reply, and,
// - Replying to zero or one users, places the cursor at the end
// of the textbox.
// - Replying to more than one user, selects any usernames past
// the first; this provides a convenient shortcut to drop
// everyone else from the conversation.
// - If we've just finished uploading an image, and have a saved
// caret position, restores the cursor to that position after the
// text changes.
componentDidUpdate (prevProps) {
const {
caretPos,
textarea,
} = this;
const {
focusDate,
isUploading,
isSubmitting,
preselectDate,
text,
} = this.props;
let selectionEnd, selectionStart;
// Caret/selection handling.
if (focusDate !== prevProps.focusDate || (prevProps.isUploading && !isUploading && !isNaN(caretPos) && caretPos !== null)) {
switch (true) {
case preselectDate !== prevProps.preselectDate:
selectionStart = text.search(/\s/) + 1;
selectionEnd = text.length;
break;
case !isNaN(caretPos) && caretPos !== null:
selectionStart = selectionEnd = caretPos;
break;
default:
selectionStart = selectionEnd = text.length;
}
if (textarea) {
textarea.setSelectionRange(selectionStart, selectionEnd);
textarea.focus();
}
// Refocuses the textarea after submitting.
} else if (textarea && prevProps.isSubmitting && !isSubmitting) {
textarea.focus();
}
}
render () {
const {
handleChangeSpoiler,
handleEmoji,
handleSecondarySubmit,
handleSelect,
handleSubmit,
handleRefTextarea,
} = this.handlers;
const { history } = this.context;
const {
acceptContentTypes,
amUnlocked,
doNotFederate,
intl,
isSubmitting,
isUploading,
layout,
media,
onCancelReply,
onChangeDescription,
onChangeSensitivity,
onChangeSpoilerness,
onChangeText,
onChangeVisibility,
onClearSuggestions,
onCloseModal,
onFetchSuggestions,
onOpenActionsModal,
onOpenDoodleModal,
onToggleAdvancedOption,
onUndoUpload,
onUpload,
privacy,
progress,
replyAccount,
replyContent,
resetFileKey,
sensitive,
showSearch,
sideArm,
spoiler,
spoilerText,
suggestions,
text,
} = this.props;
return (
<div className='composer'>
<ComposerSpoiler
hidden={!spoiler}
intl={intl}
onChange={handleChangeSpoiler}
onSubmit={handleSubmit}
text={spoilerText}
/>
{privacy === 'private' && amUnlocked ? <ComposerWarning /> : null}
{replyContent ? (
<ComposerReply
account={replyAccount}
content={replyContent}
history={history}
intl={intl}
onCancel={onCancelReply}
/>
) : null}
<ComposerTextarea
autoFocus={!showSearch && !isMobile(window.innerWidth, layout)}
disabled={isSubmitting}
intl={intl}
onChange={onChangeText}
onPaste={onUpload}
onPickEmoji={handleEmoji}
onSubmit={handleSubmit}
onSuggestionsClearRequested={onClearSuggestions}
onSuggestionsFetchRequested={onFetchSuggestions}
onSuggestionSelected={handleSelect}
ref={handleRefTextarea}
suggestions={suggestions}
value={text}
/>
{isUploading || media && media.size ? (
<ComposerUploadForm
intl={intl}
media={media}
onChangeDescription={onChangeDescription}
onRemove={onUndoUpload}
progress={progress}
uploading={isUploading}
/>
) : null}
<ComposerOptions
acceptContentTypes={acceptContentTypes}
disabled={isSubmitting}
doNotFederate={doNotFederate}
full={media.size >= 4 || media.some(
item => item.get('type') === 'video'
)}
hasMedia={!!media.size}
intl={intl}
onChangeSensitivity={onChangeSensitivity}
onChangeVisibility={onChangeVisibility}
onDoodleOpen={onOpenDoodleModal}
onModalClose={onCloseModal}
onModalOpen={onOpenActionsModal}
onToggleAdvancedOption={onToggleAdvancedOption}
onToggleSpoiler={onChangeSpoilerness}
onUpload={onUpload}
privacy={privacy}
resetFileKey={resetFileKey}
sensitive={sensitive}
spoiler={spoiler}
/>
<ComposerPublisher
countText={`${spoilerText}${countableText(text)}${doNotFederate ? ' 👁️' : ''}`}
disabled={isSubmitting || isUploading || !!text.length && !text.trim().length}
intl={intl}
onSecondarySubmit={handleSecondarySubmit}
onSubmit={handleSubmit}
privacy={privacy}
sideArm={sideArm}
/>
</div>
);
}
}
// Context
Composer.contextTypes = {
history: PropTypes.object,
};
// Props.
Composer.propTypes = {
intl: PropTypes.object.isRequired,
// State props.
acceptContentTypes: PropTypes.string,
amUnlocked: PropTypes.bool,
doNotFederate: PropTypes.bool,
focusDate: PropTypes.instanceOf(Date),
isSubmitting: PropTypes.bool,
isUploading: PropTypes.bool,
layout: PropTypes.string,
media: ImmutablePropTypes.list,
preselectDate: PropTypes.instanceOf(Date),
privacy: PropTypes.string,
progress: PropTypes.number,
replyAccount: ImmutablePropTypes.map,
replyContent: PropTypes.string,
resetFileKey: PropTypes.number,
sideArm: PropTypes.string,
sensitive: PropTypes.bool,
showSearch: PropTypes.bool,
spoiler: PropTypes.bool,
spoilerText: PropTypes.string,
suggestionToken: PropTypes.string,
suggestions: ImmutablePropTypes.list,
text: PropTypes.string,
// Dispatch props.
onCancelReply: PropTypes.func,
onChangeDescription: PropTypes.func,
onChangeSensitivity: PropTypes.func,
onChangeSpoilerText: PropTypes.func,
onChangeSpoilerness: PropTypes.func,
onChangeText: PropTypes.func,
onChangeVisibility: PropTypes.func,
onClearSuggestions: PropTypes.func,
onCloseModal: PropTypes.func,
onFetchSuggestions: PropTypes.func,
onInsertEmoji: PropTypes.func,
onOpenActionsModal: PropTypes.func,
onOpenDoodleModal: PropTypes.func,
onSelectSuggestion: PropTypes.func,
onSubmit: PropTypes.func,
onToggleAdvancedOption: PropTypes.func,
onUndoUpload: PropTypes.func,
onUpload: PropTypes.func,
};
// Connecting and export.
export { Composer as WrappedComponent };
export default wrap(Composer, mapStateToProps, mapDispatchToProps, true);

View File

@@ -1,138 +0,0 @@
// Package imports.
import PropTypes from 'prop-types';
import React from 'react';
import spring from 'react-motion/lib/spring';
// Components.
import ComposerOptionsDropdownContentItem from './item';
// Utils.
import { withPassive } from 'flavours/glitch/util/dom_helpers';
import Motion from 'flavours/glitch/util/optional_motion';
import { assignHandlers } from 'flavours/glitch/util/react_helpers';
// Handlers.
const handlers = {
// When the document is clicked elsewhere, we close the dropdown.
handleDocumentClick ({ target }) {
const { node } = this;
const { onClose } = this.props;
if (onClose && node && !node.contains(target)) {
onClose();
}
},
// Stores our node in `this.node`.
handleRef (node) {
this.node = node;
},
};
// The spring to use with our motion.
const springMotion = spring(1, {
damping: 35,
stiffness: 400,
});
// The component.
export default class ComposerOptionsDropdownContent extends React.PureComponent {
// Constructor.
constructor (props) {
super(props);
assignHandlers(this, handlers);
// Instance variables.
this.node = null;
}
// On mounting, we add our listeners.
componentDidMount () {
const { handleDocumentClick } = this.handlers;
document.addEventListener('click', handleDocumentClick, false);
document.addEventListener('touchend', handleDocumentClick, withPassive);
}
// On unmounting, we remove our listeners.
componentWillUnmount () {
const { handleDocumentClick } = this.handlers;
document.removeEventListener('click', handleDocumentClick, false);
document.removeEventListener('touchend', handleDocumentClick, withPassive);
}
// Rendering.
render () {
const { handleRef } = this.handlers;
const {
items,
onChange,
onClose,
style,
value,
} = this.props;
// The result.
return (
<Motion
defaultStyle={{
opacity: 0,
scaleX: 0.85,
scaleY: 0.75,
}}
style={{
opacity: springMotion,
scaleX: springMotion,
scaleY: springMotion,
}}
>
{({ opacity, scaleX, scaleY }) => (
<div
className='composer--options--dropdown--content'
ref={handleRef}
style={{
...style,
opacity: opacity,
transform: `scale(${scaleX}, ${scaleY})`,
}}
>
{items.map(
({
name,
...rest
}) => (
<ComposerOptionsDropdownContentItem
active={name === value}
key={name}
name={name}
onChange={onChange}
onClose={onClose}
options={rest}
/>
)
)}
</div>
)}
</Motion>
);
}
}
// Props.
ComposerOptionsDropdownContent.propTypes = {
items: PropTypes.arrayOf(PropTypes.shape({
icon: PropTypes.string,
meta: PropTypes.node,
name: PropTypes.string.isRequired,
on: PropTypes.bool,
text: PropTypes.node,
})).isRequired,
onChange: PropTypes.func,
onClose: PropTypes.func,
style: PropTypes.object,
value: PropTypes.string,
};
// Default props.
ComposerOptionsDropdownContent.defaultProps = { style: {} };

View File

@@ -1,126 +0,0 @@
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Toggle from 'react-toggle';
// Components.
import Icon from 'flavours/glitch/components/icon';
// Utils.
import { assignHandlers } from 'flavours/glitch/util/react_helpers';
// Handlers.
const handlers = {
// This function activates the dropdown item.
handleActivate (e) {
const {
name,
onChange,
onClose,
options: { on },
} = this.props;
// If the escape key was pressed, we close the dropdown.
if (e.key === 'Escape' && onClose) {
onClose();
// Otherwise, we both close the dropdown and change the value.
} else if (onChange && (!e.key || e.key === 'Enter')) {
e.preventDefault(); // Prevents change in focus on click
if ((on === null || typeof on === 'undefined') && onClose) {
onClose();
}
onChange(name);
}
},
};
// The component.
export default class ComposerOptionsDropdownContentItem extends React.PureComponent {
// Constructor.
constructor (props) {
super(props);
assignHandlers(this, handlers);
}
// Rendering.
render () {
const { handleActivate } = this.handlers;
const {
active,
options: {
icon,
meta,
on,
text,
},
} = this.props;
const computedClass = classNames('composer--options--dropdown--content--item', {
active,
lengthy: meta,
'toggled-off': !on && on !== null && typeof on !== 'undefined',
'toggled-on': on,
'with-icon': icon,
});
// The result.
return (
<div
className={computedClass}
onClick={handleActivate}
onKeyDown={handleActivate}
role='button'
tabIndex='0'
>
{function () {
// We render a `<Toggle>` if we were provided an `on`
// property, and otherwise show an `<Icon>` if available.
switch (true) {
case on !== null && typeof on !== 'undefined':
return (
<Toggle
checked={on}
onChange={handleActivate}
/>
);
case !!icon:
return (
<Icon
className='icon'
fullwidth
icon={icon}
/>
);
default:
return null;
}
}()}
{meta ? (
<div className='content'>
<strong>{text}</strong>
{meta}
</div>
) : <div className='content'>{text}</div>}
</div>
);
}
};
// Props.
ComposerOptionsDropdownContentItem.propTypes = {
active: PropTypes.bool,
name: PropTypes.string,
onChange: PropTypes.func,
onClose: PropTypes.func,
options: PropTypes.shape({
icon: PropTypes.string,
meta: PropTypes.node,
on: PropTypes.bool,
text: PropTypes.node,
}),
};

View File

@@ -1,225 +0,0 @@
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Overlay from 'react-overlays/lib/Overlay';
// Components.
import IconButton from 'flavours/glitch/components/icon_button';
import ComposerOptionsDropdownContent from './content';
// Utils.
import { isUserTouching } from 'flavours/glitch/util/is_mobile';
import { assignHandlers } from 'flavours/glitch/util/react_helpers';
// Handlers.
const handlers = {
// Closes the dropdown.
handleClose () {
this.setState({ open: false });
},
// The enter key toggles the dropdown's open state, and the escape
// key closes it.
handleKeyDown ({ key }) {
const {
handleClose,
handleToggle,
} = this.handlers;
switch (key) {
case 'Enter':
handleToggle();
break;
case 'Escape':
handleClose();
break;
}
},
// Creates an action modal object.
handleMakeModal () {
const component = this;
const {
items,
onChange,
onModalOpen,
onModalClose,
value,
} = this.props;
// Required props.
if (!(onChange && onModalOpen && onModalClose && items)) {
return null;
}
// The object.
return {
actions: items.map(
({
name,
...rest
}) => ({
...rest,
active: value && name === value,
name,
onClick (e) {
e.preventDefault(); // Prevents focus from changing
onModalClose();
onChange(name);
},
onPassiveClick (e) {
e.preventDefault(); // Prevents focus from changing
onChange(name);
component.setState({ needsModalUpdate: true });
},
})
),
};
},
// Toggles opening and closing the dropdown.
handleToggle () {
const { handleMakeModal } = this.handlers;
const { onModalOpen } = this.props;
const { open } = this.state;
// If this is a touch device, we open a modal instead of the
// dropdown.
if (isUserTouching()) {
// This gets the modal to open.
const modal = handleMakeModal();
// If we can, we then open the modal.
if (modal && onModalOpen) {
onModalOpen(modal);
return;
}
}
// Otherwise, we just set our state to open.
this.setState({ open: !open });
},
// If our modal is open and our props update, we need to also update
// the modal.
handleUpdate () {
const { handleMakeModal } = this.handlers;
const { onModalOpen } = this.props;
const { needsModalUpdate } = this.state;
// Gets our modal object.
const modal = handleMakeModal();
// Reopens the modal with the new object.
if (needsModalUpdate && modal && onModalOpen) {
onModalOpen(modal);
}
},
};
// The component.
export default class ComposerOptionsDropdown extends React.PureComponent {
// Constructor.
constructor (props) {
super(props);
assignHandlers(this, handlers);
this.state = {
needsModalUpdate: false,
open: false,
};
}
// Updates our modal as necessary.
componentDidUpdate (prevProps) {
const { handleUpdate } = this.handlers;
const { items } = this.props;
const { needsModalUpdate } = this.state;
if (needsModalUpdate && items.find(
(item, i) => item.on !== prevProps.items[i].on
)) {
handleUpdate();
this.setState({ needsModalUpdate: false });
}
}
// Rendering.
render () {
const {
handleClose,
handleKeyDown,
handleToggle,
} = this.handlers;
const {
active,
disabled,
title,
icon,
items,
onChange,
value,
} = this.props;
const { open } = this.state;
const computedClass = classNames('composer--options--dropdown', {
active,
open,
});
// The result.
return (
<div
className={computedClass}
onKeyDown={handleKeyDown}
>
<IconButton
active={open || active}
className='value'
disabled={disabled}
icon={icon}
onClick={handleToggle}
size={18}
style={{
height: null,
lineHeight: '27px',
}}
title={title}
/>
<Overlay
containerPadding={20}
placement='bottom'
show={open}
target={this}
>
<ComposerOptionsDropdownContent
items={items}
onChange={onChange}
onClose={handleClose}
value={value}
/>
</Overlay>
</div>
);
}
}
// Props.
ComposerOptionsDropdown.propTypes = {
active: PropTypes.bool,
disabled: PropTypes.bool,
icon: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({
icon: PropTypes.string,
meta: PropTypes.node,
name: PropTypes.string.isRequired,
on: PropTypes.bool,
text: PropTypes.node,
})).isRequired,
onChange: PropTypes.func,
onModalClose: PropTypes.func,
onModalOpen: PropTypes.func,
title: PropTypes.string,
value: PropTypes.string,
};

View File

@@ -1,329 +0,0 @@
// Package imports.
import PropTypes from 'prop-types';
import React from 'react';
import {
FormattedMessage,
defineMessages,
} from 'react-intl';
import spring from 'react-motion/lib/spring';
// Components.
import IconButton from 'flavours/glitch/components/icon_button';
import TextIconButton from 'flavours/glitch/components/text_icon_button';
import Dropdown from './dropdown';
// Utils.
import Motion from 'flavours/glitch/util/optional_motion';
import {
assignHandlers,
hiddenComponent,
} from 'flavours/glitch/util/react_helpers';
// Messages.
const messages = defineMessages({
advanced_options_icon_title: {
defaultMessage: 'Advanced options',
id: 'advanced_options.icon_title',
},
attach: {
defaultMessage: 'Attach...',
id: 'compose.attach',
},
change_privacy: {
defaultMessage: 'Adjust status privacy',
id: 'privacy.change',
},
direct_long: {
defaultMessage: 'Post to mentioned users only',
id: 'privacy.direct.long',
},
direct_short: {
defaultMessage: 'Direct',
id: 'privacy.direct.short',
},
doodle: {
defaultMessage: 'Draw something',
id: 'compose.attach.doodle',
},
local_only_long: {
defaultMessage: 'Do not post to other instances',
id: 'advanced-options.local-only.long',
},
local_only_short: {
defaultMessage: 'Local-only',
id: 'advanced-options.local-only.short',
},
private_long: {
defaultMessage: 'Post to followers only',
id: 'privacy.private.long',
},
private_short: {
defaultMessage: 'Followers-only',
id: 'privacy.private.short',
},
public_long: {
defaultMessage: 'Post to public timelines',
id: 'privacy.public.long',
},
public_short: {
defaultMessage: 'Public',
id: 'privacy.public.short',
},
sensitive: {
defaultMessage: 'Mark media as sensitive',
id: 'compose_form.sensitive',
},
spoiler: {
defaultMessage: 'Hide text behind warning',
id: 'compose_form.spoiler',
},
unlisted_long: {
defaultMessage: 'Do not show in public timelines',
id: 'privacy.unlisted.long',
},
unlisted_short: {
defaultMessage: 'Unlisted',
id: 'privacy.unlisted.short',
},
upload: {
defaultMessage: 'Upload a file',
id: 'compose.attach.upload',
},
});
// Handlers.
const handlers = {
// Handles file selection.
handleChangeFiles ({ target: { files } }) {
const { onUpload } = this.props;
if (files.length && onUpload) {
onUpload(files);
}
},
// Handles attachment clicks.
handleClickAttach (name) {
const { fileElement } = this;
const { onDoodleOpen } = this.props;
// We switch over the name of the option.
switch (name) {
case 'upload':
if (fileElement) {
fileElement.click();
}
return;
case 'doodle':
if (onDoodleOpen) {
onDoodleOpen();
}
return;
}
},
// Handles a ref to the file input.
handleRefFileElement (fileElement) {
this.fileElement = fileElement;
},
};
// The component.
export default class ComposerOptions extends React.PureComponent {
// Constructor.
constructor (props) {
super(props);
assignHandlers(this, handlers);
// Instance variables.
this.fileElement = null;
}
// Rendering.
render () {
const {
handleChangeFiles,
handleClickAttach,
handleRefFileElement,
} = this.handlers;
const {
acceptContentTypes,
disabled,
doNotFederate,
full,
hasMedia,
intl,
onChangeSensitivity,
onChangeVisibility,
onModalClose,
onModalOpen,
onToggleAdvancedOption,
onToggleSpoiler,
privacy,
resetFileKey,
sensitive,
spoiler,
} = this.props;
// We predefine our privacy items so that we can easily pick the
// dropdown icon later.
const privacyItems = {
direct: {
icon: 'envelope',
meta: <FormattedMessage {...messages.direct_long} />,
name: 'direct',
text: <FormattedMessage {...messages.direct_short} />,
},
private: {
icon: 'lock',
meta: <FormattedMessage {...messages.private_long} />,
name: 'private',
text: <FormattedMessage {...messages.private_short} />,
},
public: {
icon: 'globe',
meta: <FormattedMessage {...messages.public_long} />,
name: 'public',
text: <FormattedMessage {...messages.public_short} />,
},
unlisted: {
icon: 'unlock-alt',
meta: <FormattedMessage {...messages.unlisted_long} />,
name: 'unlisted',
text: <FormattedMessage {...messages.unlisted_short} />,
},
};
// The result.
return (
<div className='composer--options'>
<input
accept={acceptContentTypes}
disabled={disabled || full}
key={resetFileKey}
onChange={handleChangeFiles}
ref={handleRefFileElement}
type='file'
{...hiddenComponent}
/>
<Dropdown
disabled={disabled || full}
icon='paperclip'
items={[
{
icon: 'cloud-upload',
name: 'upload',
text: <FormattedMessage {...messages.upload} />,
},
{
icon: 'paint-brush',
name: 'doodle',
text: <FormattedMessage {...messages.doodle} />,
},
]}
onChange={handleClickAttach}
onModalClose={onModalClose}
onModalOpen={onModalOpen}
title={intl.formatMessage(messages.attach)}
/>
<Motion
defaultStyle={{ scale: 0.87 }}
style={{
scale: spring(hasMedia ? 1 : 0.87, {
stiffness: 200,
damping: 3,
}),
}}
>
{({ scale }) => (
<div
style={{
display: hasMedia ? null : 'none',
transform: `scale(${scale})`,
}}
>
<IconButton
active={sensitive}
className='sensitive'
disabled={spoiler}
icon={sensitive ? 'eye-slash' : 'eye'}
inverted
onClick={onChangeSensitivity}
size={18}
style={{
height: null,
lineHeight: null,
}}
title={intl.formatMessage(messages.sensitive)}
/>
</div>
)}
</Motion>
<hr />
<Dropdown
disabled={disabled}
icon={(privacyItems[privacy] || {}).icon}
items={[
privacyItems.public,
privacyItems.unlisted,
privacyItems.private,
privacyItems.direct,
]}
onChange={onChangeVisibility}
onModalClose={onModalClose}
onModalOpen={onModalOpen}
title={intl.formatMessage(messages.change_privacy)}
value={privacy}
/>
<TextIconButton
active={spoiler}
ariaControls='glitch.composer.spoiler.input'
label='CW'
onClick={onToggleSpoiler}
title={intl.formatMessage(messages.spoiler)}
/>
<Dropdown
active={doNotFederate}
disabled={disabled}
icon='home'
items={[
{
meta: <FormattedMessage {...messages.local_only_long} />,
name: 'do_not_federate',
on: doNotFederate,
text: <FormattedMessage {...messages.local_only_short} />,
},
]}
onChange={onToggleAdvancedOption}
onModalClose={onModalClose}
onModalOpen={onModalOpen}
title={intl.formatMessage(messages.advanced_options_icon_title)}
/>
</div>
);
}
}
// Props.
ComposerOptions.propTypes = {
acceptContentTypes: PropTypes.string,
disabled: PropTypes.bool,
doNotFederate: PropTypes.bool,
full: PropTypes.bool,
hasMedia: PropTypes.bool,
intl: PropTypes.object.isRequired,
onChangeSensitivity: PropTypes.func,
onChangeVisibility: PropTypes.func,
onDoodleOpen: PropTypes.func,
onModalClose: PropTypes.func,
onModalOpen: PropTypes.func,
onToggleAdvancedOption: PropTypes.func,
onToggleSpoiler: PropTypes.func,
onUpload: PropTypes.func,
privacy: PropTypes.string,
resetFileKey: PropTypes.number,
sensitive: PropTypes.bool,
spoiler: PropTypes.bool,
};

View File

@@ -1,121 +0,0 @@
// Package imports.
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import {
defineMessages,
FormattedMessage,
} from 'react-intl';
import { length } from 'stringz';
// Components.
import Button from 'flavours/glitch/components/button';
import Icon from 'flavours/glitch/components/icon';
// Utils.
import { maxChars } from 'flavours/glitch/util/initial_state';
// Messages.
const messages = defineMessages({
publish: {
defaultMessage: 'Toot',
id: 'compose_form.publish',
},
publishLoud: {
defaultMessage: '{publish}!',
id: 'compose_form.publish_loud',
},
});
// The component.
export default function ComposerPublisher ({
countText,
disabled,
intl,
onSecondarySubmit,
onSubmit,
privacy,
sideArm,
}) {
const diff = maxChars - length(countText || '');
const computedClass = classNames('composer--publisher', {
disabled: disabled || diff < 0,
over: diff < 0,
});
// The result.
return (
<div className={computedClass}>
<span className='count'>{diff}</span>
{sideArm && sideArm !== 'none' ? (
<Button
className='side_arm'
disabled={disabled || diff < 0}
onClick={onSecondarySubmit}
style={{ padding: null }}
text={
<span>
<Icon
icon={{
public: 'globe',
unlisted: 'unlock-alt',
private: 'lock',
direct: 'envelope',
}[sideArm]}
/>
</span>
}
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`}
/>
) : null}
<Button
className='primary'
text={function () {
switch (true) {
case !!sideArm && sideArm !== 'none':
case privacy === 'direct':
case privacy === 'private':
return (
<span>
<Icon
icon={{
direct: 'envelope',
private: 'lock',
public: 'globe',
unlisted: 'unlock-alt',
}[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` })}`}
onClick={onSubmit}
disabled={disabled || diff < 0}
/>
</div>
);
}
// Props.
ComposerPublisher.propTypes = {
countText: PropTypes.string,
disabled: PropTypes.bool,
intl: PropTypes.object.isRequired,
onSecondarySubmit: PropTypes.func,
onSubmit: PropTypes.func,
privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
};

View File

@@ -1,113 +0,0 @@
// Package imports.
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages } from 'react-intl';
// Components.
import Avatar from 'flavours/glitch/components/avatar';
import DisplayName from 'flavours/glitch/components/display_name';
import IconButton from 'flavours/glitch/components/icon_button';
// Utils.
import { assignHandlers } from 'flavours/glitch/util/react_helpers';
import { isRtl } from 'flavours/glitch/util/rtl';
// Messages.
const messages = defineMessages({
cancel: {
defaultMessage: 'Cancel',
id: 'reply_indicator.cancel',
},
});
// Handlers.
const handlers = {
// Handles a click on the "close" button.
handleClick () {
const { onCancel } = this.props;
if (onCancel) {
onCancel();
}
},
// Handles a click on the status's account.
handleClickAccount () {
const {
account,
history,
} = this.props;
if (history) {
history.push(`/accounts/${account.get('id')}`);
}
},
};
// The component.
export default class ComposerReply extends React.PureComponent {
// Constructor.
constructor (props) {
super(props);
assignHandlers(this, handlers);
}
// Rendering.
render () {
const {
handleClick,
handleClickAccount,
} = this.handlers;
const {
account,
content,
intl,
} = this.props;
// The result.
return (
<article className='composer--reply'>
<header>
<IconButton
className='cancel'
icon='times'
onClick={handleClick}
title={intl.formatMessage(messages.cancel)}
/>
{account ? (
<a
className='account'
href={account.get('url')}
onClick={handleClickAccount}
>
<Avatar
account={account}
className='avatar'
size={24}
/>
<DisplayName
account={account}
className='display_name'
/>
</a>
) : null}
</header>
<div
className='content'
dangerouslySetInnerHTML={{ __html: content || '' }}
style={{ direction: isRtl(content) ? 'rtl' : 'ltr' }}
/>
</article>
);
}
}
ComposerReply.propTypes = {
account: ImmutablePropTypes.map,
content: PropTypes.string,
history: PropTypes.object,
intl: PropTypes.object.isRequired,
onCancel: PropTypes.func,
};

View File

@@ -1,92 +0,0 @@
// Package imports.
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage } from 'react-intl';
// Components.
import Collapsable from 'flavours/glitch/components/collapsable';
// Utils.
import {
assignHandlers,
hiddenComponent,
} from 'flavours/glitch/util/react_helpers';
// Messages.
const messages = defineMessages({
placeholder: {
defaultMessage: 'Write your warning here',
id: 'compose_form.spoiler_placeholder',
},
});
// Handlers.
const handlers = {
// Handles a keypress.
handleKeyDown ({
ctrlKey,
keyCode,
metaKey,
}) {
const { onSubmit } = this.props;
// We submit the status on control/meta + enter.
if (onSubmit && keyCode === 13 && (ctrlKey || metaKey)) {
onSubmit();
}
},
};
// The component.
export default class ComposerSpoiler extends React.PureComponent {
// Constructor.
constructor (props) {
super(props);
assignHandlers(this, handlers);
}
// Rendering.
render () {
const { handleKeyDown } = this.handlers;
const {
hidden,
intl,
onChange,
text,
} = this.props;
// The result.
return (
<Collapsable
isVisible={!hidden}
fullHeight={50}
>
<label className='composer--spoiler'>
<span {...hiddenComponent}>
<FormattedMessage {...messages.placeholder} />
</span>
<input
id='glitch.composer.spoiler.input'
onChange={onChange}
onKeyDown={handleKeyDown}
placeholder={intl.formatMessage(messages.placeholder)}
type='text'
value={text}
/>
</label>
</Collapsable>
);
}
}
// Props.
ComposerSpoiler.propTypes = {
hidden: PropTypes.bool,
intl: PropTypes.object.isRequired,
onChange: PropTypes.func,
onSubmit: PropTypes.func,
text: PropTypes.string,
};

Some files were not shown because too many files have changed in this diff Show More