Merge commit 'cf3fa1e814' into kb_migration

This commit is contained in:
KMY 2023-04-17 10:35:39 +09:00
commit 14aa902fa5
83 changed files with 280 additions and 200 deletions

View file

@ -10,15 +10,7 @@ class Auth::SetupController < ApplicationController
skip_before_action :require_functional!
def show
flash.now[:notice] = begin
if @user.pending?
I18n.t('devise.registrations.signed_up_but_pending')
else
I18n.t('devise.registrations.signed_up_but_unconfirmed')
end
end
end
def show; end
def update
# This allows updating the e-mail without entering a password as is required
@ -26,14 +18,13 @@ class Auth::SetupController < ApplicationController
# that were not confirmed yet
if @user.update(user_params)
redirect_to auth_setup_path, notice: I18n.t('devise.confirmations.send_instructions')
@user.resend_confirmation_instructions unless @user.confirmed?
redirect_to auth_setup_path, notice: I18n.t('auth.setup.new_confirmation_instructions_sent')
else
render :show
end
end
helper_method :missing_email?
private
def require_unconfirmed_or_pending!
@ -51,8 +42,4 @@ class Auth::SetupController < ApplicationController
def user_params
params.require(:user).permit(:email)
end
def missing_email?
truthy_param?(:missing_email)
end
end

View file

@ -117,6 +117,10 @@ module ApplicationHelper
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end
def check_icon
content_tag(:svg, tag(:path, 'fill-rule': 'evenodd', 'clip-rule': 'evenodd', d: 'M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'), xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor')
end
def visibility_icon(status)
if status.public_visibility?
fa_icon('globe', title: I18n.t('statuses.visibilities.public'))

View file

@ -1,76 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class GIFV extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string,
lang: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
onClick: PropTypes.func,
};
state = {
loading: true,
};
handleLoadedData = () => {
this.setState({ loading: false });
};
componentWillReceiveProps (nextProps) {
if (nextProps.src !== this.props.src) {
this.setState({ loading: true });
}
}
handleClick = e => {
const { onClick } = this.props;
if (onClick) {
e.stopPropagation();
onClick();
}
};
render () {
const { src, width, height, alt, lang } = this.props;
const { loading } = this.state;
return (
<div className='gifv' style={{ position: 'relative' }}>
{loading && (
<canvas
width={width}
height={height}
role='button'
tabIndex={0}
aria-label={alt}
title={alt}
lang={lang}
onClick={this.handleClick}
/>
)}
<video
src={src}
role='button'
tabIndex={0}
aria-label={alt}
title={alt}
lang={lang}
muted
loop
autoPlay
playsInline
onClick={this.handleClick}
onLoadedData={this.handleLoadedData}
style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
/>
</div>
);
}
}

View file

@ -0,0 +1,68 @@
import React, { useCallback, useState } from 'react';
type Props = {
src: string;
key: string;
alt?: string;
lang?: string;
width: number;
height: number;
onClick?: () => void;
}
export const GIFV: React.FC<Props> = ({
src,
alt,
lang,
width,
height,
onClick,
})=> {
const [loading, setLoading] = useState(true);
const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> = useCallback(() => {
setLoading(false);
}, [setLoading]);
const handleClick: React.MouseEventHandler = useCallback((e) => {
if (onClick) {
e.stopPropagation();
onClick();
}
}, [onClick]);
return (
<div className='gifv' style={{ position: 'relative' }}>
{loading && (
<canvas
width={width}
height={height}
role='button'
tabIndex={0}
aria-label={alt}
title={alt}
lang={lang}
onClick={handleClick}
/>
)}
<video
src={src}
role='button'
tabIndex={0}
aria-label={alt}
title={alt}
lang={lang}
muted
loop
autoPlay
playsInline
onClick={handleClick}
onLoadedData={handleLoadedData}
style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
/>
</div>
);
};
export default GIFV;

View file

@ -383,7 +383,7 @@ class FocalPointModal extends ImmutablePureComponent {
{focals && (
<div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
{media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />}
{media.get('type') === 'gifv' && <GIFV src={media.get('url')} width={width} height={height} />}
{media.get('type') === 'gifv' && <GIFV src={media.get('url')} key={media.get('url')} width={width} height={height} />}
<div className='focal-point__preview'>
<strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong>

View file

@ -186,7 +186,7 @@ class MediaModal extends ImmutablePureComponent {
src={image.get('url')}
width={width}
height={height}
key={image.get('preview_url')}
key={image.get('url')}
alt={image.get('description')}
lang={language}
onClick={this.toggleNavigation}

View file

@ -4358,5 +4358,22 @@
}
],
"path": "app/javascript/mastodon/features/video/index.json"
},
{
"descriptors": [
{
"defaultMessage": "That username is taken. Try another",
"id": "username.taken"
},
{
"defaultMessage": "Password confirmation exceeds the maximum password length",
"id": "password_confirmation.exceeds_maxlength"
},
{
"defaultMessage": "Password confirmation does not match",
"id": "password_confirmation.mismatching"
}
],
"path": "app/javascript/packs/public.json"
}
]

View file

