mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-12 06:06:22 +01:00
Merge branch 'master' of https://github.com/tootsuite/mastodon
# Conflicts: # app/controllers/settings/exports_controller.rb # app/models/media_attachment.rb # app/models/status.rb # app/views/about/show.html.haml # docker_entrypoint.sh # spec/views/about/show.html.haml_spec.rb
This commit is contained in:
commit
bb6988a7ac
@ -144,14 +144,22 @@ STREAMING_CLUSTER_NUM=1
|
|||||||
# MAX_TOOT_CHARS=500
|
# MAX_TOOT_CHARS=500
|
||||||
|
|
||||||
# PAM authentication (optional)
|
# 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
|
# PAM_ENABLED=true
|
||||||
# Suffix for email address generation (nil by default)
|
# Fallback Suffix for email address generation (nil by default)
|
||||||
# PAM_DEFAULT_SUFFIX=pam
|
# PAM_DEFAULT_SUFFIX=pam
|
||||||
# Name of the pam service (pam "auth" section is evaluated)
|
# Name of the pam service (pam "auth" section is evaluated)
|
||||||
# PAM_DEFAULT_SERVICE=rpam
|
# 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)
|
||||||
# PAM_CONTROLLED_SERVICE=rpam
|
# 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) :
|
# Optional CAS authentication (cf. omniauth-cas) :
|
||||||
# CAS_ENABLED=true
|
# CAS_ENABLED=true
|
||||||
# CAS_URL=https://sso.myserver.com/
|
# CAS_URL=https://sso.myserver.com/
|
||||||
@ -187,7 +195,10 @@ STREAMING_CLUSTER_NUM=1
|
|||||||
# SAML_PRIVATE_KEY=
|
# SAML_PRIVATE_KEY=
|
||||||
# SAML_SECURITY_WANT_ASSERTION_SIGNED=true
|
# SAML_SECURITY_WANT_ASSERTION_SIGNED=true
|
||||||
# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=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_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_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_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.5.4.42"
|
||||||
# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
|
# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
|
||||||
|
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
|
||||||
|
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
|
||||||
|
16
Dockerfile
16
Dockerfile
@ -3,8 +3,10 @@ FROM ruby:2.5.0-alpine3.7
|
|||||||
LABEL maintainer="https://github.com/tootsuite/mastodon" \
|
LABEL maintainer="https://github.com/tootsuite/mastodon" \
|
||||||
description="A GNU Social-compatible microblogging server"
|
description="A GNU Social-compatible microblogging server"
|
||||||
|
|
||||||
ENV UID=991 GID=991 \
|
ARG UID=991
|
||||||
RAILS_SERVE_STATIC_FILES=true \
|
ARG GID=991
|
||||||
|
|
||||||
|
ENV RAILS_SERVE_STATIC_FILES=true \
|
||||||
RAILS_ENV=production NODE_ENV=production
|
RAILS_ENV=production NODE_ENV=production
|
||||||
|
|
||||||
ARG YARN_VERSION=1.3.2
|
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 --pure-lockfile \
|
||||||
&& yarn cache clean
|
&& 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
|
COPY --chown=mastodon:mastodon . /mastodon
|
||||||
|
|
||||||
RUN chmod +x /usr/local/bin/run
|
|
||||||
|
|
||||||
VOLUME /mastodon/public/system /mastodon/public/assets /mastodon/public/packs
|
VOLUME /mastodon/public/system /mastodon/public/assets /mastodon/public/packs
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/run"]
|
USER mastodon
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
|
2
Gemfile
2
Gemfile
@ -41,6 +41,7 @@ gem 'omniauth', '~> 1.2'
|
|||||||
|
|
||||||
gem 'doorkeeper', '~> 4.2'
|
gem 'doorkeeper', '~> 4.2'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
|
gem 'fastimage'
|
||||||
gem 'goldfinger', '~> 2.1'
|
gem 'goldfinger', '~> 2.1'
|
||||||
gem 'hiredis', '~> 0.6'
|
gem 'hiredis', '~> 0.6'
|
||||||
gem 'redis-namespace', '~> 1.5'
|
gem 'redis-namespace', '~> 1.5'
|
||||||
@ -117,6 +118,7 @@ group :development do
|
|||||||
gem 'bullet', '~> 5.5'
|
gem 'bullet', '~> 5.5'
|
||||||
gem 'letter_opener', '~> 1.4'
|
gem 'letter_opener', '~> 1.4'
|
||||||
gem 'letter_opener_web', '~> 1.3'
|
gem 'letter_opener_web', '~> 1.3'
|
||||||
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', require: false
|
gem 'rubocop', require: false
|
||||||
gem 'brakeman', '~> 4.0', require: false
|
gem 'brakeman', '~> 4.0', require: false
|
||||||
gem 'bundler-audit', '~> 0.6', require: false
|
gem 'bundler-audit', '~> 0.6', require: false
|
||||||
|
@ -185,6 +185,7 @@ GEM
|
|||||||
faraday (0.14.0)
|
faraday (0.14.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
fast_blank (1.0.0)
|
fast_blank (1.0.0)
|
||||||
|
fastimage (2.1.1)
|
||||||
ffi (1.9.18)
|
ffi (1.9.18)
|
||||||
fog-core (1.45.0)
|
fog-core (1.45.0)
|
||||||
builder
|
builder
|
||||||
@ -302,6 +303,7 @@ GEM
|
|||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
mario-redis-lock (1.2.0)
|
mario-redis-lock (1.2.0)
|
||||||
redis (~> 3, >= 3.0.5)
|
redis (~> 3, >= 3.0.5)
|
||||||
|
memory_profiler (0.9.10)
|
||||||
method_source (0.9.0)
|
method_source (0.9.0)
|
||||||
microformats (4.0.7)
|
microformats (4.0.7)
|
||||||
json
|
json
|
||||||
@ -644,6 +646,7 @@ DEPENDENCIES
|
|||||||
fabrication (~> 2.18)
|
fabrication (~> 2.18)
|
||||||
faker (~> 1.7)
|
faker (~> 1.7)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
|
fastimage
|
||||||
fog-core (~> 1.45)
|
fog-core (~> 1.45)
|
||||||
fog-local (~> 0.4)
|
fog-local (~> 0.4)
|
||||||
fog-openstack (~> 0.1)
|
fog-openstack (~> 0.1)
|
||||||
@ -666,6 +669,7 @@ DEPENDENCIES
|
|||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
lograge (~> 0.7)
|
lograge (~> 0.7)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
|
memory_profiler
|
||||||
microformats (~> 4.0)
|
microformats (~> 4.0)
|
||||||
mime-types (~> 3.1)
|
mime-types (~> 3.1)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
|
@ -16,6 +16,7 @@ module Admin
|
|||||||
show_staff_badge
|
show_staff_badge
|
||||||
bootstrap_timeline_accounts
|
bootstrap_timeline_accounts
|
||||||
thumbnail
|
thumbnail
|
||||||
|
hero
|
||||||
min_invite_role
|
min_invite_role
|
||||||
activity_api_enabled
|
activity_api_enabled
|
||||||
peers_api_enabled
|
peers_api_enabled
|
||||||
@ -34,6 +35,7 @@ module Admin
|
|||||||
|
|
||||||
UPLOAD_SETTINGS = %w(
|
UPLOAD_SETTINGS = %w(
|
||||||
thumbnail
|
thumbnail
|
||||||
|
hero
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
|
@ -21,6 +21,6 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def account_ids
|
def account_ids
|
||||||
@_account_ids ||= Array(params[:id]).map(&:to_i)
|
Array(params[:id]).map(&:to_i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -27,7 +27,7 @@ class Api::V1::MediaController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def media_params
|
def media_params
|
||||||
params.permit(:file, :description)
|
params.permit(:file, :description, :focus)
|
||||||
end
|
end
|
||||||
|
|
||||||
def file_type_error
|
def file_type_error
|
||||||
|
@ -36,7 +36,7 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def store_current_location
|
def store_current_location
|
||||||
store_location_for(:user, request.url)
|
store_location_for(:user, request.url) unless request.format == :json
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_admin!
|
def require_admin!
|
||||||
|
@ -11,6 +11,15 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
prepend_before_action :set_pack
|
prepend_before_action :set_pack
|
||||||
before_action :set_instance_presenter, only: [:new]
|
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
|
def create
|
||||||
super do |resource|
|
super do |resource|
|
||||||
remember_me(resource)
|
remember_me(resource)
|
||||||
|
@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
class Settings::ExportsController < Settings::BaseController
|
class Settings::ExportsController < Settings::BaseController
|
||||||
def show
|
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
|
||||||
end
|
end
|
||||||
|
4
app/javascript/images/icon_file_download.svg
Normal file
4
app/javascript/images/icon_file_download.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 205 B |
BIN
app/javascript/images/mailer/icon_file_download.png
Normal file
BIN
app/javascript/images/mailer/icon_file_download.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 271 B |
BIN
app/javascript/images/reticle.png
Normal file
BIN
app/javascript/images/reticle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
@ -178,11 +178,11 @@ export function uploadCompose(files) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function changeUploadCompose(id, description) {
|
export function changeUploadCompose(id, params) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(changeUploadComposeRequest());
|
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));
|
dispatch(changeUploadComposeSuccess(response.data));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(changeUploadComposeFail(id, error));
|
dispatch(changeUploadComposeFail(id, error));
|
||||||
|
@ -12,6 +12,26 @@ const messages = defineMessages({
|
|||||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
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 {
|
class Item extends React.PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
@ -24,6 +44,8 @@ class Item extends React.PureComponent {
|
|||||||
index: PropTypes.number.isRequired,
|
index: PropTypes.number.isRequired,
|
||||||
size: PropTypes.number.isRequired,
|
size: PropTypes.number.isRequired,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
|
containerWidth: PropTypes.number,
|
||||||
|
containerHeight: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -62,7 +84,7 @@ class Item extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { attachment, index, size, standalone } = this.props;
|
const { attachment, index, size, standalone, containerWidth, containerHeight } = this.props;
|
||||||
|
|
||||||
let width = 50;
|
let width = 50;
|
||||||
let height = 100;
|
let height = 100;
|
||||||
@ -116,16 +138,40 @@ class Item extends React.PureComponent {
|
|||||||
let thumbnail = '';
|
let thumbnail = '';
|
||||||
|
|
||||||
if (attachment.get('type') === 'image') {
|
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 previewWidth = attachment.getIn(['meta', 'small', 'width']);
|
||||||
|
|
||||||
const originalUrl = attachment.get('url');
|
const originalUrl = attachment.get('url');
|
||||||
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
|
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
|
||||||
|
const originalHeight = attachment.getIn(['meta', 'original', 'height']);
|
||||||
|
|
||||||
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
|
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
|
||||||
|
|
||||||
const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
|
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 = (
|
thumbnail = (
|
||||||
<a
|
<a
|
||||||
@ -134,7 +180,14 @@ class Item extends React.PureComponent {
|
|||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
>
|
>
|
||||||
<img src={previewUrl} srcSet={srcSet} sizes={sizes} alt={attachment.get('description')} title={attachment.get('description')} />
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
srcSet={srcSet}
|
||||||
|
sizes={sizes}
|
||||||
|
alt={attachment.get('description')}
|
||||||
|
title={attachment.get('description')}
|
||||||
|
style={imageStyle}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else if (attachment.get('type') === 'gifv') {
|
} else if (attachment.get('type') === 'gifv') {
|
||||||
@ -205,7 +258,7 @@ export default class MediaGallery extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleRef = (node) => {
|
handleRef = (node) => {
|
||||||
if (node && this.isStandaloneEligible()) {
|
if (node /*&& this.isStandaloneEligible()*/) {
|
||||||
// offsetWidth triggers a layout, so only calculate when we need to
|
// offsetWidth triggers a layout, so only calculate when we need to
|
||||||
this.setState({
|
this.setState({
|
||||||
width: node.offsetWidth,
|
width: node.offsetWidth,
|
||||||
@ -256,12 +309,12 @@ export default class MediaGallery extends React.PureComponent {
|
|||||||
if (this.isStandaloneEligible()) {
|
if (this.isStandaloneEligible()) {
|
||||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />;
|
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />;
|
||||||
} else {
|
} else {
|
||||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />);
|
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} containerWidth={width} containerHeight={height} />);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='media-gallery' style={style}>
|
<div className='media-gallery' style={style} ref={this.handleRef}>
|
||||||
<div className={classNames('spoiler-button', { 'spoiler-button--visible': visible })}>
|
<div className={classNames('spoiler-button', { 'spoiler-button--visible': visible })}>
|
||||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
|
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import IconButton from '../../../components/icon_button';
|
|
||||||
import Motion from '../../ui/util/optional_motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
undo: { id: 'upload_form.undo', defaultMessage: 'Undo' },
|
|
||||||
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
|
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,
|
intl: PropTypes.object.isRequired,
|
||||||
onUndo: PropTypes.func.isRequired,
|
onUndo: PropTypes.func.isRequired,
|
||||||
onDescriptionChange: PropTypes.func.isRequired,
|
onDescriptionChange: PropTypes.func.isRequired,
|
||||||
|
onOpenFocalPoint: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -33,6 +32,10 @@ export default class Upload extends ImmutablePureComponent {
|
|||||||
this.props.onUndo(this.props.media.get('id'));
|
this.props.onUndo(this.props.media.get('id'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleFocalPointClick = () => {
|
||||||
|
this.props.onOpenFocalPoint(this.props.media.get('id'));
|
||||||
|
}
|
||||||
|
|
||||||
handleInputChange = e => {
|
handleInputChange = e => {
|
||||||
this.setState({ dirtyDescription: e.target.value });
|
this.setState({ dirtyDescription: e.target.value });
|
||||||
}
|
}
|
||||||
@ -63,13 +66,20 @@ export default class Upload extends ImmutablePureComponent {
|
|||||||
const { intl, media } = this.props;
|
const { intl, media } = this.props;
|
||||||
const active = this.state.hovered || this.state.focused;
|
const active = this.state.hovered || this.state.focused;
|
||||||
const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || '';
|
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 (
|
return (
|
||||||
<div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
<div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||||
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
||||||
{({ scale }) => (
|
{({ scale }) => (
|
||||||
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})` }}>
|
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
||||||
<IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.handleUndoClick} />
|
<div className={classNames('compose-form__upload__actions', { active })}>
|
||||||
|
<button className='icon-button' onClick={this.handleUndoClick}><i className='fa fa-times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Undo' /></button>
|
||||||
|
{media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><i className='fa fa-crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={classNames('compose-form__upload-description', { active })}>
|
<div className={classNames('compose-form__upload-description', { active })}>
|
||||||
<label>
|
<label>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Upload from '../components/upload';
|
import Upload from '../components/upload';
|
||||||
import { undoUploadCompose, changeUploadCompose } from '../../../actions/compose';
|
import { undoUploadCompose, changeUploadCompose } from '../../../actions/compose';
|
||||||
|
import { openModal } from '../../../actions/modal';
|
||||||
|
|
||||||
const mapStateToProps = (state, { id }) => ({
|
const mapStateToProps = (state, { id }) => ({
|
||||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
||||||
@ -13,7 +14,11 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onDescriptionChange: (id, description) => {
|
onDescriptionChange: (id, description) => {
|
||||||
dispatch(changeUploadCompose(id, description));
|
dispatch(changeUploadCompose(id, { description }));
|
||||||
|
},
|
||||||
|
|
||||||
|
onOpenFocalPoint: id => {
|
||||||
|
dispatch(openModal('FOCAL_POINT', { id }));
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,122 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ImageLoader from './image_loader';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { changeUploadCompose } from '../../../actions/compose';
|
||||||
|
import { getPointerPosition } from '../../video';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { id }) => ({
|
||||||
|
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||||
|
|
||||||
|
onSave: (x, y) => {
|
||||||
|
dispatch(changeUploadCompose(id, { focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
@connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
export default class FocalPointModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
media: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
focusX: 0,
|
||||||
|
focusY: 0,
|
||||||
|
dragging: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
this.updatePositionFromMedia(this.props.media);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (this.props.media.get('id') !== nextProps.media.get('id')) {
|
||||||
|
this.updatePositionFromMedia(nextProps.media);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
document.removeEventListener('mousemove', this.handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseDown = e => {
|
||||||
|
document.addEventListener('mousemove', this.handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', this.handleMouseUp);
|
||||||
|
|
||||||
|
this.updatePosition(e);
|
||||||
|
this.setState({ dragging: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseMove = e => {
|
||||||
|
this.updatePosition(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp = () => {
|
||||||
|
document.removeEventListener('mousemove', this.handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||||
|
|
||||||
|
this.setState({ dragging: false });
|
||||||
|
this.props.onSave(this.state.focusX, this.state.focusY);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePosition = e => {
|
||||||
|
const { x, y } = getPointerPosition(this.node, e);
|
||||||
|
const focusX = (x - .5) * 2;
|
||||||
|
const focusY = (y - .5) * -2;
|
||||||
|
|
||||||
|
this.setState({ x, y, focusX, focusY });
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePositionFromMedia = media => {
|
||||||
|
const focusX = media.getIn(['meta', 'focus', 'x']);
|
||||||
|
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||||
|
|
||||||
|
if (focusX && focusY) {
|
||||||
|
const x = (focusX / 2) + .5;
|
||||||
|
const y = (focusY / -2) + .5;
|
||||||
|
|
||||||
|
this.setState({ x, y, focusX, focusY });
|
||||||
|
} else {
|
||||||
|
this.setState({ x: 0.5, y: 0.5, focusX: 0, focusY: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.node = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { media } = this.props;
|
||||||
|
const { x, y, dragging } = this.state;
|
||||||
|
|
||||||
|
const width = media.getIn(['meta', 'original', 'width']) || null;
|
||||||
|
const height = media.getIn(['meta', 'original', 'height']) || null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal media-modal'>
|
||||||
|
<div className={classNames('media-modal__content focal-point', { dragging })} ref={this.setRef}>
|
||||||
|
<ImageLoader
|
||||||
|
previewSrc={media.get('preview_url')}
|
||||||
|
src={media.get('url')}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} />
|
||||||
|
<div className='focal-point__overlay' onMouseDown={this.handleMouseDown} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,6 +8,7 @@ import MediaModal from './media_modal';
|
|||||||
import VideoModal from './video_modal';
|
import VideoModal from './video_modal';
|
||||||
import BoostModal from './boost_modal';
|
import BoostModal from './boost_modal';
|
||||||
import ConfirmationModal from './confirmation_modal';
|
import ConfirmationModal from './confirmation_modal';
|
||||||
|
import FocalPointModal from './focal_point_modal';
|
||||||
import {
|
import {
|
||||||
OnboardingModal,
|
OnboardingModal,
|
||||||
MuteModal,
|
MuteModal,
|
||||||
@ -27,6 +28,7 @@ const MODAL_COMPONENTS = {
|
|||||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||||
'EMBED': EmbedModal,
|
'EMBED': EmbedModal,
|
||||||
'LIST_EDITOR': ListEditor,
|
'LIST_EDITOR': ListEditor,
|
||||||
|
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ModalRoot extends React.PureComponent {
|
export default class ModalRoot extends React.PureComponent {
|
||||||
|
@ -30,7 +30,7 @@ const formatTime = secondsNum => {
|
|||||||
return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`;
|
return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const findElementPosition = el => {
|
export const findElementPosition = el => {
|
||||||
let box;
|
let box;
|
||||||
|
|
||||||
if (el.getBoundingClientRect && el.parentNode) {
|
if (el.getBoundingClientRect && el.parentNode) {
|
||||||
@ -61,7 +61,7 @@ const findElementPosition = el => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPointerPosition = (el, event) => {
|
export const getPointerPosition = (el, event) => {
|
||||||
const position = {};
|
const position = {};
|
||||||
const box = findElementPosition(el);
|
const box = findElementPosition(el);
|
||||||
const boxW = el.offsetWidth;
|
const boxW = el.offsetWidth;
|
||||||
@ -77,7 +77,7 @@ const getPointerPosition = (el, event) => {
|
|||||||
pageY = event.changedTouches[0].pageY;
|
pageY = event.changedTouches[0].pageY;
|
||||||
}
|
}
|
||||||
|
|
||||||
position.y = Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH));
|
position.y = Math.max(0, Math.min(1, (pageY - boxY) / boxH));
|
||||||
position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
|
position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
|
||||||
|
|
||||||
return position;
|
return position;
|
||||||
|
@ -34,7 +34,7 @@ import uuid from '../uuid';
|
|||||||
import { me } from '../initial_state';
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
mounted: false,
|
mounted: 0,
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
spoiler: false,
|
spoiler: false,
|
||||||
spoiler_text: '',
|
spoiler_text: '',
|
||||||
@ -159,10 +159,10 @@ export default function compose(state = initialState, action) {
|
|||||||
case STORE_HYDRATE:
|
case STORE_HYDRATE:
|
||||||
return hydrate(state, action.state.get('compose'));
|
return hydrate(state, action.state.get('compose'));
|
||||||
case COMPOSE_MOUNT:
|
case COMPOSE_MOUNT:
|
||||||
return state.set('mounted', true);
|
return state.set('mounted', state.get('mounted') + 1);
|
||||||
case COMPOSE_UNMOUNT:
|
case COMPOSE_UNMOUNT:
|
||||||
return state
|
return state
|
||||||
.set('mounted', false)
|
.set('mounted', Math.max(state.get('mounted') - 1, 0))
|
||||||
.set('is_composing', false);
|
.set('is_composing', false);
|
||||||
case COMPOSE_SENSITIVITY_CHANGE:
|
case COMPOSE_SENSITIVITY_CHANGE:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
@ -265,7 +265,7 @@ export default function compose(state = initialState, action) {
|
|||||||
.set('is_submitting', false)
|
.set('is_submitting', false)
|
||||||
.update('media_attachments', list => list.map(item => {
|
.update('media_attachments', list => list.map(item => {
|
||||||
if (item.get('id') === action.media.id) {
|
if (item.get('id') === action.media.id) {
|
||||||
return item.set('description', action.media.description);
|
return fromJS(action.media);
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
|
@ -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 {
|
.landing-page {
|
||||||
p,
|
p,
|
||||||
li {
|
li {
|
||||||
@ -116,10 +243,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
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%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
@ -152,24 +283,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mascot-container {
|
.brand {
|
||||||
max-width: 800px;
|
a {
|
||||||
margin: 0 auto;
|
padding-left: 0;
|
||||||
position: absolute;
|
padding-right: 0;
|
||||||
top: 0;
|
color: $white;
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mascot {
|
img {
|
||||||
position: absolute;
|
height: 32px;
|
||||||
bottom: -14px;
|
position: relative;
|
||||||
width: auto;
|
top: 4px;
|
||||||
height: auto;
|
left: -10px;
|
||||||
left: 60px;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,7 +304,7 @@
|
|||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.container {
|
.container-alt {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
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 {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -243,53 +355,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 447.1875 234.375" height="170" width="324"><path fill="#{hex-color($ui-base-lighter-color)}" d="M21.69 233.366c-6.45-1.268-13.347-5.63-16.704-10.564-10.705-15.734-1.513-37.724 18.632-44.57l4.8-1.632.173-17.753c.146-14.77.515-19.063 2.2-25.55 6.736-25.944 24.46-46.032 47.766-54.137 11.913-4.143 19.558-5.366 34.178-5.47l13.828-.096V71.12c0-4.755 2.853-17.457 5.238-23.327 8.588-21.137 26.735-35.957 52.153-42.593 23.248-6.07 50.153-6.415 71.863-.923 11.14 2.82 25.686 9.957 33.857 16.615 19.335 15.756 31.82 41.05 35.183 71.275.59 5.305.672 5.435 3.11 4.926 11.833-2.474 30.4-3.132 40.065-1.42 24.388 4.32 40.568 19.076 47.214 43.058 2.16 7.8 3.953 23.894 3.59 32.237l-.24 5.498 5.156 1.317c6.392 1.633 14.55 7.098 18.003 12.062 1.435 2.062 3.305 6.597 4.156 10.078 1.428 5.84 1.43 6.8.04 12.44-1.807 7.318-5.672 13.252-10.872 16.694-8.508 5.63 3.756 5.33-211.916 5.216-108.56-.056-199.22-.464-201.47-.906z"/></svg>');
|
|
||||||
}
|
|
||||||
|
|
||||||
.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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 536.25 222.1875" height="100" width="241"><path fill="#{hex-color($ui-base-lighter-color)}" d="M42.626 221.23c-14.104-1.174-26.442-5.133-32.825-10.534-4.194-3.548-7.684-10.66-8.868-18.075-1.934-12.102.633-22.265 7.528-29.81 7.61-8.328 19.998-12.76 39.855-14.257l8.47-.638-2.08-6.223c-4.826-14.422-6.357-24.813-6.37-43.255-.012-14.923.28-18.513 2.1-25.724 2.283-9.048 8.483-23.034 13.345-30.1 14.76-21.45 43.505-38.425 70.535-41.65 30.628-3.655 64.47 12.073 89.668 41.673l5.955 6.995 2.765-4.174c1.52-2.296 5.74-6.93 9.376-10.295 18.382-17.02 43.436-20.676 73.352-10.705 12.158 4.052 21.315 9.53 29.64 17.733 12.752 12.562 18.16 25.718 18.19 44.26l.02 10.98 2.312-3.01c15.64-20.365 42.29-20.485 62.438-.28 3.644 3.653 7.558 8.593 8.697 10.976 4.895 10.24 5.932 25.688 2.486 37.046-.76 2.507-1.388 4.816-1.393 5.13-.006.316 6.845.87 15.224 1.234 53.06 2.297 76.356 12.98 81.817 37.526 3.554 15.973-3.71 28.604-19.566 34.02-4.554 1.555-17.922 1.655-234.517 1.757-126.327.06-233.497-.21-238.154-.597z"/></svg>');
|
|
||||||
}
|
|
||||||
|
|
||||||
.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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 388.125 202.5" height="140" width="267"><path fill="#{hex-color($ui-base-lighter-color)}" d="M181.37 201.458c-17.184-1.81-36.762-8.944-49.523-18.05l-5.774-4.12-8.074 2.63c-11.468 3.738-21.382 4.962-35.815 4.422-14.79-.554-24.577-2.845-36.716-8.594-15.483-7.332-28.498-19.98-35.985-34.968C2.44 128.675-.94 108.435.9 91.356c3.362-31.234 18.197-53.698 43.63-66.074 12.803-6.23 22.384-8.55 37.655-9.122 14.433-.54 24.347.684 35.814 4.42l8.073 2.633 5.635-4.01c24.81-17.656 60.007-23.332 92.914-14.985 10.11 2.565 25.498 9.62 33.102 15.178l5.068 3.704 7.632-2.564c10.89-3.66 21.086-4.916 35.516-4.376 45.816 1.716 76.422 30.03 81.285 75.196 1.84 17.08-1.54 37.32-8.585 51.422-7.487 14.99-20.502 27.636-35.984 34.968-12.14 5.75-21.926 8.04-36.716 8.593-14.43.54-24.626-.716-35.516-4.376l-7.632-2.564-5.068 3.704c-12.844 9.387-32.714 16.488-51.545 18.42-10.607 1.09-13.916 1.08-24.81-.066z"/></svg>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
@ -346,18 +411,18 @@
|
|||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
|
|
||||||
.container {
|
.container-alt {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-right: 280px + 15px;
|
padding-right: 280px + 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.information-board-sections {
|
&__sections {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
&__section {
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
font-family: 'mastodon-font-sans-serif', sans-serif;
|
font-family: 'mastodon-font-sans-serif', sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@ -382,6 +447,10 @@
|
|||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
line-height: 48px;
|
line-height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $column-breakpoint) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
@ -460,111 +529,282 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.features {
|
&.alternative {
|
||||||
padding: 50px 0;
|
padding: 10px 0;
|
||||||
|
|
||||||
.container {
|
.brand {
|
||||||
display: flex;
|
text-align: center;
|
||||||
|
padding: 30px 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $small-breakpoint) {
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $column-breakpoint) {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: -10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__information,
|
||||||
|
&__forms {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__call-to-action {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: darken($ui-base-color, 4%);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 25px 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#mastodon-timeline {
|
.information-board__section {
|
||||||
display: flex;
|
padding: 0;
|
||||||
-webkit-overflow-scrolling: touch;
|
}
|
||||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
}
|
||||||
font-family: 'mastodon-font-sans-serif', sans-serif;
|
|
||||||
font-size: 13px;
|
&__logo {
|
||||||
line-height: 18px;
|
margin-right: 20px;
|
||||||
font-weight: 400;
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p a {
|
||||||
|
color: $ui-secondary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 500;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
width: 330px;
|
margin-bottom: 0;
|
||||||
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 {
|
small {
|
||||||
color: inherit;
|
color: $ui-primary-color;
|
||||||
font-family: inherit;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column {
|
span {
|
||||||
padding: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $ui-secondary-color;
|
color: $ui-secondary-color;
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-mastodon {
|
p:last-child {
|
||||||
max-width: 675px;
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
&__hero {
|
||||||
margin-bottom: 20px;
|
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;
|
||||||
|
line-height: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
margin: 0;
|
||||||
|
padding: 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 {
|
a {
|
||||||
margin-top: 20px;
|
color: $ui-secondary-color;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.features-list__row {
|
@media screen and (max-width: $column-breakpoint) {
|
||||||
display: flex;
|
height: 90vh;
|
||||||
padding: 10px 0;
|
}
|
||||||
justify-content: space-between;
|
}
|
||||||
|
|
||||||
&:first-child {
|
&__features {
|
||||||
padding-top: 0;
|
.features-list {
|
||||||
}
|
margin: 40px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.visual {
|
&__action {
|
||||||
flex: 0 0 auto;
|
text-align: center;
|
||||||
display: flex;
|
}
|
||||||
align-items: center;
|
}
|
||||||
margin-left: 15px;
|
|
||||||
|
|
||||||
.fa {
|
.features-list {
|
||||||
display: block;
|
margin-top: 20px;
|
||||||
color: $ui-primary-color;
|
|
||||||
font-size: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
.features-list__row {
|
||||||
font-size: 16px;
|
display: flex;
|
||||||
line-height: 30px;
|
padding: 10px 0;
|
||||||
color: $ui-primary-color;
|
justify-content: space-between;
|
||||||
|
|
||||||
h6 {
|
&:first-child {
|
||||||
font-size: inherit;
|
padding-top: 0;
|
||||||
line-height: inherit;
|
}
|
||||||
margin-bottom: 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -600,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) {
|
@media screen and (max-width: 840px) {
|
||||||
.container {
|
.container-alt {
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.information-board {
|
.information-board {
|
||||||
|
.container-alt {
|
||||||
.container {
|
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
position: static;
|
position: static;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@ -626,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) {
|
@media screen and (max-width: 675px) {
|
||||||
@ -651,13 +891,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header .container,
|
.header .container-alt,
|
||||||
.features .container {
|
.features .container-alt {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
|
||||||
.links {
|
.links {
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
@ -682,10 +921,6 @@
|
|||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
.floats {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
padding: 30px 20px;
|
padding: 30px 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -700,16 +935,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.features #mastodon-timeline {
|
|
||||||
height: 70vh;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 50px;
|
|
||||||
|
|
||||||
.column {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta {
|
.cta {
|
||||||
@ -720,7 +945,7 @@
|
|||||||
.features {
|
.features {
|
||||||
padding: 30px 0;
|
padding: 30px 0;
|
||||||
|
|
||||||
.container {
|
.container-alt {
|
||||||
max-width: 820px;
|
max-width: 820px;
|
||||||
|
|
||||||
#mastodon-timeline {
|
#mastodon-timeline {
|
||||||
@ -772,7 +997,7 @@
|
|||||||
.features {
|
.features {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
|
|
||||||
.container {
|
.container-alt {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
@ -808,17 +1033,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes floating {
|
|
||||||
from {
|
|
||||||
transform: translate(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
65% {
|
|
||||||
transform: translate(0, 4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: translate(0, -0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -40,14 +40,20 @@
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.button-alternative {
|
&.button-primary,
|
||||||
|
&.button-alternative,
|
||||||
|
&.button-secondary,
|
||||||
|
&.button-alternative-2 {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 36px;
|
line-height: 36px;
|
||||||
height: auto;
|
height: auto;
|
||||||
color: $ui-base-color;
|
|
||||||
background: $ui-primary-color;
|
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
padding: 4px 16px;
|
padding: 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.button-alternative {
|
||||||
|
color: $ui-base-color;
|
||||||
|
background: $ui-primary-color;
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
@ -56,15 +62,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.button-alternative-2 {
|
||||||
|
background: $ui-base-lighter-color;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
background-color: lighten($ui-base-lighter-color, 4%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.button-secondary {
|
&.button-secondary {
|
||||||
font-size: 16px;
|
|
||||||
line-height: 36px;
|
|
||||||
height: auto;
|
|
||||||
color: $ui-primary-color;
|
color: $ui-primary-color;
|
||||||
text-transform: none;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 3px 15px;
|
padding: 3px 15px;
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid $ui-primary-color;
|
border: 1px solid $ui-primary-color;
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
@ -433,6 +444,34 @@
|
|||||||
min-width: 40%;
|
min-width: 40%;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
background: linear-gradient(180deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .1s ease;
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
color: $ui-secondary-color;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 10px;
|
||||||
|
font-family: inherit;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
color: lighten($ui-secondary-color, 4%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-description {
|
&-description {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@ -470,10 +509,6 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-button {
|
|
||||||
mix-blend-mode: difference;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.compose-form__upload-thumbnail {
|
.compose-form__upload-thumbnail {
|
||||||
@ -481,8 +516,9 @@
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
height: 100px;
|
height: 140px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4133,8 +4169,12 @@ a.status-card {
|
|||||||
&,
|
&,
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: relative;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4842,3 +4882,31 @@ noscript {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.focal-point {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.dragging {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__reticle {
|
||||||
|
position: absolute;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: url('../images/reticle.png') no-repeat 0 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 0 9999em rgba($base-shadow-color, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__overlay {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.container {
|
.container-alt {
|
||||||
width: 700px;
|
width: 700px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
|
@ -116,7 +116,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank?
|
next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank?
|
||||||
|
|
||||||
href = Addressable::URI.parse(attachment['url']).normalize.to_s
|
href = Addressable::URI.parse(attachment['url']).normalize.to_s
|
||||||
media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence)
|
media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence, focus: attachment['focalPoint'])
|
||||||
media_attachments << media_attachment
|
media_attachments << media_attachment
|
||||||
|
|
||||||
next if skip_download?
|
next if skip_download?
|
||||||
|
@ -17,6 +17,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
|||||||
'conversation' => 'ostatus:conversation',
|
'conversation' => 'ostatus:conversation',
|
||||||
'toot' => 'http://joinmastodon.org/ns#',
|
'toot' => 'http://joinmastodon.org/ns#',
|
||||||
'Emoji' => 'toot:Emoji',
|
'Emoji' => 'toot:Emoji',
|
||||||
|
'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}.freeze
|
}.freeze
|
||||||
|
11
app/lib/fast_geometry_parser.rb
Normal file
11
app/lib/fast_geometry_parser.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class FastGeometryParser
|
||||||
|
def self.from_file(file)
|
||||||
|
width, height = FastImage.size(file.path)
|
||||||
|
|
||||||
|
raise Paperclip::Errors::NotIdentifiedByImageMagickError if width.nil?
|
||||||
|
|
||||||
|
Paperclip::Geometry.new(width, height)
|
||||||
|
end
|
||||||
|
end
|
@ -66,4 +66,16 @@ class UserMailer < Devise::Mailer
|
|||||||
mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject')
|
mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def backup_ready(user, backup)
|
||||||
|
@resource = user
|
||||||
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
@backup = backup
|
||||||
|
|
||||||
|
return if @resource.disabled?
|
||||||
|
|
||||||
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
|
mail to: @resource.email, subject: I18n.t('user_mailer.backup_ready.subject')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
22
app/models/backup.rb
Normal file
22
app/models/backup.rb
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: backups
|
||||||
|
#
|
||||||
|
# id :integer not null, primary key
|
||||||
|
# user_id :integer
|
||||||
|
# dump_file_name :string
|
||||||
|
# dump_content_type :string
|
||||||
|
# dump_file_size :integer
|
||||||
|
# dump_updated_at :datetime
|
||||||
|
# processed :boolean default(FALSE), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class Backup < ApplicationRecord
|
||||||
|
belongs_to :user, inverse_of: :backups
|
||||||
|
|
||||||
|
has_attached_file :dump
|
||||||
|
do_not_validate_attachment_file_type :dump
|
||||||
|
end
|
@ -7,15 +7,9 @@ module AccountAvatar
|
|||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def avatar_styles(file)
|
def avatar_styles(file)
|
||||||
styles = {}
|
styles = { original: { geometry: '120x120#', file_geometry_parser: FastGeometryParser } }
|
||||||
geometry = Paperclip::Geometry.from_file(file)
|
styles[:static] = { geometry: '120x120#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
|
||||||
|
|
||||||
styles[:original] = '120x120#' if geometry.width != geometry.height || geometry.width > 120 || geometry.height > 120
|
|
||||||
styles[:static] = { format: 'png', convert_options: '-coalesce' } if file.content_type == 'image/gif'
|
|
||||||
|
|
||||||
styles
|
styles
|
||||||
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
|
|
||||||
{}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private :avatar_styles
|
private :avatar_styles
|
||||||
@ -23,7 +17,7 @@ module AccountAvatar
|
|||||||
|
|
||||||
included do
|
included do
|
||||||
# Avatar upload
|
# Avatar upload
|
||||||
has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-strip' }
|
has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-strip' }, processors: [:lazy_thumbnail]
|
||||||
validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES
|
validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES
|
||||||
validates_attachment_size :avatar, less_than: 2.megabytes
|
validates_attachment_size :avatar, less_than: 2.megabytes
|
||||||
end
|
end
|
||||||
|
@ -7,15 +7,9 @@ module AccountHeader
|
|||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def header_styles(file)
|
def header_styles(file)
|
||||||
styles = {}
|
styles = { original: { geometry: '700x335#', file_geometry_parser: FastGeometryParser } }
|
||||||
geometry = Paperclip::Geometry.from_file(file)
|
styles[:static] = { geometry: '700x335#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
|
||||||
|
|
||||||
styles[:original] = '700x335#' unless geometry.width == 700 && geometry.height == 335
|
|
||||||
styles[:static] = { format: 'png', convert_options: '-coalesce' } if file.content_type == 'image/gif'
|
|
||||||
|
|
||||||
styles
|
styles
|
||||||
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
|
|
||||||
{}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private :header_styles
|
private :header_styles
|
||||||
@ -23,7 +17,7 @@ module AccountHeader
|
|||||||
|
|
||||||
included do
|
included do
|
||||||
# Header upload
|
# Header upload
|
||||||
has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-strip' }
|
has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-strip' }, processors: [:lazy_thumbnail]
|
||||||
validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
|
validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
|
||||||
validates_attachment_size :header, less_than: 2.megabytes
|
validates_attachment_size :header, less_than: 2.megabytes
|
||||||
end
|
end
|
||||||
|
@ -53,8 +53,11 @@ module Omniauthable
|
|||||||
private
|
private
|
||||||
|
|
||||||
def user_params_from_auth(auth)
|
def user_params_from_auth(auth)
|
||||||
email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email)
|
strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy
|
||||||
email = auth.info.email if email_is_verified && !User.exists?(email: auth.info.email)
|
assume_verified = strategy.try(:security).try(:assume_email_is_verified)
|
||||||
|
email_is_verified = auth.info.verified || auth.info.verified_email || assume_verified
|
||||||
|
email = auth.info.verified_email || auth.info.email
|
||||||
|
email = email_is_verified && !User.exists?(email: auth.info.email) && email
|
||||||
|
|
||||||
{
|
{
|
||||||
email: email ? email : "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com",
|
email: email ? email : "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com",
|
||||||
|
@ -34,7 +34,18 @@ class MediaAttachment < ApplicationRecord
|
|||||||
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze
|
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze
|
||||||
AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze
|
AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze
|
||||||
|
|
||||||
IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze
|
IMAGE_STYLES = {
|
||||||
|
original: {
|
||||||
|
geometry: '1280x1280>',
|
||||||
|
file_geometry_parser: FastGeometryParser,
|
||||||
|
},
|
||||||
|
|
||||||
|
small: {
|
||||||
|
geometry: '400x400>',
|
||||||
|
file_geometry_parser: FastGeometryParser,
|
||||||
|
},
|
||||||
|
}.freeze
|
||||||
|
|
||||||
AUDIO_STYLES = {
|
AUDIO_STYLES = {
|
||||||
original: {
|
original: {
|
||||||
format: 'mp4',
|
format: 'mp4',
|
||||||
@ -50,6 +61,7 @@ class MediaAttachment < ApplicationRecord
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
VIDEO_STYLES = {
|
VIDEO_STYLES = {
|
||||||
small: {
|
small: {
|
||||||
convert_options: {
|
convert_options: {
|
||||||
@ -97,6 +109,24 @@ class MediaAttachment < ApplicationRecord
|
|||||||
shortcode
|
shortcode
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def focus=(point)
|
||||||
|
return if point.blank?
|
||||||
|
|
||||||
|
x, y = (point.is_a?(Enumerable) ? point : point.split(',')).map(&:to_f)
|
||||||
|
|
||||||
|
meta = file.instance_read(:meta) || {}
|
||||||
|
meta['focus'] = { 'x' => x, 'y' => y }
|
||||||
|
|
||||||
|
file.instance_write(:meta, meta)
|
||||||
|
end
|
||||||
|
|
||||||
|
def focus
|
||||||
|
x = file.meta['focus']['x']
|
||||||
|
y = file.meta['focus']['y']
|
||||||
|
|
||||||
|
"#{x},#{y}"
|
||||||
|
end
|
||||||
|
|
||||||
before_create :prepare_description, unless: :local?
|
before_create :prepare_description, unless: :local?
|
||||||
before_create :set_shortcode
|
before_create :set_shortcode
|
||||||
before_post_process :set_type_and_extension
|
before_post_process :set_type_and_extension
|
||||||
@ -178,7 +208,7 @@ class MediaAttachment < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def populate_meta
|
def populate_meta
|
||||||
meta = {}
|
meta = file.instance_read(:meta) || {}
|
||||||
|
|
||||||
file.queued_for_write.each do |style, file|
|
file.queued_for_write.each do |style, file|
|
||||||
meta[style] = style == :small || image? ? image_geometry(file) : video_metadata(file)
|
meta[style] = style == :small || image? ? image_geometry(file) : video_metadata(file)
|
||||||
@ -188,16 +218,16 @@ class MediaAttachment < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def image_geometry(file)
|
def image_geometry(file)
|
||||||
geo = Paperclip::Geometry.from_file file
|
width, height = FastImage.size(file.path)
|
||||||
|
|
||||||
|
return {} if width.nil?
|
||||||
|
|
||||||
{
|
{
|
||||||
width: geo.width.to_i,
|
width: width,
|
||||||
height: geo.height.to_i,
|
height: height,
|
||||||
size: "#{geo.width.to_i}x#{geo.height.to_i}",
|
size: "#{width}x#{height}",
|
||||||
aspect: geo.width.to_f / geo.height.to_f,
|
aspect: width.to_f / height.to_f,
|
||||||
}
|
}
|
||||||
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
|
|
||||||
{}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def video_metadata(file)
|
def video_metadata(file)
|
||||||
|
@ -33,7 +33,7 @@ class PreviewCard < ApplicationRecord
|
|||||||
|
|
||||||
has_and_belongs_to_many :statuses
|
has_and_belongs_to_many :statuses
|
||||||
|
|
||||||
has_attached_file :image, styles: { original: '400x400>' }, convert_options: { all: '-quality 80 -strip' }
|
has_attached_file :image, styles: { original: { geometry: '400x400>', file_geometry_parser: FastGeometryParser } }, convert_options: { all: '-quality 80 -strip' }
|
||||||
|
|
||||||
include Attachmentable
|
include Attachmentable
|
||||||
include Remotable
|
include Remotable
|
||||||
@ -58,10 +58,11 @@ class PreviewCard < ApplicationRecord
|
|||||||
|
|
||||||
return if file.nil?
|
return if file.nil?
|
||||||
|
|
||||||
geo = Paperclip::Geometry.from_file(file)
|
width, height = FastImage.size(file.path)
|
||||||
self.width = geo.width.to_i
|
|
||||||
self.height = geo.height.to_i
|
return nil if width.nil?
|
||||||
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
|
|
||||||
nil
|
self.width = width
|
||||||
|
self.height = height
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -34,8 +34,8 @@ class SiteUpload < ApplicationRecord
|
|||||||
|
|
||||||
return if tempfile.nil?
|
return if tempfile.nil?
|
||||||
|
|
||||||
geometry = Paperclip::Geometry.from_file(tempfile)
|
width, height = FastImage.size(tempfile.path)
|
||||||
self.meta = { width: geometry.width.to_i, height: geometry.height.to_i }
|
self.meta = { width: width, height: height }
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_cache
|
def clear_cache
|
||||||
|
@ -79,7 +79,7 @@ class Status < ApplicationRecord
|
|||||||
|
|
||||||
scope :not_local_only, -> { where(local_only: [false, nil]) }
|
scope :not_local_only, -> { where(local_only: [false, nil]) }
|
||||||
|
|
||||||
cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account
|
cache_associated :account, :application, :media_attachments, :conversation, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, :conversation, mentions: :account], thread: :account
|
||||||
|
|
||||||
delegate :domain, to: :account, prefix: true
|
delegate :domain, to: :account, prefix: true
|
||||||
|
|
||||||
|
@ -60,6 +60,7 @@ class User < ApplicationRecord
|
|||||||
accepts_nested_attributes_for :account
|
accepts_nested_attributes_for :account
|
||||||
|
|
||||||
has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
|
has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
|
||||||
|
has_many :backups, inverse_of: :user
|
||||||
|
|
||||||
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
|
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
|
||||||
validates_with BlacklistedEmailValidator, if: :email_changed?
|
validates_with BlacklistedEmailValidator, if: :email_changed?
|
||||||
|
@ -15,4 +15,8 @@ class ApplicationPolicy
|
|||||||
def current_user
|
def current_user
|
||||||
current_account&.user
|
current_account&.user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def user_signed_in?
|
||||||
|
!current_user.nil?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
9
app/policies/backup_policy.rb
Normal file
9
app/policies/backup_policy.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class BackupPolicy < ApplicationPolicy
|
||||||
|
MIN_AGE = 1.week
|
||||||
|
|
||||||
|
def create?
|
||||||
|
user_signed_in? && current_user.backups.where('created_at >= ?', MIN_AGE.ago).count.zero?
|
||||||
|
end
|
||||||
|
end
|
@ -45,7 +45,7 @@ class AccountRelationshipsPresenter
|
|||||||
maps_for_account = Rails.cache.read("relationship:#{@current_account_id}:#{account_id}")
|
maps_for_account = Rails.cache.read("relationship:#{@current_account_id}:#{account_id}")
|
||||||
|
|
||||||
if maps_for_account.is_a?(Hash)
|
if maps_for_account.is_a?(Hash)
|
||||||
@cached.merge!(maps_for_account)
|
@cached.deep_merge!(maps_for_account)
|
||||||
else
|
else
|
||||||
@uncached_account_ids << account_id
|
@uncached_account_ids << account_id
|
||||||
end
|
end
|
||||||
|
@ -48,4 +48,8 @@ class InstancePresenter
|
|||||||
def thumbnail
|
def thumbnail
|
||||||
@thumbnail ||= Rails.cache.fetch('site_uploads/thumbnail') { SiteUpload.find_by(var: 'thumbnail') }
|
@thumbnail ||= Rails.cache.fetch('site_uploads/thumbnail') { SiteUpload.find_by(var: 'thumbnail') }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hero
|
||||||
|
@hero ||= Rails.cache.fetch('site_uploads/hero') { SiteUpload.find_by(var: 'hero') }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -13,8 +13,8 @@ class ActivityPub::CollectionSerializer < ActiveModel::Serializer
|
|||||||
attribute :part_of, if: -> { object.part_of.present? }
|
attribute :part_of, if: -> { object.part_of.present? }
|
||||||
|
|
||||||
has_one :first, if: -> { object.first.present? }
|
has_one :first, if: -> { object.first.present? }
|
||||||
has_many :items, key: :items, if: -> { (object.items.present? || page?) && !ordered? }
|
has_many :items, key: :items, if: -> { (!object.items.nil? || page?) && !ordered? }
|
||||||
has_many :items, key: :ordered_items, if: -> { (object.items.present? || page?) && ordered? }
|
has_many :items, key: :ordered_items, if: -> { (!object.items.nil? || page?) && ordered? }
|
||||||
|
|
||||||
def type
|
def type
|
||||||
if page?
|
if page?
|
||||||
|
@ -4,6 +4,7 @@ class ActivityPub::ImageSerializer < ActiveModel::Serializer
|
|||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
attributes :type, :media_type, :url
|
attributes :type, :media_type, :url
|
||||||
|
attribute :focal_point, if: :focal_point?
|
||||||
|
|
||||||
def type
|
def type
|
||||||
'Image'
|
'Image'
|
||||||
@ -16,4 +17,12 @@ class ActivityPub::ImageSerializer < ActiveModel::Serializer
|
|||||||
def media_type
|
def media_type
|
||||||
object.content_type
|
object.content_type
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def focal_point?
|
||||||
|
object.respond_to?(:meta) && object.meta.is_a?(Hash) && object.meta['focus'].is_a?(Hash)
|
||||||
|
end
|
||||||
|
|
||||||
|
def focal_point
|
||||||
|
[object.meta['focus']['x'], object.meta['focus']['y']]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
128
app/services/backup_service.rb
Normal file
128
app/services/backup_service.rb
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rubygems/package'
|
||||||
|
|
||||||
|
class BackupService < BaseService
|
||||||
|
attr_reader :account, :backup, :collection
|
||||||
|
|
||||||
|
def call(backup)
|
||||||
|
@backup = backup
|
||||||
|
@account = backup.user.account
|
||||||
|
|
||||||
|
build_json!
|
||||||
|
build_archive!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_json!
|
||||||
|
@collection = serialize(collection_presenter, ActivityPub::CollectionSerializer)
|
||||||
|
|
||||||
|
account.statuses.with_includes.find_in_batches do |statuses|
|
||||||
|
statuses.each do |status|
|
||||||
|
item = serialize(status, ActivityPub::ActivitySerializer)
|
||||||
|
item.delete(:'@context')
|
||||||
|
|
||||||
|
unless item[:type] == 'Announce' || item[:object][:attachment].blank?
|
||||||
|
item[:object][:attachment].each do |attachment|
|
||||||
|
attachment[:url] = Addressable::URI.parse(attachment[:url]).path.gsub(/\A\/system\//, '')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@collection[:orderedItems] << item
|
||||||
|
end
|
||||||
|
|
||||||
|
GC.start
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_archive!
|
||||||
|
tmp_file = Tempfile.new(%w(archive .tar.gz))
|
||||||
|
|
||||||
|
File.open(tmp_file, 'wb') do |file|
|
||||||
|
Zlib::GzipWriter.wrap(file) do |gz|
|
||||||
|
Gem::Package::TarWriter.new(gz) do |tar|
|
||||||
|
dump_media_attachments!(tar)
|
||||||
|
dump_outbox!(tar)
|
||||||
|
dump_actor!(tar)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
archive_filename = ['archive', Time.now.utc.strftime('%Y%m%d%H%M%S'), SecureRandom.hex(2)].join('-') + '.tar.gz'
|
||||||
|
|
||||||
|
@backup.dump = ActionDispatch::Http::UploadedFile.new(tempfile: tmp_file, filename: archive_filename)
|
||||||
|
@backup.processed = true
|
||||||
|
@backup.save!
|
||||||
|
ensure
|
||||||
|
tmp_file.close
|
||||||
|
tmp_file.unlink
|
||||||
|
end
|
||||||
|
|
||||||
|
def dump_media_attachments!(tar)
|
||||||
|
MediaAttachment.attached.where(account: account).find_in_batches do |media_attachments|
|
||||||
|
media_attachments.each do |m|
|
||||||
|
download_to_tar(tar, m.file, m.file.path)
|
||||||
|
end
|
||||||
|
|
||||||
|
GC.start
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def dump_outbox!(tar)
|
||||||
|
json = Oj.dump(collection)
|
||||||
|
|
||||||
|
tar.add_file_simple('outbox.json', 0o444, json.bytesize) do |io|
|
||||||
|
io.write(json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def dump_actor!(tar)
|
||||||
|
actor = serialize(account, ActivityPub::ActorSerializer)
|
||||||
|
|
||||||
|
actor[:icon][:url] = 'avatar' + File.extname(actor[:icon][:url]) if actor[:icon]
|
||||||
|
actor[:image][:url] = 'header' + File.extname(actor[:image][:url]) if actor[:image]
|
||||||
|
|
||||||
|
download_to_tar(tar, account.avatar, 'avatar' + File.extname(account.avatar.path)) if account.avatar.exists?
|
||||||
|
download_to_tar(tar, account.header, 'header' + File.extname(account.header.path)) if account.header.exists?
|
||||||
|
|
||||||
|
json = Oj.dump(actor)
|
||||||
|
|
||||||
|
tar.add_file_simple('actor.json', 0o444, json.bytesize) do |io|
|
||||||
|
io.write(json)
|
||||||
|
end
|
||||||
|
|
||||||
|
tar.add_file_simple('key.pem', 0o444, account.private_key.bytesize) do |io|
|
||||||
|
io.write(account.private_key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def collection_presenter
|
||||||
|
ActivityPub::CollectionPresenter.new(
|
||||||
|
id: account_outbox_url(account),
|
||||||
|
type: :ordered,
|
||||||
|
size: account.statuses_count,
|
||||||
|
items: []
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize(object, serializer)
|
||||||
|
ActiveModelSerializers::SerializableResource.new(
|
||||||
|
object,
|
||||||
|
serializer: serializer,
|
||||||
|
adapter: ActivityPub::Adapter
|
||||||
|
).as_json
|
||||||
|
end
|
||||||
|
|
||||||
|
CHUNK_SIZE = 1.megabyte
|
||||||
|
|
||||||
|
def download_to_tar(tar, attachment, filename)
|
||||||
|
adapter = Paperclip.io_adapters.for(attachment)
|
||||||
|
|
||||||
|
tar.add_file_simple(filename, 0o444, adapter.size) do |io|
|
||||||
|
while (buffer = adapter.read(CHUNK_SIZE))
|
||||||
|
io.write(buffer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
app/views/about/_forms.html.haml
Normal file
14
app/views/about/_forms.html.haml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
- if @instance_presenter.open_registrations
|
||||||
|
= render 'registration'
|
||||||
|
- else
|
||||||
|
- if @instance_presenter.closed_registrations_message.blank?
|
||||||
|
%p= t('about.closed_registrations')
|
||||||
|
- else
|
||||||
|
= @instance_presenter.closed_registrations_message.html_safe
|
||||||
|
|
||||||
|
= link_to t('auth.register'), 'https://joinmastodon.org', class: 'button button-primary'
|
||||||
|
|
||||||
|
.separator-or
|
||||||
|
%span= t('auth.or')
|
||||||
|
|
||||||
|
= link_to t('auth.login'), new_user_session_path, class: 'button button-alternative-2 webapp-btn'
|
@ -1,4 +1,4 @@
|
|||||||
.container.links
|
.container-alt.links
|
||||||
.brand
|
.brand
|
||||||
= link_to root_url do
|
= link_to root_url do
|
||||||
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
|
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
|
||||||
|
@ -10,6 +10,6 @@
|
|||||||
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
|
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('auth.register'), type: :submit, class: 'button button-alternative'
|
= f.button :button, t('auth.register'), type: :submit, class: 'button button-primary'
|
||||||
|
|
||||||
%p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path)
|
%p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path)
|
||||||
|
@ -9,34 +9,34 @@
|
|||||||
.header
|
.header
|
||||||
= render 'links'
|
= render 'links'
|
||||||
|
|
||||||
.container.hero
|
.container-alt.hero
|
||||||
.heading
|
.heading
|
||||||
%h3= t('about.description_headline', domain: site_hostname)
|
%h3= t('about.description_headline', domain: site_hostname)
|
||||||
%p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
|
%p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
|
||||||
|
|
||||||
.information-board
|
.information-board
|
||||||
.container
|
.container-alt
|
||||||
.information-board-sections
|
.information-board__sections
|
||||||
.section
|
.information-board__section
|
||||||
%span= t 'about.user_count_before'
|
%span= t 'about.user_count_before'
|
||||||
%strong= number_with_delimiter @instance_presenter.user_count
|
%strong= number_with_delimiter @instance_presenter.user_count
|
||||||
%span= t 'about.user_count_after'
|
%span= t 'about.user_count_after'
|
||||||
.section
|
.information-board__section
|
||||||
%span= t 'about.status_count_before'
|
%span= t 'about.status_count_before'
|
||||||
%strong= number_with_delimiter @instance_presenter.status_count
|
%strong= number_with_delimiter @instance_presenter.status_count
|
||||||
%span= t 'about.status_count_after'
|
%span= t 'about.status_count_after'
|
||||||
.section
|
.information-board__section
|
||||||
%span= t 'about.domain_count_before'
|
%span= t 'about.domain_count_before'
|
||||||
%strong= number_with_delimiter @instance_presenter.domain_count
|
%strong= number_with_delimiter @instance_presenter.domain_count
|
||||||
%span= t 'about.domain_count_after'
|
%span= t 'about.domain_count_after'
|
||||||
= render 'contact', contact: @instance_presenter
|
= render 'contact', contact: @instance_presenter
|
||||||
|
|
||||||
.extended-description
|
.extended-description
|
||||||
.container
|
.container-alt
|
||||||
= @instance_presenter.site_extended_description.html_safe.presence || t('about.extended_description_html')
|
= @instance_presenter.site_extended_description.html_safe.presence || t('about.extended_description_html')
|
||||||
|
|
||||||
.footer-links
|
.footer-links
|
||||||
.container
|
.container-alt
|
||||||
%p
|
%p
|
||||||
= link_to t('about.source_code'), @instance_presenter.source_url
|
= link_to t('about.source_code'), @instance_presenter.source_url
|
||||||
- if @instance_presenter.commit_hash == ""
|
- if @instance_presenter.commit_hash == ""
|
||||||
|
@ -5,62 +5,74 @@
|
|||||||
%script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
|
%script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
|
||||||
= render partial: 'shared/og'
|
= render partial: 'shared/og'
|
||||||
|
|
||||||
.landing-page
|
.landing-page.alternative
|
||||||
.header-wrapper
|
.container
|
||||||
.mascot-container
|
.row
|
||||||
= image_tag asset_pack_path('elephant-fren.png'), alt: '', role: 'presentation', class: 'mascot'
|
.column-4.hide-sm.show-xs.show-m
|
||||||
|
.landing-page__forms
|
||||||
|
.brand
|
||||||
|
= link_to root_url do
|
||||||
|
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
|
||||||
|
|
||||||
.header
|
.hide-xs
|
||||||
= render 'links'
|
= render 'forms'
|
||||||
|
|
||||||
.container.hero
|
.column-7.column-9-sm
|
||||||
.floats
|
.landing-page__hero
|
||||||
%div{ role: 'presentation', class: 'float-1' }
|
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
|
||||||
%div{ role: 'presentation', class: 'float-2' }
|
|
||||||
%div{ role: 'presentation', class: 'float-3' }
|
|
||||||
.heading
|
|
||||||
%h1
|
|
||||||
= @instance_presenter.site_title
|
|
||||||
%small= t 'about.hosted_on', domain: site_hostname
|
|
||||||
- if @instance_presenter.open_registrations
|
|
||||||
= render 'registration'
|
|
||||||
- else
|
|
||||||
.closed-registrations-message
|
|
||||||
%div
|
|
||||||
- if @instance_presenter.closed_registrations_message.blank?
|
|
||||||
%p= t('about.closed_registrations')
|
|
||||||
- else
|
|
||||||
= @instance_presenter.closed_registrations_message.html_safe
|
|
||||||
|
|
||||||
= simple_form_for(:user, html: { style: 'margin-left: -20px' }, url: session_path(:user)) do |f|
|
.landing-page__information
|
||||||
= f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
|
.landing-page__short-description
|
||||||
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
|
.row
|
||||||
|
.landing-page__logo.hide-xs
|
||||||
|
= image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon'
|
||||||
|
|
||||||
.actions
|
%h1
|
||||||
= f.button :button, t('auth.login'), type: :submit
|
= @instance_presenter.site_title
|
||||||
= link_to t('about.find_another_instance'), 'https://joinmastodon.org/', class: 'button button-alternative button--block'
|
%small!= t 'about.hosted_on', domain: content_tag(:span, site_hostname)
|
||||||
|
|
||||||
.about-short
|
%p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
|
||||||
.container
|
|
||||||
%h3= t('about.description_headline', domain: site_hostname)
|
|
||||||
%p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
|
|
||||||
|
|
||||||
.features
|
.show-xs
|
||||||
.container
|
.landing-page__forms
|
||||||
- if Setting.timeline_preview
|
= render 'forms'
|
||||||
#mastodon-timeline{ data: { props: Oj.dump(default_props) } }
|
.landing-page__call-to-action.hide-xs
|
||||||
|
.row
|
||||||
|
.column-5
|
||||||
|
.landing-page__mascot
|
||||||
|
= image_tag asset_pack_path('elephant_ui_plane.svg')
|
||||||
|
.column-5
|
||||||
|
.information-board__section
|
||||||
|
%span= t 'about.user_count_before'
|
||||||
|
%strong= number_with_delimiter @instance_presenter.user_count
|
||||||
|
%span= t 'about.user_count_after'
|
||||||
|
.column-5
|
||||||
|
.information-board__section
|
||||||
|
%span= t 'about.status_count_before'
|
||||||
|
%strong= number_with_delimiter @instance_presenter.status_count
|
||||||
|
%span= t 'about.status_count_after'
|
||||||
|
.landing-page__information
|
||||||
|
.landing-page__features
|
||||||
|
%h3= t 'about.what_is_mastodon'
|
||||||
|
%p= t 'about.about_mastodon_html'
|
||||||
|
|
||||||
.about-mastodon
|
= render 'features'
|
||||||
%h3= t 'about.what_is_mastodon'
|
|
||||||
%p= t 'about.about_mastodon_html'
|
.landing-page__features__action
|
||||||
= link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-secondary'
|
= link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-alternative'
|
||||||
= render 'features'
|
|
||||||
.footer-links
|
.landing-page__footer
|
||||||
.container
|
%p
|
||||||
%p
|
= link_to t('about.source_code'), @instance_presenter.source_url
|
||||||
= link_to t('about.source_code'), @instance_presenter.source_url
|
= " (#{@instance_presenter.version_number})"
|
||||||
- if @instance_presenter.commit_hash == ""
|
|
||||||
%strong= " (#{@instance_presenter.version_number})"
|
.column-4.column-6-sm.column-flex
|
||||||
- else
|
.show-sm.hide-xs
|
||||||
%strong= " (#{@instance_presenter.version_number}, "
|
.landing-page__forms
|
||||||
%strong= " #{@instance_presenter.commit_hash})"
|
.brand
|
||||||
|
= link_to root_url do
|
||||||
|
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
|
||||||
|
|
||||||
|
= render 'forms'
|
||||||
|
- if Setting.timeline_preview
|
||||||
|
#mastodon-timeline{ data: { props: Oj.dump(default_props) } }
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
= render 'links'
|
= render 'links'
|
||||||
|
|
||||||
.extended-description
|
.extended-description
|
||||||
.container
|
.container-alt
|
||||||
= @instance_presenter.site_terms.html_safe.presence || t('terms.body_html')
|
= @instance_presenter.site_terms.html_safe.presence || t('terms.body_html')
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: t('admin.settings.thumbnail.desc_html')
|
= f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: t('admin.settings.thumbnail.desc_html')
|
||||||
|
= f.input :hero, as: :file, wrapper: :with_block_label, label: t('admin.settings.hero.title'), hint: t('admin.settings.hero.desc_html')
|
||||||
|
|
||||||
%hr/
|
%hr/
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
- content_for :content do
|
- content_for :content do
|
||||||
.container
|
.container-alt
|
||||||
.logo-container
|
.logo-container
|
||||||
%h1
|
%h1
|
||||||
= link_to root_path do
|
= link_to root_path do
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
= link_to destroy_user_session_path, method: :delete, class: 'logout-link icon-button' do
|
= link_to destroy_user_session_path, method: :delete, class: 'logout-link icon-button' do
|
||||||
= fa_icon 'sign-out'
|
= fa_icon 'sign-out'
|
||||||
|
|
||||||
.container= yield
|
.container-alt= yield
|
||||||
.modal-layout__mastodon
|
.modal-layout__mastodon
|
||||||
%div
|
%div
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
- content_for :content do
|
- content_for :content do
|
||||||
.container= yield
|
.container-alt= yield
|
||||||
.footer
|
.footer
|
||||||
- if !user_signed_in? && single_user_mode?
|
- if !user_signed_in? && single_user_mode?
|
||||||
%span.single-user-login
|
%span.single-user-login
|
||||||
|
@ -20,3 +20,26 @@
|
|||||||
%th= t('exports.mutes')
|
%th= t('exports.mutes')
|
||||||
%td= @export.total_mutes
|
%td= @export.total_mutes
|
||||||
%td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv)
|
%td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv)
|
||||||
|
|
||||||
|
%p.muted-hint= t('exports.archive_takeout.hint_html')
|
||||||
|
|
||||||
|
- if policy(:backup).create?
|
||||||
|
%p= link_to t('exports.archive_takeout.request'), settings_export_path, class: 'button', method: :post
|
||||||
|
|
||||||
|
- unless @backups.empty?
|
||||||
|
.table-wrapper
|
||||||
|
%table.table
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th= t('exports.archive_takeout.date')
|
||||||
|
%th= t('exports.archive_takeout.size')
|
||||||
|
%th
|
||||||
|
%tbody
|
||||||
|
- @backups.each do |backup|
|
||||||
|
%tr
|
||||||
|
%td= l backup.created_at
|
||||||
|
- if backup.processed?
|
||||||
|
%td= number_to_human_size backup.dump_file_size
|
||||||
|
%td= table_link_to 'download', t('exports.archive_takeout.download'), backup.dump.url
|
||||||
|
- else
|
||||||
|
%td{ colspan: 2 }= t('exports.archive_takeout.in_progress')
|
||||||
|
59
app/views/user_mailer/backup_ready.html.haml
Normal file
59
app/views/user_mailer/backup_ready.html.haml
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.hero
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center.padded
|
||||||
|
%table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
= image_tag full_pack_url('icon_file_download.png'), alt: ''
|
||||||
|
|
||||||
|
%h1= t 'user_mailer.backup_ready.title'
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.content-start
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center
|
||||||
|
%p= t 'user_mailer.backup_ready.explanation'
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.button-cell
|
||||||
|
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.button-primary
|
||||||
|
= link_to full_asset_url(@backup.dump.url) do
|
||||||
|
%span= t 'exports.archive_takeout.download'
|
7
app/views/user_mailer/backup_ready.text.erb
Normal file
7
app/views/user_mailer/backup_ready.text.erb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<%= t 'user_mailer.backup_ready.title' %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t 'user_mailer.backup_ready.explanation' %>
|
||||||
|
|
||||||
|
=> <%= full_asset_url(@backup.dump.url) %>
|
17
app/workers/backup_worker.rb
Normal file
17
app/workers/backup_worker.rb
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class BackupWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
sidekiq_options queue: 'pull'
|
||||||
|
|
||||||
|
def perform(backup_id)
|
||||||
|
backup = Backup.find(backup_id)
|
||||||
|
user = backup.user
|
||||||
|
|
||||||
|
BackupService.new.call(backup)
|
||||||
|
|
||||||
|
user.backups.where.not(id: backup.id).destroy_all
|
||||||
|
UserMailer.backup_ready(user, backup).deliver_later
|
||||||
|
end
|
||||||
|
end
|
16
app/workers/scheduler/backup_cleanup_scheduler.rb
Normal file
16
app/workers/scheduler/backup_cleanup_scheduler.rb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
require 'sidekiq-scheduler'
|
||||||
|
|
||||||
|
class Scheduler::BackupCleanupScheduler
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
def perform
|
||||||
|
old_backups.find_each(&:destroy!)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def old_backups
|
||||||
|
Backup.where('created_at < ?', 7.days.ago)
|
||||||
|
end
|
||||||
|
end
|
@ -7,6 +7,7 @@ require 'rails/all'
|
|||||||
Bundler.require(*Rails.groups)
|
Bundler.require(*Rails.groups)
|
||||||
|
|
||||||
require_relative '../app/lib/exceptions'
|
require_relative '../app/lib/exceptions'
|
||||||
|
require_relative '../lib/paperclip/lazy_thumbnail'
|
||||||
require_relative '../lib/paperclip/gif_transcoder'
|
require_relative '../lib/paperclip/gif_transcoder'
|
||||||
require_relative '../lib/paperclip/video_transcoder'
|
require_relative '../lib/paperclip/video_transcoder'
|
||||||
require_relative '../lib/paperclip/audio_transcoder'
|
require_relative '../lib/paperclip/audio_transcoder'
|
||||||
|
@ -9,6 +9,7 @@ Chewy.settings = {
|
|||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
journal: false,
|
journal: false,
|
||||||
|
sidekiq: { queue: 'pull' },
|
||||||
}
|
}
|
||||||
|
|
||||||
Chewy.root_strategy = enabled ? :sidekiq : :bypass
|
Chewy.root_strategy = enabled ? :sidekiq : :bypass
|
||||||
|
@ -4,10 +4,12 @@ end
|
|||||||
|
|
||||||
Devise.setup do |config|
|
Devise.setup do |config|
|
||||||
# Devise omniauth strategies
|
# Devise omniauth strategies
|
||||||
|
options = {}
|
||||||
|
options[:redirect_at_sign_in] = ENV['OAUTH_REDIRECT_AT_SIGN_IN'] == 'true'
|
||||||
|
|
||||||
# CAS strategy
|
# CAS strategy
|
||||||
if ENV['CAS_ENABLED'] == 'true'
|
if ENV['CAS_ENABLED'] == 'true'
|
||||||
cas_options = {}
|
cas_options = options
|
||||||
cas_options[:url] = ENV['CAS_URL'] if ENV['CAS_URL']
|
cas_options[:url] = ENV['CAS_URL'] if ENV['CAS_URL']
|
||||||
cas_options[:host] = ENV['CAS_HOST'] if ENV['CAS_HOST']
|
cas_options[:host] = ENV['CAS_HOST'] if ENV['CAS_HOST']
|
||||||
cas_options[:port] = ENV['CAS_PORT'] if ENV['CAS_PORT']
|
cas_options[:port] = ENV['CAS_PORT'] if ENV['CAS_PORT']
|
||||||
@ -18,7 +20,7 @@ Devise.setup do |config|
|
|||||||
cas_options[:login_url] = ENV['CAS_LOGIN_URL'] if ENV['CAS_LOGIN_URL']
|
cas_options[:login_url] = ENV['CAS_LOGIN_URL'] if ENV['CAS_LOGIN_URL']
|
||||||
cas_options[:uid_field] = ENV['CAS_UID_FIELD'] || 'user' if ENV['CAS_UID_FIELD']
|
cas_options[:uid_field] = ENV['CAS_UID_FIELD'] || 'user' if ENV['CAS_UID_FIELD']
|
||||||
cas_options[:ca_path] = ENV['CAS_CA_PATH'] if ENV['CAS_CA_PATH']
|
cas_options[:ca_path] = ENV['CAS_CA_PATH'] if ENV['CAS_CA_PATH']
|
||||||
cas_options[:disable_ssl_verification] = ENV['CAS_DISABLE_SSL_VERIFICATION'] == 'true' if ENV['CAS_DISABLE_SSL_VERIFICATION']
|
cas_options[:disable_ssl_verification] = ENV['CAS_DISABLE_SSL_VERIFICATION'] == 'true'
|
||||||
cas_options[:uid_key] = ENV['CAS_UID_KEY'] || 'user'
|
cas_options[:uid_key] = ENV['CAS_UID_KEY'] || 'user'
|
||||||
cas_options[:name_key] = ENV['CAS_NAME_KEY'] || 'name'
|
cas_options[:name_key] = ENV['CAS_NAME_KEY'] || 'name'
|
||||||
cas_options[:email_key] = ENV['CAS_EMAIL_KEY'] || 'email'
|
cas_options[:email_key] = ENV['CAS_EMAIL_KEY'] || 'email'
|
||||||
@ -33,7 +35,7 @@ Devise.setup do |config|
|
|||||||
|
|
||||||
# SAML strategy
|
# SAML strategy
|
||||||
if ENV['SAML_ENABLED'] == 'true'
|
if ENV['SAML_ENABLED'] == 'true'
|
||||||
saml_options = {}
|
saml_options = options
|
||||||
saml_options[:assertion_consumer_service_url] = ENV['SAML_ACS_URL'] if ENV['SAML_ACS_URL']
|
saml_options[:assertion_consumer_service_url] = ENV['SAML_ACS_URL'] if ENV['SAML_ACS_URL']
|
||||||
saml_options[:issuer] = ENV['SAML_ISSUER'] if ENV['SAML_ISSUER']
|
saml_options[:issuer] = ENV['SAML_ISSUER'] if ENV['SAML_ISSUER']
|
||||||
saml_options[:idp_sso_target_url] = ENV['SAML_IDP_SSO_TARGET_URL'] if ENV['SAML_IDP_SSO_TARGET_URL']
|
saml_options[:idp_sso_target_url] = ENV['SAML_IDP_SSO_TARGET_URL'] if ENV['SAML_IDP_SSO_TARGET_URL']
|
||||||
@ -48,10 +50,13 @@ Devise.setup do |config|
|
|||||||
saml_options[:security] = {}
|
saml_options[:security] = {}
|
||||||
saml_options[:security][:want_assertions_signed] = ENV['SAML_SECURITY_WANT_ASSERTION_SIGNED'] == 'true'
|
saml_options[:security][:want_assertions_signed] = ENV['SAML_SECURITY_WANT_ASSERTION_SIGNED'] == 'true'
|
||||||
saml_options[:security][:want_assertions_encrypted] = ENV['SAML_SECURITY_WANT_ASSERTION_ENCRYPTED'] == 'true'
|
saml_options[:security][:want_assertions_encrypted] = ENV['SAML_SECURITY_WANT_ASSERTION_ENCRYPTED'] == 'true'
|
||||||
|
saml_options[:security][:assume_email_is_verified] = ENV['SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED'] == 'true'
|
||||||
saml_options[:attribute_statements] = {}
|
saml_options[:attribute_statements] = {}
|
||||||
saml_options[:attribute_statements][:uid] = [ENV['SAML_ATTRIBUTES_STATEMENTS_UID']] if ENV['SAML_ATTRIBUTES_STATEMENTS_UID']
|
saml_options[:attribute_statements][:uid] = [ENV['SAML_ATTRIBUTES_STATEMENTS_UID']] if ENV['SAML_ATTRIBUTES_STATEMENTS_UID']
|
||||||
saml_options[:attribute_statements][:email] = [ENV['SAML_ATTRIBUTES_STATEMENTS_EMAIL']] if ENV['SAML_ATTRIBUTES_STATEMENTS_EMAIL']
|
saml_options[:attribute_statements][:email] = [ENV['SAML_ATTRIBUTES_STATEMENTS_EMAIL']] if ENV['SAML_ATTRIBUTES_STATEMENTS_EMAIL']
|
||||||
saml_options[:attribute_statements][:full_name] = [ENV['SAML_ATTRIBUTES_STATEMENTS_FULL_NAME']] if ENV['SAML_ATTRIBUTES_STATEMENTS_FULL_NAME']
|
saml_options[:attribute_statements][:full_name] = [ENV['SAML_ATTRIBUTES_STATEMENTS_FULL_NAME']] if ENV['SAML_ATTRIBUTES_STATEMENTS_FULL_NAME']
|
||||||
|
saml_options[:attribute_statements][:verified] = [ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED']] if ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED']
|
||||||
|
saml_options[:attribute_statements][:verified_email] = [ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL']] if ENV['SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL']
|
||||||
saml_options[:uid_attribute] = ENV['SAML_UID_ATTRIBUTE'] if ENV['SAML_UID_ATTRIBUTE']
|
saml_options[:uid_attribute] = ENV['SAML_UID_ATTRIBUTE'] if ENV['SAML_UID_ATTRIBUTE']
|
||||||
config.omniauth :saml, saml_options
|
config.omniauth :saml, saml_options
|
||||||
end
|
end
|
||||||
|
@ -14,7 +14,6 @@ ar:
|
|||||||
humane_approach_title: أسلوب يعيد الإعتبار للإنسان
|
humane_approach_title: أسلوب يعيد الإعتبار للإنسان
|
||||||
not_a_product_title: إنك إنسان و لست سلعة
|
not_a_product_title: إنك إنسان و لست سلعة
|
||||||
real_conversation_title: مبني لتحقيق تواصل حقيقي
|
real_conversation_title: مبني لتحقيق تواصل حقيقي
|
||||||
find_another_instance: إبحث عن مثيل خادوم آخر
|
|
||||||
generic_description: "%{domain} هو سيرفر من بين سيرفرات الشبكة"
|
generic_description: "%{domain} هو سيرفر من بين سيرفرات الشبكة"
|
||||||
hosted_on: ماستدون مُستضاف على %{domain}
|
hosted_on: ماستدون مُستضاف على %{domain}
|
||||||
learn_more: تعلم المزيد
|
learn_more: تعلم المزيد
|
||||||
|
@ -23,7 +23,6 @@ ca:
|
|||||||
real_conversation_title: Construït per a converses reals
|
real_conversation_title: Construït per a converses reals
|
||||||
within_reach_body: Diverses aplicacions per a iOS, Android i altres plataformes gràcies a un ecosistema API amable amb el desenvolupador, et permet mantenir-te al dia amb els amics en qualsevol lloc..
|
within_reach_body: Diverses aplicacions per a iOS, Android i altres plataformes gràcies a un ecosistema API amable amb el desenvolupador, et permet mantenir-te al dia amb els amics en qualsevol lloc..
|
||||||
within_reach_title: Sempre a l'abast
|
within_reach_title: Sempre a l'abast
|
||||||
find_another_instance: Troba altres instàncies
|
|
||||||
generic_description: "%{domain} és un servidor a la xarxa"
|
generic_description: "%{domain} és un servidor a la xarxa"
|
||||||
hosted_on: Mastodon allotjat a %{domain}
|
hosted_on: Mastodon allotjat a %{domain}
|
||||||
learn_more: Més informació
|
learn_more: Més informació
|
||||||
|
@ -23,7 +23,6 @@ de:
|
|||||||
real_conversation_title: Für das echte Gespräch gemacht
|
real_conversation_title: Für das echte Gespräch gemacht
|
||||||
within_reach_body: Verschiedene Apps für iOS, Android und andere Plattformen erlauben dir dank unserem blühenden API-Ökosystem, dich von überall auf dem Laufenden zu halten.
|
within_reach_body: Verschiedene Apps für iOS, Android und andere Plattformen erlauben dir dank unserem blühenden API-Ökosystem, dich von überall auf dem Laufenden zu halten.
|
||||||
within_reach_title: Immer für dich da
|
within_reach_title: Immer für dich da
|
||||||
find_another_instance: Eine andere Instanz finden
|
|
||||||
generic_description: "%{domain} ist ein Server im Netzwerk"
|
generic_description: "%{domain} ist ein Server im Netzwerk"
|
||||||
hosted_on: Mastodon, beherbergt auf %{domain}
|
hosted_on: Mastodon, beherbergt auf %{domain}
|
||||||
learn_more: Mehr erfahren
|
learn_more: Mehr erfahren
|
||||||
|
@ -23,7 +23,6 @@ en:
|
|||||||
real_conversation_title: Built for real conversation
|
real_conversation_title: Built for real conversation
|
||||||
within_reach_body: Multiple apps for iOS, Android, and other platforms thanks to a developer-friendly API ecosystem allow you to keep up with your friends anywhere.
|
within_reach_body: Multiple apps for iOS, Android, and other platforms thanks to a developer-friendly API ecosystem allow you to keep up with your friends anywhere.
|
||||||
within_reach_title: Always within reach
|
within_reach_title: Always within reach
|
||||||
find_another_instance: Find another instance
|
|
||||||
generic_description: "%{domain} is one server in the network"
|
generic_description: "%{domain} is one server in the network"
|
||||||
hosted_on: Mastodon hosted on %{domain}
|
hosted_on: Mastodon hosted on %{domain}
|
||||||
learn_more: Learn more
|
learn_more: Learn more
|
||||||
@ -274,6 +273,9 @@ en:
|
|||||||
contact_information:
|
contact_information:
|
||||||
email: Business e-mail
|
email: Business e-mail
|
||||||
username: Contact username
|
username: Contact username
|
||||||
|
hero:
|
||||||
|
desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to instance thumbnail
|
||||||
|
title: Hero image
|
||||||
peers_api_enabled:
|
peers_api_enabled:
|
||||||
desc_html: Domain names this instance has encountered in the fediverse
|
desc_html: Domain names this instance has encountered in the fediverse
|
||||||
title: Publish list of discovered instances
|
title: Publish list of discovered instances
|
||||||
@ -421,6 +423,13 @@ en:
|
|||||||
title: This page is not correct
|
title: This page is not correct
|
||||||
noscript_html: To use the Mastodon web application, please enable JavaScript. Alternatively, try one of the <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">native apps</a> for Mastodon for your platform.
|
noscript_html: To use the Mastodon web application, please enable JavaScript. Alternatively, try one of the <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">native apps</a> for Mastodon for your platform.
|
||||||
exports:
|
exports:
|
||||||
|
archive_takeout:
|
||||||
|
date: Date
|
||||||
|
download: Download your archive
|
||||||
|
hint_html: You can request an archive of your <strong>toots and uploaded media</strong>. The exported data will be in ActivityPub format, readable by any compliant software.
|
||||||
|
in_progress: Compiling your archive...
|
||||||
|
request: Request your archive
|
||||||
|
size: Size
|
||||||
blocks: You block
|
blocks: You block
|
||||||
csv: CSV
|
csv: CSV
|
||||||
follows: You follow
|
follows: You follow
|
||||||
@ -742,6 +751,10 @@ en:
|
|||||||
setup: Set up
|
setup: Set up
|
||||||
wrong_code: The entered code was invalid! Are server time and device time correct?
|
wrong_code: The entered code was invalid! Are server time and device time correct?
|
||||||
user_mailer:
|
user_mailer:
|
||||||
|
backup_ready:
|
||||||
|
explanation: You requested a full backup of your Mastodon account. It's now ready for download!
|
||||||
|
subject: Your archive is ready for download
|
||||||
|
title: Archive takeout
|
||||||
welcome:
|
welcome:
|
||||||
edit_profile_action: Setup profile
|
edit_profile_action: Setup profile
|
||||||
edit_profile_step: You can customize your profile by uploading an avatar, header, changing your display name and more. If you’d like to review new followers before they’re allowed to follow you, you can lock your account.
|
edit_profile_step: You can customize your profile by uploading an avatar, header, changing your display name and more. If you’d like to review new followers before they’re allowed to follow you, you can lock your account.
|
||||||
|
@ -23,7 +23,6 @@ es:
|
|||||||
real_conversation_title: Hecho para verdaderas conversaciones
|
real_conversation_title: Hecho para verdaderas conversaciones
|
||||||
within_reach_body: Aplicaciones múltiples para iOS, Android, y otras plataformas gracias a un ecosistema de APIs amigable al desarrollador para permitirte estar con tus amigos donde sea.
|
within_reach_body: Aplicaciones múltiples para iOS, Android, y otras plataformas gracias a un ecosistema de APIs amigable al desarrollador para permitirte estar con tus amigos donde sea.
|
||||||
within_reach_title: Siempre al alcance
|
within_reach_title: Siempre al alcance
|
||||||
find_another_instance: Busca otra instancia
|
|
||||||
generic_description: "%{domain} es un servidor en la red"
|
generic_description: "%{domain} es un servidor en la red"
|
||||||
hosted_on: Mastodon hosteado en %{domain}
|
hosted_on: Mastodon hosteado en %{domain}
|
||||||
learn_more: Aprende más
|
learn_more: Aprende más
|
||||||
|
@ -23,7 +23,6 @@ fa:
|
|||||||
real_conversation_title: برای گفتگوهای واقعی
|
real_conversation_title: برای گفتگوهای واقعی
|
||||||
within_reach_body: اپهای متنوع برای iOS، اندروید، و سیستمهای دیگر به خاطر وجود یک اکوسیستم API دوستانه برای برنامهنویسان. از همه جا با دوستان خود ارتباط داشته باشید.
|
within_reach_body: اپهای متنوع برای iOS، اندروید، و سیستمهای دیگر به خاطر وجود یک اکوسیستم API دوستانه برای برنامهنویسان. از همه جا با دوستان خود ارتباط داشته باشید.
|
||||||
within_reach_title: همیشه در دسترس
|
within_reach_title: همیشه در دسترس
|
||||||
find_another_instance: یافتن سرورهای دیگر
|
|
||||||
generic_description: "%{domain} یک سرور روی شبکه است"
|
generic_description: "%{domain} یک سرور روی شبکه است"
|
||||||
hosted_on: ماستدون، میزبانیشده روی %{domain}
|
hosted_on: ماستدون، میزبانیشده روی %{domain}
|
||||||
learn_more: بیشتر بدانید
|
learn_more: بیشتر بدانید
|
||||||
|
@ -21,7 +21,6 @@ fi:
|
|||||||
real_conversation_title: Rakennettu oikealle keskustelulle
|
real_conversation_title: Rakennettu oikealle keskustelulle
|
||||||
within_reach_body: Kehittäjäystävällisen rajapintaekosysteemin ansiosta useita appeja Androidille, iOS:lle ja muille alustoille, jotka mahdollistavat yhteydenpidon ystäviesi kanssa missä vain.
|
within_reach_body: Kehittäjäystävällisen rajapintaekosysteemin ansiosta useita appeja Androidille, iOS:lle ja muille alustoille, jotka mahdollistavat yhteydenpidon ystäviesi kanssa missä vain.
|
||||||
within_reach_title: Aina lähellä
|
within_reach_title: Aina lähellä
|
||||||
find_another_instance: Löydä toinen instanssi
|
|
||||||
learn_more: Lisätietoja
|
learn_more: Lisätietoja
|
||||||
other_instances: Muut palvelimet
|
other_instances: Muut palvelimet
|
||||||
source_code: Lähdekoodi
|
source_code: Lähdekoodi
|
||||||
|
@ -23,7 +23,6 @@ fr:
|
|||||||
real_conversation_title: Construit pour de vraies conversations
|
real_conversation_title: Construit pour de vraies conversations
|
||||||
within_reach_body: Grâce à l’existence d’un environnement API accueillant pour les développeur·se·s, de multiples applications pour iOS, Android et d’autres plateformes vous permettent de rester en contact avec vos ami·e·s où que vous soyez.
|
within_reach_body: Grâce à l’existence d’un environnement API accueillant pour les développeur·se·s, de multiples applications pour iOS, Android et d’autres plateformes vous permettent de rester en contact avec vos ami·e·s où que vous soyez.
|
||||||
within_reach_title: Toujours à portée de main
|
within_reach_title: Toujours à portée de main
|
||||||
find_another_instance: Trouver une autre instance
|
|
||||||
generic_description: "%{domain} est seulement un serveur du réseau"
|
generic_description: "%{domain} est seulement un serveur du réseau"
|
||||||
hosted_on: Instance Mastodon hébergée par %{domain}
|
hosted_on: Instance Mastodon hébergée par %{domain}
|
||||||
learn_more: En savoir plus
|
learn_more: En savoir plus
|
||||||
|
@ -23,7 +23,6 @@ gl:
|
|||||||
real_conversation_title: Construído para conversacións reais
|
real_conversation_title: Construído para conversacións reais
|
||||||
within_reach_body: Existen múltiples aplicativos para iOS, Android e outras plataformas grazas a un entorno API amigable para o desenvolvedor que lle permite estar ao tanto cos seus amigos en calquer lugar.
|
within_reach_body: Existen múltiples aplicativos para iOS, Android e outras plataformas grazas a un entorno API amigable para o desenvolvedor que lle permite estar ao tanto cos seus amigos en calquer lugar.
|
||||||
within_reach_title: Sempre en contacto
|
within_reach_title: Sempre en contacto
|
||||||
find_another_instance: Atope outra instancia
|
|
||||||
generic_description: "%{domain} é un servidor na rede"
|
generic_description: "%{domain} é un servidor na rede"
|
||||||
hosted_on: Mastodon aloxado en %{domain}
|
hosted_on: Mastodon aloxado en %{domain}
|
||||||
learn_more: Coñeza máis
|
learn_more: Coñeza máis
|
||||||
|
@ -23,7 +23,6 @@ he:
|
|||||||
real_conversation_title: בנוי לשיחות אמתיות
|
real_conversation_title: בנוי לשיחות אמתיות
|
||||||
within_reach_body: שלל אפליקציות עבור iOS, אנדרואיד ופלטפורמות אחרות שיאפשרו לך לשמור על קשר עם חברים בכל מקום, תודות למערכת מנשקי תוכנה ידידותיים למפתחים.
|
within_reach_body: שלל אפליקציות עבור iOS, אנדרואיד ופלטפורמות אחרות שיאפשרו לך לשמור על קשר עם חברים בכל מקום, תודות למערכת מנשקי תוכנה ידידותיים למפתחים.
|
||||||
within_reach_title: תמיד במרחק נגיעה
|
within_reach_title: תמיד במרחק נגיעה
|
||||||
find_another_instance: לאיתור שרת אחר
|
|
||||||
generic_description: "%{domain} הוא שרת אחד בתוך הרשת"
|
generic_description: "%{domain} הוא שרת אחד בתוך הרשת"
|
||||||
hosted_on: מסטודון שיושב בכתובת %{domain}
|
hosted_on: מסטודון שיושב בכתובת %{domain}
|
||||||
learn_more: מידע נוסף
|
learn_more: מידע נוסף
|
||||||
|
@ -23,7 +23,6 @@ hu:
|
|||||||
real_conversation_title: Valódi beszélgetésekre tervezve
|
real_conversation_title: Valódi beszélgetésekre tervezve
|
||||||
within_reach_body: A fejlesztőbarát API-nak köszönhetően számos iOS, Android és egyéb platformra írt alkalmazás teszi lehetővé, hogy bármikor, bárhonnan részt vehess a társalgásban.
|
within_reach_body: A fejlesztőbarát API-nak köszönhetően számos iOS, Android és egyéb platformra írt alkalmazás teszi lehetővé, hogy bármikor, bárhonnan részt vehess a társalgásban.
|
||||||
within_reach_title: Mindig elérhetőnek lenni
|
within_reach_title: Mindig elérhetőnek lenni
|
||||||
find_another_instance: További instanciák keresése
|
|
||||||
generic_description: "%{domain} csak egy a számtalan szerver közül a föderációban"
|
generic_description: "%{domain} csak egy a számtalan szerver közül a föderációban"
|
||||||
hosted_on: "%{domain} Mastodon instancia"
|
hosted_on: "%{domain} Mastodon instancia"
|
||||||
learn_more: Tudj meg többet
|
learn_more: Tudj meg többet
|
||||||
|
@ -23,7 +23,6 @@ ja:
|
|||||||
real_conversation_title: 本当のコミュニケーションのために
|
real_conversation_title: 本当のコミュニケーションのために
|
||||||
within_reach_body: デベロッパーフレンドリーな API により実現された、iOS や Android、その他様々なプラットフォームのためのアプリでどこでも友人とやりとりできます。
|
within_reach_body: デベロッパーフレンドリーな API により実現された、iOS や Android、その他様々なプラットフォームのためのアプリでどこでも友人とやりとりできます。
|
||||||
within_reach_title: いつでも身近に
|
within_reach_title: いつでも身近に
|
||||||
find_another_instance: 他のインスタンスを探す
|
|
||||||
generic_description: "%{domain} は、Mastodon インスタンスの一つです"
|
generic_description: "%{domain} は、Mastodon インスタンスの一つです"
|
||||||
hosted_on: Mastodon hosted on %{domain}
|
hosted_on: Mastodon hosted on %{domain}
|
||||||
learn_more: もっと詳しく
|
learn_more: もっと詳しく
|
||||||
|
@ -23,7 +23,6 @@ ko:
|
|||||||
real_conversation_title: 진정한 커뮤니케이션을 위하여
|
real_conversation_title: 진정한 커뮤니케이션을 위하여
|
||||||
within_reach_body: 개발자 친화적인 API에 의해서 실현된 iOS나 Android, 그 외의 여러 Platform들 덕분에 어디서든 친구들과 자유롭게 메세지를 주고 받을 수 있습니다.
|
within_reach_body: 개발자 친화적인 API에 의해서 실현된 iOS나 Android, 그 외의 여러 Platform들 덕분에 어디서든 친구들과 자유롭게 메세지를 주고 받을 수 있습니다.
|
||||||
within_reach_title: 언제나 유저의 곁에서
|
within_reach_title: 언제나 유저의 곁에서
|
||||||
find_another_instance: 다른 인스턴스 찾기
|
|
||||||
generic_description: "%{domain} 은 Mastodon의 인스턴스 입니다."
|
generic_description: "%{domain} 은 Mastodon의 인스턴스 입니다."
|
||||||
hosted_on: "%{domain}에서 호스팅 되는 마스토돈"
|
hosted_on: "%{domain}에서 호스팅 되는 마스토돈"
|
||||||
learn_more: 자세히
|
learn_more: 자세히
|
||||||
|
@ -23,7 +23,6 @@ nl:
|
|||||||
real_conversation_title: Voor echte gesprekken gemaakt
|
real_conversation_title: Voor echte gesprekken gemaakt
|
||||||
within_reach_body: Meerdere apps voor iOS, Android en andere platformen, met dank aan het ontwikkelaarsvriendelijke API-systeem, zorgen ervoor dat je overal op de hoogte blijft.
|
within_reach_body: Meerdere apps voor iOS, Android en andere platformen, met dank aan het ontwikkelaarsvriendelijke API-systeem, zorgen ervoor dat je overal op de hoogte blijft.
|
||||||
within_reach_title: Altijd binnen bereik
|
within_reach_title: Altijd binnen bereik
|
||||||
find_another_instance: Vind een andere server
|
|
||||||
generic_description: "%{domain} is een server in het Mastodonnetwerk"
|
generic_description: "%{domain} is een server in het Mastodonnetwerk"
|
||||||
hosted_on: Mastodon op %{domain}
|
hosted_on: Mastodon op %{domain}
|
||||||
learn_more: Meer leren
|
learn_more: Meer leren
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
real_conversation_title: Laget for ekte samtaler
|
real_conversation_title: Laget for ekte samtaler
|
||||||
within_reach_body: Takket være et utviklingsvennlig API-økosystem vil flere apper for iOS, Android og andre plattformer la deg holde kontakten med dine venner hvor som helst.
|
within_reach_body: Takket være et utviklingsvennlig API-økosystem vil flere apper for iOS, Android og andre plattformer la deg holde kontakten med dine venner hvor som helst.
|
||||||
within_reach_title: Alltid innen rekkevidde
|
within_reach_title: Alltid innen rekkevidde
|
||||||
find_another_instance: Finn en annen instans
|
|
||||||
generic_description: "%{domain} er en tjener i nettverket"
|
generic_description: "%{domain} er en tjener i nettverket"
|
||||||
hosted_on: Mastodon driftet på %{domain}
|
hosted_on: Mastodon driftet på %{domain}
|
||||||
learn_more: Lær mer
|
learn_more: Lær mer
|
||||||
|
@ -23,7 +23,6 @@ oc:
|
|||||||
real_conversation_title: Fach per de conversacions vertadièras
|
real_conversation_title: Fach per de conversacions vertadièras
|
||||||
within_reach_body: Multiplas aplicacion per iOS, Android, e autras plataformas mercés a un entorn API de bon utilizar, vos permet de gardar lo contacte pertot.
|
within_reach_body: Multiplas aplicacion per iOS, Android, e autras plataformas mercés a un entorn API de bon utilizar, vos permet de gardar lo contacte pertot.
|
||||||
within_reach_title: Totjorn al costat
|
within_reach_title: Totjorn al costat
|
||||||
find_another_instance: Trobar mai instàncias
|
|
||||||
generic_description: "%{domain} es un dels servidors del malhum"
|
generic_description: "%{domain} es un dels servidors del malhum"
|
||||||
hosted_on: Mastodon albergat sus %{domain}
|
hosted_on: Mastodon albergat sus %{domain}
|
||||||
learn_more: Ne saber mai
|
learn_more: Ne saber mai
|
||||||
|
@ -23,7 +23,6 @@ pl:
|
|||||||
real_conversation_title: Zaprojektowany do prawdziwych rozmów
|
real_conversation_title: Zaprojektowany do prawdziwych rozmów
|
||||||
within_reach_body: Wiele aplikacji dla Androida, iOS i innych platform dzięki przyjaznemu programistom API sprawia, że możesz utrzymywać kontakt ze znajomymi praktycznie wszędzie.
|
within_reach_body: Wiele aplikacji dla Androida, iOS i innych platform dzięki przyjaznemu programistom API sprawia, że możesz utrzymywać kontakt ze znajomymi praktycznie wszędzie.
|
||||||
within_reach_title: Zawsze w Twoim zasięgu
|
within_reach_title: Zawsze w Twoim zasięgu
|
||||||
find_another_instance: Znajdź inną instancję
|
|
||||||
generic_description: "%{domain} jest jednym z serwerów sieci"
|
generic_description: "%{domain} jest jednym z serwerów sieci"
|
||||||
hosted_on: Mastodon uruchomiony na %{domain}
|
hosted_on: Mastodon uruchomiony na %{domain}
|
||||||
learn_more: Dowiedz się więcej
|
learn_more: Dowiedz się więcej
|
||||||
@ -275,6 +274,9 @@ pl:
|
|||||||
contact_information:
|
contact_information:
|
||||||
email: Służbowy adres e-mail
|
email: Służbowy adres e-mail
|
||||||
username: Nazwa użytkownika do kontaktu
|
username: Nazwa użytkownika do kontaktu
|
||||||
|
hero:
|
||||||
|
desc_html: Wyświetlany na stronie głównej. Zalecany jest rozmiar przynajmniej 600x100 pikseli. Jeżeli nie ustawiony, zostanie użyta miniatura instancji.
|
||||||
|
title: Obraz bohatera
|
||||||
peers_api_enabled:
|
peers_api_enabled:
|
||||||
desc_html: Nazwy domen, z którymi ta instancja wchodziła w interakcje
|
desc_html: Nazwy domen, z którymi ta instancja wchodziła w interakcje
|
||||||
title: Publikuj listę znanych instancji
|
title: Publikuj listę znanych instancji
|
||||||
@ -422,6 +424,13 @@ pl:
|
|||||||
title: Ta strona jest nieprawidłowa
|
title: Ta strona jest nieprawidłowa
|
||||||
noscript_html: Aby korzystać z aplikacji Mastodon, włącz JavaScript. Możesz też skorzystać z jednej z <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">natywnych aplikacji</a> obsługującej Twoje urządzenie.
|
noscript_html: Aby korzystać z aplikacji Mastodon, włącz JavaScript. Możesz też skorzystać z jednej z <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">natywnych aplikacji</a> obsługującej Twoje urządzenie.
|
||||||
exports:
|
exports:
|
||||||
|
archive_takeout:
|
||||||
|
date: Data
|
||||||
|
download: Pobierz swoje archiwum
|
||||||
|
hint_html: Możesz uzyskać archiwum swoich <strong>wpisów i wysłanej zawartości multimedialnej</strong>. Wyeksportowane dane będą dostępne w formacie ActivityPub, obsługiwanym przez odpowiednie programy.
|
||||||
|
in_progress: Tworzenie archiwum…
|
||||||
|
request: Uzyskaj archiwum
|
||||||
|
size: Rozmiar
|
||||||
blocks: Zablokowani
|
blocks: Zablokowani
|
||||||
csv: CSV
|
csv: CSV
|
||||||
follows: Śledzeni
|
follows: Śledzeni
|
||||||
@ -749,6 +758,10 @@ pl:
|
|||||||
setup: Skonfiguruj
|
setup: Skonfiguruj
|
||||||
wrong_code: Wprowadzony kod jest niepoprawny! Czy czas serwera i urządzenia jest poprawny?
|
wrong_code: Wprowadzony kod jest niepoprawny! Czy czas serwera i urządzenia jest poprawny?
|
||||||
user_mailer:
|
user_mailer:
|
||||||
|
backup_ready:
|
||||||
|
explanation: Zażądałeś pełnej kopii zapasowej konta na Mastodonie. Jest ono dostępne do pobrania
|
||||||
|
subject: Twoje archiwum jest gotowe do pobrania
|
||||||
|
title: Odbiór archiwum
|
||||||
welcome:
|
welcome:
|
||||||
edit_profile_action: Skonfiguruj profil
|
edit_profile_action: Skonfiguruj profil
|
||||||
edit_profile_step: Możesz dostować profil wysyłając awatar, obraz nagłówka, zmieniając wyświetlaną nazwę i wiele więcej. Jeżeli chcesz, możesz zablokować konto, aby kontrolować, kto może Cię śledzić.
|
edit_profile_step: Możesz dostować profil wysyłając awatar, obraz nagłówka, zmieniając wyświetlaną nazwę i wiele więcej. Jeżeli chcesz, możesz zablokować konto, aby kontrolować, kto może Cię śledzić.
|
||||||
|
@ -23,7 +23,6 @@ pt-BR:
|
|||||||
real_conversation_title: Feito para conversas reais
|
real_conversation_title: Feito para conversas reais
|
||||||
within_reach_body: Vários apps para iOS, Android e outras plataformas graças a um ecossistema de API amigável para desenvolvedores permitem que você possa se manter atualizado sobre seus amigos de qualquer lugar.
|
within_reach_body: Vários apps para iOS, Android e outras plataformas graças a um ecossistema de API amigável para desenvolvedores permitem que você possa se manter atualizado sobre seus amigos de qualquer lugar.
|
||||||
within_reach_title: Sempre ao seu alcance
|
within_reach_title: Sempre ao seu alcance
|
||||||
find_another_instance: Encontre outra instância
|
|
||||||
generic_description: "%{domain} é um servidor na rede"
|
generic_description: "%{domain} é um servidor na rede"
|
||||||
hosted_on: Mastodon hospedado em %{domain}
|
hosted_on: Mastodon hospedado em %{domain}
|
||||||
learn_more: Saiba mais
|
learn_more: Saiba mais
|
||||||
|
@ -23,7 +23,6 @@ pt:
|
|||||||
real_conversation_title: Feito para conversas reais
|
real_conversation_title: Feito para conversas reais
|
||||||
within_reach_body: Várias aplicações para iOS, Android e outras plataformas graças a um ecossistema de API amigável para desenvolvedores, permitem-te que te mantenhas em contacto com os teus amigos em qualquer lugar.
|
within_reach_body: Várias aplicações para iOS, Android e outras plataformas graças a um ecossistema de API amigável para desenvolvedores, permitem-te que te mantenhas em contacto com os teus amigos em qualquer lugar.
|
||||||
within_reach_title: Sempre ao teu alcance
|
within_reach_title: Sempre ao teu alcance
|
||||||
find_another_instance: Encontra outra instância
|
|
||||||
generic_description: "%{domain} é um servidor na rede"
|
generic_description: "%{domain} é um servidor na rede"
|
||||||
hosted_on: Mastodon em %{domain}
|
hosted_on: Mastodon em %{domain}
|
||||||
learn_more: Saber mais
|
learn_more: Saber mais
|
||||||
|
@ -23,7 +23,6 @@ ru:
|
|||||||
real_conversation_title: Создан для настоящего общения
|
real_conversation_title: Создан для настоящего общения
|
||||||
within_reach_body: Различные приложения для iOS, Android и других платформ, написанные благодаря дружественной к разработчикам экосистеме API, позволят Вам держать связь с Вашими друзьями где угодно.
|
within_reach_body: Различные приложения для iOS, Android и других платформ, написанные благодаря дружественной к разработчикам экосистеме API, позволят Вам держать связь с Вашими друзьями где угодно.
|
||||||
within_reach_title: Всегда под рукой
|
within_reach_title: Всегда под рукой
|
||||||
find_another_instance: Найти другой узел
|
|
||||||
generic_description: "%{domain} - один из серверов сети"
|
generic_description: "%{domain} - один из серверов сети"
|
||||||
hosted_on: Mastodon размещен на %{domain}
|
hosted_on: Mastodon размещен на %{domain}
|
||||||
learn_more: Узнать больше
|
learn_more: Узнать больше
|
||||||
|
@ -23,7 +23,6 @@ sk:
|
|||||||
real_conversation_title: Vytvorený pre reálnu konverzáciu
|
real_conversation_title: Vytvorený pre reálnu konverzáciu
|
||||||
within_reach_body: Viacero aplikácií pre iOS, Android a iné platformy, ktoré vďaka jednoduchému API ekosystému vám dovoľujú byť online so svojimi priateľmi kdekoľvek.
|
within_reach_body: Viacero aplikácií pre iOS, Android a iné platformy, ktoré vďaka jednoduchému API ekosystému vám dovoľujú byť online so svojimi priateľmi kdekoľvek.
|
||||||
within_reach_title: Stále v dosahu
|
within_reach_title: Stále v dosahu
|
||||||
find_another_instance: Nájdi inú inštanciu
|
|
||||||
generic_description: "%{domain} je jeden server v sieti"
|
generic_description: "%{domain} je jeden server v sieti"
|
||||||
hosted_on: Mastodon hostovaný na %{domain}
|
hosted_on: Mastodon hostovaný na %{domain}
|
||||||
learn_more: Dozvedieť sa viac
|
learn_more: Dozvedieť sa viac
|
||||||
|
@ -23,7 +23,6 @@ sr-Latn:
|
|||||||
real_conversation_title: Pravljen za pravi razgovor
|
real_conversation_title: Pravljen za pravi razgovor
|
||||||
within_reach_body: Više aplikacija za iOS, Android, kao i druge platforme zahvaljujući ekosistemu dobrih API-ja će Vam omogućiti da ostanete u kontaktu sa prijateljima svuda.
|
within_reach_body: Više aplikacija za iOS, Android, kao i druge platforme zahvaljujući ekosistemu dobrih API-ja će Vam omogućiti da ostanete u kontaktu sa prijateljima svuda.
|
||||||
within_reach_title: Uvek u kontaktu
|
within_reach_title: Uvek u kontaktu
|
||||||
find_another_instance: Nađite drugu instancu
|
|
||||||
generic_description: "%{domain} je server na mreži"
|
generic_description: "%{domain} je server na mreži"
|
||||||
hosted_on: Mastodont hostovan na %{domain}
|
hosted_on: Mastodont hostovan na %{domain}
|
||||||
learn_more: Saznajte više
|
learn_more: Saznajte više
|
||||||
|
@ -23,7 +23,6 @@ sr:
|
|||||||
real_conversation_title: Прављен за прави разговор
|
real_conversation_title: Прављен за прави разговор
|
||||||
within_reach_body: Више апликација за iOS, Андроид, као и друге платформе захваљујући екосистему добрих API-ја ће Вам омогућити да останете у контакту са пријатељима свуда.
|
within_reach_body: Више апликација за iOS, Андроид, као и друге платформе захваљујући екосистему добрих API-ја ће Вам омогућити да останете у контакту са пријатељима свуда.
|
||||||
within_reach_title: Увек у контакту
|
within_reach_title: Увек у контакту
|
||||||
find_another_instance: Нађите другу инстанцу
|
|
||||||
generic_description: "%{domain} је сервер на мрежи"
|
generic_description: "%{domain} је сервер на мрежи"
|
||||||
hosted_on: Мастодонт хостован на %{domain}
|
hosted_on: Мастодонт хостован на %{domain}
|
||||||
learn_more: Сазнајте више
|
learn_more: Сазнајте више
|
||||||
|
@ -23,7 +23,6 @@ sv:
|
|||||||
real_conversation_title: Byggd för riktiga konversationer
|
real_conversation_title: Byggd för riktiga konversationer
|
||||||
within_reach_body: Flera appar för iOS, Android och andra plattformar tack vare ett utvecklingsvänligt API-ekosystem gör att du kan hålla kontakten med dina vänner var som helst.
|
within_reach_body: Flera appar för iOS, Android och andra plattformar tack vare ett utvecklingsvänligt API-ekosystem gör att du kan hålla kontakten med dina vänner var som helst.
|
||||||
within_reach_title: Alltid inom räckhåll
|
within_reach_title: Alltid inom räckhåll
|
||||||
find_another_instance: Hitta en annan instans
|
|
||||||
generic_description: "%{domain} är en server i nätverket"
|
generic_description: "%{domain} är en server i nätverket"
|
||||||
hosted_on: Mastodon värd på %{domain}
|
hosted_on: Mastodon värd på %{domain}
|
||||||
learn_more: Lär dig mer
|
learn_more: Lär dig mer
|
||||||
|
@ -23,7 +23,6 @@ zh-CN:
|
|||||||
real_conversation_title: 为真正的交流而生
|
real_conversation_title: 为真正的交流而生
|
||||||
within_reach_body: 通过一个面向开发者友好的 API 生态系统,Mastodon 让你可以随时随地通过众多 iOS、Android 以及其他平台的应用与朋友们保持联系。
|
within_reach_body: 通过一个面向开发者友好的 API 生态系统,Mastodon 让你可以随时随地通过众多 iOS、Android 以及其他平台的应用与朋友们保持联系。
|
||||||
within_reach_title: 始终触手可及
|
within_reach_title: 始终触手可及
|
||||||
find_another_instance: 寻找另一个实例
|
|
||||||
generic_description: "%{domain} 是这个庞大网络中的一台服务器"
|
generic_description: "%{domain} 是这个庞大网络中的一台服务器"
|
||||||
hosted_on: 一个在 %{domain} 上运行的 Mastodon 实例
|
hosted_on: 一个在 %{domain} 上运行的 Mastodon 实例
|
||||||
learn_more: 详细了解
|
learn_more: 详细了解
|
||||||
|
@ -83,7 +83,7 @@ Rails.application.routes.draw do
|
|||||||
resource :notifications, only: [:show, :update]
|
resource :notifications, only: [:show, :update]
|
||||||
resource :import, only: [:show, :create]
|
resource :import, only: [:show, :create]
|
||||||
|
|
||||||
resource :export, only: [:show]
|
resource :export, only: [:show, :create]
|
||||||
namespace :exports, constraints: { format: :csv } do
|
namespace :exports, constraints: { format: :csv } do
|
||||||
resources :follows, only: :index, controller: :following_accounts
|
resources :follows, only: :index, controller: :following_accounts
|
||||||
resources :blocks, only: :index, controller: :blocked_accounts
|
resources :blocks, only: :index, controller: :blocked_accounts
|
||||||
|
@ -30,3 +30,6 @@
|
|||||||
email_scheduler:
|
email_scheduler:
|
||||||
cron: '0 10 * * 2'
|
cron: '0 10 * * 2'
|
||||||
class: Scheduler::EmailScheduler
|
class: Scheduler::EmailScheduler
|
||||||
|
backup_cleanup_scheduler:
|
||||||
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
||||||
|
class: Scheduler::BackupCleanupScheduler
|
||||||
|
11
db/migrate/20180211015820_create_backups.rb
Normal file
11
db/migrate/20180211015820_create_backups.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
class CreateBackups < ActiveRecord::Migration[5.1]
|
||||||
|
def change
|
||||||
|
create_table :backups do |t|
|
||||||
|
t.references :user, foreign_key: { on_delete: :nullify }
|
||||||
|
t.attachment :dump
|
||||||
|
t.boolean :processed, null: false, default: false
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
db/schema.rb
14
db/schema.rb
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20180206000000) do
|
ActiveRecord::Schema.define(version: 20180211015820) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@ -92,6 +92,18 @@ ActiveRecord::Schema.define(version: 20180206000000) do
|
|||||||
t.index ["target_type", "target_id"], name: "index_admin_action_logs_on_target_type_and_target_id"
|
t.index ["target_type", "target_id"], name: "index_admin_action_logs_on_target_type_and_target_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "backups", force: :cascade do |t|
|
||||||
|
t.bigint "user_id"
|
||||||
|
t.string "dump_file_name"
|
||||||
|
t.string "dump_content_type"
|
||||||
|
t.integer "dump_file_size"
|
||||||
|
t.datetime "dump_updated_at"
|
||||||
|
t.boolean "processed", default: false, null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["user_id"], name: "index_backups_on_user_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "blocks", force: :cascade do |t|
|
create_table "blocks", force: :cascade do |t|
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
### 1. Adds local user (UID and GID are provided from environment variables).
|
|
||||||
### 2. Updates permissions, except for ./public/system (should be chown on previous installations).
|
|
||||||
### 3. Executes the command as that user.
|
|
||||||
|
|
||||||
echo "Creating mastodon user (UID : ${UID} and GID : ${GID})..."
|
|
||||||
addgroup -g ${GID} mastodon && adduser -h /mastodon -s /bin/sh -D -G mastodon -u ${UID} mastodon
|
|
||||||
|
|
||||||
echo "Updating permissions..."
|
|
||||||
find /mastodon -path /mastodon/public/system -prune -o -not -user mastodon -not -group mastodon -print0 | xargs -0 chown -f mastodon:mastodon
|
|
||||||
|
|
||||||
echo "Executing process..."
|
|
||||||
LD_PRELOAD=/lib/stack-fix.so exec su-exec mastodon:mastodon /sbin/tini -- "$@"
|
|
@ -16,7 +16,7 @@ module Paperclip
|
|||||||
|
|
||||||
final_file = Paperclip::Transcoder.make(file, options, attachment)
|
final_file = Paperclip::Transcoder.make(file, options, attachment)
|
||||||
|
|
||||||
attachment.instance.file_file_name = 'media.mp4'
|
attachment.instance.file_file_name = File.basename(attachment.instance.file_file_name, '.*') + '.mp4'
|
||||||
attachment.instance.file_content_type = 'video/mp4'
|
attachment.instance.file_content_type = 'video/mp4'
|
||||||
attachment.instance.type = MediaAttachment.types[:gifv]
|
attachment.instance.type = MediaAttachment.types[:gifv]
|
||||||
|
|
||||||
|
24
lib/paperclip/lazy_thumbnail.rb
Normal file
24
lib/paperclip/lazy_thumbnail.rb
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Paperclip
|
||||||
|
class LazyThumbnail < Paperclip::Thumbnail
|
||||||
|
def make
|
||||||
|
return File.open(@file.path) unless needs_convert?
|
||||||
|
Paperclip::Thumbnail.make(file, options, attachment)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def needs_convert?
|
||||||
|
needs_different_geometry? || needs_different_format?
|
||||||
|
end
|
||||||
|
|
||||||
|
def needs_different_geometry?
|
||||||
|
!@target_geometry.nil? && @current_geometry.width != @target_geometry.width && @current_geometry.height != @target_geometry.height
|
||||||
|
end
|
||||||
|
|
||||||
|
def needs_different_format?
|
||||||
|
@format.present? && @current_format != @format
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -752,6 +752,7 @@ namespace :mastodon do
|
|||||||
|
|
||||||
if [404, 410].include?(res.code)
|
if [404, 410].include?(res.code)
|
||||||
if options[:force]
|
if options[:force]
|
||||||
|
SuspendAccountService.new.call(account)
|
||||||
account.destroy
|
account.destroy
|
||||||
else
|
else
|
||||||
progress_bar.pause
|
progress_bar.pause
|
||||||
@ -764,6 +765,7 @@ namespace :mastodon do
|
|||||||
if confirm.casecmp('n').zero?
|
if confirm.casecmp('n').zero?
|
||||||
next
|
next
|
||||||
else
|
else
|
||||||
|
SuspendAccountService.new.call(account)
|
||||||
account.destroy
|
account.destroy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -66,6 +66,28 @@ describe Api::V1::Accounts::RelationshipsController do
|
|||||||
expect(json.second[:requested]).to be false
|
expect(json.second[:requested]).to be false
|
||||||
expect(json.second[:domain_blocking]).to be false
|
expect(json.second[:domain_blocking]).to be false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns JSON with correct data on cached requests too' do
|
||||||
|
get :index, params: { id: [simon.id] }
|
||||||
|
|
||||||
|
json = body_as_json
|
||||||
|
|
||||||
|
expect(json).to be_a Enumerable
|
||||||
|
expect(json.first[:following]).to be true
|
||||||
|
expect(json.first[:showing_reblogs]).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns JSON with correct data after change too' do
|
||||||
|
user.account.unfollow!(simon)
|
||||||
|
|
||||||
|
get :index, params: { id: [simon.id] }
|
||||||
|
|
||||||
|
json = body_as_json
|
||||||
|
|
||||||
|
expect(json).to be_a Enumerable
|
||||||
|
expect(json.first[:following]).to be false
|
||||||
|
expect(json.first[:showing_reblogs]).to be false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
3
spec/fabricators/backup_fabricator.rb
Normal file
3
spec/fabricators/backup_fabricator.rb
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Fabricator(:backup) do
|
||||||
|
user
|
||||||
|
end
|
@ -34,4 +34,9 @@ class UserMailerPreview < ActionMailer::Preview
|
|||||||
def welcome
|
def welcome
|
||||||
UserMailer.welcome(User.first)
|
UserMailer.welcome(User.first)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/backup_ready
|
||||||
|
def backup_ready
|
||||||
|
UserMailer.backup_ready(User.first, Backup.first)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
5
spec/models/backup_spec.rb
Normal file
5
spec/models/backup_spec.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Backup, type: :model do
|
||||||
|
|
||||||
|
end
|
@ -18,6 +18,9 @@ describe 'about/show.html.haml', without_verify_partial_doubles: true do
|
|||||||
source_url: 'https://github.com/tootsuite/mastodon',
|
source_url: 'https://github.com/tootsuite/mastodon',
|
||||||
open_registrations: false,
|
open_registrations: false,
|
||||||
thumbnail: nil,
|
thumbnail: nil,
|
||||||
|
hero: nil,
|
||||||
|
user_count: 0,
|
||||||
|
status_count: 0,
|
||||||
closed_registrations_message: 'yes',
|
closed_registrations_message: 'yes',
|
||||||
commit_hash: commit_hash)
|
commit_hash: commit_hash)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user