diff --git a/.env.production.sample b/.env.production.sample index 54d62d672ff..d46768d09ac 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -144,14 +144,22 @@ STREAMING_CLUSTER_NUM=1 # MAX_TOOT_CHARS=500 # PAM authentication (optional) +# PAM authentication uses for the email generation the "email" pam variable +# and optional as fallback PAM_DEFAULT_SUFFIX +# The pam environment variable "email" is provided by: +# https://github.com/devkral/pam_email_extractor # PAM_ENABLED=true -# Suffix for email address generation (nil by default) +# Fallback Suffix for email address generation (nil by default) # 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) # PAM_CONTROLLED_SERVICE=rpam +# Global OAuth settings (optional) : +# If you have only one strategy, you may want to enable this +# OAUTH_REDIRECT_AT_SIGN_IN=true + # Optional CAS authentication (cf. omniauth-cas) : # CAS_ENABLED=true # CAS_URL=https://sso.myserver.com/ @@ -187,7 +195,10 @@ STREAMING_CLUSTER_NUM=1 # SAML_PRIVATE_KEY= # SAML_SECURITY_WANT_ASSERTION_SIGNED=true # SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true +# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true # SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1" # SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" # SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.5.4.42" # SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1" +# SAML_ATTRIBUTES_STATEMENTS_VERIFIED= +# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL= diff --git a/Dockerfile b/Dockerfile index 6d8465ddc6f..a50122057e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,10 @@ FROM ruby:2.5.0-alpine3.7 LABEL maintainer="https://github.com/tootsuite/mastodon" \ description="A GNU Social-compatible microblogging server" -ENV UID=991 GID=991 \ - RAILS_SERVE_STATIC_FILES=true \ +ARG UID=991 +ARG GID=991 + +ENV RAILS_SERVE_STATIC_FILES=true \ RAILS_ENV=production NODE_ENV=production ARG YARN_VERSION=1.3.2 @@ -71,12 +73,12 @@ RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-in && yarn --pure-lockfile \ && yarn cache clean -COPY . /mastodon +RUN addgroup -g ${GID} mastodon && adduser -h /mastodon -s /bin/sh -D -G mastodon -u ${UID} mastodon -COPY docker_entrypoint.sh /usr/local/bin/run - -RUN chmod +x /usr/local/bin/run +COPY --chown=mastodon:mastodon . /mastodon VOLUME /mastodon/public/system /mastodon/public/assets /mastodon/public/packs -ENTRYPOINT ["/usr/local/bin/run"] +USER mastodon + +ENTRYPOINT ["/sbin/tini", "--"] diff --git a/Gemfile b/Gemfile index 33f9374cf15..b6962861fe4 100644 --- a/Gemfile +++ b/Gemfile @@ -41,6 +41,7 @@ gem 'omniauth', '~> 1.2' gem 'doorkeeper', '~> 4.2' gem 'fast_blank', '~> 1.0' +gem 'fastimage' gem 'goldfinger', '~> 2.1' gem 'hiredis', '~> 0.6' gem 'redis-namespace', '~> 1.5' @@ -117,6 +118,7 @@ group :development do gem 'bullet', '~> 5.5' gem 'letter_opener', '~> 1.4' gem 'letter_opener_web', '~> 1.3' + gem 'memory_profiler' gem 'rubocop', require: false gem 'brakeman', '~> 4.0', require: false gem 'bundler-audit', '~> 0.6', require: false diff --git a/Gemfile.lock b/Gemfile.lock index d4ebd0a4047..1905cf3e1f6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -185,6 +185,7 @@ GEM faraday (0.14.0) multipart-post (>= 1.2, < 3) fast_blank (1.0.0) + fastimage (2.1.1) ffi (1.9.18) fog-core (1.45.0) builder @@ -302,6 +303,7 @@ GEM mini_mime (>= 0.1.1) mario-redis-lock (1.2.0) redis (~> 3, >= 3.0.5) + memory_profiler (0.9.10) method_source (0.9.0) microformats (4.0.7) json @@ -644,6 +646,7 @@ DEPENDENCIES fabrication (~> 2.18) faker (~> 1.7) fast_blank (~> 1.0) + fastimage fog-core (~> 1.45) fog-local (~> 0.4) fog-openstack (~> 0.1) @@ -666,6 +669,7 @@ DEPENDENCIES link_header (~> 0.0) lograge (~> 0.7) mario-redis-lock (~> 1.2) + memory_profiler microformats (~> 4.0) mime-types (~> 3.1) nokogiri (~> 1.8) diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index a6214dc3fc1..ce32082099e 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -16,6 +16,7 @@ module Admin show_staff_badge bootstrap_timeline_accounts thumbnail + hero min_invite_role activity_api_enabled peers_api_enabled @@ -34,6 +35,7 @@ module Admin UPLOAD_SETTINGS = %w( thumbnail + hero ).freeze def edit diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index 6cc3da49851..70236d1a8bf 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -21,6 +21,6 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController end def account_ids - @_account_ids ||= Array(params[:id]).map(&:to_i) + Array(params[:id]).map(&:to_i) end end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index 9f330f0dfe9..d4e6337e764 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -27,7 +27,7 @@ class Api::V1::MediaController < Api::BaseController private def media_params - params.permit(:file, :description) + params.permit(:file, :description, :focus) end def file_type_error diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7534b537569..a296d96db6e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -36,7 +36,7 @@ class ApplicationController < ActionController::Base end def store_current_location - store_location_for(:user, request.url) + store_location_for(:user, request.url) unless request.format == :json end def require_admin! diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index ce9cf98d70c..475cd540a3c 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -11,6 +11,15 @@ class Auth::SessionsController < Devise::SessionsController prepend_before_action :set_pack before_action :set_instance_presenter, only: [:new] + def new + Devise.omniauth_configs.each do |provider, config| + if config.strategy.redirect_at_sign_in + return redirect_to(omniauth_authorize_path(resource_name, provider)) + end + end + super + end + def create super do |resource| remember_me(resource) diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 9c03ece8600..cf874557602 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -1,7 +1,19 @@ # frozen_string_literal: true class Settings::ExportsController < Settings::BaseController + include Authorization + def show - @export = Export.new(current_account) + @export = Export.new(current_account) + @backups = current_user.backups + end + + def create + authorize :backup, :create? + + backup = current_user.backups.create! + BackupWorker.perform_async(backup.id) + + redirect_to settings_export_path end end diff --git a/app/javascript/flavours/glitch/styles/about.scss b/app/javascript/flavours/glitch/styles/about.scss index 31c079cc57d..2985f8947ae 100644 --- a/app/javascript/flavours/glitch/styles/about.scss +++ b/app/javascript/flavours/glitch/styles/about.scss @@ -1,3 +1,130 @@ +$maximum-width: 1235px; +$fluid-breakpoint: $maximum-width + 20px; +$column-breakpoint: 700px; +$small-breakpoint: 960px; + +.container { + box-sizing: border-box; + max-width: $maximum-width; + margin: 0 auto; + position: relative; + + @media screen and (max-width: $fluid-breakpoint) { + width: 100%; + padding: 0 10px; + } +} + +.show-xs, +.show-sm { + display: none; +} + +.show-m { + display: block; +} + +@media screen and (max-width: $small-breakpoint) { + .hide-sm { + display: none !important; + } + + .show-sm { + display: block !important; + } +} + +@media screen and (max-width: $column-breakpoint) { + .hide-xs { + display: none !important; + } + + .show-xs { + display: block !important; + } +} + +.row { + display: flex; + flex-wrap: wrap; + margin: 0 -5px; + + @for $i from 1 through 15 { + .column-#{$i} { + box-sizing: border-box; + min-height: 1px; + flex: 0 0 percentage($i / 15); + max-width: percentage($i / 15); + padding: 0 5px; + + @media screen and (max-width: $small-breakpoint) { + &-sm { + box-sizing: border-box; + min-height: 1px; + flex: 0 0 percentage($i / 15); + max-width: percentage($i / 15); + padding: 0 5px; + + @media screen and (max-width: $column-breakpoint) { + max-width: 100%; + flex: 0 0 100%; + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + } + } + } + + @media screen and (max-width: $column-breakpoint) { + max-width: 100%; + flex: 0 0 100%; + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + } + } + } +} + +.column-flex { + display: flex; + flex-direction: column; +} + +.separator-or { + position: relative; + margin: 40px 0; + text-align: center; + + &::before { + content: ""; + display: block; + width: 100%; + height: 0; + border-bottom: 1px solid rgba($ui-base-lighter-color, .6); + position: absolute; + top: 50%; + left: 0; + } + + span { + display: inline-block; + background: $ui-base-color; + font-size: 12px; + font-weight: 500; + color: $ui-primary-color; + text-transform: uppercase; + position: relative; + z-index: 1; + padding: 0 8px; + cursor: default; + } +} + .landing-page { p, li { @@ -116,10 +243,14 @@ } hr { - border-color: rgba($ui-base-lighter-color, .6); + width: 100%; + height: 0; + border: 0; + border-bottom: 1px solid rgba($ui-base-lighter-color, .6); + margin: 20px 0; } - .container { + .container-alt { width: 100%; box-sizing: border-box; max-width: 800px; @@ -152,24 +283,20 @@ } } } + } - .mascot-container { - max-width: 800px; - margin: 0 auto; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 100%; + .brand { + a { + padding-left: 0; + padding-right: 0; + color: $white; } - .mascot { - position: absolute; - bottom: -14px; - width: auto; - height: auto; - left: 60px; - z-index: 3; + img { + height: 32px; + position: relative; + top: 4px; + left: -10px; } } @@ -177,7 +304,7 @@ line-height: 30px; overflow: hidden; - .container { + .container-alt { display: flex; justify-content: space-between; } @@ -203,21 +330,6 @@ } } - .brand { - a { - padding-left: 0; - padding-right: 0; - color: $white; - } - - img { - height: 32px; - position: relative; - top: 4px; - left: -10px; - } - } - ul { list-style: none; margin: 0; @@ -243,53 +355,6 @@ align-items: center; position: relative; - .floats { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - - div { - position: absolute; - transition: all 0.1s linear; - animation-name: floating; - animation-iteration-count: infinite; - animation-direction: alternate; - animation-timing-function: ease-in-out; - z-index: 2; - } - - .float-1 { - width: 324px; - height: 170px; - right: -120px; - bottom: 0; - animation-duration: 3s; - background-image: url('data:image/svg+xml;utf8,'); - } - - .float-2 { - width: 241px; - height: 100px; - right: 210px; - bottom: 0; - animation-duration: 3.5s; - animation-delay: 0.2s; - background-image: url('data:image/svg+xml;utf8,'); - } - - .float-3 { - width: 267px; - height: 140px; - right: 110px; - top: -30px; - animation-duration: 4s; - animation-delay: 0.5s; - background-image: url('data:image/svg+xml;utf8,'); - } - } - .heading { position: relative; z-index: 4; @@ -346,18 +411,18 @@ background: darken($ui-base-color, 4%); padding: 20px 0; - .container { + .container-alt { position: relative; padding-right: 280px + 15px; } - .information-board-sections { + &__sections { display: flex; justify-content: space-between; flex-wrap: wrap; } - .section { + &__section { flex: 1 0 0; font-family: 'mastodon-font-sans-serif', sans-serif; font-size: 16px; @@ -382,6 +447,10 @@ font-size: 32px; line-height: 48px; } + + @media screen and (max-width: $column-breakpoint) { + text-align: center; + } } .panel { @@ -428,15 +497,12 @@ height: 80px; margin: 0 auto; margin-bottom: 15px; - @include avatar-size(80px); img { display: block; width: 80px; height: 80px; border-radius: 48px; - @include avatar-radius(); - @include avatar-size(80px); } } @@ -463,111 +529,282 @@ } } - .features { - padding: 50px 0; + &.alternative { + padding: 10px 0; - .container { - display: flex; - } + .brand { + text-align: center; + padding: 30px 0; + margin-bottom: 10px; - #mastodon-timeline { - display: flex; - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; - font-family: 'mastodon-font-sans-serif', sans-serif; - font-size: 13px; - line-height: 18px; - font-weight: 400; - color: $primary-text-color; - width: 330px; - margin-right: 30px; - flex: 0 0 auto; - background: $ui-base-color; - overflow: hidden; - border-radius: 4px; - box-shadow: 0 0 6px rgba($black, 0.1); - - .column-header { - color: inherit; - font-family: inherit; - font-size: 16px; - line-height: inherit; - font-weight: inherit; - margin: 0; - padding: 15px; + img { + position: static; } - .column { + @media screen and (max-width: $small-breakpoint) { + padding: 15px 0; + } + + @media screen and (max-width: $column-breakpoint) { padding: 0; - border-radius: 4px; - overflow: hidden; + margin-bottom: -10px; } + } + } - .scrollable { - height: 400px; - } + &__information, + &__forms { + padding: 20px; + } - p { - font-size: inherit; - line-height: inherit; - font-weight: inherit; - color: $primary-text-color; + &__call-to-action { + margin-bottom: 10px; + background: darken($ui-base-color, 4%); + border-radius: 4px; + padding: 25px 40px; + overflow: hidden; + + .row { + align-items: center; + } + + .information-board__section { + padding: 0; + } + } + + &__logo { + margin-right: 20px; + + img { + height: 50px; + width: auto; + mix-blend-mode: lighten; + } + } + + &__information { + padding: 45px 40px; + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + + @media screen and (max-width: $column-breakpoint) { + padding: 25px 20px; + } + } + + &__information, + &__forms, + #mastodon-timeline { + box-sizing: border-box; + background: $ui-base-color; + border-radius: 4px; + box-shadow: 0 0 6px rgba($black, 0.1); + } + + &__mascot { + height: 104px; + position: relative; + left: -40px; + bottom: 25px; + + img { + height: 190px; + width: auto; + } + } + + &__short-description { + .row { + align-items: center; + margin-bottom: 40px; + } + + @media screen and (max-width: $column-breakpoint) { + .row { margin-bottom: 20px; + } + } - &:last-child { - margin-bottom: 0; - } + p a { + color: $ui-secondary-color; + } - a { + h1 { + font-weight: 500; + color: $primary-text-color; + margin-bottom: 0; + + small { + color: $ui-primary-color; + + span { color: $ui-secondary-color; - text-decoration: none; } } } - .about-mastodon { - max-width: 675px; + p:last-child { + margin-bottom: 0; + } + } - p { - margin-bottom: 20px; + &__hero { + margin-bottom: 10px; + + img { + display: block; + margin: 0; + max-width: 100%; + height: auto; + border-radius: 4px; + } + } + + &__forms { + height: 100%; + + @media screen and (max-width: $small-breakpoint) { + margin-bottom: 10px; + height: auto; + } + + @media screen and (max-width: $column-breakpoint) { + background: transparent; + box-shadow: none; + padding: 0 20px; + margin-top: 30px; + margin-bottom: 40px; + + .separator-or { + span { + background: darken($ui-base-color, 8%); + } + } + } + + hr { + margin: 40px 0; + } + + .button { + display: block; + } + + .subtle-hint a { + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + } + + #mastodon-timeline { + display: flex; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + font-family: 'mastodon-font-sans-serif', sans-serif; + font-size: 13px; + line-height: 18px; + font-weight: 400; + color: $primary-text-color; + width: 100%; + flex: 1 1 auto; + overflow: hidden; + + .column-header { + color: inherit; + font-family: inherit; + font-size: 16px; + padding: 15px; + line-height: inherit; + font-weight: inherit; + margin: 0; + } + + .column { + padding: 0; + border-radius: 4px; + overflow: hidden; + width: 100%; + } + + .scrollable { + height: 400px; + } + + p { + font-size: inherit; + line-height: inherit; + font-weight: inherit; + color: $primary-text-color; + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; } - .features-list { - margin-top: 20px; + a { + color: $ui-secondary-color; + text-decoration: none; + } + } - .features-list__row { - display: flex; - padding: 10px 0; - justify-content: space-between; + @media screen and (max-width: $column-breakpoint) { + height: 90vh; + } + } - &:first-child { - padding-top: 0; - } + &__features { + .features-list { + margin: 40px 0 !important; + } - .visual { - flex: 0 0 auto; - display: flex; - align-items: center; - margin-left: 15px; + &__action { + text-align: center; + } + } - .fa { - display: block; - color: $ui-primary-color; - font-size: 48px; - } - } + .features-list { + margin-top: 20px; - .text { - font-size: 16px; - line-height: 30px; - color: $ui-primary-color; + .features-list__row { + display: flex; + padding: 10px 0; + justify-content: space-between; - h6 { - font-size: inherit; - line-height: inherit; - margin-bottom: 0; - } - } + &:first-child { + padding-top: 0; + } + + .visual { + flex: 0 0 auto; + display: flex; + align-items: center; + margin-left: 15px; + + .fa { + display: block; + color: $ui-primary-color; + font-size: 48px; + } + } + + .text { + font-size: 16px; + line-height: 30px; + color: $ui-primary-color; + + h6 { + font-size: inherit; + line-height: inherit; + margin-bottom: 0; } } } @@ -603,21 +840,31 @@ } } + &__footer { + margin-top: 10px; + text-align: center; + color: $ui-base-lighter-color; + + p { + font-size: 14px; + + a { + color: inherit; + text-decoration: underline; + } + } + } + @media screen and (max-width: 840px) { - .container { + .container-alt { padding: 0 20px; } .information-board { - - .container { + .container-alt { padding-right: 20px; } - .section { - text-align: center; - } - .panel { position: static; margin-top: 20px; @@ -629,16 +876,6 @@ } } } - - .header-wrapper .mascot { - left: 20px; - } - } - - @media screen and (max-width: 689px) { - .header-wrapper .mascot { - display: none; - } } @media screen and (max-width: 675px) { @@ -654,13 +891,12 @@ } } - .header .container, - .features .container { + .header .container-alt, + .features .container-alt { display: block; } .header { - .links { padding-top: 15px; background: darken($ui-base-color, 4%); @@ -685,10 +921,6 @@ margin-top: 30px; padding: 0; - .floats { - display: none; - } - .heading { padding: 30px 20px; text-align: center; @@ -703,16 +935,6 @@ } } } - - .features #mastodon-timeline { - height: 70vh; - width: 100%; - margin-bottom: 50px; - - .column { - width: 100%; - } - } } .cta { @@ -723,7 +945,7 @@ .features { padding: 30px 0; - .container { + .container-alt { max-width: 820px; #mastodon-timeline { @@ -775,7 +997,7 @@ .features { padding: 10px 0; - .container { + .container-alt { display: flex; flex-direction: column; @@ -811,17 +1033,3 @@ } } } - -@keyframes floating { - from { - transform: translate(0, 0); - } - - 65% { - transform: translate(0, 4px); - } - - to { - transform: translate(0, -0); - } -} diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss index b5d77ff63bc..15fbb1c8927 100644 --- a/app/javascript/flavours/glitch/styles/basics.scss +++ b/app/javascript/flavours/glitch/styles/basics.scss @@ -118,5 +118,6 @@ button { height: 100%; align-items: center; justify-content: center; + outline: 0 !important; } } diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index 6e03650edc4..5dcfab4d1a9 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -242,14 +242,29 @@ .column-header { display: flex; - padding: 15px; font-size: 16px; + padding: 15px; background: lighten($ui-base-color, 4%); flex: 0 0 auto; cursor: pointer; position: relative; z-index: 2; outline: 0; + overflow: hidden; + + & > button { + margin: 0; + border: none; + padding: 15px 0 15px 15px; + color: inherit; + background: transparent; + font: inherit; + text-align: left; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex: 1; + } &.active { box-shadow: 0 1px 0 rgba($ui-highlight-color, 0.3); diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 8f06209c618..10cad345edc 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -40,14 +40,20 @@ cursor: default; } - &.button-alternative { + &.button-primary, + &.button-alternative, + &.button-secondary, + &.button-alternative-2 { font-size: 16px; line-height: 36px; height: auto; - color: $ui-base-color; - background: $ui-primary-color; text-transform: none; padding: 4px 16px; + } + + &.button-alternative { + color: $ui-base-color; + background: $ui-primary-color; &:active, &:focus, @@ -56,6 +62,16 @@ } } + &.button-alternative-2 { + background: $ui-base-lighter-color; + + &:active, + &:focus, + &:hover { + background-color: lighten($ui-base-lighter-color, 4%); + } + } + &.button-secondary { font-size: 16px; line-height: 36px; diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index af2589e23c3..6fa1fa38f51 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -1,4 +1,4 @@ -.container { +.container-alt { width: 700px; margin: 0 auto; margin-top: 40px; diff --git a/app/javascript/images/icon_file_download.svg b/app/javascript/images/icon_file_download.svg new file mode 100644 index 00000000000..53e97e4f8af --- /dev/null +++ b/app/javascript/images/icon_file_download.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/javascript/images/mailer/icon_file_download.png b/app/javascript/images/mailer/icon_file_download.png new file mode 100644 index 00000000000..8a6a8673bcb Binary files /dev/null and b/app/javascript/images/mailer/icon_file_download.png differ diff --git a/app/javascript/images/reticle.png b/app/javascript/images/reticle.png new file mode 100644 index 00000000000..998994f5c00 Binary files /dev/null and b/app/javascript/images/reticle.png differ diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 8a35049b32c..1732ff189ed 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -178,11 +178,11 @@ export function uploadCompose(files) { }; }; -export function changeUploadCompose(id, description) { +export function changeUploadCompose(id, params) { return (dispatch, getState) => { dispatch(changeUploadComposeRequest()); - api(getState).put(`/api/v1/media/${id}`, { description }).then(response => { + api(getState).put(`/api/v1/media/${id}`, params).then(response => { dispatch(changeUploadComposeSuccess(response.data)); }).catch(error => { dispatch(changeUploadComposeFail(id, error)); diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index a3ffc45eae4..9e1bb77c2c1 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -12,6 +12,26 @@ const messages = defineMessages({ toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, }); +const shiftToPoint = (containerToImageRatio, containerSize, imageSize, focusSize, toMinus) => { + const containerCenter = Math.floor(containerSize / 2); + const focusFactor = (focusSize + 1) / 2; + const scaledImage = Math.floor(imageSize / containerToImageRatio); + + let focus = Math.floor(focusFactor * scaledImage); + + if (toMinus) focus = scaledImage - focus; + + let focusOffset = focus - containerCenter; + + const remainder = scaledImage - focus; + const containerRemainder = containerSize - containerCenter; + + if (remainder < containerRemainder) focusOffset -= containerRemainder - remainder; + if (focusOffset < 0) focusOffset = 0; + + return (focusOffset * -100 / containerSize) + '%'; +}; + class Item extends React.PureComponent { static contextTypes = { @@ -24,6 +44,8 @@ class Item extends React.PureComponent { index: PropTypes.number.isRequired, size: PropTypes.number.isRequired, onClick: PropTypes.func.isRequired, + containerWidth: PropTypes.number, + containerHeight: PropTypes.number, }; static defaultProps = { @@ -62,7 +84,7 @@ class Item extends React.PureComponent { } render () { - const { attachment, index, size, standalone } = this.props; + const { attachment, index, size, standalone, containerWidth, containerHeight } = this.props; let width = 50; let height = 100; @@ -116,16 +138,40 @@ class Item extends React.PureComponent { let thumbnail = ''; if (attachment.get('type') === 'image') { - const previewUrl = attachment.get('preview_url'); + const previewUrl = attachment.get('preview_url'); const previewWidth = attachment.getIn(['meta', 'small', 'width']); - const originalUrl = attachment.get('url'); - const originalWidth = attachment.getIn(['meta', 'original', 'width']); + const originalUrl = attachment.get('url'); + const originalWidth = attachment.getIn(['meta', 'original', 'width']); + const originalHeight = attachment.getIn(['meta', 'original', 'height']); const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number'; const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null; - const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null; + const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null; + + const focusX = attachment.getIn(['meta', 'focus', 'x']); + const focusY = attachment.getIn(['meta', 'focus', 'y']); + const imageStyle = {}; + + if (originalWidth && originalHeight && containerWidth && containerHeight && focusX && focusY) { + const widthRatio = originalWidth / (containerWidth * (width / 100)); + const heightRatio = originalHeight / (containerHeight * (height / 100)); + + let hShift = 0; + let vShift = 0; + + if (widthRatio > heightRatio) { + hShift = shiftToPoint(heightRatio, (containerWidth * (width / 100)), originalWidth, focusX); + } else if(widthRatio < heightRatio) { + vShift = shiftToPoint(widthRatio, (containerHeight * (height / 100)), originalHeight, focusY, true); + } + + imageStyle.top = vShift; + imageStyle.left = hShift; + } else { + imageStyle.height = '100%'; + } thumbnail = ( - {attachment.get('description')} + {attachment.get('description')} ); } else if (attachment.get('type') === 'gifv') { @@ -205,7 +258,7 @@ export default class MediaGallery extends React.PureComponent { } handleRef = (node) => { - if (node && this.isStandaloneEligible()) { + if (node /*&& this.isStandaloneEligible()*/) { // offsetWidth triggers a layout, so only calculate when we need to this.setState({ width: node.offsetWidth, @@ -256,12 +309,12 @@ 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) => ); } } return ( -
+
diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js index 3a3d1771008..61b2d19e0db 100644 --- a/app/javascript/mastodon/features/compose/components/upload.js +++ b/app/javascript/mastodon/features/compose/components/upload.js @@ -1,15 +1,13 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import IconButton from '../../../components/icon_button'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; const messages = defineMessages({ - undo: { id: 'upload_form.undo', defaultMessage: 'Undo' }, description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, }); @@ -21,6 +19,7 @@ export default class Upload extends ImmutablePureComponent { intl: PropTypes.object.isRequired, onUndo: PropTypes.func.isRequired, onDescriptionChange: PropTypes.func.isRequired, + onOpenFocalPoint: PropTypes.func.isRequired, }; state = { @@ -33,6 +32,10 @@ export default class Upload extends ImmutablePureComponent { this.props.onUndo(this.props.media.get('id')); } + handleFocalPointClick = () => { + this.props.onOpenFocalPoint(this.props.media.get('id')); + } + handleInputChange = e => { this.setState({ dirtyDescription: e.target.value }); } @@ -63,13 +66,20 @@ export default class Upload extends ImmutablePureComponent { const { intl, media } = this.props; const active = this.state.hovered || this.state.focused; const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''; + 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; return (
{({ scale }) => ( -
- +
+
+ + {media.get('type') === 'image' && } +