@ -445,6 +445,8 @@
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"password_confirmation.exceeds_maxlength": "Password confirmation exceeds the maximum password length",
"password_confirmation.mismatching": "Password confirmation does not match",
"picture_in_picture.restore": "Put it back",
"poll.closed": "Closed",
"poll.refresh": "Refresh",
@ -664,6 +666,7 @@
"upload_modal.preview_label": "Preview ({ratio})",
"upload_progress.label": "Uploading...",
"upload_progress.processing": "Processing…",
"username.taken": "That username is taken. Try another",
"video.close": "Close video",
"video.download": "Download file",
"video.exit_fullscreen": "Exit full screen",

View file

@ -5,6 +5,15 @@ import ready from '../mastodon/ready';
import { start } from '../mastodon/common';
import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions';
import 'cocoon-js-vanilla';
import axios from 'axios';
import { throttle } from 'lodash';
import { defineMessages } from 'react-intl';
const messages = defineMessages({
usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' },
passwordExceedsLength: { id: 'password_confirmation.exceeds_maxlength', defaultMessage: 'Password confirmation exceeds the maximum password length' },
passwordDoesNotMatch: { id: 'password_confirmation.mismatching', defaultMessage: 'Password confirmation does not match' },
});
start();
@ -30,7 +39,7 @@ function main() {
const { delegate } = require('@rails/ujs');
const emojify = require('../mastodon/features/emoji/emoji').default;
const { getLocale } = require('../mastodon/locales');
const { messages } = getLocale();
const { localeData } = getLocale();
const React = require('react');
const ReactDOM = require('react-dom');
const { createBrowserHistory } = require('history');
@ -75,6 +84,11 @@ function main() {
hour12: false,
});
const formatMessage = ({ id, defaultMessage }, values) => {
const messageFormat = new IntlMessageFormat(localeData[id] || defaultMessage, locale);
return messageFormat.format(values);
};
[].forEach.call(document.querySelectorAll('.emojify'), (content) => {
content.innerHTML = emojify(content.innerHTML);
});
@ -94,7 +108,7 @@ function main() {
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
};
const todayFormat = new IntlMessageFormat(messages['relative_format.today'] || 'Today at {time}', locale);
const todayFormat = new IntlMessageFormat(localeData['relative_format.today'] || 'Today at {time}', locale);
[].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
@ -120,7 +134,7 @@ function main() {
const timeGiven = content.getAttribute('datetime').includes('T');
content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime);
content.textContent = timeAgoString({
formatMessage: ({ id, defaultMessage }, values) => (new IntlMessageFormat(messages[id] || defaultMessage, locale)).format(values),
formatMessage,
formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date),
}, datetime, now, now.getFullYear(), timeGiven);
});
@ -150,17 +164,19 @@ function main() {
scrollToDetailedStatus();
}
delegate(document, '#registration_user_password_confirmation,#registration_user_password', 'input', () => {
const password = document.getElementById('registration_user_password');
const confirmation = document.getElementById('registration_user_password_confirmation');
if (confirmation.value && confirmation.value.length > password.maxLength) {
confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format());
} else if (password.value && password.value !== confirmation.value) {
confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format());
delegate(document, '#user_account_attributes_username', 'input', throttle(() => {
const username = document.getElementById('user_account_attributes_username');
if (username.value && username.value.length > 0) {
axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => {
username.setCustomValidity(formatMessage(messages.usernameTaken));
}).catch(() => {
username.setCustomValidity('');
});
} else {
confirmation.setCustomValidity('');
username.setCustomValidity('');
}
});
}, 500, { leading: false, trailing: true }));
delegate(document, '#user_password,#user_password_confirmation', 'input', () => {
const password = document.getElementById('user_password');
@ -168,9 +184,9 @@ function main() {
if (!confirmation) return;
if (confirmation.value && confirmation.value.length > password.maxLength) {
confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format());
confirmation.setCustomValidity(formatMessage(messages.passwordExceedsLength));
} else if (password.value && password.value !== confirmation.value) {
confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format());
confirmation.setCustomValidity(formatMessage(messages.passwordDoesNotMatch));
} else {
confirmation.setCustomValidity('');
}
@ -184,10 +200,10 @@ function main() {
if (statusEl.dataset.spoiler === 'expanded') {
statusEl.dataset.spoiler = 'folded';
this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
this.textContent = (new IntlMessageFormat(localeData['status.show_more'] || 'Show more', locale)).format();
} else {
statusEl.dataset.spoiler = 'expanded';
this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
this.textContent = (new IntlMessageFormat(localeData['status.show_less'] || 'Show less', locale)).format();
}
return false;
@ -195,7 +211,7 @@ function main() {
[].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
const statusEl = spoilerLink.parentNode.parentNode;
const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more');
spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
});
});

View file

