diff --git a/.env.production.sample b/.env.production.sample
index d46768d09a..d920f18e9f 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -33,7 +33,6 @@ LOCAL_DOMAIN=example.com
# Application secrets
# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
-PAPERCLIP_SECRET=
SECRET_KEY_BASE=
OTP_SECRET=
@@ -143,6 +142,16 @@ STREAMING_CLUSTER_NUM=1
# Maximum allowed character count
# MAX_TOOT_CHARS=500
+# LDAP authentication (optional)
+# LDAP_ENABLED=true
+# LDAP_HOST=localhost
+# LDAP_PORT=389
+# LDAP_METHOD=simple_tls
+# LDAP_BASE=
+# LDAP_BIND_DN=
+# LDAP_PASSWORD=
+# LDAP_UID=cn
+
# PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable
# and optional as fallback PAM_DEFAULT_SUFFIX
@@ -153,7 +162,7 @@ STREAMING_CLUSTER_NUM=1
# PAM_DEFAULT_SUFFIX=pam
# Name of the pam service (pam "auth" section is evaluated)
# PAM_DEFAULT_SERVICE=rpam
-# Name of the pam service used for checking if an user can register (pam "account" section is evaluated)
+# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
# PAM_CONTROLLED_SERVICE=rpam
# Global OAuth settings (optional) :
@@ -186,7 +195,7 @@ STREAMING_CLUSTER_NUM=1
# Optional SAML authentication (cf. omniauth-saml)
# SAML_ENABLED=true
# SAML_ACS_URL=
-# SAML_ISSUER=http://localhost:3000/auth/auth/saml/metadata
+# SAML_ISSUER=http://localhost:3000/auth/auth/saml/callback
# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
# SAML_IDP_CERT=
# SAML_IDP_CERT_FINGERPRINT=
diff --git a/Gemfile b/Gemfile
index b6962861fe..bf53d4c774 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,7 +7,6 @@ gem 'pkg-config', '~> 1.2'
gem 'puma', '~> 3.10'
gem 'rails', '~> 5.1.4'
-gem 'uglifier', '~> 3.2'
gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 0.20'
@@ -35,8 +34,9 @@ gem 'devise', '~> 4.4'
gem 'devise-two-factor', '~> 3.0'
gem 'devise_pam_authenticatable2', '~> 8.0', install_if: -> { ENV['PAM_ENABLED'] == 'true' }
+gem 'net-ldap', '~> 0.10', install_if: -> { ENV['LDAP_ENABLED'] == 'true' }
gem 'omniauth-cas', '~> 1.1', install_if: -> { ENV['CAS_ENABLED'] == 'true' }
-gem 'omniauth-saml', '~> 1.8', install_if: -> { ENV['SAML_ENABLED'] == 'true' }
+gem 'omniauth-saml', '~> 1.10', install_if: -> { ENV['SAML_ENABLED'] == 'true' }
gem 'omniauth', '~> 1.2'
gem 'doorkeeper', '~> 4.2'
@@ -98,6 +98,10 @@ group :development, :test do
gem 'rspec-rails', '~> 3.7'
end
+group :production, :test do
+ gem 'private_address_check', '~> 0.4.1'
+end
+
group :test do
gem 'capybara', '~> 2.15'
gem 'climate_control', '~> 0.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index 1905cf3e1f..f0ad8ab55f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -178,7 +178,6 @@ GEM
et-orbi (1.0.8)
tzinfo
excon (0.59.0)
- execjs (2.7.0)
fabrication (2.18.0)
faker (1.8.4)
i18n (~> 0.5)
@@ -319,6 +318,7 @@ GEM
multi_json (1.12.2)
multipart-post (2.0.0)
necromancer (0.4.0)
+ net-ldap (0.16.1)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (4.2.0)
@@ -340,9 +340,9 @@ GEM
addressable (~> 2.3)
nokogiri (~> 1.5)
omniauth (~> 1.2)
- omniauth-saml (1.9.0)
+ omniauth-saml (1.10.0)
omniauth (~> 1.3, >= 1.3.2)
- ruby-saml (~> 1.4, >= 1.4.3)
+ ruby-saml (~> 1.7)
orm_adapter (0.5.0)
ostatus2 (2.0.3)
addressable (~> 2.5)
@@ -379,6 +379,7 @@ GEM
premailer-rails (1.10.1)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
+ private_address_check (0.4.1)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
@@ -498,7 +499,7 @@ GEM
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-oembed (0.12.0)
ruby-progressbar (1.9.0)
- ruby-saml (1.6.1)
+ ruby-saml (1.7.2)
nokogiri (>= 1.5.10)
rufus-scheduler (3.4.2)
et-orbi (~> 1.0)
@@ -586,8 +587,6 @@ GEM
thread_safe (~> 0.1)
tzinfo-data (1.2017.3)
tzinfo (>= 1.0.0)
- uglifier (3.2.0)
- execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.4)
@@ -672,12 +671,13 @@ DEPENDENCIES
memory_profiler
microformats (~> 4.0)
mime-types (~> 3.1)
+ net-ldap (~> 0.10)
nokogiri (~> 1.8)
nsa (~> 0.2)
oj (~> 3.3)
omniauth (~> 1.2)
omniauth-cas (~> 1.1)
- omniauth-saml (~> 1.8)
+ omniauth-saml (~> 1.10)
ostatus2 (~> 2.0)
ox (~> 2.8)
paperclip (~> 5.1)
@@ -688,6 +688,7 @@ DEPENDENCIES
pkg-config (~> 1.2)
posix-spawn
premailer-rails
+ private_address_check (~> 0.4.1)
pry-rails (~> 0.3)
puma (~> 3.10)
pundit (~> 1.1)
@@ -724,7 +725,6 @@ DEPENDENCIES
tty-prompt
twitter-text (~> 1.14)
tzinfo-data (~> 1.2017)
- uglifier (~> 3.2)
webmock (~> 3.0)
webpacker (~> 3.0)
webpush
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index c9725ed007..1efaf619b0 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class AccountsController < ApplicationController
+ PAGE_SIZE = 20
+
include AccountControllerConcern
before_action :set_cache_headers
@@ -17,13 +19,16 @@ class AccountsController < ApplicationController
end
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
- @statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id])
+ @statuses = filtered_status_page(params)
@statuses = cache_collection(@statuses, Status)
- @next_url = next_url unless @statuses.empty?
+ unless @statuses.empty?
+ @older_url = older_url if @statuses.last.id > filtered_statuses.last.id
+ @newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id
+ end
end
format.atom do
- @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
+ @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id])
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
end
@@ -70,13 +75,22 @@ class AccountsController < ApplicationController
@account = Account.find_local!(params[:username])
end
- def next_url
+ def older_url
+ ::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}")
+ pagination_url(max_id: @statuses.last.id)
+ end
+
+ def newer_url
+ pagination_url(min_id: @statuses.first.id)
+ end
+
+ def pagination_url(max_id: nil, min_id: nil)
if media_requested?
- short_account_media_url(@account, max_id: @statuses.last.id)
+ short_account_media_url(@account, max_id: max_id, min_id: min_id)
elsif replies_requested?
- short_account_with_replies_url(@account, max_id: @statuses.last.id)
+ short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
else
- short_account_url(@account, max_id: @statuses.last.id)
+ short_account_url(@account, max_id: max_id, min_id: min_id)
end
end
@@ -87,4 +101,12 @@ class AccountsController < ApplicationController
def replies_requested?
request.path.ends_with?('/with_replies')
end
+
+ def filtered_status_page(params)
+ if params[:min_id].present?
+ filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
+ else
+ filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a
+ end
+ end
end
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 52e68ab35f..7b5168b314 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -51,6 +51,10 @@ class Api::BaseController < ApplicationController
[params[:limit].to_i.abs, default_limit * 2].min
end
+ def truthy_param?(key)
+ ActiveModel::Type::Boolean.new.cast(params[key])
+ end
+
def current_resource_owner
@current_user ||= User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
end
diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb
index 11e647c3cb..7649da4339 100644
--- a/app/controllers/api/v1/accounts/search_controller.rb
+++ b/app/controllers/api/v1/accounts/search_controller.rb
@@ -22,8 +22,4 @@ class Api::V1::Accounts::SearchController < Api::BaseController
following: truthy_param?(:following)
)
end
-
- def truthy_param?(key)
- params[key] == 'true'
- end
end
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index 095f6937b0..7261ccd247 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -28,9 +28,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def account_statuses
default_statuses.tap do |statuses|
- statuses.merge!(only_media_scope) if params[:only_media]
- statuses.merge!(pinned_scope) if params[:pinned]
- statuses.merge!(no_replies_scope) if params[:exclude_replies]
+ statuses.merge!(only_media_scope) if truthy_param?(:only_media)
+ statuses.merge!(pinned_scope) if truthy_param?(:pinned)
+ statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
end
end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 4e73e9e8b5..d643259441 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -13,9 +13,9 @@ class Api::V1::AccountsController < Api::BaseController
end
def follow
- FollowService.new.call(current_user.account, @account.acct, reblogs: params[:reblogs])
+ FollowService.new.call(current_user.account, @account.acct, reblogs: truthy_param?(:reblogs))
- options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: params[:reblogs] } }, requested_map: { @account.id => false } }
+ options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
end
@@ -26,7 +26,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def mute
- MuteService.new.call(current_user.account, @account, notifications: params[:notifications])
+ MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications))
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb
index 22828217d2..f5095e0730 100644
--- a/app/controllers/api/v1/reports_controller.rb
+++ b/app/controllers/api/v1/reports_controller.rb
@@ -13,14 +13,14 @@ class Api::V1::ReportsController < Api::BaseController
end
def create
- @report = current_account.reports.create!(
- target_account: reported_account,
+ @report = ReportService.new.call(
+ current_account,
+ reported_account,
status_ids: reported_status_ids,
- comment: report_params[:comment]
+ comment: report_params[:comment],
+ forward: report_params[:forward]
)
- User.staff.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
-
render json: @report, serializer: REST::ReportSerializer
end
@@ -39,6 +39,6 @@ class Api::V1::ReportsController < Api::BaseController
end
def report_params
- params.permit(:account_id, :comment, status_ids: [])
+ params.permit(:account_id, :comment, :forward, status_ids: [])
end
end
diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb
index d1b4e04022..99b635ad90 100644
--- a/app/controllers/api/v1/search_controller.rb
+++ b/app/controllers/api/v1/search_controller.rb
@@ -33,12 +33,8 @@ class Api::V1::SearchController < Api::BaseController
SearchService.new.call(
params[:q],
RESULTS_LIMIT,
- resolving_search?,
+ truthy_param?(:resolve),
current_account
)
end
-
- def resolving_search?
- params[:resolve] == 'true'
- end
end
diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb
index 49887778e0..d7d70b94d5 100644
--- a/app/controllers/api/v1/timelines/public_controller.rb
+++ b/app/controllers/api/v1/timelines/public_controller.rb
@@ -21,15 +21,23 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end
def public_statuses
- public_timeline_statuses.paginate_by_max_id(
+ statuses = public_timeline_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
+
+ if truthy_param?(:only_media)
+ # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
+ status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id)
+ statuses.where(id: status_ids)
+ else
+ statuses
+ end
end
def public_timeline_statuses
- Status.as_public_timeline(current_account, params[:local])
+ Status.as_public_timeline(current_account, truthy_param?(:local))
end
def insert_pagination_headers
@@ -37,7 +45,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end
def pagination_params(core_params)
- params.permit(:local, :limit).merge(core_params)
+ params.permit(:local, :limit, :only_media).merge(core_params)
end
def next_path
diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb
index 08db04a39d..eb32611ad4 100644
--- a/app/controllers/api/v1/timelines/tag_controller.rb
+++ b/app/controllers/api/v1/timelines/tag_controller.rb
@@ -29,16 +29,24 @@ class Api::V1::Timelines::TagController < Api::BaseController
if @tag.nil?
[]
else
- tag_timeline_statuses.paginate_by_max_id(
+ statuses = tag_timeline_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
+
+ if truthy_param?(:only_media)
+ # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
+ status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id)
+ statuses.where(id: status_ids)
+ else
+ statuses
+ end
end
end
def tag_timeline_statuses
- Status.as_tag_timeline(@tag, current_account, params[:local])
+ Status.as_tag_timeline(@tag, current_account, truthy_param?(:local))
end
def insert_pagination_headers
@@ -46,7 +54,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
end
def pagination_params(core_params)
- params.permit(:local, :limit).merge(core_params)
+ params.permit(:local, :limit, :only_media).merge(core_params)
end
def next_path
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index a296d96db6..fc745eaece 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -15,7 +15,7 @@ class ApplicationController < ActionController::Base
helper_method :current_flavour
helper_method :current_skin
helper_method :single_user_mode?
- helper_method :use_pam?
+ helper_method :use_seamless_external_login?
rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found
@@ -146,8 +146,8 @@ class ApplicationController < ActionController::Base
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists?
end
- def use_pam?
- Devise.pam_authentication
+ def use_seamless_external_login?
+ Devise.pam_authentication || Devise.ldap_authentication
end
def current_account
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index 475cd540a3..c9e507343f 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -38,7 +38,7 @@ class Auth::SessionsController < Devise::SessionsController
if session[:otp_user_id]
User.find(session[:otp_user_id])
elsif user_params[:email]
- if use_pam? && Devise.check_at_sign && user_params[:email].index('@').nil?
+ if use_seamless_external_login? && Devise.check_at_sign && user_params[:email].index('@').nil?
User.joins(:account).find_by(accounts: { username: user_params[:email] })
else
User.find_for_authentication(email: user_params[:email])
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index 080cbde114..c74d3f86d8 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -9,6 +9,8 @@ class FollowerAccountsController < ApplicationController
respond_to do |format|
format.html do
use_pack 'public'
+
+ @relationships = AccountRelationshipsPresenter.new(@follows.map(&:account_id), current_user.account_id) if user_signed_in?
end
format.json do
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 74e83ad814..4c1e3f327b 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -9,6 +9,8 @@ class FollowingAccountsController < ApplicationController
respond_to do |format|
format.html do
use_pack 'public'
+
+ @relationships = AccountRelationshipsPresenter.new(@follows.map(&:target_account_id), current_user.account_id) if user_signed_in?
end
format.json do
diff --git a/app/javascript/mastodon/actions/reports.js b/app/javascript/mastodon/actions/reports.js
index b19a07285b..afa0c34121 100644
--- a/app/javascript/mastodon/actions/reports.js
+++ b/app/javascript/mastodon/actions/reports.js
@@ -10,6 +10,7 @@ export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
+export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE';
export function initReport(account, status) {
return dispatch => {
@@ -45,6 +46,7 @@ export function submitReport() {
account_id: getState().getIn(['reports', 'new', 'account_id']),
status_ids: getState().getIn(['reports', 'new', 'status_ids']),
comment: getState().getIn(['reports', 'new', 'comment']),
+ forward: getState().getIn(['reports', 'new', 'forward']),
}).then(response => {
dispatch(closeModal());
dispatch(submitReportSuccess(response.data));
@@ -78,3 +80,10 @@ export function changeReportComment(comment) {
comment,
};
};
+
+export function changeReportForward(forward) {
+ return {
+ type: REPORT_FORWARD_CHANGE,
+ forward,
+ };
+};
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index df6a363795..858a12b152 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -120,7 +120,7 @@ export function refreshTimeline(timelineId, path, params = {}) {
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
-export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
+export const refreshAccountTimeline = (accountId, withReplies) => refreshTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
export const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
@@ -161,7 +161,7 @@ export function expandTimeline(timelineId, path, params = {}) {
export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
-export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
+export const expandAccountTimeline = (accountId, withReplies) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
export const expandListTimeline = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index 9e1bb77c2c..3568a84407 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -283,8 +283,9 @@ export default class MediaGallery extends React.PureComponent {
if (width) {
style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
}
+ } else if (width) {
+ style.height = width / (16/9);
} else {
- // crop the image
style.height = height;
}
@@ -309,7 +310,7 @@ export default class MediaGallery extends React.PureComponent {
if (this.isStandaloneEligible()) {
children = ;
} else {
- children = media.take(4).map((attachment, i) => );
+ children = media.take(4).map((attachment, i) => );
}
}
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index c030510a09..c52cd5f090 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -184,6 +184,7 @@ export default class Status extends ImmutablePureComponent {
src={video.get('url')}
width={239}
height={110}
+ inline
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
/>
diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js
index b4f812c9a0..b538fa5fcb 100644
--- a/app/javascript/mastodon/features/account/components/action_bar.js
+++ b/app/javascript/mastodon/features/account/components/action_bar.js
@@ -53,11 +53,11 @@ export default class ActionBar extends React.PureComponent {
let extraInfo = '';
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
+
if ('share' in navigator) {
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
}
- menu.push(null);
- menu.push({ text: intl.formatMessage(messages.media), to: `/accounts/${account.get('id')}/media` });
+
menu.push(null);
if (account.get('id') === me) {
diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js
index dda3d4e37b..59c805c38d 100644
--- a/app/javascript/mastodon/features/account_gallery/components/media_item.js
+++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js
@@ -12,24 +12,26 @@ export default class MediaItem extends ImmutablePureComponent {
render () {
const { media } = this.props;
const status = media.get('status');
+ const focusX = media.getIn(['meta', 'focus', 'x']);
+ const focusY = media.getIn(['meta', 'focus', 'y']);
+ const x = ((focusX / 2) + .5) * 100;
+ const y = ((focusY / -2) + .5) * 100;
+ const style = {};
- let content, style;
+ let content;
if (media.get('type') === 'gifv') {
content = GIF;
}
if (!status.get('sensitive')) {
- style = { backgroundImage: `url(${media.get('preview_url')})` };
+ style.backgroundImage = `url(${media.get('preview_url')})`;
+ style.backgroundPosition = `${x}% ${y}%`;
}
return (
diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js
index ece219a3d5..4b408256a1 100644
--- a/app/javascript/mastodon/features/account_gallery/index.js
+++ b/app/javascript/mastodon/features/account_gallery/index.js
@@ -11,7 +11,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { getAccountGallery } from '../../selectors';
import MediaItem from './components/media_item';
import HeaderContainer from '../account_timeline/containers/header_container';
-import { FormattedMessage } from 'react-intl';
import { ScrollContainer } from 'react-router-scroll-4';
import LoadMore from '../../components/load_more';
@@ -89,10 +88,6 @@ export default class AccountGallery extends ImmutablePureComponent {
-
-
-
-
{medias.map(media => (
+
+
+
+
+
+
);
}
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index f8c85c296d..aed009ef08 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/app/javascript/mastodon/features/account_timeline/index.js
@@ -12,11 +12,15 @@ import ColumnBackButton from '../../components/column_back_button';
import { List as ImmutableList } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
-const mapStateToProps = (state, props) => ({
- statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()),
- isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']),
- hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']),
-});
+const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
+ const path = withReplies ? `${accountId}:with_replies` : accountId;
+
+ return {
+ statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
+ isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
+ hasMore: !!state.getIn(['timelines', `account:${path}`, 'next']),
+ };
+};
@connect(mapStateToProps)
export default class AccountTimeline extends ImmutablePureComponent {
@@ -27,23 +31,24 @@ export default class AccountTimeline extends ImmutablePureComponent {
statusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
+ withReplies: PropTypes.bool,
};
componentWillMount () {
this.props.dispatch(fetchAccount(this.props.params.accountId));
- this.props.dispatch(refreshAccountTimeline(this.props.params.accountId));
+ this.props.dispatch(refreshAccountTimeline(this.props.params.accountId, this.props.withReplies));
}
componentWillReceiveProps (nextProps) {
- if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
+ if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
this.props.dispatch(fetchAccount(nextProps.params.accountId));
- this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId));
+ this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId, nextProps.params.withReplies));
}
}
handleScrollToBottom = () => {
if (!this.props.isLoading && this.props.hasMore) {
- this.props.dispatch(expandAccountTimeline(this.props.params.accountId));
+ this.props.dispatch(expandAccountTimeline(this.props.params.accountId, this.props.withReplies));
}
}
diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js
index 398fc44cee..71c0a203f0 100644
--- a/app/javascript/mastodon/features/compose/components/search.js
+++ b/app/javascript/mastodon/features/compose/components/search.js
@@ -4,6 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Overlay from 'react-overlays/lib/Overlay';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
+import { searchEnabled } from '../../../initial_state';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
@@ -17,7 +18,7 @@ class SearchPopout extends React.PureComponent {
render () {
const { style } = this.props;
-
+ const extraInformation = searchEnabled ?
:
;
return (
@@ -32,7 +33,7 @@ class SearchPopout extends React.PureComponent {
URL
-
+ {extraInformation}
)}
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index abdb9a3f68..d4f21fc32b 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -57,6 +57,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
src={video.get('url')}
width={300}
height={150}
+ inline
onOpenVideo={this.handleOpenVideo}
sensitive={status.get('sensitive')}
/>
diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.js
index fc88e0c70f..06a6c9cddf 100644
--- a/app/javascript/mastodon/features/ui/components/bundle.js
+++ b/app/javascript/mastodon/features/ui/components/bundle.js
@@ -26,7 +26,7 @@ class Bundle extends React.Component {
onFetchFail: noop,
}
- static cache = {}
+ static cache = new Map
state = {
mod: undefined,
@@ -51,13 +51,12 @@ class Bundle extends React.Component {
load = (props) => {
const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props;
+ const cachedMod = Bundle.cache.get(fetchComponent);
onFetch();
- if (Bundle.cache[fetchComponent.name]) {
- const mod = Bundle.cache[fetchComponent.name];
-
- this.setState({ mod: mod.default });
+ if (cachedMod) {
+ this.setState({ mod: cachedMod.default });
onFetchSuccess();
return Promise.resolve();
}
@@ -71,7 +70,7 @@ class Bundle extends React.Component {
return fetchComponent()
.then((mod) => {
- Bundle.cache[fetchComponent.name] = mod;
+ Bundle.cache.set(fetchComponent, mod);
this.setState({ mod: mod.default });
onFetchSuccess();
})
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index a01e5a390e..e82c46402b 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -6,6 +6,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import ReactSwipeableViews from 'react-swipeable-views';
import { links, getIndex, getLink } from './tabs_bar';
+import { Link } from 'react-router-dom';
import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading';
@@ -152,11 +153,19 @@ export default class ColumnsArea extends ImmutablePureComponent {
this.pendingIndex = null;
if (singleColumn) {
- return columnIndex !== -1 ? (
-
+ const floatingActionButton = this.context.router.history.location.pathname === '/statuses/new' ? null : ;
+
+ return columnIndex !== -1 ? [
+
{links.map(this.renderView)}
-
- ) : {children}
;
+ ,
+
+ floatingActionButton,
+ ] : [
+
{children}
,
+
+ floatingActionButton,
+ ];
}
return (
diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js
index b5dfa422e4..3ae97646fc 100644
--- a/app/javascript/mastodon/features/ui/components/report_modal.js
+++ b/app/javascript/mastodon/features/ui/components/report_modal.js
@@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
-import { changeReportComment, submitReport } from '../../../actions/reports';
+import { changeReportComment, changeReportForward, submitReport } from '../../../actions/reports';
import { refreshAccountTimeline } from '../../../actions/timelines';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
@@ -10,8 +10,11 @@ import StatusCheckBox from '../../report/containers/status_check_box_container';
import { OrderedSet } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Button from '../../../components/button';
+import Toggle from 'react-toggle';
+import IconButton from '../../../components/icon_button';
const messages = defineMessages({
+ close: { id: 'lightbox.close', defaultMessage: 'Close' },
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
submit: { id: 'report.submit', defaultMessage: 'Submit' },
});
@@ -26,6 +29,7 @@ const makeMapStateToProps = () => {
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
account: getAccount(state, accountId),
comment: state.getIn(['reports', 'new', 'comment']),
+ forward: state.getIn(['reports', 'new', 'forward']),
statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
};
};
@@ -42,14 +46,19 @@ export default class ReportModal extends ImmutablePureComponent {
account: ImmutablePropTypes.map,
statusIds: ImmutablePropTypes.orderedSet.isRequired,
comment: PropTypes.string.isRequired,
+ forward: PropTypes.bool,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
- handleCommentChange = (e) => {
+ handleCommentChange = e => {
this.props.dispatch(changeReportComment(e.target.value));
}
+ handleForwardChange = e => {
+ this.props.dispatch(changeReportForward(e.target.checked));
+ }
+
handleSubmit = () => {
this.props.dispatch(submitReport());
}
@@ -65,26 +74,25 @@ export default class ReportModal extends ImmutablePureComponent {
}
render () {
- const { account, comment, intl, statusIds, isSubmitting } = this.props;
+ const { account, comment, intl, statusIds, isSubmitting, forward, onClose } = this.props;
if (!account) {
return null;
}
+ const domain = account.get('acct').split('@')[1];
+
return (
+
{account.get('acct')} }} />
-
-
- {statusIds.map(statusId => )}
-
-
-
+
+
+
+ {domain && (
+
+ )}
+
+
-
-
-
+
+
+ {statusIds.map(statusId => )}
+
+
);
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js
index 7694e5ab33..77fe5f5e2f 100644
--- a/app/javascript/mastodon/features/ui/components/tabs_bar.js
+++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js
@@ -6,14 +6,13 @@ import { debounce } from 'lodash';
import { isUserTouching } from '../../../is_mobile';
export const links = [
-
,
,
,
,
,
-
,
+
,
];
export function getIndex (path) {
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 5b0d7246a6..ef909136f8 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -398,6 +398,7 @@ export default class UI extends React.Component {
+
diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
index 43007ddc3d..32dfe320b9 100644
--- a/app/javascript/mastodon/features/ui/util/react_router_helpers.js
+++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
@@ -35,14 +35,19 @@ export class WrappedRoute extends React.Component {
component: PropTypes.func.isRequired,
content: PropTypes.node,
multiColumn: PropTypes.bool,
- }
+ componentParams: PropTypes.object,
+ };
+
+ static defaultProps = {
+ componentParams: {},
+ };
renderComponent = ({ match }) => {
- const { component, content, multiColumn } = this.props;
+ const { component, content, multiColumn, componentParams } = this.props;
return (
- {Component => {content}}
+ {Component => {content}}
);
}
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index c81a5cb5fc..98ebcb6f91 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -97,6 +97,7 @@ export default class Video extends React.PureComponent {
onOpenVideo: PropTypes.func,
onCloseVideo: PropTypes.func,
detailed: PropTypes.bool,
+ inline: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@@ -105,6 +106,7 @@ export default class Video extends React.PureComponent {
duration: 0,
paused: true,
dragging: false,
+ containerWidth: false,
fullscreen: false,
hovered: false,
muted: false,
@@ -113,6 +115,12 @@ export default class Video extends React.PureComponent {
setPlayerRef = c => {
this.player = c;
+
+ if (c) {
+ this.setState({
+ containerWidth: c.offsetWidth,
+ });
+ }
}
setVideoRef = c => {
@@ -246,12 +254,23 @@ export default class Video extends React.PureComponent {
}
render () {
- const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed } = this.props;
- const { currentTime, duration, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
+ const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed } = this.props;
+ const { containerWidth, currentTime, duration, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
const progress = (currentTime / duration) * 100;
+ const playerStyle = {};
+
+ let { width, height } = this.props;
+
+ if (inline && containerWidth) {
+ width = containerWidth;
+ height = containerWidth / (16/9);
+
+ playerStyle.width = width;
+ playerStyle.height = height;
+ }
return (
-
+