Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@ -17,6 +17,9 @@ class Api::V1::ReportsController < Api::BaseController
|
||||
status_ids: reported_status_ids,
|
||||
comment: report_params[:comment]
|
||||
)
|
||||
|
||||
User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
|
||||
|
||||
render :show
|
||||
end
|
||||
|
||||
|
@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
|
||||
include UserTrackingConcern
|
||||
|
||||
helper_method :current_account
|
||||
helper_method :current_session
|
||||
helper_method :single_user_mode?
|
||||
|
||||
rescue_from ActionController::RoutingError, with: :not_found
|
||||
@ -68,6 +69,10 @@ class ApplicationController < ActionController::Base
|
||||
@current_account ||= current_user.try(:account)
|
||||
end
|
||||
|
||||
def current_session
|
||||
@current_session ||= SessionActivation.find_by(session_id: session['auth_id'])
|
||||
end
|
||||
|
||||
def cache_collection(raw, klass)
|
||||
return raw unless klass.respond_to?(:with_includes)
|
||||
|
||||
|
@ -5,6 +5,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
|
||||
before_action :check_enabled_registrations, only: [:new, :create]
|
||||
before_action :configure_sign_up_params, only: [:create]
|
||||
before_action :set_sessions, only: [:edit, :update]
|
||||
|
||||
def destroy
|
||||
not_found
|
||||
@ -41,4 +42,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
def determine_layout
|
||||
%w(edit update).include?(action_name) ? 'admin' : 'auth'
|
||||
end
|
||||
|
||||
def set_sessions
|
||||
@sessions = current_user.session_activations
|
||||
end
|
||||
end
|
||||
|
@ -5,7 +5,7 @@ class HomeController < ApplicationController
|
||||
|
||||
def index
|
||||
@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 || {}
|
||||
@admin = Account.find_local(Setting.site_contact_username)
|
||||
@streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
|
||||
@ -16,14 +16,4 @@ class HomeController < ApplicationController
|
||||
def authenticate_user!
|
||||
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
|
||||
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
|
||||
|
@ -7,7 +7,9 @@ module Settings
|
||||
before_action :authenticate_user!
|
||||
before_action :verify_otp_required, only: [:create]
|
||||
|
||||
def show; end
|
||||
def show
|
||||
@confirmation = Form::TwoFactorConfirmation.new
|
||||
end
|
||||
|
||||
def create
|
||||
current_user.otp_secret = User.generate_otp_secret(32)
|
||||
@ -16,13 +18,23 @@ module Settings
|
||||
end
|
||||
|
||||
def destroy
|
||||
current_user.otp_required_for_login = false
|
||||
current_user.save!
|
||||
redirect_to settings_two_factor_authentication_path
|
||||
if current_user.validate_and_consume_otp!(confirmation_params[:code])
|
||||
current_user.otp_required_for_login = false
|
||||
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
|
||||
|
||||
private
|
||||
|
||||
def confirmation_params
|
||||
params.require(:form_two_factor_confirmation).permit(:code)
|
||||
end
|
||||
|
||||
def verify_otp_required
|
||||
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
|
||||
end
|
||||
|
@ -41,4 +41,16 @@ module SettingsHelper
|
||||
def hash_to_object(hash)
|
||||
HashObject.new(hash)
|
||||
end
|
||||
|
||||
def session_device_icon(session)
|
||||
device = session.detection.device
|
||||
|
||||
if device.mobile?
|
||||
'mobile'
|
||||
elsif device.tablet?
|
||||
'tablet'
|
||||
else
|
||||
'desktop'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,4 +1,5 @@
|
||||
import api from '../api';
|
||||
import { openModal, closeModal } from './modal';
|
||||
|
||||
export const REPORT_INIT = 'REPORT_INIT';
|
||||
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 function initReport(account, status) {
|
||||
return {
|
||||
type: REPORT_INIT,
|
||||
account,
|
||||
status,
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: REPORT_INIT,
|
||||
account,
|
||||
status,
|
||||
});
|
||||
|
||||
dispatch(openModal('REPORT'));
|
||||
};
|
||||
};
|
||||
|
||||
@ -40,7 +45,10 @@ export function submitReport() {
|
||||
account_id: getState().getIn(['reports', 'new', 'account_id']),
|
||||
status_ids: getState().getIn(['reports', 'new', 'status_ids']),
|
||||
comment: getState().getIn(['reports', 'new', 'comment']),
|
||||
}).then(response => dispatch(submitReportSuccess(response.data))).catch(error => dispatch(submitReportFail(error)));
|
||||
}).then(response => {
|
||||
dispatch(closeModal());
|
||||
dispatch(submitReportSuccess(response.data));
|
||||
}).catch(error => dispatch(submitReportFail(error)));
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -132,7 +132,7 @@ export default class ColumnHeader extends React.PureComponent {
|
||||
</div>
|
||||
|
||||
<div className={collapsibleClassName} onTransitionEnd={this.handleTransitionEnd}>
|
||||
<div>
|
||||
<div className='column-header__collapsible-inner'>
|
||||
{(!collapsed || animating) && collapsedContent}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -85,14 +85,24 @@ class Item extends React.PureComponent {
|
||||
let thumbnail = '';
|
||||
|
||||
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 = (
|
||||
<a // eslint-disable-line jsx-a11y/anchor-has-content
|
||||
<a
|
||||
className='media-gallery__item-thumbnail'
|
||||
href={attachment.get('remote_url') || attachment.get('url')}
|
||||
href={attachment.get('remote_url') || originalUrl}
|
||||
onClick={this.handleClick}
|
||||
target='_blank'
|
||||
style={{ backgroundImage: `url(${attachment.get('preview_url')})` }}
|
||||
/>
|
||||
>
|
||||
<img src={previewUrl} srcSet={srcSet} sizes={sizes} alt='' />
|
||||
</a>
|
||||
);
|
||||
} else if (attachment.get('type') === 'gifv') {
|
||||
const autoPlay = !isIOS() && this.props.autoPlayGif;
|
||||
|
@ -25,7 +25,7 @@ export default class Permalink extends React.PureComponent {
|
||||
const { href, children, className, ...other } = this.props;
|
||||
|
||||
return (
|
||||
<a href={href} onClick={this.handleClick} {...other} className={'permalink ' + className}>
|
||||
<a href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
|
@ -149,7 +149,7 @@ class StatusUnextended extends ImmutablePureComponent {
|
||||
|
||||
saveHeight = () => {
|
||||
if (this.node && this.node.children.length !== 0) {
|
||||
this.height = this.node.clientHeight;
|
||||
this.height = this.node.getBoundingClientRect().height;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||
|
||||
handleReport = () => {
|
||||
this.props.onReport(this.props.status);
|
||||
this.context.router.history.push('/report');
|
||||
}
|
||||
|
||||
handleConversationMuteClick = () => {
|
||||
|
@ -38,7 +38,6 @@ export default class Header extends ImmutablePureComponent {
|
||||
|
||||
handleReport = () => {
|
||||
this.props.onReport(this.props.account);
|
||||
this.context.router.history.push('/report');
|
||||
}
|
||||
|
||||
handleMute = () => {
|
||||
|
@ -67,6 +67,12 @@ export default class ComposeForm extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites';
|
||||
import Column from '../ui/components/column';
|
||||
@ -14,7 +15,9 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
||||
loaded: state.getIn(['status_lists', 'favourites', 'loaded']),
|
||||
me: state.getIn(['meta', 'me']),
|
||||
});
|
||||
|
||||
@connect(mapStateToProps)
|
||||
@ -23,8 +26,10 @@ export default class Favourites extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
loaded: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
me: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
|
@ -13,6 +13,7 @@ import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import { createSelector } from 'reselect';
|
||||
import Immutable from 'immutable';
|
||||
import LoadMore from '../../components/load_more';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||
@ -50,19 +51,27 @@ export default class Notifications extends React.PureComponent {
|
||||
trackScroll: true,
|
||||
};
|
||||
|
||||
dispatchExpandNotifications = debounce(() => {
|
||||
this.props.dispatch(expandNotifications());
|
||||
}, 300, { leading: true });
|
||||
|
||||
dispatchScrollToTop = debounce((top) => {
|
||||
this.props.dispatch(scrollTopNotifications(top));
|
||||
}, 100);
|
||||
|
||||
handleScroll = (e) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
this._oldScrollPosition = scrollHeight - scrollTop;
|
||||
|
||||
if (250 > offset && !this.props.isLoading) {
|
||||
if (this.props.hasMore) {
|
||||
this.props.dispatch(expandNotifications());
|
||||
}
|
||||
} else if (scrollTop < 100) {
|
||||
this.props.dispatch(scrollTopNotifications(true));
|
||||
if (250 > offset && this.props.hasMore && !this.props.isLoading) {
|
||||
this.dispatchExpandNotifications();
|
||||
}
|
||||
|
||||
if (scrollTop < 100) {
|
||||
this.dispatchScrollToTop(true);
|
||||
} else {
|
||||
this.props.dispatch(scrollTopNotifications(false));
|
||||
this.dispatchScrollToTop(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,7 +83,7 @@ export default class Notifications extends React.PureComponent {
|
||||
|
||||
handleLoadMore = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.dispatch(expandNotifications());
|
||||
this.dispatchExpandNotifications();
|
||||
}
|
||||
|
||||
handlePin = () => {
|
||||
|
@ -56,7 +56,6 @@ export default class ActionBar extends React.PureComponent {
|
||||
|
||||
handleReport = () => {
|
||||
this.props.onReport(this.props.status);
|
||||
this.context.router.history.push('/report');
|
||||
}
|
||||
|
||||
render () {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class ImageLoader extends React.PureComponent {
|
||||
|
||||
@ -20,46 +21,121 @@ export default class ImageLoader extends React.PureComponent {
|
||||
error: false,
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._loadImage(this.props.src);
|
||||
removers = [];
|
||||
|
||||
get canvasContext() {
|
||||
if (!this.canvas) {
|
||||
return null;
|
||||
}
|
||||
this._canvasContext = this._canvasContext || this.canvas.getContext('2d');
|
||||
return this._canvasContext;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this._loadImage(props.src);
|
||||
componentDidMount () {
|
||||
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 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 });
|
||||
image.onload = () => this.setState({ loading: false, error: false });
|
||||
|
||||
image.src = src;
|
||||
|
||||
this.setState({ loading: true });
|
||||
clearPreviewCanvas () {
|
||||
const { width, height } = this.canvas;
|
||||
this.canvasContext.clearRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { alt, src, previewSrc, width, height } = this.props;
|
||||
loadOriginalImage = ({ src }) => new Promise((resolve, reject) => {
|
||||
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 className = classNames('image-loader', {
|
||||
'image-loader--loading': loading,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='image-loader'>
|
||||
<img
|
||||
alt={alt}
|
||||
className='image-loader__img'
|
||||
src={src}
|
||||
<div className={className}>
|
||||
<canvas
|
||||
className='image-loader__preview-canvas'
|
||||
width={width}
|
||||
height={height}
|
||||
ref={this.setCanvasRef}
|
||||
/>
|
||||
|
||||
{loading &&
|
||||
{!loading && (
|
||||
<img
|
||||
alt=''
|
||||
src={previewSrc}
|
||||
className='image-loader__preview-img'
|
||||
alt={alt}
|
||||
className='image-loader__img'
|
||||
src={src}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import OnboardingModal from './onboarding_modal';
|
||||
import VideoModal from './video_modal';
|
||||
import BoostModal from './boost_modal';
|
||||
import ConfirmationModal from './confirmation_modal';
|
||||
import ReportModal from './report_modal';
|
||||
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
@ -14,6 +15,7 @@ const MODAL_COMPONENTS = {
|
||||
'VIDEO': VideoModal,
|
||||
'BOOST': BoostModal,
|
||||
'CONFIRM': ConfirmationModal,
|
||||
'REPORT': ReportModal,
|
||||
};
|
||||
|
||||
export default class ModalRoot extends React.PureComponent {
|
||||
|
@ -3,6 +3,7 @@ import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ReactSwipeable from 'react-swipeable';
|
||||
import classNames from 'classnames';
|
||||
import Permalink from '../../../components/permalink';
|
||||
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'>
|
||||
<TransitionMotion styles={styles}>
|
||||
{interpolatedStyles => (
|
||||
<div className='onboarding-modal__pager'>
|
||||
<ReactSwipeable onSwipedRight={this.handlePrev} onSwipedLeft={this.handleNext} className='onboarding-modal__pager'>
|
||||
{interpolatedStyles.map(({ key, data, style }, i) => {
|
||||
const className = classNames('onboarding-modal__page__wrapper', {
|
||||
'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>
|
||||
</ReactSwipeable>
|
||||
)}
|
||||
</TransitionMotion>
|
||||
|
||||
|
@ -1,19 +1,17 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { changeReportComment, submitReport } from '../../actions/reports';
|
||||
import { refreshAccountTimeline } from '../../actions/timelines';
|
||||
import { changeReportComment, submitReport } from '../../../actions/reports';
|
||||
import { refreshAccountTimeline } from '../../../actions/timelines';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Column from '../ui/components/column';
|
||||
import Button from '../../components/button';
|
||||
import { makeGetAccount } from '../../selectors';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
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 ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Button from '../../../components/button';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'report.heading', defaultMessage: 'New report' },
|
||||
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
|
||||
submit: { id: 'report.submit', defaultMessage: 'Submit' },
|
||||
});
|
||||
@ -37,11 +35,7 @@ const makeMapStateToProps = () => {
|
||||
|
||||
@connect(makeMapStateToProps)
|
||||
@injectIntl
|
||||
export default class Report extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
export default class ReportModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isSubmitting: PropTypes.bool,
|
||||
@ -52,17 +46,15 @@ export default class Report extends React.PureComponent {
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
if (!this.props.account) {
|
||||
this.context.router.history.replace('/');
|
||||
}
|
||||
handleCommentChange = (e) => {
|
||||
this.props.dispatch(changeReportComment(e.target.value));
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.dispatch(submitReport());
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (!this.props.account) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 () {
|
||||
const { account, comment, intl, statusIds, isSubmitting } = this.props;
|
||||
|
||||
@ -89,36 +72,33 @@ export default class Report extends React.PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<Column heading={intl.formatMessage(messages.heading)} icon='flag'>
|
||||
<ColumnBackButtonSlim />
|
||||
<div className='modal-root__modal report-modal'>
|
||||
<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__target'>
|
||||
<FormattedMessage id='report.target' defaultMessage='Reporting' />
|
||||
<strong>{account.get('acct')}</strong>
|
||||
</div>
|
||||
|
||||
<div className='scrollable report__statuses'>
|
||||
<div className='report-modal__container'>
|
||||
<div className='report-modal__statuses'>
|
||||
<div>
|
||||
{statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='report__textarea-wrapper'>
|
||||
<div className='report-modal__comment'>
|
||||
<textarea
|
||||
className='report__textarea'
|
||||
className='setting-text light'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={comment}
|
||||
onChange={this.handleCommentChange}
|
||||
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>
|
||||
</Column>
|
||||
|
||||
<div className='report-modal__action-bar'>
|
||||
<Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import { refreshHomeTimeline } from '../../actions/timelines';
|
||||
import { refreshNotifications } from '../../actions/notifications';
|
||||
import UploadArea from './components/upload_area';
|
||||
import ColumnsAreaContainer from './containers/columns_area_container';
|
||||
|
||||
import Status from '../../features/status';
|
||||
import GettingStarted from '../../features/getting_started';
|
||||
import PublicTimeline from '../../features/public_timeline';
|
||||
@ -35,7 +34,6 @@ import GenericNotFound from '../../features/generic_not_found';
|
||||
import FavouritedStatuses from '../../features/favourited_statuses';
|
||||
import Blocks from '../../features/blocks';
|
||||
import Mutes from '../../features/mutes';
|
||||
import Report from '../../features/report';
|
||||
|
||||
// Small wrapper to pass multiColumn to the route components
|
||||
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='/blocks' component={Blocks} content={children} />
|
||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||
<WrappedRoute path='/report' component={Report} content={children} />
|
||||
|
||||
<WrappedRoute component={GenericNotFound} content={children} />
|
||||
</WrappedSwitch>
|
||||
|
@ -1151,6 +1151,23 @@
|
||||
],
|
||||
"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": [
|
||||
{
|
||||
|
@ -140,10 +140,10 @@
|
||||
"privacy.unlisted.long": "Do not post to public timelines",
|
||||
"privacy.unlisted.short": "Unlisted",
|
||||
"reply_indicator.cancel": "Cancel",
|
||||
"report.heading": "New report",
|
||||
"report.heading": "Report {target}",
|
||||
"report.placeholder": "Additional comments",
|
||||
"report.submit": "Submit",
|
||||
"report.target": "Reporting",
|
||||
"report.target": "Reporting {target}",
|
||||
"search.placeholder": "Search",
|
||||
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
|
||||
"status.cannot_reblog": "This post cannot be boosted",
|
||||
|
@ -27,8 +27,8 @@
|
||||
"column.notifications": "Notifications",
|
||||
"column.public": "Fil public global",
|
||||
"column_back_button.label": "Retour",
|
||||
"column_header.pin": "Pin",
|
||||
"column_header.unpin": "Unpin",
|
||||
"column_header.pin": "Épingler",
|
||||
"column_header.unpin": "Retirer",
|
||||
"column_subheading.navigation": "Navigation",
|
||||
"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.",
|
||||
@ -101,7 +101,7 @@
|
||||
"notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
|
||||
"notifications.column_settings.alert": "Notifications locales",
|
||||
"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.reblog": "Partages :",
|
||||
"notifications.column_settings.show": "Afficher dans la colonne",
|
||||
|
@ -136,10 +136,10 @@
|
||||
"privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu",
|
||||
"privacy.unlisted.short": "Niewidoczne",
|
||||
"reply_indicator.cancel": "Anuluj",
|
||||
"report.heading": "Nowe zgłoszenie",
|
||||
"report.heading": "Zgłoś {target}",
|
||||
"report.placeholder": "Dodatkowe komentarze",
|
||||
"report.submit": "Wyślij",
|
||||
"report.target": "Zgłaszanie",
|
||||
"report.target": "Zgłaszanie {target}",
|
||||
"search.placeholder": "Szukaj",
|
||||
"search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
|
||||
"status.cannot_reblog": "Ten post nie może zostać podbity",
|
||||
|
@ -129,6 +129,11 @@
|
||||
color: $ui-primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.positive-hint {
|
||||
color: $valid-value-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.simple_form {
|
||||
|
@ -58,37 +58,6 @@
|
||||
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 {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
color: $ui-primary-color;
|
||||
@ -670,13 +639,15 @@
|
||||
}
|
||||
|
||||
.status-check-box {
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
border-bottom: 1px solid $ui-secondary-color;
|
||||
display: flex;
|
||||
|
||||
.status__content {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
flex: 1 1 auto;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1233,20 +1204,22 @@
|
||||
|
||||
.image-loader {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-loader__preview-img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: blur(2px);
|
||||
}
|
||||
&.image-loader--loading {
|
||||
.image-loader__preview-canvas {
|
||||
filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
.media-modal img.image-loader__preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.image-loader__img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar {
|
||||
@ -1980,6 +1953,17 @@
|
||||
@include limited-single-column('screen and (max-width: 600px)') {
|
||||
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';
|
||||
@ -2231,11 +2215,6 @@ button.icon-button.active i.fa-retweet {
|
||||
transition: max-height 150ms ease-in-out, opacity 300ms linear;
|
||||
opacity: 1;
|
||||
|
||||
& > div {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
max-height: 0;
|
||||
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 {
|
||||
&:hover {
|
||||
color: lighten($ui-primary-color, 4%);
|
||||
@ -2437,67 +2421,6 @@ button.icon-button.active i.fa-retweet {
|
||||
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 {
|
||||
color: lighten($ui-base-color, 20%);
|
||||
background: $ui-base-color;
|
||||
@ -3086,6 +3009,7 @@ button.icon-button.active i.fa-retweet {
|
||||
position: relative;
|
||||
|
||||
img,
|
||||
canvas,
|
||||
video {
|
||||
max-width: 80vw;
|
||||
max-height: 80vh;
|
||||
@ -3093,7 +3017,8 @@ button.icon-button.active i.fa-retweet {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
img {
|
||||
img,
|
||||
canvas {
|
||||
display: block;
|
||||
background: url('../images/void.png') repeat;
|
||||
}
|
||||
@ -3279,6 +3204,7 @@ button.icon-button.active i.fa-retweet {
|
||||
@media screen and (max-width: 400px) {
|
||||
.onboarding-modal__page-one {
|
||||
flex-direction: column;
|
||||
align-items: normal;
|
||||
}
|
||||
|
||||
.onboarding-modal__page-one__elephant-friend {
|
||||
@ -3393,7 +3319,8 @@ button.icon-button.active i.fa-retweet {
|
||||
}
|
||||
|
||||
.boost-modal,
|
||||
.confirmation-modal {
|
||||
.confirmation-modal,
|
||||
.report-modal {
|
||||
background: lighten($ui-secondary-color, 8%);
|
||||
color: $ui-base-color;
|
||||
border-radius: 8px;
|
||||
@ -3429,7 +3356,8 @@ button.icon-button.active i.fa-retweet {
|
||||
}
|
||||
|
||||
.boost-modal__action-bar,
|
||||
.confirmation-modal__action-bar {
|
||||
.confirmation-modal__action-bar,
|
||||
.report-modal__action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
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__cancel-button {
|
||||
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;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
@ -3601,10 +3547,15 @@ button.icon-button.active i.fa-retweet {
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
cursor: zoom-in;
|
||||
display: block;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&,
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.media-gallery__gifv {
|
||||
|
@ -358,7 +358,6 @@ code {
|
||||
}
|
||||
|
||||
.user_filtered_languages {
|
||||
|
||||
& > label {
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
|
@ -10,7 +10,6 @@
|
||||
.recovery-codes {
|
||||
list-style: none;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
|
||||
li {
|
||||
font-size: 125%;
|
||||
|
@ -42,6 +42,18 @@
|
||||
strong {
|
||||
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 {
|
||||
|
13
app/mailers/admin_mailer.rb
Normal file
13
app/mailers/admin_mailer.rb
Normal 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
|
@ -4,4 +4,12 @@ class ApplicationMailer < ActionMailer::Base
|
||||
default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' }
|
||||
layout 'mailer'
|
||||
helper :instance
|
||||
|
||||
protected
|
||||
|
||||
def locale_for_account(account)
|
||||
I18n.with_locale(account.user_locale || I18n.default_locale) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -67,12 +67,4 @@ class NotificationMailer < ApplicationMailer
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def locale_for_account(account)
|
||||
I18n.with_locale(account.user_locale || I18n.default_locale) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3,36 +3,78 @@
|
||||
#
|
||||
# Table name: session_activations
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer not null
|
||||
# session_id :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer not null
|
||||
# session_id :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# user_agent :string default(""), not null
|
||||
# ip :inet
|
||||
# access_token_id :integer
|
||||
#
|
||||
|
||||
class SessionActivation < ApplicationRecord
|
||||
LIMIT = Rails.configuration.x.max_session_activations
|
||||
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy
|
||||
|
||||
def self.active?(id)
|
||||
id && where(session_id: id).exists?
|
||||
delegate :token,
|
||||
to: :access_token,
|
||||
allow_nil: true
|
||||
|
||||
def detection
|
||||
@detection ||= Browser.new(user_agent)
|
||||
end
|
||||
|
||||
def self.activate(id)
|
||||
activation = create!(session_id: id)
|
||||
purge_old
|
||||
activation
|
||||
def browser
|
||||
detection.id
|
||||
end
|
||||
|
||||
def self.deactivate(id)
|
||||
return unless id
|
||||
where(session_id: id).destroy_all
|
||||
def platform
|
||||
detection.platform.id
|
||||
end
|
||||
|
||||
def self.purge_old
|
||||
order('created_at desc').offset(LIMIT).destroy_all
|
||||
before_create :assign_access_token
|
||||
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
|
||||
|
||||
def self.exclusive(id)
|
||||
where('session_id != ?', id).destroy_all
|
||||
private
|
||||
|
||||
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
|
||||
|
@ -91,8 +91,10 @@ class User < ApplicationRecord
|
||||
settings.auto_play_gif
|
||||
end
|
||||
|
||||
def activate_session
|
||||
session_activations.activate(SecureRandom.hex).session_id
|
||||
def activate_session(request)
|
||||
session_activations.activate(session_id: SecureRandom.hex,
|
||||
user_agent: request.user_agent,
|
||||
ip: request.ip).session_id
|
||||
end
|
||||
|
||||
def exclusive_session(id)
|
||||
|
@ -13,7 +13,8 @@ class SendInteractionService < BaseService
|
||||
return if block_notification?
|
||||
|
||||
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
|
||||
|
||||
private
|
||||
|
5
app/views/admin_mailer/new_report.text.erb
Normal file
5
app/views/admin_mailer/new_report.text.erb
Normal 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) %>
|
23
app/views/auth/registrations/_sessions.html.haml
Normal file
23
app/views/auth/registrations/_sessions.html.haml
Normal 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)
|
@ -12,6 +12,10 @@
|
||||
.actions
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
|
||||
%hr/
|
||||
|
||||
= render 'sessions'
|
||||
|
||||
- if open_deletion?
|
||||
%hr/
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
- content_for :page_title do
|
||||
= 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
|
||||
- @recovery_codes.each do |code|
|
||||
|
@ -1,26 +1,34 @@
|
||||
- content_for :page_title do
|
||||
= t('settings.two_factor_authentication')
|
||||
|
||||
.simple_form
|
||||
%p.hint
|
||||
= t('two_factor_authentication.description_html')
|
||||
- if current_user.otp_required_for_login
|
||||
%p.positive-hint
|
||||
= 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'),
|
||||
settings_two_factor_authentication_path,
|
||||
data: { method: :post },
|
||||
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'
|
||||
|
@ -1,3 +1,3 @@
|
||||
<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>
|
||||
|
@ -1,3 +1,3 @@
|
||||
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.
|
||||
|
@ -1,6 +1,6 @@
|
||||
<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>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
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) %>
|
||||
|
||||
|
11
app/workers/scheduler/doorkeeper_cleanup_scheduler.rb
Normal file
11
app/workers/scheduler/doorkeeper_cleanup_scheduler.rb
Normal 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
|
Reference in New Issue
Block a user