@ -1112,3 +1112,89 @@ code {
white-space: nowrap;
}
}
.progress-tracker {
display: flex;
align-items: center;
padding-bottom: 30px;
margin-bottom: 30px;
li {
flex: 0 0 auto;
position: relative;
}
.separator {
height: 2px;
background: $ui-base-lighter-color;
flex: 1 1 auto;
&.completed {
background: $highlight-text-color;
}
}
.circle {
box-sizing: border-box;
position: relative;
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid $ui-base-lighter-color;
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
svg {
width: 16px;
}
}
.label {
position: absolute;
font-size: 14px;
font-weight: 500;
color: $secondary-text-color;
padding-top: 10px;
text-align: center;
width: 100px;
left: 50%;
transform: translateX(-50%);
}
li:first-child .label {
left: auto;
inset-inline-start: 0;
text-align: start;
transform: none;
}
li:last-child .label {
left: auto;
inset-inline-end: 0;
text-align: end;
transform: none;
}
.active .circle {
border-color: $highlight-text-color;
&::before {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: $highlight-text-color;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
.completed .circle {
border-color: $highlight-text-color;
background: $highlight-text-color;
}
}

View file

@ -5,6 +5,8 @@
= render partial: 'shared/og', locals: { description: description_for_sign_up }
= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { novalidate: false }) do |f|
= render 'auth/shared/progress', stage: 'details'
%h1.title= t('auth.sign_up.title', domain: site_hostname)
%p.lead= t('auth.sign_up.preamble')
@ -18,7 +20,7 @@
.fields-group
= f.simple_fields_for :account do |ff|
= ff.input :display_name, wrapper: :with_label, label: false, required: false, input_html: { 'aria-label': t('simple_form.labels.defaults.display_name'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.display_name') }
= ff.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.username'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false
= ff.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.username'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}"
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'username' }, hint: false
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.password'), autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last }, hint: false
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.confirm_password'), autocomplete: 'new-password' }, hint: false
@ -26,9 +28,11 @@
= f.input :website, as: :url, wrapper: :with_label, label: t('simple_form.labels.defaults.honeypot', label: 'Website'), required: false, input_html: { 'aria-label': t('simple_form.labels.defaults.honeypot', label: 'Website'), autocomplete: 'off' }
- if approved_registrations? && !@invite.present?
%p.lead= t('auth.sign_up.manual_review', domain: site_hostname)
.fields-group
= f.simple_fields_for :invite_request, resource.invite_request || resource.build_invite_request do |invite_request_fields|
= invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: Setting.require_invite_text
= invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: Setting.require_invite_text, label: false, hint: false
= hidden_field_tag :accept, params[:accept]

View file

@ -5,6 +5,8 @@
= render partial: 'shared/og', locals: { description: description_for_sign_up }
.simple_form
= render 'auth/shared/progress', stage: 'rules'
%h1.title= t('auth.rules.title')
%p.lead= t('auth.rules.preamble', domain: site_hostname)

View file

@ -1,20 +1,22 @@
- content_for :page_title do
= t('auth.setup.title')
- if missing_email?
= simple_form_for(@user, url: auth_setup_path) do |f|
= render 'shared/error_messages', object: @user
= simple_form_for(@user, url: auth_setup_path) do |f|
= render 'auth/shared/progress', stage: 'confirm'
.fields-group
%p.hint= t('auth.setup.email_below_hint_html')
%h1.title= t('auth.setup.title')
%p.lead= t('auth.setup.email_settings_hint_html', email: content_tag(:strong, @user.email))
.fields-group
= f.input :email, required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'off' }
= render 'shared/error_messages', object: @user
.actions
= f.submit t('admin.accounts.change_email.label'), class: 'button'
- else
.simple_form
%p.hint= t('auth.setup.email_settings_hint_html', email: content_tag(:strong, @user.email))
%p.lead
%strong= t('auth.setup.link_not_received')
%p.lead= t('auth.setup.email_below_hint_html')
.fields-group
= f.input :email, required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'off' }
.actions
= f.submit t('auth.resend_confirmation'), class: 'button'
.form-footer= render 'auth/shared/links'

View file

@ -14,5 +14,5 @@
- if controller_name != 'confirmations' && (!user_signed_in? || !current_user.confirmed? || current_user.unconfirmed_email.present?)
%li= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
- if user_signed_in? && controller_name != 'setup'
- if user_signed_in?
%li= link_to t('auth.logout'), destroy_user_session_path, data: { method: :delete }

View file

@ -0,0 +1,25 @@
- progress_index = { rules: 0, details: 1, confirm: 2 }[stage.to_sym]
%ol.progress-tracker
%li{ class: progress_index.positive? ? 'completed' : 'active' }
.circle
- if progress_index.positive?
= check_icon
.label= t('auth.progress.rules')
%li.separator{ class: progress_index.positive? ? 'completed' : nil }
%li{ class: [progress_index > 1 && 'completed', progress_index == 1 && 'active'] }
.circle
- if progress_index > 1
= check_icon
.label= t('auth.progress.details')
%li.separator{ class: progress_index > 1 ? 'completed' : nil }
%li{ class: [progress_index > 2 && 'completed', progress_index == 2 && 'active'] }
.circle
- if progress_index > 2
= check_icon
.label= t('auth.progress.confirm')
- if approved_registrations?
%li.separator{ class: progress_index > 2 ? 'completed' : nil }
%li
.circle
.label= t('auth.progress.review')