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 = (
-
+
);
} 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' && }
+