diff options
Diffstat (limited to 'app')
48 files changed, 498 insertions, 338 deletions
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 71df76e92..8e7070d07 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9cb397aa8..865fcd125 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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) diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index d385c08e1..60ace04d7 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -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 diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 1d41892cd..6209a3ae9 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -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 diff --git a/app/controllers/settings/two_factor_authentications_controller.rb b/app/controllers/settings/two_factor_authentications_controller.rb index f66c3a908..983483881 100644 --- a/app/controllers/settings/two_factor_authentications_controller.rb +++ b/app/controllers/settings/two_factor_authentications_controller.rb @@ -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 diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 172ef33ca..847eff2e7 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -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 diff --git a/app/javascript/mastodon/actions/reports.js b/app/javascript/mastodon/actions/reports.js index 9b632be74..b19a07285 100644 --- a/app/javascript/mastodon/actions/reports.js +++ b/app/javascript/mastodon/actions/reports.js @@ -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))); }; }; diff --git a/app/javascript/mastodon/components/column_collapsable.js b/app/javascript/mastodon/components/column_collapsable.js deleted file mode 100644 index d6b4edb9f..000000000 --- a/app/javascript/mastodon/components/column_collapsable.js +++ /dev/null @@ -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> - ); - } - -} diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index a309f74e8..ec9379320 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -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> diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 78ff35130..2cb1ce51c 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -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; diff --git a/app/javascript/mastodon/components/permalink.js b/app/javascript/mastodon/components/permalink.js index 5d3e4738d..0b7d0a65a 100644 --- a/app/javascript/mastodon/components/permalink.js +++ b/app/javascript/mastodon/components/permalink.js @@ -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> ); diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 0077a9191..9e9e1c3c7 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -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; } } diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index a1e1a135a..6693548c7 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -88,7 +88,6 @@ export default class StatusActionBar extends ImmutablePureComponent { handleReport = () => { this.props.onReport(this.props.status); - this.context.router.history.push('/report'); } handleConversationMuteClick = () => { diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index 7f80e39e8..167a2097e 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -38,7 +38,6 @@ export default class Header extends ImmutablePureComponent { handleReport = () => { this.props.onReport(this.props.account); - this.context.router.history.push('/report'); } handleMute = () => { diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index c379c1855..f7eeedc69 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -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(); } diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js index 137e55089..8cef6a1e4 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.js +++ b/app/javascript/mastodon/features/favourited_statuses/index.js @@ -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 () { diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 1dd1b9a71..ed4b3ad98 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -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 = () => { diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 29080529d..5e150842e 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -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 () { diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js index 5c3879970..52c3a898b 100644 --- a/app/javascript/mastodon/features/ui/components/image_loader.js +++ b/app/javascript/mastodon/features/ui/components/image_loader.js @@ -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; + } + + componentDidMount () { + this.loadImage(this.props); + } + + componentWillReceiveProps (nextProps) { + if (this.props.src !== nextProps.src) { + this.loadImage(nextProps); + } } - componentWillReceiveProps(props) { - this._loadImage(props.src); + 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 })); } - _loadImage(src) { + 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 }); + clearPreviewCanvas () { + const { width, height } = this.canvas; + this.canvasContext.clearRect(0, 0, width, height); + } + 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); + }); - this.setState({ loading: true }); + removeEventListeners () { + this.removers.forEach(listeners => listeners()); + this.removers = []; } - render() { - const { alt, src, previewSrc, width, height } = this.props; + 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> ); } diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 2e4f9876d..48b048eb7 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -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 { diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js index 4c1c0f7c1..dab5e47ea 100644 --- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js +++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js @@ -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> diff --git a/app/javascript/mastodon/features/report/index.js b/app/javascript/mastodon/features/ui/components/report_modal.js index bfb09e193..c989d2c9b 100644 --- a/app/javascript/mastodon/features/report/index.js +++ b/app/javascript/mastodon/features/ui/components/report_modal.js @@ -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)); } - componentDidMount () { - if (!this.props.account) { - return; - } + handleSubmit = () => { + this.props.dispatch(submitReport()); + } + componentDidMount () { 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='report scrollable'> - <div className='report__target'> - <FormattedMessage id='report.target' defaultMessage='Reporting' /> - <strong>{account.get('acct')}</strong> - </div> + <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='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> ); } diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index e5915ffe0..4d38c2677 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -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> diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 45353e9a3..5ab914477 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -1154,6 +1154,23 @@ { "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": [ + { "defaultMessage": "Compose", "id": "tabs_bar.compose" }, diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 3b2d198b1..d0c0ca137 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 52d6b0f87..1a69235c8 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -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", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 6dcb872df..bf425501f 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -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", diff --git a/app/javascript/styles/admin.scss b/app/javascript/styles/admin.scss index c2bfc10a0..3bc713566 100644 --- a/app/javascript/styles/admin.scss +++ b/app/javascript/styles/admin.scss @@ -129,6 +129,11 @@ color: $ui-primary-color; } } + + .positive-hint { + color: $valid-value-color; + font-weight: 500; + } } .simple_form { diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 85566a653..a7c982cb2 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -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 { diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss index 059c4a7d8..7a181f36b 100644 --- a/app/javascript/styles/forms.scss +++ b/app/javascript/styles/forms.scss @@ -358,7 +358,6 @@ code { } .user_filtered_languages { - & > label { font-family: inherit; font-size: 16px; diff --git a/app/javascript/styles/lists.scss b/app/javascript/styles/lists.scss index 47805663f..6019cd800 100644 --- a/app/javascript/styles/lists.scss +++ b/app/javascript/styles/lists.scss @@ -10,7 +10,6 @@ .recovery-codes { list-style: none; margin: 0 auto; - text-align: center; li { font-size: 125%; diff --git a/app/javascript/styles/tables.scss b/app/javascript/styles/tables.scss index f7def8cf3..6e54c59c0 100644 --- a/app/javascript/styles/tables.scss +++ b/app/javascript/styles/tables.scss @@ -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 { diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb new file mode 100644 index 000000000..fc19a6d40 --- /dev/null +++ b/app/mailers/admin_mailer.rb @@ -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 diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index e5dbfeeda..2e730c19b 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -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 diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index a944db137..12b92bf45 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -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 diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb index 71e9f023c..02a918e8a 100644 --- a/app/models/session_activation.rb +++ b/app/models/session_activation.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index fccf1089b..c31a0c644 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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) diff --git a/app/services/send_interaction_service.rb b/app/services/send_interaction_service.rb index 504f41c72..34c8f9e34 100644 --- a/app/services/send_interaction_service.rb +++ b/app/services/send_interaction_service.rb @@ -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 diff --git a/app/views/admin_mailer/new_report.text.erb b/app/views/admin_mailer/new_report.text.erb new file mode 100644 index 000000000..6fa744bc3 --- /dev/null +++ b/app/views/admin_mailer/new_report.text.erb @@ -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) %> diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml new file mode 100644 index 000000000..11c0d4e31 --- /dev/null +++ b/app/views/auth/registrations/_sessions.html.haml @@ -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) diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index 38d4349cb..fbc8d017b 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -12,6 +12,10 @@ .actions = f.button :button, t('generic.save_changes'), type: :submit +%hr/ + += render 'sessions' + - if open_deletion? %hr/ diff --git a/app/views/settings/two_factor_authentication/recovery_codes/index.html.haml b/app/views/settings/two_factor_authentication/recovery_codes/index.html.haml index 7d409826e..d47ee840e 100644 --- a/app/views/settings/two_factor_authentication/recovery_codes/index.html.haml +++ b/app/views/settings/two_factor_authentication/recovery_codes/index.html.haml @@ -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| diff --git a/app/views/settings/two_factor_authentications/show.html.haml b/app/views/settings/two_factor_authentications/show.html.haml index 88b5bd20e..8ba42a101 100644 --- a/app/views/settings/two_factor_authentications/show.html.haml +++ b/app/views/settings/two_factor_authentications/show.html.haml @@ -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' - - 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' + %hr/ -- if current_user.otp_required_for_login - .simple_form - %p.hint - = t('two_factor_authentication.lost_recovery_codes') + = 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') + + = link_to t('two_factor_authentication.setup'), + settings_two_factor_authentication_path, data: { method: :post }, class: 'block-button' diff --git a/app/views/user_mailer/password_change.en.html.erb b/app/views/user_mailer/password_change.en.html.erb index a1bc77463..414e05a29 100644 --- a/app/views/user_mailer/password_change.en.html.erb +++ b/app/views/user_mailer/password_change.en.html.erb @@ -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> diff --git a/app/views/user_mailer/password_change.en.text.erb b/app/views/user_mailer/password_change.en.text.erb index 27581e604..3ae461c97 100644 --- a/app/views/user_mailer/password_change.en.text.erb +++ b/app/views/user_mailer/password_change.en.text.erb @@ -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. diff --git a/app/views/user_mailer/reset_password_instructions.en.html.erb b/app/views/user_mailer/reset_password_instructions.en.html.erb index 643b43319..cfb129e22 100644 --- a/app/views/user_mailer/reset_password_instructions.en.html.erb +++ b/app/views/user_mailer/reset_password_instructions.en.html.erb @@ -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> diff --git a/app/views/user_mailer/reset_password_instructions.en.text.erb b/app/views/user_mailer/reset_password_instructions.en.text.erb index fe73b0165..7ed22dc2c 100644 --- a/app/views/user_mailer/reset_password_instructions.en.text.erb +++ b/app/views/user_mailer/reset_password_instructions.en.text.erb @@ -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) %> diff --git a/app/workers/scheduler/doorkeeper_cleanup_scheduler.rb b/app/workers/scheduler/doorkeeper_cleanup_scheduler.rb new file mode 100644 index 000000000..6488798cd --- /dev/null +++ b/app/workers/scheduler/doorkeeper_cleanup_scheduler.rb @@ -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 |