Merge remote-tracking branch 'upstream/master'

This commit is contained in:
beatrix-bitrot 2017-06-27 20:46:13 +00:00
commit ddafde942c
91 changed files with 1274 additions and 751 deletions

14
.gitattributes vendored Normal file
View file

@ -0,0 +1,14 @@
* text=auto eol=lf
*.eot -text
*.gif -text
*.gz -text
*.ico -text
*.jpg -text
*.mp3 -text
*.ogg -text
*.png -text
*.ttf -text
*.webm -text
*.woff -text
*.woff2 -text
spec/fixtures/requests/** -text !eol

View file

@ -20,6 +20,7 @@ gem 'paperclip-av-transcoder', '~> 0.6'
gem 'addressable', '~> 2.5' gem 'addressable', '~> 2.5'
gem 'bootsnap' gem 'bootsnap'
gem 'browser'
gem 'cld3', '~> 3.1' gem 'cld3', '~> 3.1'
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0' gem 'devise-two-factor', '~> 3.0'

View file

@ -1,40 +1,40 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (5.1.1) actioncable (5.1.2)
actionpack (= 5.1.1) actionpack (= 5.1.2)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (~> 0.6.1) websocket-driver (~> 0.6.1)
actionmailer (5.1.1) actionmailer (5.1.2)
actionpack (= 5.1.1) actionpack (= 5.1.2)
actionview (= 5.1.1) actionview (= 5.1.2)
activejob (= 5.1.1) activejob (= 5.1.2)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (5.1.1) actionpack (5.1.2)
actionview (= 5.1.1) actionview (= 5.1.2)
activesupport (= 5.1.1) activesupport (= 5.1.2)
rack (~> 2.0) rack (~> 2.0)
rack-test (~> 0.6.3) rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.1) actionview (5.1.2)
activesupport (= 5.1.1) activesupport (= 5.1.2)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3) rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_record_query_trace (1.5.4) active_record_query_trace (1.5.4)
activejob (5.1.1) activejob (5.1.2)
activesupport (= 5.1.1) activesupport (= 5.1.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (5.1.1) activemodel (5.1.2)
activesupport (= 5.1.1) activesupport (= 5.1.2)
activerecord (5.1.1) activerecord (5.1.2)
activemodel (= 5.1.1) activemodel (= 5.1.2)
activesupport (= 5.1.1) activesupport (= 5.1.2)
arel (~> 8.0) arel (~> 8.0)
activesupport (5.1.1) activesupport (5.1.2)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7) i18n (~> 0.7)
minitest (~> 5.1) minitest (~> 5.1)
@ -70,6 +70,7 @@ GEM
bootsnap (1.0.0) bootsnap (1.0.0)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (3.6.2) brakeman (3.6.2)
browser (2.4.0)
builder (3.2.3) builder (3.2.3)
bullet (5.5.1) bullet (5.5.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -297,17 +298,17 @@ GEM
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rack-timeout (0.4.2) rack-timeout (0.4.2)
rails (5.1.1) rails (5.1.2)
actioncable (= 5.1.1) actioncable (= 5.1.2)
actionmailer (= 5.1.1) actionmailer (= 5.1.2)
actionpack (= 5.1.1) actionpack (= 5.1.2)
actionview (= 5.1.1) actionview (= 5.1.2)
activejob (= 5.1.1) activejob (= 5.1.2)
activemodel (= 5.1.1) activemodel (= 5.1.2)
activerecord (= 5.1.1) activerecord (= 5.1.2)
activesupport (= 5.1.1) activesupport (= 5.1.2)
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 5.1.1) railties (= 5.1.2)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2) rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1) actionpack (~> 5.x, >= 5.0.1)
@ -323,9 +324,9 @@ GEM
railties (~> 5.0) railties (~> 5.0)
rails-settings-cached (0.6.5) rails-settings-cached (0.6.5)
rails (>= 4.2.0) rails (>= 4.2.0)
railties (5.1.1) railties (5.1.2)
actionpack (= 5.1.1) actionpack (= 5.1.2)
activesupport (= 5.1.1) activesupport (= 5.1.2)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
@ -483,6 +484,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
bootsnap bootsnap
brakeman (~> 3.6) brakeman (~> 3.6)
browser
bullet (~> 5.5) bullet (~> 5.5)
bundler-audit (~> 0.5) bundler-audit (~> 0.5)
capistrano (~> 3.8) capistrano (~> 3.8)

View file

@ -17,6 +17,9 @@ class Api::V1::ReportsController < Api::BaseController
status_ids: reported_status_ids, status_ids: reported_status_ids,
comment: report_params[:comment] comment: report_params[:comment]
) )
User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
render :show render :show
end end

View file

@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
include UserTrackingConcern include UserTrackingConcern
helper_method :current_account helper_method :current_account
helper_method :current_session
helper_method :single_user_mode? helper_method :single_user_mode?
rescue_from ActionController::RoutingError, with: :not_found rescue_from ActionController::RoutingError, with: :not_found
@ -68,6 +69,10 @@ class ApplicationController < ActionController::Base
@current_account ||= current_user.try(:account) @current_account ||= current_user.try(:account)
end end
def current_session
@current_session ||= SessionActivation.find_by(session_id: session['auth_id'])
end
def cache_collection(raw, klass) def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes) return raw unless klass.respond_to?(:with_includes)

View file

@ -5,6 +5,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :check_enabled_registrations, only: [:new, :create] before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create] before_action :configure_sign_up_params, only: [:create]
before_action :set_sessions, only: [:edit, :update]
def destroy def destroy
not_found not_found
@ -41,4 +42,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def determine_layout def determine_layout
%w(edit update).include?(action_name) ? 'admin' : 'auth' %w(edit update).include?(action_name) ? 'admin' : 'auth'
end end
def set_sessions
@sessions = current_user.session_activations
end
end end

View file

@ -5,7 +5,7 @@ class HomeController < ApplicationController
def index def index
@body_classes = 'app-body' @body_classes = 'app-body'
@token = find_or_create_access_token.token @token = current_session.token
@web_settings = Web::Setting.find_by(user: current_user)&.data || {} @web_settings = Web::Setting.find_by(user: current_user)&.data || {}
@admin = Account.find_local(Setting.site_contact_username) @admin = Account.find_local(Setting.site_contact_username)
@streaming_api_base_url = Rails.configuration.x.streaming_api_base_url @streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
@ -16,14 +16,4 @@ class HomeController < ApplicationController
def authenticate_user! def authenticate_user!
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in? redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
end end
def find_or_create_access_token
Doorkeeper::AccessToken.find_or_create_for(
Doorkeeper::Application.where(superapp: true).first,
current_user.id,
Doorkeeper::OAuth::Scopes.from_string('read write follow'),
Doorkeeper.configuration.access_token_expires_in,
Doorkeeper.configuration.refresh_token_enabled?
)
end
end end

View file

@ -7,7 +7,9 @@ module Settings
before_action :authenticate_user! before_action :authenticate_user!
before_action :verify_otp_required, only: [:create] before_action :verify_otp_required, only: [:create]
def show; end def show
@confirmation = Form::TwoFactorConfirmation.new
end
def create def create
current_user.otp_secret = User.generate_otp_secret(32) current_user.otp_secret = User.generate_otp_secret(32)
@ -16,13 +18,23 @@ module Settings
end end
def destroy def destroy
current_user.otp_required_for_login = false if current_user.validate_and_consume_otp!(confirmation_params[:code])
current_user.save! current_user.otp_required_for_login = false
redirect_to settings_two_factor_authentication_path current_user.save!
redirect_to settings_two_factor_authentication_path
else
flash.now[:alert] = I18n.t('two_factor_authentication.wrong_code')
@confirmation = Form::TwoFactorConfirmation.new
render :show
end
end end
private private
def confirmation_params
params.require(:form_two_factor_confirmation).permit(:code)
end
def verify_otp_required def verify_otp_required
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login? redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
end end

View file

@ -41,4 +41,16 @@ module SettingsHelper
def hash_to_object(hash) def hash_to_object(hash)
HashObject.new(hash) HashObject.new(hash)
end end
def session_device_icon(session)
device = session.detection.device
if device.mobile?
'mobile'
elsif device.tablet?
'tablet'
else
'desktop'
end
end
end end

View file

@ -1,4 +1,5 @@
import api from '../api'; import api from '../api';
import { openModal, closeModal } from './modal';
export const REPORT_INIT = 'REPORT_INIT'; export const REPORT_INIT = 'REPORT_INIT';
export const REPORT_CANCEL = 'REPORT_CANCEL'; export const REPORT_CANCEL = 'REPORT_CANCEL';
@ -11,10 +12,14 @@ export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE'; export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
export function initReport(account, status) { export function initReport(account, status) {
return { return dispatch => {
type: REPORT_INIT, dispatch({
account, type: REPORT_INIT,
status, account,
status,
});
dispatch(openModal('REPORT'));
}; };
}; };
@ -40,7 +45,10 @@ export function submitReport() {
account_id: getState().getIn(['reports', 'new', 'account_id']), account_id: getState().getIn(['reports', 'new', 'account_id']),
status_ids: getState().getIn(['reports', 'new', 'status_ids']), status_ids: getState().getIn(['reports', 'new', 'status_ids']),
comment: getState().getIn(['reports', 'new', 'comment']), comment: getState().getIn(['reports', 'new', 'comment']),
}).then(response => dispatch(submitReportSuccess(response.data))).catch(error => dispatch(submitReportFail(error))); }).then(response => {
dispatch(closeModal());
dispatch(submitReportSuccess(response.data));
}).catch(error => dispatch(submitReportFail(error)));
}; };
}; };

View file

@ -1,50 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class ColumnCollapsable extends React.PureComponent {
static propTypes = {
icon: PropTypes.string.isRequired,
title: PropTypes.string,
fullHeight: PropTypes.number.isRequired,
children: PropTypes.node,
onCollapse: PropTypes.func,
};
state = {
collapsed: true,
animating: false,
};
handleToggleCollapsed = () => {
const currentState = this.state.collapsed;
this.setState({ collapsed: !currentState, animating: true });
if (!currentState && this.props.onCollapse) {
this.props.onCollapse();
}
}
handleTransitionEnd = () => {
this.setState({ animating: false });
}
render () {
const { icon, title, fullHeight, children } = this.props;
const { collapsed, animating } = this.state;
return (
<div className={`column-collapsable ${collapsed ? 'collapsed' : ''}`} onTransitionEnd={this.handleTransitionEnd}>
<div role='button' tabIndex='0' title={`${title}`} className='column-collapsable__button column-icon' onClick={this.handleToggleCollapsed}>
<i className={`fa fa-${icon}`} />
</div>
<div className='column-collapsable__content' style={{ height: `${fullHeight}px` }}>
{(!collapsed || animating) && children}
</div>
</div>
);
}
}

View file

@ -132,7 +132,7 @@ export default class ColumnHeader extends React.PureComponent {
</div> </div>
<div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}> <div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}>
<div> <div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent} {(!collapsed || animating) && collapsedContent}
</div> </div>
</div> </div>

View file

@ -85,14 +85,24 @@ 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 previewWidth = attachment.getIn(['meta', 'small', 'width']);
const originalUrl = attachment.get('url');
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
const srcSet = `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`;
const sizes = `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`;
thumbnail = ( thumbnail = (
<a // eslint-disable-line jsx-a11y/anchor-has-content <a
className='media-gallery__item-thumbnail' className='media-gallery__item-thumbnail'
href={attachment.get('remote_url') || attachment.get('url')} href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick} onClick={this.handleClick}
target='_blank' target='_blank'
style={{ backgroundImage: `url(${attachment.get('preview_url')})` }} >
/> <img src={previewUrl} srcSet={srcSet} sizes={sizes} alt='' />
</a>
); );
} else if (attachment.get('type') === 'gifv') { } else if (attachment.get('type') === 'gifv') {
const autoPlay = !isIOS() && this.props.autoPlayGif; const autoPlay = !isIOS() && this.props.autoPlayGif;

View file

@ -25,7 +25,7 @@ export default class Permalink extends React.PureComponent {
const { href, children, className, ...other } = this.props; const { href, children, className, ...other } = this.props;
return ( return (
<a href={href} onClick={this.handleClick} {...other} className={'permalink ' + className}> <a href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
{children} {children}
</a> </a>
); );

View file

@ -149,7 +149,7 @@ class StatusUnextended extends ImmutablePureComponent {
saveHeight = () => { saveHeight = () => {
if (this.node && this.node.children.length !== 0) { if (this.node && this.node.children.length !== 0) {
this.height = this.node.clientHeight; this.height = this.node.getBoundingClientRect().height;
} }
} }

View file

@ -88,7 +88,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
handleReport = () => { handleReport = () => {
this.props.onReport(this.props.status); this.props.onReport(this.props.status);
this.context.router.history.push('/report');
} }
handleConversationMuteClick = () => { handleConversationMuteClick = () => {

View file

@ -38,7 +38,6 @@ export default class Header extends ImmutablePureComponent {
handleReport = () => { handleReport = () => {
this.props.onReport(this.props.account); this.props.onReport(this.props.account);
this.context.router.history.push('/report');
} }
handleMute = () => { handleMute = () => {

View file

@ -67,6 +67,12 @@ export default class ComposeForm extends ImmutablePureComponent {
} }
handleSubmit = () => { handleSubmit = () => {
if (this.props.text !== this.autosuggestTextarea.textarea.value) {
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// Update the state to match the current text
this.props.onChange(this.autosuggestTextarea.textarea.value);
}
this.props.onSubmit(); this.props.onSubmit();
} }

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites'; import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
@ -14,7 +15,9 @@ const messages = defineMessages({
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
loaded: state.getIn(['status_lists', 'favourites', 'loaded']), loaded: state.getIn(['status_lists', 'favourites', 'loaded']),
me: state.getIn(['meta', 'me']),
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@ -23,8 +26,10 @@ export default class Favourites extends ImmutablePureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
loaded: PropTypes.bool, loaded: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
me: PropTypes.number.isRequired,
}; };
componentWillMount () { componentWillMount () {

View file

@ -13,6 +13,7 @@ import ColumnSettingsContainer from './containers/column_settings_container';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import Immutable from 'immutable'; import Immutable from 'immutable';
import LoadMore from '../../components/load_more'; import LoadMore from '../../components/load_more';
import { debounce } from 'lodash';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' }, title: { id: 'column.notifications', defaultMessage: 'Notifications' },
@ -50,19 +51,27 @@ export default class Notifications extends React.PureComponent {
trackScroll: true, trackScroll: true,
}; };
dispatchExpandNotifications = debounce(() => {
this.props.dispatch(expandNotifications());
}, 300, { leading: true });
dispatchScrollToTop = debounce((top) => {
this.props.dispatch(scrollTopNotifications(top));
}, 100);
handleScroll = (e) => { handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target; const { scrollTop, scrollHeight, clientHeight } = e.target;
const offset = scrollHeight - scrollTop - clientHeight; const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop; this._oldScrollPosition = scrollHeight - scrollTop;
if (250 > offset && !this.props.isLoading) { if (250 > offset && this.props.hasMore && !this.props.isLoading) {
if (this.props.hasMore) { this.dispatchExpandNotifications();
this.props.dispatch(expandNotifications()); }
}
} else if (scrollTop < 100) { if (scrollTop < 100) {
this.props.dispatch(scrollTopNotifications(true)); this.dispatchScrollToTop(true);
} else { } else {
this.props.dispatch(scrollTopNotifications(false)); this.dispatchScrollToTop(false);
} }
} }
@ -74,7 +83,7 @@ export default class Notifications extends React.PureComponent {
handleLoadMore = (e) => { handleLoadMore = (e) => {
e.preventDefault(); e.preventDefault();
this.props.dispatch(expandNotifications()); this.dispatchExpandNotifications();
} }
handlePin = () => { handlePin = () => {

View file

@ -56,7 +56,6 @@ export default class ActionBar extends React.PureComponent {
handleReport = () => { handleReport = () => {
this.props.onReport(this.props.status); this.props.onReport(this.props.status);
this.context.router.history.push('/report');
} }
render () { render () {

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
export default class ImageLoader extends React.PureComponent { export default class ImageLoader extends React.PureComponent {
@ -20,46 +21,121 @@ export default class ImageLoader extends React.PureComponent {
error: false, error: false,
} }
componentWillMount() { removers = [];
this._loadImage(this.props.src);
get canvasContext() {
if (!this.canvas) {
return null;
}
this._canvasContext = this._canvasContext || this.canvas.getContext('2d');
return this._canvasContext;
} }
componentWillReceiveProps(props) { componentDidMount () {
this._loadImage(props.src); this.loadImage(this.props);
} }
_loadImage(src) { componentWillReceiveProps (nextProps) {
if (this.props.src !== nextProps.src) {
this.loadImage(nextProps);
}
}
loadImage (props) {
this.removeEventListeners();
this.setState({ loading: true, error: false });
Promise.all([
this.loadPreviewCanvas(props),
this.loadOriginalImage(props),
])
.then(() => {
this.setState({ loading: false, error: false });
this.clearPreviewCanvas();
})
.catch(() => this.setState({ loading: false, error: true }));
}
loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => {
const image = new Image(); const image = new Image();
const removeEventListeners = () => {
image.removeEventListener('error', handleError);
image.removeEventListener('load', handleLoad);
};
const handleError = () => {
removeEventListeners();
reject();
};
const handleLoad = () => {
removeEventListeners();
this.canvasContext.drawImage(image, 0, 0, width, height);
resolve();
};
image.addEventListener('error', handleError);
image.addEventListener('load', handleLoad);
image.src = previewSrc;
this.removers.push(removeEventListeners);
})
image.onerror = () => this.setState({ loading: false, error: true }); clearPreviewCanvas () {
image.onload = () => this.setState({ loading: false, error: false }); const { width, height } = this.canvas;
this.canvasContext.clearRect(0, 0, width, height);
image.src = src;
this.setState({ loading: true });
} }
render() { loadOriginalImage = ({ src }) => new Promise((resolve, reject) => {
const { alt, src, previewSrc, width, height } = this.props; const image = new Image();
const removeEventListeners = () => {
image.removeEventListener('error', handleError);
image.removeEventListener('load', handleLoad);
};
const handleError = () => {
removeEventListeners();
reject();
};
const handleLoad = () => {
removeEventListeners();
resolve();
};
image.addEventListener('error', handleError);
image.addEventListener('load', handleLoad);
image.src = src;
this.removers.push(removeEventListeners);
});
removeEventListeners () {
this.removers.forEach(listeners => listeners());
this.removers = [];
}
setCanvasRef = c => {
this.canvas = c;
}
render () {
const { alt, src, width, height } = this.props;
const { loading } = this.state; const { loading } = this.state;
const className = classNames('image-loader', {
'image-loader--loading': loading,
});
return ( return (
<div className='image-loader'> <div className={className}>
<img <canvas
alt={alt} className='image-loader__preview-canvas'
className='image-loader__img'
src={src}
width={width} width={width}
height={height} height={height}
ref={this.setCanvasRef}
/> />
{loading && {!loading && (
<img <img
alt='' alt={alt}
src={previewSrc} className='image-loader__img'
className='image-loader__preview-img' src={src}
width={width}
height={height}
/> />
} )}
</div> </div>
); );
} }

View file

@ -5,6 +5,7 @@ import OnboardingModal from './onboarding_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 ReportModal from './report_modal';
import TransitionMotion from 'react-motion/lib/TransitionMotion'; import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring'; import spring from 'react-motion/lib/spring';
@ -14,6 +15,7 @@ const MODAL_COMPONENTS = {
'VIDEO': VideoModal, 'VIDEO': VideoModal,
'BOOST': BoostModal, 'BOOST': BoostModal,
'CONFIRM': ConfirmationModal, 'CONFIRM': ConfirmationModal,
'REPORT': ReportModal,
}; };
export default class ModalRoot extends React.PureComponent { export default class ModalRoot extends React.PureComponent {

View file

@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ReactSwipeable from 'react-swipeable';
import classNames from 'classnames'; import classNames from 'classnames';
import Permalink from '../../../components/permalink'; import Permalink from '../../../components/permalink';
import TransitionMotion from 'react-motion/lib/TransitionMotion'; import TransitionMotion from 'react-motion/lib/TransitionMotion';
@ -274,7 +275,7 @@ export default class OnboardingModal extends React.PureComponent {
<div className='modal-root__modal onboarding-modal'> <div className='modal-root__modal onboarding-modal'>
<TransitionMotion styles={styles}> <TransitionMotion styles={styles}>
{interpolatedStyles => ( {interpolatedStyles => (
<div className='onboarding-modal__pager'> <ReactSwipeable onSwipedRight={this.handlePrev} onSwipedLeft={this.handleNext} className='onboarding-modal__pager'>
{interpolatedStyles.map(({ key, data, style }, i) => { {interpolatedStyles.map(({ key, data, style }, i) => {
const className = classNames('onboarding-modal__page__wrapper', { const className = classNames('onboarding-modal__page__wrapper', {
'onboarding-modal__page__wrapper--active': i === currentIndex, 'onboarding-modal__page__wrapper--active': i === currentIndex,
@ -283,7 +284,7 @@ export default class OnboardingModal extends React.PureComponent {
<div key={key} style={style} className={className}>{data}</div> <div key={key} style={style} className={className}>{data}</div>
); );
})} })}
</div> </ReactSwipeable>
)} )}
</TransitionMotion> </TransitionMotion>

View file

@ -1,19 +1,17 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { changeReportComment, submitReport } from '../../actions/reports'; import { changeReportComment, submitReport } from '../../../actions/reports';
import { refreshAccountTimeline } from '../../actions/timelines'; import { refreshAccountTimeline } from '../../../actions/timelines';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import Column from '../ui/components/column'; import { makeGetAccount } from '../../../selectors';
import Button from '../../components/button';
import { makeGetAccount } from '../../selectors';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import StatusCheckBox from './containers/status_check_box_container'; import StatusCheckBox from '../../report/containers/status_check_box_container';
import Immutable from 'immutable'; import Immutable from 'immutable';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Button from '../../../components/button';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'report.heading', defaultMessage: 'New report' },
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' }, placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
submit: { id: 'report.submit', defaultMessage: 'Submit' }, submit: { id: 'report.submit', defaultMessage: 'Submit' },
}); });
@ -37,11 +35,7 @@ const makeMapStateToProps = () => {
@connect(makeMapStateToProps) @connect(makeMapStateToProps)
@injectIntl @injectIntl
export default class Report extends React.PureComponent { export default class ReportModal extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
isSubmitting: PropTypes.bool, isSubmitting: PropTypes.bool,
@ -52,17 +46,15 @@ export default class Report extends React.PureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
componentWillMount () { handleCommentChange = (e) => {
if (!this.props.account) { this.props.dispatch(changeReportComment(e.target.value));
this.context.router.history.replace('/'); }
}
handleSubmit = () => {
this.props.dispatch(submitReport());
} }
componentDidMount () { componentDidMount () {
if (!this.props.account) {
return;
}
this.props.dispatch(refreshAccountTimeline(this.props.account.get('id'))); this.props.dispatch(refreshAccountTimeline(this.props.account.get('id')));
} }
@ -72,15 +64,6 @@ export default class Report extends React.PureComponent {
} }
} }
handleCommentChange = (e) => {
this.props.dispatch(changeReportComment(e.target.value));
}
handleSubmit = () => {
this.props.dispatch(submitReport());
this.context.router.history.replace('/');
}
render () { render () {
const { account, comment, intl, statusIds, isSubmitting } = this.props; const { account, comment, intl, statusIds, isSubmitting } = this.props;
@ -89,36 +72,33 @@ export default class Report extends React.PureComponent {
} }
return ( return (
<Column heading={intl.formatMessage(messages.heading)} icon='flag'> <div className='modal-root__modal report-modal'>
<ColumnBackButtonSlim /> <div className='report-modal__target'>
<FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} />
</div>
<div className='report scrollable'> <div className='report-modal__container'>
<div className='report__target'> <div className='report-modal__statuses'>
<FormattedMessage id='report.target' defaultMessage='Reporting' />
<strong>{account.get('acct')}</strong>
</div>
<div className='scrollable report__statuses'>
<div> <div>
{statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)} {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}
</div> </div>
</div> </div>
<div className='report__textarea-wrapper'> <div className='report-modal__comment'>
<textarea <textarea
className='report__textarea' className='setting-text light'
placeholder={intl.formatMessage(messages.placeholder)} placeholder={intl.formatMessage(messages.placeholder)}
value={comment} value={comment}
onChange={this.handleCommentChange} onChange={this.handleCommentChange}
disabled={isSubmitting} disabled={isSubmitting}
/> />
<div className='report__submit'>
<div className='report__submit-button'><Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /></div>
</div>
</div> </div>
</div> </div>
</Column>
<div className='report-modal__action-bar'>
<Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} />
</div>
</div>
); );
} }

View file

@ -15,7 +15,6 @@ import { refreshHomeTimeline } from '../../actions/timelines';
import { refreshNotifications } from '../../actions/notifications'; import { refreshNotifications } from '../../actions/notifications';
import UploadArea from './components/upload_area'; import UploadArea from './components/upload_area';
import ColumnsAreaContainer from './containers/columns_area_container'; import ColumnsAreaContainer from './containers/columns_area_container';
import Status from '../../features/status'; import Status from '../../features/status';
import GettingStarted from '../../features/getting_started'; import GettingStarted from '../../features/getting_started';
import PublicTimeline from '../../features/public_timeline'; import PublicTimeline from '../../features/public_timeline';
@ -35,7 +34,6 @@ import GenericNotFound from '../../features/generic_not_found';
import FavouritedStatuses from '../../features/favourited_statuses'; import FavouritedStatuses from '../../features/favourited_statuses';
import Blocks from '../../features/blocks'; import Blocks from '../../features/blocks';
import Mutes from '../../features/mutes'; import Mutes from '../../features/mutes';
import Report from '../../features/report';
// Small wrapper to pass multiColumn to the route components // Small wrapper to pass multiColumn to the route components
const WrappedSwitch = ({ multiColumn, children }) => ( const WrappedSwitch = ({ multiColumn, children }) => (
@ -222,7 +220,6 @@ export default class UI extends React.PureComponent {
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
<WrappedRoute path='/blocks' component={Blocks} content={children} /> <WrappedRoute path='/blocks' component={Blocks} content={children} />
<WrappedRoute path='/mutes' component={Mutes} content={children} /> <WrappedRoute path='/mutes' component={Mutes} content={children} />
<WrappedRoute path='/report' component={Report} content={children} />
<WrappedRoute component={GenericNotFound} content={children} /> <WrappedRoute component={GenericNotFound} content={children} />
</WrappedSwitch> </WrappedSwitch>

View file

@ -1151,6 +1151,23 @@
], ],
"path": "app/javascript/mastodon/features/ui/components/onboarding_modal.json" "path": "app/javascript/mastodon/features/ui/components/onboarding_modal.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Additional comments",
"id": "report.placeholder"
},
{
"defaultMessage": "Submit",
"id": "report.submit"
},
{
"defaultMessage": "Report {target}",
"id": "report.target"
}
],
"path": "app/javascript/mastodon/features/ui/components/report_modal.json"
},
{ {
"descriptors": [ "descriptors": [
{ {

View file

@ -140,10 +140,10 @@
"privacy.unlisted.long": "Do not post to public timelines", "privacy.unlisted.long": "Do not post to public timelines",
"privacy.unlisted.short": "Unlisted", "privacy.unlisted.short": "Unlisted",
"reply_indicator.cancel": "Cancel", "reply_indicator.cancel": "Cancel",
"report.heading": "New report", "report.heading": "Report {target}",
"report.placeholder": "Additional comments", "report.placeholder": "Additional comments",
"report.submit": "Submit", "report.submit": "Submit",
"report.target": "Reporting", "report.target": "Reporting {target}",
"search.placeholder": "Search", "search.placeholder": "Search",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}", "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",

View file

@ -27,8 +27,8 @@
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.public": "Fil public global", "column.public": "Fil public global",
"column_back_button.label": "Retour", "column_back_button.label": "Retour",
"column_header.pin": "Pin", "column_header.pin": "Épingler",
"column_header.unpin": "Unpin", "column_header.unpin": "Retirer",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Paramètres", "column_subheading.settings": "Paramètres",
"compose_form.lock_disclaimer": "Votre compte n'est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.", "compose_form.lock_disclaimer": "Votre compte n'est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.",
@ -101,7 +101,7 @@
"notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?", "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
"notifications.column_settings.alert": "Notifications locales", "notifications.column_settings.alert": "Notifications locales",
"notifications.column_settings.favourite": "Favoris :", "notifications.column_settings.favourite": "Favoris :",
"notifications.column_settings.follow": "Nouveaux abonné⋅e⋅s :", "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :",
"notifications.column_settings.mention": "Mentions :", "notifications.column_settings.mention": "Mentions :",
"notifications.column_settings.reblog": "Partages :", "notifications.column_settings.reblog": "Partages :",
"notifications.column_settings.show": "Afficher dans la colonne", "notifications.column_settings.show": "Afficher dans la colonne",

View file

@ -136,10 +136,10 @@
"privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu", "privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu",
"privacy.unlisted.short": "Niewidoczne", "privacy.unlisted.short": "Niewidoczne",
"reply_indicator.cancel": "Anuluj", "reply_indicator.cancel": "Anuluj",
"report.heading": "Nowe zgłoszenie", "report.heading": "Zgłoś {target}",
"report.placeholder": "Dodatkowe komentarze", "report.placeholder": "Dodatkowe komentarze",
"report.submit": "Wyślij", "report.submit": "Wyślij",
"report.target": "Zgłaszanie", "report.target": "Zgłaszanie {target}",
"search.placeholder": "Szukaj", "search.placeholder": "Szukaj",
"search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}", "search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
"status.cannot_reblog": "Ten post nie może zostać podbity", "status.cannot_reblog": "Ten post nie może zostać podbity",

View file

@ -129,6 +129,11 @@
color: $ui-primary-color; color: $ui-primary-color;
} }
} }
.positive-hint {
color: $valid-value-color;
font-weight: 500;
}
} }
.simple_form { .simple_form {

View file

@ -58,37 +58,6 @@
position: relative; position: relative;
} }
.column-collapsable {
position: relative;
.column-collapsable__content {
overflow: auto;
transition: 300ms ease;
opacity: 1;
max-height: 70vh;
}
&.collapsed .column-collapsable__content {
height: 0 !important;
opacity: 0;
}
.column-collapsable__button {
color: $primary-text-color;
background: lighten($ui-base-color, 8%);
&:hover {
color: $primary-text-color;
background: lighten($ui-base-color, 8%);
}
}
&.collapsed .column-collapsable__button {
color: $ui-primary-color;
background: lighten($ui-base-color, 4%);
}
}
.column-icon { .column-icon {
background: lighten($ui-base-color, 4%); background: lighten($ui-base-color, 4%);
color: $ui-primary-color; color: $ui-primary-color;
@ -670,13 +639,15 @@
} }
.status-check-box { .status-check-box {
border-bottom: 1px solid lighten($ui-base-color, 8%); border-bottom: 1px solid $ui-secondary-color;
display: flex; display: flex;
.status__content { .status__content {
background: lighten($ui-base-color, 4%);
flex: 1 1 auto; flex: 1 1 auto;
padding: 10px; padding: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
@ -1233,20 +1204,22 @@
.image-loader { .image-loader {
position: relative; position: relative;
}
.image-loader__preview-img { &.image-loader--loading {
position: absolute; .image-loader__preview-canvas {
top: 0; filter: blur(2px);
left: 0; }
width: 100%; }
height: 100%;
filter: blur(2px);
}
.media-modal img.image-loader__preview-img { .image-loader__img {
width: 100%; position: absolute;
height: 100%; top: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
background-image: none;
}
} }
.navigation-bar { .navigation-bar {
@ -1980,6 +1953,17 @@
@include limited-single-column('screen and (max-width: 600px)') { @include limited-single-column('screen and (max-width: 600px)') {
font-size: 16px; font-size: 16px;
} }
&.light {
color: $ui-base-color;
border-bottom: 2px solid lighten($ui-base-color, 27%);
&:focus,
&:active {
color: $ui-base-color;
border-bottom-color: $ui-highlight-color;
}
}
} }
@import 'boost'; @import 'boost';
@ -2231,11 +2215,6 @@ button.icon-button.active i.fa-retweet {
transition: max-height 150ms ease-in-out, opacity 300ms linear; transition: max-height 150ms ease-in-out, opacity 300ms linear;
opacity: 1; opacity: 1;
& > div {
background: lighten($ui-base-color, 8%);
padding: 15px;
}
&.collapsed { &.collapsed {
max-height: 0; max-height: 0;
opacity: 0.5; opacity: 0.5;
@ -2246,6 +2225,11 @@ button.icon-button.active i.fa-retweet {
} }
} }
.column-header__collapsible-inner {
background: lighten($ui-base-color, 8%);
padding: 15px;
}
.column-header__setting-btn { .column-header__setting-btn {
&:hover { &:hover {
color: lighten($ui-primary-color, 4%); color: lighten($ui-primary-color, 4%);
@ -2437,67 +2421,6 @@ button.icon-button.active i.fa-retweet {
vertical-align: middle; vertical-align: middle;
} }
.report.scrollable {
box-sizing: border-box;
display: flex;
flex-direction: column;
max-height: 100%;
}
.report__target {
border-bottom: 1px solid lighten($ui-base-color, 4%);
color: $ui-secondary-color;
flex: 0 0 auto;
padding: 10px;
strong {
display: block;
color: $primary-text-color;
font-weight: 500;
}
}
.report__statuses {
flex: 1 1 auto;
}
.report__textarea-wrapper {
flex: 0 0 100px;
padding: 10px;
}
.report__textarea {
background: transparent;
box-sizing: border-box;
border: 0;
border-bottom: 2px solid $ui-primary-color;
border-radius: 2px 2px 0 0;
color: $primary-text-color;
display: block;
font-family: inherit;
font-size: 14px;
margin-bottom: 10px;
outline: 0;
padding: 7px 4px;
resize: vertical;
width: 100%;
&:active,
&:focus {
border-bottom-color: $ui-highlight-color;
background: rgba($base-overlay-background, 0.1);
}
}
.report__submit {
margin-top: 10px;
overflow: hidden;
}
.report__submit-button {
float: right;
}
.empty-column-indicator { .empty-column-indicator {
color: lighten($ui-base-color, 20%); color: lighten($ui-base-color, 20%);
background: $ui-base-color; background: $ui-base-color;
@ -3086,6 +3009,7 @@ button.icon-button.active i.fa-retweet {
position: relative; position: relative;
img, img,
canvas,
video { video {
max-width: 80vw; max-width: 80vw;
max-height: 80vh; max-height: 80vh;
@ -3093,7 +3017,8 @@ button.icon-button.active i.fa-retweet {
height: auto; height: auto;
} }
img { img,
canvas {
display: block; display: block;
background: url('../images/void.png') repeat; background: url('../images/void.png') repeat;
} }
@ -3279,6 +3204,7 @@ button.icon-button.active i.fa-retweet {
@media screen and (max-width: 400px) { @media screen and (max-width: 400px) {
.onboarding-modal__page-one { .onboarding-modal__page-one {
flex-direction: column; flex-direction: column;
align-items: normal;
} }
.onboarding-modal__page-one__elephant-friend { .onboarding-modal__page-one__elephant-friend {
@ -3393,7 +3319,8 @@ button.icon-button.active i.fa-retweet {
} }
.boost-modal, .boost-modal,
.confirmation-modal { .confirmation-modal,
.report-modal {
background: lighten($ui-secondary-color, 8%); background: lighten($ui-secondary-color, 8%);
color: $ui-base-color; color: $ui-base-color;
border-radius: 8px; border-radius: 8px;
@ -3429,7 +3356,8 @@ button.icon-button.active i.fa-retweet {
} }
.boost-modal__action-bar, .boost-modal__action-bar,
.confirmation-modal__action-bar { .confirmation-modal__action-bar,
.report-modal__action-bar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
background: $ui-secondary-color; background: $ui-secondary-color;
@ -3465,6 +3393,23 @@ button.icon-button.active i.fa-retweet {
} }
} }
.report-modal__statuses,
.report-modal__comment {
padding: 10px;
}
.report-modal__statuses {
min-height: 20vh;
overflow-y: auto;
overflow-x: hidden;
}
.report-modal__comment {
.setting-text {
margin-top: 10px;
}
}
.confirmation-modal__action-bar { .confirmation-modal__action-bar {
.confirmation-modal__cancel-button { .confirmation-modal__cancel-button {
background-color: transparent; background-color: transparent;
@ -3480,7 +3425,8 @@ button.icon-button.active i.fa-retweet {
} }
} }
.confirmation-modal__container { .confirmation-modal__container,
.report-modal__target {
padding: 30px; padding: 30px;
font-size: 16px; font-size: 16px;
text-align: center; text-align: center;
@ -3601,10 +3547,15 @@ button.icon-button.active i.fa-retweet {
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
cursor: zoom-in; cursor: zoom-in;
display: block; display: flex;
height: 100%; align-items: center;
text-decoration: none; text-decoration: none;
width: 100%; height: 100%;
&,
img {
width: 100%;
}
} }
.media-gallery__gifv { .media-gallery__gifv {

View file

@ -358,7 +358,6 @@ code {
} }
.user_filtered_languages { .user_filtered_languages {
& > label { & > label {
font-family: inherit; font-family: inherit;
font-size: 16px; font-size: 16px;

View file

@ -10,7 +10,6 @@
.recovery-codes { .recovery-codes {
list-style: none; list-style: none;
margin: 0 auto; margin: 0 auto;
text-align: center;
li { li {
font-size: 125%; font-size: 125%;

View file

@ -42,6 +42,18 @@
strong { strong {
font-weight: 500; font-weight: 500;
} }
&.inline-table {
td,
th {
padding: 8px 0;
}
& > tbody > tr:nth-child(odd) > td,
& > tbody > tr:nth-child(odd) > th {
background: transparent;
}
}
} }
samp { samp {

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class AdminMailer < ApplicationMailer
def new_report(recipient, report)
@report = report
@me = recipient
@instance = Rails.configuration.x.local_domain
locale_for_account(@me) do
mail to: @me.user_email, subject: I18n.t('admin_mailer.new_report.subject', instance: @instance, id: @report.id)
end
end
end

View file

@ -4,4 +4,12 @@ class ApplicationMailer < ActionMailer::Base
default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' } default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' }
layout 'mailer' layout 'mailer'
helper :instance helper :instance
protected
def locale_for_account(account)
I18n.with_locale(account.user_locale || I18n.default_locale) do
yield
end
end
end end

View file

@ -67,12 +67,4 @@ class NotificationMailer < ApplicationMailer
) )
end end
end end
private
def locale_for_account(account)
I18n.with_locale(account.user_locale || I18n.default_locale) do
yield
end
end
end end

View file

@ -3,36 +3,78 @@
# #
# Table name: session_activations # Table name: session_activations
# #
# id :integer not null, primary key # id :integer not null, primary key
# user_id :integer not null # user_id :integer not null
# session_id :string not null # session_id :string not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# user_agent :string default(""), not null
# ip :inet
# access_token_id :integer
# #
class SessionActivation < ApplicationRecord class SessionActivation < ApplicationRecord
LIMIT = Rails.configuration.x.max_session_activations belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy
def self.active?(id) delegate :token,
id && where(session_id: id).exists? to: :access_token,
allow_nil: true
def detection
@detection ||= Browser.new(user_agent)
end end
def self.activate(id) def browser
activation = create!(session_id: id) detection.id
purge_old
activation
end end
def self.deactivate(id) def platform
return unless id detection.platform.id
where(session_id: id).destroy_all
end end
def self.purge_old before_create :assign_access_token
order('created_at desc').offset(LIMIT).destroy_all before_save :assign_user_agent
class << self
def active?(id)
id && where(session_id: id).exists?
end
def activate(options = {})
activation = create!(options)
purge_old
activation
end
def deactivate(id)
return unless id
where(session_id: id).destroy_all
end
def purge_old
order('created_at desc').offset(Rails.configuration.x.max_session_activations).destroy_all
end
def exclusive(id)
where('session_id != ?', id).destroy_all
end
end end
def self.exclusive(id) private
where('session_id != ?', id).destroy_all
def assign_user_agent
self.user_agent = '' if user_agent.nil?
end
def assign_access_token
superapp = Doorkeeper::Application.find_by(superapp: true)
return if superapp.nil?
self.access_token = Doorkeeper::AccessToken.create!(application_id: superapp.id,
resource_owner_id: user_id,
scopes: 'read write follow',
expires_in: Doorkeeper.configuration.access_token_expires_in,
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?)
end end
end end

View file

@ -91,8 +91,10 @@ class User < ApplicationRecord
settings.auto_play_gif settings.auto_play_gif
end end
def activate_session def activate_session(request)
session_activations.activate(SecureRandom.hex).session_id session_activations.activate(session_id: SecureRandom.hex,
user_agent: request.user_agent,
ip: request.ip).session_id
end end
def exclusive_session(id) def exclusive_session(id)

View file

@ -13,7 +13,8 @@ class SendInteractionService < BaseService
return if block_notification? return if block_notification?
envelope = salmon.pack(@xml, @source_account.keypair) envelope = salmon.pack(@xml, @source_account.keypair)
salmon.post(@target_account.salmon_url, envelope) delivery = salmon.post(@target_account.salmon_url, envelope)
raise "Delivery failed for #{target_account.salmon_url}: HTTP #{delivery.code}" unless delivery.code > 199 && delivery.code < 300
end end
private private

View file

@ -0,0 +1,5 @@
<%= display_name(@me) %>,
<%= raw t('admin_mailer.new_report.body', target: @report.target_account.acct, reporter: @report.account.acct) %>
<%= raw t('application_mailer.view')%> <%= admin_report_url(@report) %>

View file

@ -0,0 +1,23 @@
%h6= t 'sessions.title'
%p.muted-hint= t 'sessions.explanation'
%table.table.inline-table
%thead
%tr
%th= t 'sessions.browser'
%th= t 'sessions.ip'
%th= t 'sessions.activity'
%tbody
- @sessions.each do |session|
%tr
%td
%span{ title: session.user_agent }= fa_icon session_device_icon(session)
= ' '
= t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}")
%td
%samp= session.ip
%td
- if request.session['auth_id'] == session.session_id
= t 'sessions.current_session'
- else
%time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at)

View file

@ -12,6 +12,10 @@
.actions .actions
= f.button :button, t('generic.save_changes'), type: :submit = f.button :button, t('generic.save_changes'), type: :submit
%hr/
= render 'sessions'
- if open_deletion? - if open_deletion?
%hr/ %hr/

View file

@ -1,7 +1,7 @@
- content_for :page_title do - content_for :page_title do
= t('settings.two_factor_authentication') = t('settings.two_factor_authentication')
%p.hint= t('two_factor_authentication.recovery_instructions') %p.hint= t('two_factor_authentication.recovery_instructions_html')
%ol.recovery-codes %ol.recovery-codes
- @recovery_codes.each do |code| - @recovery_codes.each do |code|

View file

@ -1,26 +1,34 @@
- content_for :page_title do - content_for :page_title do
= t('settings.two_factor_authentication') = t('settings.two_factor_authentication')
.simple_form - if current_user.otp_required_for_login
%p.hint %p.positive-hint
= t('two_factor_authentication.description_html') = fa_icon 'check'
= ' '
= t 'two_factor_authentication.enabled'
%hr/
= simple_form_for @confirmation, url: settings_two_factor_authentication_path, method: :delete do |f|
= f.input :code, hint: t('two_factor_authentication.code_hint'), placeholder: t('simple_form.labels.defaults.otp_attempt')
.actions
= f.button :button, t('two_factor_authentication.disable'), type: :submit
%hr/
%h6= t('two_factor_authentication.recovery_codes')
%p.muted-hint
= t('two_factor_authentication.lost_recovery_codes')
= link_to t('two_factor_authentication.generate_recovery_codes'),
settings_two_factor_authentication_recovery_codes_path,
data: { method: :post }
- else
.simple_form
%p.hint= t('two_factor_authentication.description_html')
- if current_user.otp_required_for_login
= link_to t('two_factor_authentication.disable'),
settings_two_factor_authentication_path,
data: { method: :delete },
class: 'block-button'
- else
= link_to t('two_factor_authentication.setup'), = link_to t('two_factor_authentication.setup'),
settings_two_factor_authentication_path, settings_two_factor_authentication_path,
data: { method: :post }, data: { method: :post },
class: 'block-button' class: 'block-button'
- if current_user.otp_required_for_login
.simple_form
%p.hint
= t('two_factor_authentication.lost_recovery_codes')
= link_to t('two_factor_authentication.generate_recovery_codes'),
settings_two_factor_authentication_recovery_codes_path,
data: { method: :post },
class: 'block-button'

View file

@ -1,3 +1,3 @@
<p>Hello <%= @resource.email %>!</p> <p>Hello <%= @resource.email %>!</p>
<p>We're contacting you to notify you that your password on Mastodon has been changed.</p> <p>We're contacting you to notify you that your password on <%= @instance %> has been changed.</p>

View file

@ -1,3 +1,3 @@
Hello <%= @resource.email %>! Hello <%= @resource.email %>!
We're contacting you to notify you that your password on Mastodon has been changed. We're contacting you to notify you that your password on <%= @instance %> has been changed.

View file

@ -1,6 +1,6 @@
<p>Hello <%= @resource.email %>!</p> <p>Hello <%= @resource.email %>!</p>
<p>Someone has requested a link to change your password on Mastodon. You can do this through the link below.</p> <p>Someone has requested a link to change your password on <%= @instance %>. You can do this through the link below.</p>
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p> <p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>

View file

@ -1,6 +1,6 @@
Hello <%= @resource.email %>! Hello <%= @resource.email %>!
Someone has requested a link to change your password on Mastodon. You can do this through the link below. Someone has requested a link to change your password on <%= @instance %>. You can do this through the link below.
<%= edit_password_url(@resource, reset_password_token: @token) %> <%= edit_password_url(@resource, reset_password_token: @token) %>

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'sidekiq-scheduler'
class Scheduler::DoorkeeperCleanupScheduler
include Sidekiq::Worker
def perform
Doorkeeper::AccessToken.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all
Doorkeeper::AccessGrant.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all
end
end

View file

@ -7,6 +7,10 @@ default: &default
development: development:
<<: *default <<: *default
database: mastodon_development database: mastodon_development
username: <%= ENV['DB_USER'] %>
password: <%= ENV['DB_PASS'] %>
host: <%= ENV['DB_HOST'] %>
port: <%= ENV['DB_PORT'] %>
# Warning: The database defined as "test" will be erased and # Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake". # re-generated from your development database when you run "rake".
@ -14,6 +18,10 @@ development:
test: test:
<<: *default <<: *default
database: mastodon_test<%= ENV['TEST_ENV_NUMBER'] %> database: mastodon_test<%= ENV['TEST_ENV_NUMBER'] %>
username: <%= ENV['DB_USER'] %>
password: <%= ENV['DB_PASS'] %>
host: <%= ENV['DB_HOST'] %>
port: <%= ENV['DB_PORT'] %>
production: production:
<<: *default <<: *default

View file

@ -1,6 +1,6 @@
Warden::Manager.after_set_user except: :fetch do |user, warden| Warden::Manager.after_set_user except: :fetch do |user, warden|
SessionActivation.deactivate warden.raw_session['auth_id'] SessionActivation.deactivate warden.raw_session['auth_id']
warden.raw_session['auth_id'] = user.activate_session warden.raw_session['auth_id'] = user.activate_session(warden.request)
end end
Warden::Manager.after_fetch do |user, warden| Warden::Manager.after_fetch do |user, warden|

View file

@ -360,7 +360,7 @@ ca:
lost_recovery_codes: Els codis de recuperació et permeten recuperar l'accés al teu compte si perds el telèfon. Si has perdut els teus codis de recuperació els pots regenerar aquí. Els codis de recuperació anteriors seran anul·lats. lost_recovery_codes: Els codis de recuperació et permeten recuperar l'accés al teu compte si perds el telèfon. Si has perdut els teus codis de recuperació els pots regenerar aquí. Els codis de recuperació anteriors seran anul·lats.
manual_instructions: 'Si no pots escanejar el codi QR code i necessites introduir-lo manualment, aquí tens el secret en text plà:' manual_instructions: 'Si no pots escanejar el codi QR code i necessites introduir-lo manualment, aquí tens el secret en text plà:'
recovery_codes_regenerated: Codis de recuperació regenerats amb èxit recovery_codes_regenerated: Codis de recuperació regenerats amb èxit
recovery_instructions: Si alguna vegada perds l'accéss al telèfon pots utilitzar un dels codis de recuperació a continuació per recuperar l'accés al teu compte. Cal mantenir els codis de recuperació en lloc segur, per exemple imprimint-los i guardar-los amb altres documents importants. recovery_instructions_html: Si alguna vegada perds l'accéss al telèfon pots utilitzar un dels codis de recuperació a continuació per recuperar l'accés al teu compte. Cal mantenir els codis de recuperació en lloc segur, per exemple imprimint-los i guardar-los amb altres documents importants.
setup: Establir setup: Establir
wrong_code: El codi introduït es invalid! Es correcta la hora del servidor i del dispositiu? wrong_code: El codi introduït es invalid! Es correcta la hora del servidor i del dispositiu?
users: users:

View file

@ -304,7 +304,7 @@ de:
lost_recovery_codes: Wiederherstellungscodes erlauben dir, wieder den Zugang zu deinem Konto zu erlangen, falls du dein Telefon verlierst. Wenn du deine Wiederherstellungscodes verloren hast, kannst du sie hier regenerieren. Deine alten Wiederherstellungscodes werden damit ungültig gemacht. lost_recovery_codes: Wiederherstellungscodes erlauben dir, wieder den Zugang zu deinem Konto zu erlangen, falls du dein Telefon verlierst. Wenn du deine Wiederherstellungscodes verloren hast, kannst du sie hier regenerieren. Deine alten Wiederherstellungscodes werden damit ungültig gemacht.
manual_instructions: 'Wenn du den QR-Code nicht einlesen kannst und ihn manuell eingeben musst, ist hier das Klartext-Geheimnis:' manual_instructions: 'Wenn du den QR-Code nicht einlesen kannst und ihn manuell eingeben musst, ist hier das Klartext-Geheimnis:'
recovery_codes_regenerated: Wiederherstellungscodes erfolgreich regeneriert recovery_codes_regenerated: Wiederherstellungscodes erfolgreich regeneriert
recovery_instructions: Wenn du jemals den Zugang zu deinem Telefon verlierst, kannst du einen der Wiederherstellungscodes unten benutzen, um wieder auf dein Konto zugreifen zu können. Bewahre die Wiederherstellungscodes sicher auf, indem du sie beispielsweise ausdruckst und sie zusammen mit anderen wichtigen Dokumenten lagerst. recovery_instructions_html: Wenn du jemals den Zugang zu deinem Telefon verlierst, kannst du einen der Wiederherstellungscodes unten benutzen, um wieder auf dein Konto zugreifen zu können. Bewahre die Wiederherstellungscodes sicher auf, indem du sie beispielsweise ausdruckst und sie zusammen mit anderen wichtigen Dokumenten lagerst.
setup: Einrichten setup: Einrichten
wrong_code: Der eingegebene Code war ungültig! Sind die Server- und die Gerätezeit korrekt? wrong_code: Der eingegebene Code war ungültig! Sind die Server- und die Gerätezeit korrekt?
users: users:

View file

@ -3,8 +3,8 @@ pl:
devise: devise:
confirmations: confirmations:
confirmed: Twój adres e-mail został poprawnie zweryfikowany. confirmed: Twój adres e-mail został poprawnie zweryfikowany.
send_instructions: W ciągu kilku minut otrzymasz wiadomosć e-mail z instrukcją jak potwierdzić Twój adres e-mail. send_instructions: W ciągu kilku minut otrzymasz wiadomosć e-mail z instrukcją jak potwierdzić Twój adres e-mail. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
send_paranoid_instructions: Jeśli Twój adres e-mail już istnieje w naszej bazie danych, w ciągu kilku minut otrzymasz wiadomość e-mail z instrukcją jak potwierdzić Twój adres e-mail. send_paranoid_instructions: Jeśli Twój adres e-mail już istnieje w naszej bazie danych, w ciągu kilku minut otrzymasz wiadomość e-mail z instrukcją jak potwierdzić Twój adres e-mail. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
failure: failure:
already_authenticated: Jesteś już zalogowany/zalogowana. already_authenticated: Jesteś już zalogowany/zalogowana.
inactive: Twoje konto nie zostało jeszcze aktywowane. inactive: Twoje konto nie zostało jeszcze aktywowane.
@ -29,8 +29,8 @@ pl:
success: Uwierzytelnienie przez %{kind} powiodło się. success: Uwierzytelnienie przez %{kind} powiodło się.
passwords: passwords:
no_token: Dostęp do tej strony możliwy jest wyłącznie za pomocą odnośnika z e-maila z instrukcjami ustawienia nowego hasła. Jeśli skorzystałeś/aś z takiego odnośnika, upewnij się, że został wykorzystany/skopiowany cały odnośnik. no_token: Dostęp do tej strony możliwy jest wyłącznie za pomocą odnośnika z e-maila z instrukcjami ustawienia nowego hasła. Jeśli skorzystałeś/aś z takiego odnośnika, upewnij się, że został wykorzystany/skopiowany cały odnośnik.
send_instructions: W ciągu kilku minut otrzymasz wiadomość e-mail z instrukcją ustawienia nowego hasła. send_instructions: W ciągu kilku minut otrzymasz wiadomość e-mail z instrukcją ustawienia nowego hasła. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
send_paranoid_instructions: Jeśli Twój adres e-mail już istnieje w naszej bazie danych, w ciągu kilku minut otrzymasz wiadomość e-mail zawierającą odnośnik pozwalający na ustawienie nowego hasła. send_paranoid_instructions: Jeśli Twój adres e-mail już istnieje w naszej bazie danych, w ciągu kilku minut otrzymasz wiadomość e-mail zawierającą odnośnik pozwalający na ustawienie nowego hasła. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
updated: Twoje hasło zostało zmienione. Jesteś zalogowany/a. updated: Twoje hasło zostało zmienione. Jesteś zalogowany/a.
updated_not_active: Twoje hasło zostało zmienione. updated_not_active: Twoje hasło zostało zmienione.
registrations: registrations:
@ -38,16 +38,16 @@ pl:
signed_up: Twoje konto zostało utworzone. Witamy! signed_up: Twoje konto zostało utworzone. Witamy!
signed_up_but_inactive: Twoje konto zostało utworzone. Nie mogliśmy Cię jednak zalogować, ponieważ konto nie zostało jeszcze aktywowane. signed_up_but_inactive: Twoje konto zostało utworzone. Nie mogliśmy Cię jednak zalogować, ponieważ konto nie zostało jeszcze aktywowane.
signed_up_but_locked: Twoje konto zostało utworzone. Nie mogliśmy Cię jednak zalogować, ponieważ konto jest zablokowane. signed_up_but_locked: Twoje konto zostało utworzone. Nie mogliśmy Cię jednak zalogować, ponieważ konto jest zablokowane.
signed_up_but_unconfirmed: Na Twój adres e-mail została wysłana wiadomosć z odnośnikiem potwierdzającym. Kliknij w odnośnik aby aktywować konto. signed_up_but_unconfirmed: Na Twój adres e-mail została wysłana wiadomosć z odnośnikiem potwierdzającym. Kliknij w odnośnik aby aktywować konto. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
update_needs_confirmation: Konto zostało zaktualizowane, musimy jednak zweryfikować Twój nowy adres e-mail. Została na niego wysłana wiadomość z odnośnikiem potwierdzającym. update_needs_confirmation: Konto zostało zaktualizowane, musimy jednak zweryfikować Twój nowy adres e-mail. Została na niego wysłana wiadomość z odnośnikiem potwierdzającym. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
updated: Konto zostało zaktualizowane. updated: Konto zostało zaktualizowane.
sessions: sessions:
already_signed_out: Zostałeś/aś wylogowany/a. already_signed_out: Zostałeś/aś wylogowany/a.
signed_in: Zostałeś/aś zalogowany/a. signed_in: Zostałeś/aś zalogowany/a.
signed_out: Zostałeś/aś wylogowany/a. signed_out: Zostałeś/aś wylogowany/a.
unlocks: unlocks:
send_instructions: W ciągu kilku minut otrzymasz wiadomość e-mail z instrukcjami odblokowania konta. send_instructions: W ciągu kilku minut otrzymasz wiadomość e-mail z instrukcjami odblokowania konta. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
send_paranoid_instructions: Jeśli Twoje konto istnieje, instrukcje odblokowania go otrzymasz w wiadomości e-mail w ciągu kilku minut. send_paranoid_instructions: Jeśli Twoje konto istnieje, instrukcje odblokowania go otrzymasz w wiadomości e-mail w ciągu kilku minut. Jeżeli nie otrzymano wiadomości, sprawdź folder ze spamem.
unlocked: Twoje konto zostało odblokowane. Zaloguj się aby kontynuować. unlocked: Twoje konto zostało odblokowane. Zaloguj się aby kontynuować.
errors: errors:
messages: messages:

View file

@ -193,6 +193,10 @@ en:
title: PubSubHubbub title: PubSubHubbub
topic: Topic topic: Topic
title: Administration title: Administration
admin_mailer:
new_report:
body: "%{reporter} has reported %{target}"
subject: New report for %{instance} (#%{id})
application_mailer: application_mailer:
settings: 'Change e-mail preferences: %{link}' settings: 'Change e-mail preferences: %{link}'
signature: Mastodon notifications from %{instance} signature: Mastodon notifications from %{instance}
@ -200,7 +204,7 @@ en:
applications: applications:
invalid_url: The provided URL is invalid invalid_url: The provided URL is invalid
auth: auth:
change_password: Credentials change_password: Security
delete_account: Delete account delete_account: Delete account
delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation. delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
didnt_get_confirmation: Didn't receive confirmation instructions? didnt_get_confirmation: Didn't receive confirmation instructions?
@ -320,6 +324,43 @@ en:
missing_resource: Could not find the required redirect URL for your account missing_resource: Could not find the required redirect URL for your account
proceed: Proceed to follow proceed: Proceed to follow
prompt: 'You are going to follow:' prompt: 'You are going to follow:'
sessions:
activity: Last activity
browser: Browser
browsers:
alipay: Alipay
blackberry: Blackberry
chrome: Chrome
edge: Microsoft Edge
firefox: Firefox
generic: Unknown browser
ie: Internet Explorer
micro_messenger: MicroMessenger
nokia: Nokia S40 Ovi Browser
opera: Opera
phantom_js: PhantomJS
qq: QQ Browser
safari: Safari
uc_browser: UCBrowser
weibo: Weibo
current_session: Current session
description: "%{browser} on %{platform}"
explanation: These are the web browsers currently logged in to your Mastodon account.
ip: IP
platforms:
adobe_air: Adobe Air
android: Android
blackberry: Blackberry
chrome_os: ChromeOS
firefox_os: Firefox OS
ios: iOS
linux: Linux
mac: Mac
other: unknown platform
windows: Windows
windows_mobile: Windows Mobile
windows_phone: Windows Phone
title: Sessions
settings: settings:
authorized_apps: Authorized apps authorized_apps: Authorized apps
back: Back to Mastodon back: Back to Mastodon
@ -354,13 +395,15 @@ en:
description_html: If you enable <strong>two-factor authentication</strong>, logging in will require you to be in possession of your phone, which will generate tokens for you to enter. description_html: If you enable <strong>two-factor authentication</strong>, logging in will require you to be in possession of your phone, which will generate tokens for you to enter.
disable: Disable disable: Disable
enable: Enable enable: Enable
enabled: Two-factor authentication is enabled
enabled_success: Two-factor authentication successfully enabled enabled_success: Two-factor authentication successfully enabled
generate_recovery_codes: Generate Recovery Codes generate_recovery_codes: Generate recovery codes
instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in." instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated. lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated.
manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:' manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
recovery_codes: Backup recovery codes
recovery_codes_regenerated: Recovery codes successfully regenerated recovery_codes_regenerated: Recovery codes successfully regenerated
recovery_instructions: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. Keep the recovery codes safe. (For example, you may print them and store them with other important documents.) recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. <strong>Keep the recovery codes safe</strong>. For example, you may print them and store them with other important documents.
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?
users: users:

View file

@ -334,7 +334,7 @@ fa:
lost_recovery_codes: با کدهای بازیابی می‌توانید اگر تلفن خود را گم کردید به حساب خود دسترسی داشته باشید. اگر کدهای بازیابی خود را گم کردید، آن‌ها را این‌جا دوباره بسازید. کدهای بازیابی قبلی شما نامعتبر خواهند شد. lost_recovery_codes: با کدهای بازیابی می‌توانید اگر تلفن خود را گم کردید به حساب خود دسترسی داشته باشید. اگر کدهای بازیابی خود را گم کردید، آن‌ها را این‌جا دوباره بسازید. کدهای بازیابی قبلی شما نامعتبر خواهند شد.
manual_instructions: 'اگر نمی‌توانید کدها را اسکن کنید و باید آن‌ها را دستی وارد کنید، متن کد امنیتی این‌جاست:' manual_instructions: 'اگر نمی‌توانید کدها را اسکن کنید و باید آن‌ها را دستی وارد کنید، متن کد امنیتی این‌جاست:'
recovery_codes_regenerated: کدهای بازیابی با موفقیت ساخته شدند recovery_codes_regenerated: کدهای بازیابی با موفقیت ساخته شدند
recovery_instructions: اگر تلفن خود را گم کردید، می‌توانید با یکی از کدهای بازیابی زیر کنترل حساب خود را به دست بگیرید. این کدها را در جای امنی نگه دارید، مثلاً آن‌ها را چاپ کنید و کنار سایر مدارک مهم خود قرار دهید recovery_instructions_html: اگر تلفن خود را گم کردید، می‌توانید با یکی از کدهای بازیابی زیر کنترل حساب خود را به دست بگیرید. این کدها را در جای امنی نگه دارید، مثلاً آن‌ها را چاپ کنید و کنار سایر مدارک مهم خود قرار دهید
setup: راه اندازی setup: راه اندازی
wrong_code: کدی که وارد کردید نامعتبر بود! آیا ساعت سرور و ساعت دستگاه شما درست تنظیم شده‌اند؟ wrong_code: کدی که وارد کردید نامعتبر بود! آیا ساعت سرور و ساعت دستگاه شما درست تنظیم شده‌اند؟
users: users:

View file

@ -238,7 +238,7 @@ fr:
mention: "%{name} vous a mentionné⋅e" mention: "%{name} vous a mentionné⋅e"
new_followers_summary: new_followers_summary:
one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi ! one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi !
other: Vous avez %{count} nouveaux abonné⋅es ! Incroyable ! other: Vous avez %{count} nouveaux⋅elles abonné⋅es ! Incroyable !
subject: subject:
one: "Une nouvelle notification depuis votre dernière visite \U0001F418" one: "Une nouvelle notification depuis votre dernière visite \U0001F418"
other: "%{count} nouvelles notifications depuis votre dernière visite \U0001F418" other: "%{count} nouvelles notifications depuis votre dernière visite \U0001F418"
@ -300,7 +300,7 @@ fr:
lost_recovery_codes: Les codes de récupération vous permettent de retrouver les accès à votre comptre si vous perdez votre téléphone. Si vous perdez vos codes de récupération, vous pouvez les générer à nouveau ici. Vos anciens codes de récupération seront invalidés. lost_recovery_codes: Les codes de récupération vous permettent de retrouver les accès à votre comptre si vous perdez votre téléphone. Si vous perdez vos codes de récupération, vous pouvez les générer à nouveau ici. Vos anciens codes de récupération seront invalidés.
manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l''entrer manuellement, voici le secret en clair :' manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l''entrer manuellement, voici le secret en clair :'
recovery_codes_regenerated: Codes de récupération régénérés avec succès recovery_codes_regenerated: Codes de récupération régénérés avec succès
recovery_instructions: Si vous perdez l'accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l'accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants. recovery_instructions_html: Si vous perdez l'accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l'accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants.
setup: Installer setup: Installer
wrong_code: Les codes entrés sont incorrects ! L'heure du serveur et celle de votre appareil sont-elles correctes ? wrong_code: Les codes entrés sont incorrects ! L'heure du serveur et celle de votre appareil sont-elles correctes ?
users: users:

View file

@ -342,7 +342,7 @@ he:
lost_recovery_codes: קודי האחזור מאפשרים אחזור גישה לחשבון במידה ומכשירך אבד. במידה וקודי האחזור אבדו, ניתן לייצרם מחדש כאן. תוקף קודי האחזור הישנים יפוג. lost_recovery_codes: קודי האחזור מאפשרים אחזור גישה לחשבון במידה ומכשירך אבד. במידה וקודי האחזור אבדו, ניתן לייצרם מחדש כאן. תוקף קודי האחזור הישנים יפוג.
manual_instructions: 'במידה ולא ניתן לסרוק את קוד ה-QR אלא יש צורך להקליד אותו ידנית, להלן סוד כמוס בלתי מוצפן:' manual_instructions: 'במידה ולא ניתן לסרוק את קוד ה-QR אלא יש צורך להקליד אותו ידנית, להלן סוד כמוס בלתי מוצפן:'
recovery_codes_regenerated: קודי האחזור יוצרו בהצלחה recovery_codes_regenerated: קודי האחזור יוצרו בהצלחה
recovery_instructions: במידה והגישה למכשירך תאבד, ניתן לייצר קודי אחזור למטה על מנת לאחזר גישה לחשבונך בכל עת. נא לשמור על קודי הגישה במקום בטוח )לדוגמא על ידי הדפסתם ושמירתם עם מסמכים חשובים אחרים, או שימוש בתוכנה ייעודית לניהול סיסמאות וסודות( recovery_instructions_html: במידה והגישה למכשירך תאבד, ניתן לייצר קודי אחזור למטה על מנת לאחזר גישה לחשבונך בכל עת. נא לשמור על קודי הגישה במקום בטוח )לדוגמא על ידי הדפסתם ושמירתם עם מסמכים חשובים אחרים, או שימוש בתוכנה ייעודית לניהול סיסמאות וסודות(
setup: הכנה setup: הכנה
wrong_code: הקוד שהוזן שגוי! האם הזמן בשרת והזמן במכשירך נכונים? wrong_code: הקוד שהוזן שגוי! האם הזמן בשרת והזמן במכשירך נכונים?
users: users:

View file

@ -331,7 +331,7 @@ id:
lost_recovery_codes: Kode pemulihan bisa anda gunakan untuk mendapatkan kembali akses pada akun anda jika anda kehilangan handphone anda. Jika anda kehilangan kode pemulihan, anda bisa membuatnya ulang disini. Kode pemulihan anda yang lama tidak akan bisa digunakan lagi. lost_recovery_codes: Kode pemulihan bisa anda gunakan untuk mendapatkan kembali akses pada akun anda jika anda kehilangan handphone anda. Jika anda kehilangan kode pemulihan, anda bisa membuatnya ulang disini. Kode pemulihan anda yang lama tidak akan bisa digunakan lagi.
manual_instructions: 'Jika anda tidak bisa memindai kode QR dan harus memasukkannya secara manual, ini dia kode yang harus dimasukkan:' manual_instructions: 'Jika anda tidak bisa memindai kode QR dan harus memasukkannya secara manual, ini dia kode yang harus dimasukkan:'
recovery_codes_regenerated: Kode Pemulihan berhasil dibuat ulang recovery_codes_regenerated: Kode Pemulihan berhasil dibuat ulang
recovery_instructions: Jika anda kehilangan akses pada handphone anda, anda bisa menggunakan kode pemulihan dibawah ini untuk mendapatkan kembali akses pada akun anda. Simpan kode pemulihan anda baik-baik, misalnya dengan mencetaknya atau menyimpannya bersama dokumen penting lainnya. recovery_instructions_html: Jika anda kehilangan akses pada handphone anda, anda bisa menggunakan kode pemulihan dibawah ini untuk mendapatkan kembali akses pada akun anda. Simpan kode pemulihan anda baik-baik, misalnya dengan mencetaknya atau menyimpannya bersama dokumen penting lainnya.
setup: Persiapan setup: Persiapan
wrong_code: Kode yang dimasukkan tidak cocok! Apa waktu server dan waktu di handphone sudah cocok? wrong_code: Kode yang dimasukkan tidak cocok! Apa waktu server dan waktu di handphone sudah cocok?
users: users:

View file

@ -303,7 +303,7 @@ io:
lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated. lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated.
manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:' manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
recovery_codes_regenerated: Recovery codes successfully regenerated recovery_codes_regenerated: Recovery codes successfully regenerated
recovery_instructions: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. Keep the recovery codes safe, for example by printing them and storing them with other important documents. recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. Keep the recovery codes safe, for example by printing them and storing them with other important documents.
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?
users: users:

View file

@ -360,7 +360,7 @@ ja:
lost_recovery_codes: リカバリーコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリーコードは無効になります。 lost_recovery_codes: リカバリーコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリーコードは無効になります。
manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:' manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:'
recovery_codes_regenerated: リカバリーコードが再生成されました。 recovery_codes_regenerated: リカバリーコードが再生成されました。
recovery_instructions: 携帯電話を紛失した場合、以下の内どれかのリカバリーコードを使用してアカウントへアクセスすることができます。 リカバリーコードは印刷して安全に保管してください。 recovery_instructions_html: 携帯電話を紛失した場合、以下の内どれかのリカバリーコードを使用してアカウントへアクセスすることができます。 リカバリーコードは印刷して安全に保管してください。
setup: 初期設定 setup: 初期設定
wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。 wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。
users: users:

View file

@ -228,7 +228,7 @@ nl:
lost_recovery_codes: Met herstelcodes kun je toegang tot jouw account krijgen wanneer je jouw telefoon bent kwijtgeraakt. Wanneer je jouw herstelcodes bent kwijtgeraakt, kan je ze hier opnieuw genereren. Jouw oude herstelcodes zijn daarna ongeldig. lost_recovery_codes: Met herstelcodes kun je toegang tot jouw account krijgen wanneer je jouw telefoon bent kwijtgeraakt. Wanneer je jouw herstelcodes bent kwijtgeraakt, kan je ze hier opnieuw genereren. Jouw oude herstelcodes zijn daarna ongeldig.
manual_instructions: 'Hieronder vind je de geheime code in platte tekst. Voor het geval je de QR-code niet kunt scannen en het handmatig moet invoeren.' manual_instructions: 'Hieronder vind je de geheime code in platte tekst. Voor het geval je de QR-code niet kunt scannen en het handmatig moet invoeren.'
recovery_codes_regenerated: Opnieuw genereren herstelcodes geslaagd recovery_codes_regenerated: Opnieuw genereren herstelcodes geslaagd
recovery_instructions: Wanneer je ooit de toegang verliest tot jouw telefoon, kan je met behulp van een van de herstelcodes hieronder opnieuw toegang krijgen tot jouw account. Zorg ervoor dat je de herstelcodes op een veilige plek bewaard. (Je kunt ze bijvoorbeeld printen en ze samen met andere belangrijke documenten bewaren.) recovery_instructions_html: Wanneer je ooit de toegang verliest tot jouw telefoon, kan je met behulp van een van de herstelcodes hieronder opnieuw toegang krijgen tot jouw account. Zorg ervoor dat je de herstelcodes op een veilige plek bewaard. (Je kunt ze bijvoorbeeld printen en ze samen met andere belangrijke documenten bewaren.)
setup: Instellen setup: Instellen
wrong_code: De ingevoerde code is ongeldig! Klopt de systeemtijd van de server en die van jouw apparaat? wrong_code: De ingevoerde code is ongeldig! Klopt de systeemtijd van de server en die van jouw apparaat?
users: users:

View file

@ -335,7 +335,7 @@
lost_recovery_codes: Gjenopprettingskoder lar deg gjenoppnå tilgang til din konto hvis du mister din telefon. Hvis du har mistet gjenopprettingskodene, kan du regenerere dem her. Dine gamle gjenopprettingskoder vil bli ugyldige. lost_recovery_codes: Gjenopprettingskoder lar deg gjenoppnå tilgang til din konto hvis du mister din telefon. Hvis du har mistet gjenopprettingskodene, kan du regenerere dem her. Dine gamle gjenopprettingskoder vil bli ugyldige.
manual_instructions: 'Hvis du ikke får scannet QR-koden må du skrive inn følgende kode manuelt:' manual_instructions: 'Hvis du ikke får scannet QR-koden må du skrive inn følgende kode manuelt:'
recovery_codes_regenerated: Generering av gjenopprettingskoder vellykket recovery_codes_regenerated: Generering av gjenopprettingskoder vellykket
recovery_instructions: Hvis du skulle miste tilgang til telefonen din, kan du bruke en av gjenopprettingskodene nedenfor til å gjenopprette tilgang til din konto. Oppbevar gjenopprettingskodene sikkert, for eksempel ved å skrive dem ut og lagre dem sammen med andre viktige dokumenter. recovery_instructions_html: Hvis du skulle miste tilgang til telefonen din, kan du bruke en av gjenopprettingskodene nedenfor til å gjenopprette tilgang til din konto. Oppbevar gjenopprettingskodene sikkert, for eksempel ved å skrive dem ut og lagre dem sammen med andre viktige dokumenter.
setup: Sett opp setup: Sett opp
wrong_code: Den angitte koden var ugyldig! Stemmer instansens tid overalt med enhetens tid? wrong_code: Den angitte koden var ugyldig! Stemmer instansens tid overalt med enhetens tid?
users: users:

View file

@ -220,7 +220,7 @@ oc:
- dv - dv
- ds - ds
abbr_month_names: abbr_month_names:
- -
- gen - gen
- feb - feb
- mar - mar
@ -246,7 +246,7 @@ oc:
long: Lo %B %d de %Y long: Lo %B %d de %Y
short: "%b %d" short: "%b %d"
month_names: month_names:
- -
- de genièr - de genièr
- de febrièr - de febrièr
- de març - de març
@ -411,7 +411,7 @@ oc:
lost_recovery_codes: Los còdi de recuperacion vos permeton daccedir a vòstre compte se perdètz vòstre mobil. Savètz perdut vòstres còdis de recuperacion los podètz tornar generar aquí. Los ancians còdis seràn pas mai valides. lost_recovery_codes: Los còdi de recuperacion vos permeton daccedir a vòstre compte se perdètz vòstre mobil. Savètz perdut vòstres còdis de recuperacion los podètz tornar generar aquí. Los ancians còdis seràn pas mai valides.
manual_instructions: 'Se podètz pas numerizar lo còdi QR e que vos cal picar lo còdi a la man, vaquí lo còdi en clar :' manual_instructions: 'Se podètz pas numerizar lo còdi QR e que vos cal picar lo còdi a la man, vaquí lo còdi en clar :'
recovery_codes_regenerated: Los còdis de recuperacion son ben estats tornats generar recovery_codes_regenerated: Los còdis de recuperacion son ben estats tornats generar
recovery_instructions: Se vos arriba de perdre vòstre mobil, podètz utilizar un dels còdis de recuperacion cai-jos per poder tornar accedir a vòstre compte. Gardatz los còdis en seguretat, per exemple, imprimissètz los e gardatz los amb vòstres documents importants. recovery_instructions_html: Se vos arriba de perdre vòstre mobil, podètz utilizar un dels còdis de recuperacion cai-jos per poder tornar accedir a vòstre compte. Gardatz los còdis en seguretat, per exemple, imprimissètz los e gardatz los amb vòstres documents importants.
setup: Paramètres setup: Paramètres
wrong_code: Lo còdi picat es invalid ! Lora es la bona sul servidor e lo mobil ? wrong_code: Lo còdi picat es invalid ! Lora es la bona sul servidor e lo mobil ?
users: users:

View file

@ -193,14 +193,18 @@ pl:
title: PubSubHubbub title: PubSubHubbub
topic: Temat topic: Temat
title: Administracja title: Administracja
admin_mailer:
new_report:
body: "Użytkownik %{reporter} zgłosił %{target}"
subject: Nowe zgłoszenie na %{instance} (#%{id})
application_mailer: application_mailer:
settings: 'Zmień ustawienia powiadamiania: %{link}' settings: 'Zmień ustawienia powiadamiania: %{link}'
signature: Powiadomienie Mastodona, wysłane przez %{instance} signature: Powiadomienie Mastodona z instancji %{instance}
view: 'Zobacz:' view: 'Zobacz:'
applications: applications:
invalid_url: Ten URL jest nieprawidłowy invalid_url: Ten URL jest nieprawidłowy
auth: auth:
change_password: Uwierzytelnienie change_password: Bezpieczeństwo
delete_account: Usunięcie konta delete_account: Usunięcie konta
delete_account_html: Jeżeli próbowałeś usunąć konto, <a href="%{path}">przejdź tutaj</a>. Otrzymasz prośbę o potwierdzenie. delete_account_html: Jeżeli próbowałeś usunąć konto, <a href="%{path}">przejdź tutaj</a>. Otrzymasz prośbę o potwierdzenie.
didnt_get_confirmation: Nie otrzymałeś instrukcji weryfikacji? didnt_get_confirmation: Nie otrzymałeś instrukcji weryfikacji?
@ -323,7 +327,44 @@ pl:
acct: Podaj swój adres (nazwa@domena), z którego chcesz śledzić acct: Podaj swój adres (nazwa@domena), z którego chcesz śledzić
missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny
proceed: Śledź proceed: Śledź
prompt: 'Śledzony będzie:' prompt: 'Zamierzasz śledzić:'
sessions:
activity: Ostatnia aktywność
browser: Przeglądarka
browsers:
alipay: Alipay
blackberry: Blackberry
chrome: Chrome
edge: Microsoft Edge
firefox: Firefox
generic: nieznana przeglądarka
ie: Internet Explorer
micro_messenger: MicroMessenger
nokia: Nokia S40 Ovi Browser
opera: Opera
phantom_js: PhantomJS
qq: QQ Browser
safari: Safari
uc_browser: UCBrowser
weibo: Weibo
current_session: Obecna sesja
description: "%{browser} na %{platform}"
explanation: Przeglądarki z aktywną sesją Twojego konta.
ip: Adres IP
platforms:
adobe_air: Adobe Air
android: Android
blackberry: Blackberry
chrome_os: ChromeOS
firefox_os: Firefox OS
ios: iOS
linux: Linux
mac: macOS
other: nieznana platforma
windows: Windows
windows_mobile: Windows Mobile
windows_phone: Windows Phone
title: Sesje
settings: settings:
authorized_apps: Uwierzytelnione aplikacje authorized_apps: Uwierzytelnione aplikacje
back: Powrót do Mastodona back: Powrót do Mastodona
@ -358,13 +399,15 @@ pl:
description_html: Jeśli włączysz <strong>uwierzytelnianie dwuetapowe</strong>, logowanie się będzie wymagało podania tokenu wyświetlonego na Twoim telefonie. description_html: Jeśli włączysz <strong>uwierzytelnianie dwuetapowe</strong>, logowanie się będzie wymagało podania tokenu wyświetlonego na Twoim telefonie.
disable: Wyłącz disable: Wyłącz
enable: Włącz enable: Włącz
enabled: Uwierzytelnianie dwuetapowe jest włączone
enabled_success: Pomyślnie aktywowano uwierzytelnianie dwuetapowe enabled_success: Pomyślnie aktywowano uwierzytelnianie dwuetapowe
generate_recovery_codes: Generuj kody zapasowe generate_recovery_codes: Generuj kody zapasowe
instructions_html: "<strong>Zeskanuj ten kod QR na swoim urządzeniu za pomocą Google Authenticator, FreeOTP lub podobnej aplikacji</strong>. Od teraz będzie ona generowała kody wymagane przy logowaniu." instructions_html: "<strong>Zeskanuj ten kod QR na swoim urządzeniu za pomocą Google Authenticator, FreeOTP lub podobnej aplikacji</strong>. Od teraz będzie ona generowała kody wymagane przy logowaniu."
lost_recovery_codes: Kody zapasowe pozwolą uzyskać dostęp do portalu, jeżeli utracisz dostęp do telefonu. Jeżeli utracisz dostęp do nich, możesz wygenerować je ponownie tutaj. Poprzednie zostaną unieważnione. lost_recovery_codes: Kody zapasowe pozwolą uzyskać dostęp do portalu, jeżeli utracisz dostęp do telefonu. Jeżeli utracisz dostęp do nich, możesz wygenerować je ponownie tutaj. Poprzednie zostaną unieważnione.
manual_instructions: 'Jeżeli nie możesz zeskanować kodu QR, musisz wprowadzić ten kod ręcznie:' manual_instructions: 'Jeżeli nie możesz zeskanować kodu QR, musisz wprowadzić ten kod ręcznie:'
recovery_codes: Przywróć kody zapasowe
recovery_codes_regenerated: Pomyślnie wygenerowano ponownie kody zapasowe recovery_codes_regenerated: Pomyślnie wygenerowano ponownie kody zapasowe
recovery_instructions: Jeżeli kiedykolwiek utracisz dostęp do telefonu, możesz wykorzystać jeden z kodów zapasowych, aby odzyskać dostęp do konta. Trzymaj je w bezpiecznym miejscu. (Na przykład, wydrukuj je i przechowuj z ważnymu dokumentami.) recovery_instructions_html: Jeżeli kiedykolwiek utracisz dostęp do telefonu, możesz wykorzystać jeden z kodów zapasowych, aby odzyskać dostęp do konta. <strong>Trzymaj je w bezpiecznym miejscu</strong>. Na przykład, wydrukuj je i przechowuj z ważnymu dokumentami.
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?
users: users:

View file

@ -333,7 +333,7 @@ pt-BR:
lost_recovery_codes: Códigos de recuperação permite que você recupere o acesso a sua conta se você perder seu telefone. Se você perder os códigos de recuperação, você pode regera-los aqui. Seus códigos antigos serão invalidados. lost_recovery_codes: Códigos de recuperação permite que você recupere o acesso a sua conta se você perder seu telefone. Se você perder os códigos de recuperação, você pode regera-los aqui. Seus códigos antigos serão invalidados.
manual_instructions: 'Se você não puder scanear o código QR e precisa digita-los manualmente, aqui está o segredo em texto.:' manual_instructions: 'Se você não puder scanear o código QR e precisa digita-los manualmente, aqui está o segredo em texto.:'
recovery_codes_regenerated: Códigos de recuperação foram gerados com sucesso recovery_codes_regenerated: Códigos de recuperação foram gerados com sucesso
recovery_instructions: Se algum dia você perder o acesso ao seu telefone, você pode usar um dos códigos de abaixo para recupera o acesso a sua conta. Guarde os códigos de acesso em local seguro, por exemplo imprimindo ou guardados com documentos importantes. recovery_instructions_html: Se algum dia você perder o acesso ao seu telefone, você pode usar um dos códigos de abaixo para recupera o acesso a sua conta. Guarde os códigos de acesso em local seguro, por exemplo imprimindo ou guardados com documentos importantes.
setup: Configurar setup: Configurar
wrong_code: O código digitado é inválido! Os relógios do servidor e do dispositivo estão corretos? wrong_code: O código digitado é inválido! Os relógios do servidor e do dispositivo estão corretos?
users: users:

View file

@ -332,7 +332,7 @@ ru:
lost_recovery_codes: Коды восстановления позволяют вернуть доступ к аккаунту в случае утери телефона. Если Вы потеряли Ваши коды восстановления, вы можете заново сгенерировать их здесь. Ваши старые коды восстановления будут аннулированы. lost_recovery_codes: Коды восстановления позволяют вернуть доступ к аккаунту в случае утери телефона. Если Вы потеряли Ваши коды восстановления, вы можете заново сгенерировать их здесь. Ваши старые коды восстановления будут аннулированы.
manual_instructions: 'Если Вы не можете отсканировать QR-код и хотите ввести его вручную, секрет представлен здесь открытым текстом:' manual_instructions: 'Если Вы не можете отсканировать QR-код и хотите ввести его вручную, секрет представлен здесь открытым текстом:'
recovery_codes_regenerated: Коды восстановления успешно сгенерированы recovery_codes_regenerated: Коды восстановления успешно сгенерированы
recovery_instructions: В случае утери доступа к Вашему телефону Вы можете использовать один из кодов восстановления, указанных ниже, чтобы вернуть доступ к аккаунту. Держите коды восстановления в безопасности, например, распечатав их и храня с другими важными документами. recovery_instructions_html: В случае утери доступа к Вашему телефону Вы можете использовать один из кодов восстановления, указанных ниже, чтобы вернуть доступ к аккаунту. Держите коды восстановления в безопасности, например, распечатав их и храня с другими важными документами.
setup: Настроить setup: Настроить
wrong_code: Введенный код неверен! Правильно ли установлены серверное время и время устройства? wrong_code: Введенный код неверен! Правильно ли установлены серверное время и время устройства?
users: users:

View file

@ -335,7 +335,7 @@ th:
lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated. lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated.
manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:' manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
recovery_codes_regenerated: Recovery codes successfully regenerated recovery_codes_regenerated: Recovery codes successfully regenerated
recovery_instructions: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. Keep the recovery codes safe, for example by printing them and storing them with other important documents. recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. Keep the recovery codes safe, for example by printing them and storing them with other important documents.
setup: ตั้งค่า setup: ตั้งค่า
wrong_code: รหัสที่กรอกไม่ถูกต้อง! Are server time and device time correct? wrong_code: รหัสที่กรอกไม่ถูกต้อง! Are server time and device time correct?
users: users:

View file

@ -333,7 +333,7 @@ tr:
lost_recovery_codes: Kurtarma kodları telefonunuzu kaybettiğiniz durumlarda hesabınıza erişim yapabilmenize olanak tanır. Eğer kurtarma kodlarınızı kaybettiyseniz burada tekrar oluşturabilirsiniz. Eski kurtarma kodlarınız geçersiz hale gelecektir. lost_recovery_codes: Kurtarma kodları telefonunuzu kaybettiğiniz durumlarda hesabınıza erişim yapabilmenize olanak tanır. Eğer kurtarma kodlarınızı kaybettiyseniz burada tekrar oluşturabilirsiniz. Eski kurtarma kodlarınız geçersiz hale gelecektir.
manual_instructions: 'Eğer QR kodunu taratamıyorsanız ve elle giriş yapmanız gerekiyorsa buradaki gizli düz metni girebilirsiniz:' manual_instructions: 'Eğer QR kodunu taratamıyorsanız ve elle giriş yapmanız gerekiyorsa buradaki gizli düz metni girebilirsiniz:'
recovery_codes_regenerated: Kurtarma kodları başarıyla oluşturuldu recovery_codes_regenerated: Kurtarma kodları başarıyla oluşturuldu
recovery_instructions: 'Eğer telefonunuza erişiminizi kaybederseniz, aşağıdaki kurtarma kodlarından birini kullanarak hesabınıza giriş yapabilirsiniz. Kurtarma kodlarınızı güvenli halde tutunuz. Örneğin: kodların çıktısını alıp diğer önemli belgeleriniz ile birlikte saklayabilirsiniz.' recovery_instructions_html: 'Eğer telefonunuza erişiminizi kaybederseniz, aşağıdaki kurtarma kodlarından birini kullanarak hesabınıza giriş yapabilirsiniz. Kurtarma kodlarınızı güvenli halde tutunuz. Örneğin: kodların çıktısını alıp diğer önemli belgeleriniz ile birlikte saklayabilirsiniz.'
setup: Kuruluma başla setup: Kuruluma başla
wrong_code: Girdiğiniz kod geçersiz! Telefonunuzun saati geri/ileri kalmış olabilir. wrong_code: Girdiğiniz kod geçersiz! Telefonunuzun saati geri/ileri kalmış olabilir.
users: users:

View file

@ -319,7 +319,7 @@ uk:
lost_recovery_codes: Коди відновлення дозволяють повернути доступ до акаунту у випадку втрати телефону. Якщо Ви втратили Ваші коди відновлення, Ви можете знову згенерувати їх тут. Ваші старі коди відновлення будуть анульовані. lost_recovery_codes: Коди відновлення дозволяють повернути доступ до акаунту у випадку втрати телефону. Якщо Ви втратили Ваші коди відновлення, Ви можете знову згенерувати їх тут. Ваші старі коди відновлення будуть анульовані.
manual_instructions: 'Якщо Ви не можете відсканувати QR-код та хочете ввести його вручну, секрет представлений тут відкритим текстом:' manual_instructions: 'Якщо Ви не можете відсканувати QR-код та хочете ввести його вручну, секрет представлений тут відкритим текстом:'
recovery_codes_regenerated: Коди відновлення успішно згенеровані recovery_codes_regenerated: Коди відновлення успішно згенеровані
recovery_instructions: У випадку втрати доступу до Вашого телефона Ви можете використати один з кодів відновлення, вказаних нижче, щоб повернути доступ до акаунту. Тримайте коди відновлення у безпеці, наприклад, роздрукувавши їх та тримаючи їх з іншими важливими документами. recovery_instructions_html: У випадку втрати доступу до Вашого телефона Ви можете використати один з кодів відновлення, вказаних нижче, щоб повернути доступ до акаунту. Тримайте коди відновлення у безпеці, наприклад, роздрукувавши їх та тримаючи їх з іншими важливими документами.
setup: Налаштувати setup: Налаштувати
wrong_code: Введений код неправильний! Чи правильно встановлені серверний час та час пристрою? wrong_code: Введений код неправильний! Чи правильно встановлені серверний час та час пристрою?
users: users:

View file

@ -339,7 +339,7 @@ zh-CN:
lost_recovery_codes: 如果你丢了手机,你可以用恢复代码重新访问你的账户。如果你丢了恢复代码,也可以在这里重新生成一个,不过以前的恢复代码就失效了。<del>(废话)</del> lost_recovery_codes: 如果你丢了手机,你可以用恢复代码重新访问你的账户。如果你丢了恢复代码,也可以在这里重新生成一个,不过以前的恢复代码就失效了。<del>(废话)</del>
manual_instructions: 如果你无法扫描 QR 二维码,请手动输入这个文本密码︰ manual_instructions: 如果你无法扫描 QR 二维码,请手动输入这个文本密码︰
recovery_codes_regenerated: 已成功重新生成恢复代码 recovery_codes_regenerated: 已成功重新生成恢复代码
recovery_instructions: 如果你的手机无法使用,你可以使用下面的任何恢复代码来恢复你的账号。请保管好你的恢复代码以防泄漏(例如你可以打印好它们并和重要文档一起保存)。 recovery_instructions_html: 如果你的手机无法使用,你可以使用下面的任何恢复代码来恢复你的账号。请保管好你的恢复代码以防泄漏(例如你可以打印好它们并和重要文档一起保存)。
setup: 设置 setup: 设置
wrong_code: 你输入的认证码并不正确!可能服务器时间和你手机不一致,请检查你手机的时钟,或与本站管理员联系。 wrong_code: 你输入的认证码并不正确!可能服务器时间和你手机不一致,请检查你手机的时钟,或与本站管理员联系。
users: users:

View file

@ -334,7 +334,7 @@ zh-HK:
lost_recovery_codes: 讓你可以在遺失電話時,使用備用驗證碼登入。如果你遺失了備用驗證碼,可以在這裏產生一批新的,舊有的備用驗證碼將會失效。 lost_recovery_codes: 讓你可以在遺失電話時,使用備用驗證碼登入。如果你遺失了備用驗證碼,可以在這裏產生一批新的,舊有的備用驗證碼將會失效。
manual_instructions: 如果你無法掃描 QR 圖形碼,請手動輸入這個文字密碼︰ manual_instructions: 如果你無法掃描 QR 圖形碼,請手動輸入這個文字密碼︰
recovery_codes_regenerated: 成功產生新的備用驗證碼 recovery_codes_regenerated: 成功產生新的備用驗證碼
recovery_instructions: 如果你遺失了安裝認證器的裝置(如︰你的電話),你可以使用備用驗證碼進行登入。請確保將備用驗證碼收藏穩當,(如列印出來,和你其他重要文件一起存放) recovery_instructions_html: 如果你遺失了安裝認證器的裝置(如︰你的電話),你可以使用備用驗證碼進行登入。請確保將備用驗證碼收藏穩當,(如列印出來,和你其他重要文件一起存放)
setup: 設定 setup: 設定
wrong_code: 你輸入的認證碼並不正確!可能伺服器時間和你手機不一致,請檢查你手機的時鐘,或與本站管理員聯絡。 wrong_code: 你輸入的認證碼並不正確!可能伺服器時間和你手機不一致,請檢查你手機的時鐘,或與本站管理員聯絡。
users: users:

View file

@ -7,7 +7,7 @@ SimpleNavigation::Configuration.run do |navigation|
primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings| primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings|
settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url
settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url
settings.item :password, safe_join([fa_icon('cog fw'), t('auth.change_password')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete} settings.item :password, safe_join([fa_icon('lock fw'), t('auth.change_password')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete}
settings.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_url, highlights_on: %r{/settings/two_factor_authentication} settings.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_url, highlights_on: %r{/settings/two_factor_authentication}
settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url
settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url

View file

@ -15,3 +15,6 @@
feed_cleanup_scheduler: feed_cleanup_scheduler:
cron: '0 0 * * *' cron: '0 0 * * *'
class: Scheduler::FeedCleanupScheduler class: Scheduler::FeedCleanupScheduler
doorkeeper_cleanup_scheduler:
cron: '1 1 * * 0'
class: Scheduler::DoorkeeperCleanupScheduler

View file

@ -1,7 +1,10 @@
module.exports = { module.exports = {
test: /\.js$/, test: /\.js$/,
// include react-intl because transform-react-remove-prop-types needs to apply to it // include react-intl because transform-react-remove-prop-types needs to apply to it
exclude: /node_modules\/(?!react-intl)/, exclude: {
test: /node_modules/,
exclude: /react-intl[\/\\](?!locale-data)/,
},
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
forceEnv: process.env.NODE_ENV || 'development', forceEnv: process.env.NODE_ENV || 'development',

View file

@ -0,0 +1,7 @@
class AddDescriptionToSessionActivations < ActiveRecord::Migration[5.1]
def change
add_column :session_activations, :user_agent, :string, null: false, default: ''
add_column :session_activations, :ip, :inet
add_foreign_key :session_activations, :users, on_delete: :cascade
end
end

View file

@ -0,0 +1,6 @@
class AddAccessTokenIdToSessionActivations < ActiveRecord::Migration[5.1]
def change
add_column :session_activations, :access_token_id, :integer
add_foreign_key :session_activations, :oauth_access_tokens, column: :access_token_id, on_delete: :cascade
end
end

View file

@ -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: 20170623152212) do ActiveRecord::Schema.define(version: 20170625140443) 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"
@ -255,6 +255,9 @@ ActiveRecord::Schema.define(version: 20170623152212) do
t.string "session_id", null: false t.string "session_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "user_agent", default: "", null: false
t.inet "ip"
t.integer "access_token_id"
t.index ["session_id"], name: "index_session_activations_on_session_id", unique: true t.index ["session_id"], name: "index_session_activations_on_session_id", unique: true
t.index ["user_id"], name: "index_session_activations_on_user_id" t.index ["user_id"], name: "index_session_activations_on_user_id"
end end
@ -404,6 +407,8 @@ ActiveRecord::Schema.define(version: 20170623152212) do
add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", on_delete: :nullify add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", on_delete: :nullify
add_foreign_key "reports", "accounts", column: "target_account_id", on_delete: :cascade add_foreign_key "reports", "accounts", column: "target_account_id", on_delete: :cascade
add_foreign_key "reports", "accounts", on_delete: :cascade add_foreign_key "reports", "accounts", on_delete: :cascade
add_foreign_key "session_activations", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade
add_foreign_key "session_activations", "users", on_delete: :cascade
add_foreign_key "statuses", "accounts", column: "in_reply_to_account_id", on_delete: :nullify add_foreign_key "statuses", "accounts", column: "in_reply_to_account_id", on_delete: :nullify
add_foreign_key "statuses", "accounts", on_delete: :cascade add_foreign_key "statuses", "accounts", on_delete: :cascade
add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify

View file

@ -42,6 +42,37 @@ namespace :mastodon do
end end
end end
desc 'Add a user by providing their email, username and initial password.' \
'The user will receive a confirmation email, then they must reset their password before logging in.'
task add_user: :environment do
print 'Enter email: '
email = STDIN.gets.chomp
print 'Enter username: '
username = STDIN.gets.chomp
print 'Create user and send them confirmation mail [y/N]: '
confirm = STDIN.gets.chomp
puts
if confirm.casecmp?('y')
password = SecureRandom.hex
user = User.new(email: email, password: password, account_attributes: { username: username })
if user.save
puts 'User added and confirmation mail sent to user\'s email address.'
puts "Here is the random password generated for the user: #{password}"
else
puts 'Following errors occured while creating new user:'
user.errors.each do |key, val|
puts "#{key}: #{val}"
end
end
else
puts 'Aborted by user.'
end
puts
end
namespace :media do namespace :media do
desc 'Removes media attachments that have not been assigned to any status for longer than a day' desc 'Removes media attachments that have not been assigned to any status for longer than a day'
task clear: :environment do task clear: :environment do

View file

@ -3,10 +3,10 @@
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"postversion": "git push --tags", "postversion": "git push --tags",
"build:development": "cross-env RAILS_ENV=development ASSET_HOST=http://0.0.0.0:8080 ./bin/webpack", "build:development": "cross-env RAILS_ENV=development ./bin/webpack",
"build:production": "cross-env RAILS_ENV=production ./bin/webpack", "build:production": "cross-env RAILS_ENV=production ./bin/webpack",
"manage:translations": "node ./config/webpack/translationRunner.js", "manage:translations": "node ./config/webpack/translationRunner.js",
"start": "rimraf ./tmp/streaming && babel ./streaming/index.js --out-dir ./tmp && node ./tmp/streaming/index.js", "start": "node ./streaming/index.js",
"storybook": "cross-env NODE_ENV=test start-storybook -s ./public -p 9001 -c storybook", "storybook": "cross-env NODE_ENV=test start-storybook -s ./public -p 9001 -c storybook",
"test": "npm run test:lint && npm run test:mocha", "test": "npm run test:lint && npm run test:mocha",
"test:lint": "eslint -c .eslintrc.yml --ext=js app/javascript/ config/webpack/ spec/javascript/ storybook/ streaming/", "test:lint": "eslint -c .eslintrc.yml --ext=js app/javascript/ config/webpack/ spec/javascript/ storybook/ streaming/",
@ -24,7 +24,7 @@
"axios": "^0.16.2", "axios": "^0.16.2",
"babel-cli": "^6.24.1", "babel-cli": "^6.24.1",
"babel-core": "^6.25.0", "babel-core": "^6.25.0",
"babel-loader": "^7.0.0", "babel-loader": "^7.1.0",
"babel-plugin-lodash": "^3.2.11", "babel-plugin-lodash": "^3.2.11",
"babel-plugin-react-intl": "^2.3.1", "babel-plugin-react-intl": "^2.3.1",
"babel-plugin-react-transform": "^2.0.2", "babel-plugin-react-transform": "^2.0.2",
@ -35,7 +35,7 @@
"babel-plugin-transform-object-rest-spread": "^6.23.0", "babel-plugin-transform-object-rest-spread": "^6.23.0",
"babel-plugin-transform-react-jsx-self": "^6.22.0", "babel-plugin-transform-react-jsx-self": "^6.22.0",
"babel-plugin-transform-react-jsx-source": "^6.22.0", "babel-plugin-transform-react-jsx-source": "^6.22.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.5", "babel-plugin-transform-react-remove-prop-types": "^0.4.6",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.5.2", "babel-preset-env": "^1.5.2",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
@ -55,7 +55,7 @@
"glob": "^7.1.1", "glob": "^7.1.1",
"http-link-header": "^0.8.0", "http-link-header": "^0.8.0",
"immutable": "^3.8.1", "immutable": "^3.8.1",
"intersection-observer": "^0.3.0", "intersection-observer": "^0.3.2",
"intl": "^1.2.5", "intl": "^1.2.5",
"intl-relativeformat": "^1.3.0", "intl-relativeformat": "^1.3.0",
"is-nan": "^1.2.1", "is-nan": "^1.2.1",
@ -65,71 +65,71 @@
"marky": "^1.2.0", "marky": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"node-sass": "^4.5.2", "node-sass": "^4.5.2",
"npmlog": "^4.1.0", "npmlog": "^4.1.2",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"path-complete-extname": "^0.1.0", "path-complete-extname": "^0.1.0",
"pg": "^6.2.4", "pg": "^6.4.0",
"postcss-loader": "^2.0.5", "postcss-loader": "^2.0.6",
"postcss-smart-import": "^0.7.4", "postcss-smart-import": "^0.7.4",
"precss": "^1.4.0", "precss": "^1.4.0",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"punycode": "^2.1.0", "punycode": "^2.1.0",
"rails-ujs": "^5.1.1", "rails-ujs": "^5.1.2",
"react": "^15.6.0", "react": "^15.6.1",
"react-addons-perf": "^15.4.2", "react-addons-perf": "^15.4.2",
"react-addons-shallow-compare": "^15.5.2", "react-addons-shallow-compare": "^15.6.0",
"react-dom": "^15.6.0", "react-dom": "^15.6.1",
"react-immutable-proptypes": "^2.1.0", "react-immutable-proptypes": "^2.1.0",
"react-immutable-pure-component": "^1.0.0", "react-immutable-pure-component": "^1.0.0",
"react-intl": "^2.3.0", "react-intl": "^2.3.0",
"react-motion": "^0.5.0", "react-motion": "^0.5.0",
"react-notification": "^6.7.0", "react-notification": "^6.7.1",
"react-redux": "^5.0.4", "react-redux": "^5.0.4",
"react-redux-loading-bar": "^2.9.2", "react-redux-loading-bar": "^2.9.2",
"react-router-dom": "^4.1.1", "react-router-dom": "^4.1.1",
"react-router-scroll": "ytase/react-router-scroll#build", "react-router-scroll": "ytase/react-router-scroll#build",
"react-simple-dropdown": "^3.0.0", "react-simple-dropdown": "^3.0.0",
"react-swipeable": "^4.0.1", "react-swipeable": "^4.0.1",
"react-textarea-autosize": "^5.0.6", "react-textarea-autosize": "^5.0.7",
"react-toggle": "^4.0.1", "react-toggle": "^4.0.1",
"redis": "^2.7.1", "redis": "^2.7.1",
"redux": "^3.6.0", "redux": "^3.7.1",
"redux-immutable": "^4.0.0", "redux-immutable": "^4.0.0",
"redux-thunk": "^2.2.0", "redux-thunk": "^2.2.0",
"requestidlecallback": "^0.3.0", "requestidlecallback": "^0.3.0",
"reselect": "^3.0.1", "reselect": "^3.0.1",
"resolve-url-loader": "^2.0.2", "resolve-url-loader": "^2.1.0",
"rimraf": "^2.6.1", "rimraf": "^2.6.1",
"sass-loader": "^6.0.5", "sass-loader": "^6.0.6",
"stringz": "^0.2.1", "stringz": "^0.2.2",
"style-loader": "^0.18.2", "style-loader": "^0.18.2",
"throng": "^4.0.0", "throng": "^4.0.0",
"tiny-queue": "^0.2.1", "tiny-queue": "^0.2.1",
"uuid": "^3.0.1", "uuid": "^3.1.0",
"uws": "^0.14.5", "uws": "^0.14.5",
"webpack": "^3.0.0", "webpack": "^3.0.0",
"webpack-bundle-analyzer": "^2.8.2", "webpack-bundle-analyzer": "^2.8.2",
"webpack-manifest-plugin": "^1.1.0", "webpack-manifest-plugin": "^1.1.0",
"webpack-merge": "^4.1.0", "webpack-merge": "^4.1.0",
"websocket.js": "^0.1.10" "websocket.js": "^0.1.12"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-actions": "^3.1.2", "@storybook/addon-actions": "^3.1.6",
"@storybook/react": "^3.1.3", "@storybook/react": "^3.1.6",
"babel-eslint": "^7.2.3", "babel-eslint": "^7.2.3",
"chai": "^4.0.1", "chai": "^4.0.1",
"chai-enzyme": "^0.7.1", "chai-enzyme": "^0.7.1",
"enzyme": "^2.8.2", "enzyme": "^2.9.1",
"eslint": "^3.19.0", "eslint": "^3.19.0",
"eslint-plugin-jsx-a11y": "^4.0.0", "eslint-plugin-jsx-a11y": "^4.0.0",
"eslint-plugin-react": "^6.10.3", "eslint-plugin-react": "^6.10.3",
"jsdom": "^10.1.0", "jsdom": "^10.1.0",
"mocha": "^3.4.1", "mocha": "^3.4.1",
"react-intl-translations-manager": "^5.0.0", "react-intl-translations-manager": "^5.0.0",
"react-test-renderer": "^15.5.4", "react-test-renderer": "^15.6.1",
"sinon": "^2.3.4", "sinon": "^2.3.5",
"webpack-dev-server": "lencioni/webpack-dev-server#patch-1", "webpack-dev-server": "lencioni/webpack-dev-server#patch-1",
"yargs": "^8.0.1" "yargs": "^8.0.2"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "*" "fsevents": "*"

View file

@ -8,17 +8,30 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
end end
describe 'GET #index' do describe 'GET #index' do
it 'returns http success' do around do |example|
get :index default_per_page = DomainBlock.default_per_page
DomainBlock.paginates_per 1
example.run
DomainBlock.paginates_per default_per_page
end
it 'renders domain blocks' do
2.times { Fabricate(:domain_block) }
get :index, params: { page: 2 }
assigned = assigns(:domain_blocks)
expect(assigned.count).to eq 1
expect(assigned.klass).to be DomainBlock
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
end end
end end
describe 'GET #new' do describe 'GET #new' do
it 'returns http success' do it 'assigns a new domain block' do
get :new get :new
expect(assigns(:domain_block)).to be_instance_of(DomainBlock)
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
end end
end end
@ -33,13 +46,25 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
end end
describe 'POST #create' do describe 'POST #create' do
it 'blocks the domain' do it 'blocks the domain when succeeded to save' do
allow(DomainBlockWorker).to receive(:perform_async).and_return(true) allow(DomainBlockWorker).to receive(:perform_async).and_return(true)
post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } } post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } }
expect(DomainBlockWorker).to have_received(:perform_async) expect(DomainBlockWorker).to have_received(:perform_async)
expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg')
expect(response).to redirect_to(admin_domain_blocks_path) expect(response).to redirect_to(admin_domain_blocks_path)
end end
it 'renders new when failed to save' do
Fabricate(:domain_block, domain: 'example.com')
allow(DomainBlockWorker).to receive(:perform_async).and_return(true)
post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } }
expect(DomainBlockWorker).not_to have_received(:perform_async)
expect(response).to render_template :new
end
end end
describe 'DELETE #destroy' do describe 'DELETE #destroy' do
@ -50,6 +75,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
delete :destroy, params: { id: domain_block.id, domain_block: { retroactive: '1' } } delete :destroy, params: { id: domain_block.id, domain_block: { retroactive: '1' } }
expect(service).to have_received(:call).with(domain_block, true) expect(service).to have_received(:call).with(domain_block, true)
expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.destroyed_msg')
expect(response).to redirect_to(admin_domain_blocks_path) expect(response).to redirect_to(admin_domain_blocks_path)
end end
end end

View file

@ -21,12 +21,21 @@ RSpec.describe Api::V1::ReportsController, type: :controller do
end end
describe 'POST #create' do describe 'POST #create' do
it 'creates a report' do let!(:status) { Fabricate(:status) }
status = Fabricate(:status) let!(:admin) { Fabricate(:user, admin: true) }
post :create, params: { status_ids: [status.id], account_id: status.account.id, comment: 'reasons' }
before do
allow(AdminMailer).to receive(:new_report).and_return(double('email', deliver_later: nil))
post :create, params: { status_ids: [status.id], account_id: status.account.id, comment: 'reasons' }
end
it 'creates a report' do
expect(status.reload.account.targeted_reports).not_to be_empty expect(status.reload.account.targeted_reports).not_to be_empty
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
end end
it 'sends e-mails to admins' do
expect(AdminMailer).to have_received(:new_report).with(admin.account, Report)
end
end end
end end

View file

@ -3,37 +3,110 @@ require 'rails_helper'
RSpec.describe Auth::RegistrationsController, type: :controller do RSpec.describe Auth::RegistrationsController, type: :controller do
render_views render_views
shared_examples 'checks for enabled registrations' do |path|
around do |example|
open_registrations = Setting.open_registrations
example.run
Setting.open_registrations = open_registrations
end
it 'redirects if it is in single user mode while it is open for registration' do
Fabricate(:account)
Setting.open_registrations = true
expect(Rails.configuration.x).to receive(:single_user_mode).and_return(true)
get path
expect(response).to redirect_to '/'
end
it 'redirects if it is not open for registration while it is not in single user mode' do
Setting.open_registrations = false
expect(Rails.configuration.x).to receive(:single_user_mode).and_return(false)
get path
expect(response).to redirect_to '/'
end
end
describe 'GET #edit' do
it 'returns http success' do
request.env["devise.mapping"] = Devise.mappings[:user]
sign_in(Fabricate(:user))
get :edit
expect(response).to have_http_status(:success)
end
end
describe 'GET #update' do
it 'returns http success' do
request.env["devise.mapping"] = Devise.mappings[:user]
sign_in(Fabricate(:user), scope: :user)
post :update
expect(response).to have_http_status(:success)
end
end
describe 'GET #new' do describe 'GET #new' do
before do before do
Setting.open_registrations = true
request.env["devise.mapping"] = Devise.mappings[:user] request.env["devise.mapping"] = Devise.mappings[:user]
end end
it 'returns http success' do context do
get :new around do |example|
expect(response).to have_http_status(:success) open_registrations = Setting.open_registrations
example.run
Setting.open_registrations = open_registrations
end
it 'returns http success' do
Setting.open_registrations = true
get :new
expect(response).to have_http_status(:success)
end
end end
include_examples 'checks for enabled registrations', :new
end end
describe 'POST #create' do describe 'POST #create' do
let(:accept_language) { Rails.application.config.i18n.available_locales.sample.to_s } let(:accept_language) { Rails.application.config.i18n.available_locales.sample.to_s }
before do before { request.env["devise.mapping"] = Devise.mappings[:user] }
Setting.open_registrations = true
request.env["devise.mapping"] = Devise.mappings[:user] context do
request.headers["Accept-Language"] = accept_language around do |example|
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } } open_registrations = Setting.open_registrations
example.run
Setting.open_registrations = open_registrations
end
subject do
Setting.open_registrations = true
request.headers["Accept-Language"] = accept_language
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } }
end
it 'redirects to login page' do
subject
expect(response).to redirect_to new_user_session_path
end
it 'creates user' do
subject
user = User.find_by(email: 'test@example.com')
expect(user).to_not be_nil
expect(user.locale).to eq(accept_language)
end
end end
it 'redirects to login page' do it 'does nothing if user already exists' do
expect(response).to redirect_to new_user_session_path Fabricate(:user, account: Fabricate(:account, username: 'test'))
subject
end end
it 'creates user' do include_examples 'checks for enabled registrations', :create
user = User.find_by(email: 'test@example.com')
expect(user).to_not be_nil
expect(user.locale).to eq(accept_language)
end
end end
describe 'DELETE #destroy' do describe 'DELETE #destroy' do

View file

@ -79,13 +79,41 @@ describe Settings::TwoFactorAuthenticationsController do
user.update(otp_required_for_login: true) user.update(otp_required_for_login: true)
end end
it 'turns off otp requirement if signed in' do context 'when signed in' do
sign_in user, scope: :user before do
post :destroy sign_in user, scope: :user
end
expect(response).to redirect_to(settings_two_factor_authentication_path) it 'turns off otp requirement with correct code' do
user.reload expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, arg|
expect(user.otp_required_for_login).to eq(false) expect(value).to eq user
expect(arg).to eq '123456'
true
end
post :destroy, params: { form_two_factor_confirmation: { code: '123456' } }
expect(response).to redirect_to(settings_two_factor_authentication_path)
user.reload
expect(user.otp_required_for_login).to eq(false)
end
it 'does not turn off otp if code is incorrect' do
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, arg|
expect(value).to eq user
expect(arg).to eq '057772'
false
end
post :destroy, params: { form_two_factor_confirmation: { code: '057772' } }
user.reload
expect(user.otp_required_for_login).to eq(true)
end
it 'raises ActionController::ParameterMissing if code is missing' do
expect { post :destroy }.to raise_error(ActionController::ParameterMissing)
end
end end
it 'redirects if not signed in' do it 'redirects if not signed in' do

View file

@ -1,3 +1,3 @@
Fabricator(:domain_block) do Fabricator(:domain_block) do
domain "example.com" domain { sequence(:domain) { |i| "#{i}#{Faker::Internet.domain_name}" } }
end end

View file

@ -23,7 +23,7 @@ Devise::Test::ControllerHelpers.module_eval do
original_sign_in(resource, scope: scope) original_sign_in(resource, scope: scope)
SessionActivation.deactivate warden.raw_session["auth_id"] SessionActivation.deactivate warden.raw_session["auth_id"]
warden.raw_session["auth_id"] = resource.activate_session warden.raw_session["auth_id"] = resource.activate_session(warden.request)
end end
end end

View file

@ -1,15 +0,0 @@
{
"presets": [
[
"env",
{
"targets": {
"node": "current"
}
}
]
],
"plugins": [
"transform-object-rest-spread"
]
}

View file

@ -1,14 +1,14 @@
import os from 'os'; const os = require('os');
import throng from 'throng'; const throng = require('throng');
import dotenv from 'dotenv'; const dotenv = require('dotenv');
import express from 'express'; const express = require('express');
import http from 'http'; const http = require('http');
import redis from 'redis'; const redis = require('redis');
import pg from 'pg'; const pg = require('pg');
import log from 'npmlog'; const log = require('npmlog');
import url from 'url'; const url = require('url');
import WebSocket from 'uws'; const WebSocket = require('uws');
import uuid from 'uuid'; const uuid = require('uuid');
const env = process.env.NODE_ENV || 'development'; const env = process.env.NODE_ENV || 'development';
@ -78,7 +78,11 @@ const startWorker = (workerId) => {
const pgConfigs = { const pgConfigs = {
development: { development: {
user: process.env.DB_USER || pg.defaults.user,
password: process.env.DB_PASS || pg.defaults.password,
database: 'mastodon_development', database: 'mastodon_development',
host: process.env.DB_HOST || pg.defaults.host,
port: process.env.DB_PORT || pg.defaults.port,
max: 10, max: 10,
}, },
@ -242,7 +246,7 @@ const startWorker = (workerId) => {
accountFromRequest(req, next); accountFromRequest(req, next);
}; };
const errorMiddleware = (err, req, res, next) => { // eslint-disable-line no-unused-vars const errorMiddleware = (err, req, res, {}) => {
log.error(req.requestId, err.toString()); log.error(req.requestId, err.toString());
res.writeHead(err.statusCode || 500, { 'Content-Type': 'application/json' }); res.writeHead(err.statusCode || 500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: err.statusCode ? err.toString() : 'An unexpected error occurred' })); res.end(JSON.stringify({ error: err.statusCode ? err.toString() : 'An unexpected error occurred' }));

578
yarn.lock

File diff suppressed because it is too large Load diff