mirror of
https://github.com/mastodon/mastodon.git
synced 2024-08-20 21:08:15 -07:00
Redesign forms, verify link ownership with rel="me" (#8703)
* Verify link ownership with rel="me" * Add explanation about verification to UI * Perform link verifications * Add click-to-copy widget for verification HTML * Redesign edit profile page * Redesign forms * Improve responsive design of settings pages * Restore landing page sign-up form * Fix typo * Support <link> tags, add spec * Fix links not being verified on first discovery and passive updates
This commit is contained in:
parent
f8b54d229f
commit
f4d549d300
46 changed files with 764 additions and 313 deletions
|
@ -48,4 +48,12 @@ module HomeHelper
|
|||
'1+'
|
||||
end
|
||||
end
|
||||
|
||||
def custom_field_classes(field)
|
||||
if field.verified?
|
||||
'verified'
|
||||
else
|
||||
'emojify'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,8 +15,18 @@ const messages = defineMessages({
|
|||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
|
||||
});
|
||||
|
||||
const dateFormatOptions = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
};
|
||||
|
||||
class Avatar extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
@ -163,7 +173,10 @@ class Header extends ImmutablePureComponent {
|
|||
{fields.map((pair, i) => (
|
||||
<dl key={i}>
|
||||
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
|
||||
<dd dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} title={pair.get('value_plain')} />
|
||||
|
||||
<dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
|
||||
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><i className='fa fa-check verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
|
||||
</dd>
|
||||
</dl>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -68,6 +68,7 @@ function main() {
|
|||
});
|
||||
|
||||
const reactComponents = document.querySelectorAll('[data-component]');
|
||||
|
||||
if (reactComponents.length > 0) {
|
||||
import(/* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container')
|
||||
.then(({ default: MediaContainer }) => {
|
||||
|
@ -80,6 +81,7 @@ function main() {
|
|||
}
|
||||
|
||||
const parallaxComponents = document.querySelectorAll('.parallax');
|
||||
|
||||
if (parallaxComponents.length > 0 ) {
|
||||
new Rellax('.parallax', { speed: -1 });
|
||||
}
|
||||
|
@ -87,6 +89,7 @@ function main() {
|
|||
const history = createHistory();
|
||||
const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status');
|
||||
const location = history.location;
|
||||
|
||||
if (detailedStatuses.length === 1 && (!location.state || !location.state.scrolledToDetailedStatus)) {
|
||||
detailedStatuses[0].scrollIntoView();
|
||||
history.replace(location.pathname, { ...location.state, scrolledToDetailedStatus: true });
|
||||
|
@ -175,6 +178,30 @@ function main() {
|
|||
lock.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
delegate(document, '.input-copy input', 'click', ({ target }) => {
|
||||
target.select();
|
||||
});
|
||||
|
||||
delegate(document, '.input-copy button', 'click', ({ target }) => {
|
||||
const input = target.parentNode.querySelector('input');
|
||||
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
try {
|
||||
if (document.execCommand('copy')) {
|
||||
input.blur();
|
||||
target.parentNode.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
target.parentNode.classList.remove('copied');
|
||||
}, 700);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadPolyfills().then(main).catch(error => {
|
||||
|
|
|
@ -265,6 +265,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
.verified {
|
||||
border: 1px solid rgba($valid-value-color, 0.5);
|
||||
background: rgba($valid-value-color, 0.25);
|
||||
|
||||
a {
|
||||
color: $valid-value-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__mark {
|
||||
color: $valid-value-color;
|
||||
}
|
||||
}
|
||||
|
||||
dl:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
$no-columns-breakpoint: 600px;
|
||||
|
||||
.admin-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -24,12 +26,22 @@
|
|||
height: 100px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
& > a:first-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
border-radius: 4px 0 0 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 15px;
|
||||
|
@ -62,19 +74,23 @@
|
|||
a {
|
||||
border: 0;
|
||||
padding: 15px 35px;
|
||||
|
||||
&.selected {
|
||||
color: $primary-text-color;
|
||||
background-color: $ui-highlight-color;
|
||||
border-bottom: 0;
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($ui-highlight-color, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.simple-navigation-active-leaf a {
|
||||
color: $primary-text-color;
|
||||
background-color: $ui-highlight-color;
|
||||
border-bottom: 0;
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($ui-highlight-color, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > ul > .simple-navigation-active-leaf a {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,11 +105,19 @@
|
|||
padding-top: 60px;
|
||||
padding-left: 25px;
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
max-width: none;
|
||||
padding: 15px;
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $secondary-text-color;
|
||||
font-size: 24px;
|
||||
line-height: 28px;
|
||||
font-weight: 400;
|
||||
padding-bottom: 40px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
|
@ -108,7 +132,7 @@
|
|||
h4 {
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-weight: 700;
|
||||
color: $darker-text-color;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
@ -122,6 +146,11 @@
|
|||
font-weight: 400;
|
||||
}
|
||||
|
||||
.fields-group h6 {
|
||||
color: $primary-text-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
& > p {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
|
@ -172,30 +201,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.simple_form {
|
||||
max-width: 400px;
|
||||
|
||||
&.edit_user,
|
||||
&.new_form_admin_settings,
|
||||
&.new_form_two_factor_confirmation,
|
||||
&.new_form_delete_confirmation,
|
||||
&.new_import,
|
||||
&.new_domain_block,
|
||||
&.edit_domain_block {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.form_two_factor_confirmation_code,
|
||||
.form_delete_confirmation_password {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
@ -209,16 +215,8 @@
|
|||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
|
||||
.logo {
|
||||
margin: 20px auto;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
@function hex-color($color) {
|
||||
@if type-of($color) == 'color' {
|
||||
$color: str-slice(ie-hex-str($color), 4);
|
||||
}
|
||||
@return '%23' + unquote($color)
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'mastodon-font-sans-serif', sans-serif;
|
||||
background: darken($ui-base-color, 8%);
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -5363,9 +5363,11 @@ noscript {
|
|||
overflow: hidden;
|
||||
margin: 20px -10px -20px;
|
||||
border-bottom: 0;
|
||||
border-top: 0;
|
||||
|
||||
dl {
|
||||
border-top: 1px solid lighten($ui-base-color, 8%);
|
||||
border-top: 1px solid lighten($ui-base-color, 4%);
|
||||
border-bottom: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
@ -5392,6 +5394,11 @@ noscript {
|
|||
flex: 1 1 auto;
|
||||
color: $primary-text-color;
|
||||
background: $ui-base-color;
|
||||
|
||||
&.verified {
|
||||
border: 1px solid rgba($valid-value-color, 0.5);
|
||||
background: rgba($valid-value-color, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -718,6 +718,14 @@
|
|||
a {
|
||||
color: lighten($ui-highlight-color, 8%);
|
||||
}
|
||||
|
||||
dl:first-child .verified {
|
||||
border-radius: 0 4px 0 0;
|
||||
}
|
||||
|
||||
.verified a {
|
||||
color: $valid-value-color;
|
||||
}
|
||||
}
|
||||
|
||||
.account__header__content {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
$no-columns-breakpoint: 600px;
|
||||
|
||||
code {
|
||||
font-family: 'mastodon-font-monospace', monospace;
|
||||
font-weight: 400;
|
||||
|
@ -13,6 +15,60 @@ code {
|
|||
.input {
|
||||
margin-bottom: 15px;
|
||||
overflow: hidden;
|
||||
|
||||
&.hidden {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.radio_buttons {
|
||||
.radio {
|
||||
margin-bottom: 15px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.radio > label {
|
||||
position: relative;
|
||||
padding-left: 28px;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.boolean {
|
||||
position: relative;
|
||||
margin-bottom: 0;
|
||||
|
||||
.label_input > label {
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
padding-top: 5px;
|
||||
color: $primary-text-color;
|
||||
display: block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.label_input,
|
||||
.hint {
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
.label_input__wrapper {
|
||||
position: static;
|
||||
}
|
||||
|
||||
label.checkbox {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
|
@ -27,9 +83,22 @@ code {
|
|||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: $darker-text-color;
|
||||
|
||||
a {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
|
||||
code {
|
||||
border-radius: 3px;
|
||||
padding: 0.2em 0.4em;
|
||||
background: darken($ui-base-color, 12%);
|
||||
}
|
||||
}
|
||||
|
||||
span.hint {
|
||||
display: block;
|
||||
color: $darker-text-color;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
@ -44,17 +113,6 @@ code {
|
|||
line-height: 18px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 0;
|
||||
color: $darker-text-color;
|
||||
|
||||
a {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
border-radius: 3px;
|
||||
padding: 0.2em 0.4em;
|
||||
background: darken($ui-base-color, 12%);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,87 +130,60 @@ code {
|
|||
}
|
||||
}
|
||||
|
||||
.label_input {
|
||||
display: flex;
|
||||
.input.with_floating_label {
|
||||
.label_input {
|
||||
display: flex;
|
||||
|
||||
label {
|
||||
flex: 0 0 auto;
|
||||
& > label {
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
color: $primary-text-color;
|
||||
font-weight: 500;
|
||||
min-width: 150px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1 1 auto;
|
||||
&.select .hint {
|
||||
margin-top: 6px;
|
||||
margin-left: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.input.with_label {
|
||||
padding: 15px 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
.label_input {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&.file .label_input {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
&.select .label_input {
|
||||
align-items: initial;
|
||||
}
|
||||
|
||||
.label_input > label {
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
color: $primary-text-color;
|
||||
display: block;
|
||||
padding-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
margin-bottom: 8px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.select {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
& ~ * {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
ul {
|
||||
flex: 390px;
|
||||
}
|
||||
|
||||
&.boolean {
|
||||
padding: initial;
|
||||
margin-bottom: initial;
|
||||
|
||||
.label_input > label {
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
color: $primary-text-color;
|
||||
display: block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
label.checkbox {
|
||||
position: relative;
|
||||
padding-left: 25px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input.with_block_label {
|
||||
padding-top: 15px;
|
||||
max-width: none;
|
||||
|
||||
& > label {
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
color: $primary-text-color;
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
|
@ -165,8 +196,59 @@ code {
|
|||
}
|
||||
}
|
||||
|
||||
.required abbr {
|
||||
text-decoration: none;
|
||||
color: lighten($error-value-color, 12%);
|
||||
}
|
||||
|
||||
.fields-group {
|
||||
margin-bottom: 25px;
|
||||
|
||||
.input:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fields-row {
|
||||
display: flex;
|
||||
margin: 0 -10px;
|
||||
padding-top: 5px;
|
||||
margin-bottom: 25px;
|
||||
|
||||
.input {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
&__column {
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 1px;
|
||||
|
||||
&-6 {
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.fields-group:last-child,
|
||||
.fields-row__column.fields-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-columns-breakpoint) {
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
|
||||
&__column {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.fields-group:last-child,
|
||||
.fields-row__column.fields-group,
|
||||
.fields-row__column {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input.radio_buttons .radio label {
|
||||
|
@ -178,36 +260,6 @@ code {
|
|||
width: auto;
|
||||
}
|
||||
|
||||
.input.boolean {
|
||||
margin-bottom: 5px;
|
||||
|
||||
label {
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
color: $primary-text-color;
|
||||
display: block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
label.checkbox {
|
||||
position: relative;
|
||||
padding-left: 25px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hint {
|
||||
padding-left: 25px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.check_boxes {
|
||||
.checkbox {
|
||||
label {
|
||||
|
@ -236,12 +288,7 @@ code {
|
|||
input[type=email],
|
||||
input[type=password],
|
||||
textarea {
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
border: 0;
|
||||
border-bottom: 2px solid $ui-primary-color;
|
||||
border-radius: 2px 2px 0 0;
|
||||
padding: 7px 4px;
|
||||
font-size: 16px;
|
||||
color: $primary-text-color;
|
||||
display: block;
|
||||
|
@ -249,23 +296,31 @@ code {
|
|||
outline: 0;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
background: darken($ui-base-color, 10%);
|
||||
border: 1px solid darken($ui-base-color, 14%);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
|
||||
&:invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:focus:invalid {
|
||||
border-bottom-color: lighten($error-red, 12%);
|
||||
border-color: lighten($error-red, 12%);
|
||||
}
|
||||
|
||||
&:required:valid {
|
||||
border-bottom-color: $valid-value-color;
|
||||
border-color: $valid-value-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: darken($ui-base-color, 20%);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
border-bottom-color: $highlight-text-color;
|
||||
background: rgba($base-overlay-background, 0.1);
|
||||
border-color: $highlight-text-color;
|
||||
background: darken($ui-base-color, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -349,22 +404,32 @@ code {
|
|||
}
|
||||
|
||||
select {
|
||||
appearance: none;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
max-height: 29px;
|
||||
color: $primary-text-color;
|
||||
display: block;
|
||||
width: 100%;
|
||||
outline: 0;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
background: darken($ui-base-color, 10%) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 12%))}'/></svg>") no-repeat right 8px center / auto 16px;
|
||||
border: 1px solid darken($ui-base-color, 14%);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
height: 41px;
|
||||
}
|
||||
|
||||
.input-with-append {
|
||||
position: relative;
|
||||
|
||||
.input input {
|
||||
padding-right: 142px;
|
||||
.label_input {
|
||||
&__wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.append {
|
||||
&__append {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding: 7px 4px;
|
||||
right: 1px;
|
||||
top: 1px;
|
||||
padding: 10px;
|
||||
padding-bottom: 9px;
|
||||
font-size: 16px;
|
||||
color: $dark-text-color;
|
||||
|
@ -383,7 +448,7 @@ code {
|
|||
right: 0;
|
||||
bottom: 1px;
|
||||
width: 5px;
|
||||
background-image: linear-gradient(to right, rgba($ui-base-color, 0), $ui-base-color);
|
||||
background-image: linear-gradient(to right, rgba(darken($ui-base-color, 10%), 0), darken($ui-base-color, 10%));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -459,6 +524,30 @@ code {
|
|||
}
|
||||
}
|
||||
|
||||
.quick-nav {
|
||||
list-style: none;
|
||||
margin-bottom: 25px;
|
||||
font-size: 14px;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $highlight-text-color;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: lighten($highlight-text-color, 8%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.oauth-prompt,
|
||||
.follow-prompt {
|
||||
margin-bottom: 30px;
|
||||
|
@ -632,3 +721,49 @@ code {
|
|||
font-family: 'mastodon-font-monospace', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.input-copy {
|
||||
background: darken($ui-base-color, 10%);
|
||||
border: 1px solid darken($ui-base-color, 14%);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 4px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
transition: border-color 300ms linear;
|
||||
|
||||
&__wrapper {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
font-family: 'mastodon-font-monospace', monospace;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 0 0 auto;
|
||||
margin: 4px;
|
||||
text-transform: none;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
padding: 7px 18px;
|
||||
padding-bottom: 6px;
|
||||
width: auto;
|
||||
transition: background 300ms linear;
|
||||
}
|
||||
|
||||
&.copied {
|
||||
border-color: $valid-value-color;
|
||||
transition: none;
|
||||
|
||||
button {
|
||||
background: $valid-value-color;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
|||
|
||||
def update_account
|
||||
return if @account.uri != object_uri
|
||||
|
||||
ActivityPub::ProcessAccountService.new.call(@account.username, @account.domain, @object, signed_with_known_key: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -223,11 +223,19 @@ class Account < ApplicationRecord
|
|||
end
|
||||
|
||||
def fields_attributes=(attributes)
|
||||
fields = []
|
||||
fields = []
|
||||
old_fields = self[:fields] || []
|
||||
|
||||
if attributes.is_a?(Hash)
|
||||
attributes.each_value do |attr|
|
||||
next if attr[:name].blank?
|
||||
|
||||
previous = old_fields.find { |item| item['value'] == attr[:value] }
|
||||
|
||||
if previous && previous['verified_at'].present?
|
||||
attr[:verified_at] = previous['verified_at']
|
||||
end
|
||||
|
||||
fields << attr
|
||||
end
|
||||
end
|
||||
|
@ -235,13 +243,18 @@ class Account < ApplicationRecord
|
|||
self[:fields] = fields
|
||||
end
|
||||
|
||||
def build_fields
|
||||
return if fields.size >= 4
|
||||
DEFAULT_FIELDS_SIZE = 4
|
||||
|
||||
raw_fields = self[:fields] || []
|
||||
add_fields = 4 - raw_fields.size
|
||||
add_fields.times { raw_fields << { name: '', value: '' } }
|
||||
self.fields = raw_fields
|
||||
def build_fields
|
||||
return if fields.size >= DEFAULT_FIELDS_SIZE
|
||||
|
||||
tmp = self[:fields] || []
|
||||
|
||||
(DEFAULT_FIELDS_SIZE - tmp.size).times do
|
||||
tmp << { name: '', value: '' }
|
||||
end
|
||||
|
||||
self.fields = tmp
|
||||
end
|
||||
|
||||
def magic_key
|
||||
|
@ -294,17 +307,32 @@ class Account < ApplicationRecord
|
|||
end
|
||||
|
||||
class Field < ActiveModelSerializers::Model
|
||||
attributes :name, :value, :account, :errors
|
||||
attributes :name, :value, :verified_at, :account, :errors
|
||||
|
||||
def initialize(account, attr)
|
||||
@account = account
|
||||
@name = attr['name'].strip[0, 255]
|
||||
@value = attr['value'].strip[0, 255]
|
||||
@errors = {}
|
||||
def initialize(account, attributes)
|
||||
@account = account
|
||||
@attributes = attributes
|
||||
@name = attributes['name'].strip[0, 255]
|
||||
@value = attributes['value'].strip[0, 255]
|
||||
@verified_at = attributes['verified_at']&.to_datetime
|
||||
@errors = {}
|
||||
end
|
||||
|
||||
def verified?
|
||||
verified_at.present?
|
||||
end
|
||||
|
||||
def verifiable?
|
||||
value.present? && /\A#{FetchLinkCardService::URL_PATTERN}\z/ =~ value
|
||||
end
|
||||
|
||||
def mark_verified!
|
||||
@verified_at = Time.now.utc
|
||||
@attributes['verified_at'] = @verified_at
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ name: @name, value: @value }
|
||||
{ name: @name, value: @value, verified_at: @verified_at }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -13,6 +13,10 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
class FieldSerializer < ActiveModel::Serializer
|
||||
attributes :name, :value
|
||||
|
||||
attribute :verified_at, if: :verifiable?
|
||||
|
||||
delegate :verifiable?, to: :object
|
||||
|
||||
def value
|
||||
Formatter.instance.format_field(object.account, object.value)
|
||||
end
|
||||
|
|
|
@ -34,6 +34,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
after_protocol_change! if protocol_changed?
|
||||
after_key_change! if key_changed? && !@options[:signed_with_known_key]
|
||||
check_featured_collection! if @account.featured_collection_url.present?
|
||||
check_links! unless @account.fields.empty?
|
||||
|
||||
@account
|
||||
rescue Oj::ParseError
|
||||
|
@ -99,6 +100,10 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id)
|
||||
end
|
||||
|
||||
def check_links!
|
||||
VerifyAccountLinksWorker.perform_async(@account.id)
|
||||
end
|
||||
|
||||
def actor_type
|
||||
if @json['type'].is_a?(Array)
|
||||
@json['type'].find { |type| ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(type) }
|
||||
|
|
|
@ -29,7 +29,7 @@ class FetchLinkCardService < BaseService
|
|||
end
|
||||
|
||||
attach_card if @card&.persisted?
|
||||
rescue HTTP::Error, Addressable::URI::InvalidURIError, Mastodon::LengthValidationError => e
|
||||
rescue HTTP::Error, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e
|
||||
Rails.logger.debug "Error fetching link #{@url}: #{e}"
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -2,11 +2,14 @@
|
|||
|
||||
class UpdateAccountService < BaseService
|
||||
def call(account, params, raise_error: false)
|
||||
was_locked = account.locked
|
||||
was_locked = account.locked
|
||||
update_method = raise_error ? :update! : :update
|
||||
|
||||
account.send(update_method, params).tap do |ret|
|
||||
next unless ret
|
||||
|
||||
authorize_all_follow_requests(account) if was_locked && !account.locked
|
||||
VerifyAccountLinksWorker.perform_async(@account.id) if account.fields_changed?
|
||||
end
|
||||
end
|
||||
|
||||
|
|
32
app/services/verify_link_service.rb
Normal file
32
app/services/verify_link_service.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class VerifyLinkService < BaseService
|
||||
def call(field)
|
||||
@link_back = ActivityPub::TagManager.instance.url_for(field.account)
|
||||
@url = field.value
|
||||
|
||||
perform_request!
|
||||
|
||||
return unless link_back_present?
|
||||
|
||||
field.mark_verified!
|
||||
field.account.save!
|
||||
rescue HTTP::Error, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e
|
||||
Rails.logger.debug "Error fetching link #{@url}: #{e}"
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def perform_request!
|
||||
@body = Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
|
||||
res.code != 200 ? nil : res.body_with_limit
|
||||
end
|
||||
end
|
||||
|
||||
def link_back_present?
|
||||
return false if @body.empty?
|
||||
|
||||
Nokogiri::HTML(@body).xpath('//a[@rel="me"]|//link[@rel="me"]').any? { |link| link['href'] == @link_back }
|
||||
end
|
||||
end
|
|
@ -1,13 +1,10 @@
|
|||
= simple_form_for(new_user, url: user_registration_path) do |f|
|
||||
= f.simple_fields_for :account do |account_fields|
|
||||
.input-with-append
|
||||
= account_fields.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off' }
|
||||
.append
|
||||
= "@#{site_hostname}"
|
||||
= account_fields.input :username, wrapper: :with_label, autofocus: true, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username') }, append: "@#{site_hostname}", hint: false
|
||||
|
||||
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }
|
||||
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
|
||||
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
|
||||
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, 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 => 'off' }, 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 => 'off' }, hint: false
|
||||
|
||||
.actions
|
||||
= f.button :button, t('auth.register'), type: :submit, class: 'button button-primary'
|
||||
|
|
|
@ -4,8 +4,11 @@
|
|||
- account.fields.each do |field|
|
||||
%dl
|
||||
%dt.emojify{ title: field.name }= Formatter.instance.format_field(account, field.name, custom_emojify: true)
|
||||
%dd.emojify{ title: field.value }= Formatter.instance.format_field(account, field.value, custom_emojify: true)
|
||||
|
||||
%dd{ title: field.value, class: custom_field_classes(field) }
|
||||
- if field.verified?
|
||||
%span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) }
|
||||
= fa_icon 'check'
|
||||
= Formatter.instance.format_field(account, field.value, custom_emojify: true)
|
||||
= account_badge(account)
|
||||
|
||||
- if account.note.present?
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
= t('admin.accounts.change_email.title', username: @account.acct)
|
||||
|
||||
= simple_form_for @user, url: admin_account_change_email_path(@account.id) do |f|
|
||||
= f.input :email, wrapper: :with_label, disabled: true, label: t('admin.accounts.change_email.current_email')
|
||||
= f.input :unconfirmed_email, wrapper: :with_label, label: t('admin.accounts.change_email.new_email')
|
||||
= f.button :submit, class: "button", value: t('admin.accounts.change_email.submit')
|
||||
.fields-group
|
||||
= f.input :email, wrapper: :with_label, disabled: true, label: t('admin.accounts.change_email.current_email')
|
||||
|
||||
.fields-group
|
||||
= f.input :unconfirmed_email, wrapper: :with_label, label: t('admin.accounts.change_email.new_email')
|
||||
|
||||
.actions
|
||||
= f.button :submit, class: "button", value: t('admin.accounts.change_email.submit')
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
= render 'shared/error_messages', object: @custom_emoji
|
||||
|
||||
.fields-group
|
||||
= f.input :shortcode, placeholder: t('admin.custom_emojis.shortcode'), hint: t('admin.custom_emojis.shortcode_hint')
|
||||
= f.input :image, input_html: { accept: 'image/png' }, hint: t('admin.custom_emojis.image_hint')
|
||||
= f.input :shortcode, wrapper: :with_label, label: t('admin.custom_emojis.shortcode'), hint: t('admin.custom_emojis.shortcode_hint')
|
||||
.fields-group
|
||||
= f.input :image, wrapper: :with_label, input_html: { accept: 'image/png' }, hint: t('admin.custom_emojis.image_hint')
|
||||
|
||||
.actions
|
||||
= f.button :button, t('admin.custom_emojis.upload'), type: :submit
|
||||
|
|
|
@ -7,14 +7,15 @@
|
|||
= simple_form_for @domain_block, url: admin_domain_blocks_path do |f|
|
||||
= render 'shared/error_messages', object: @domain_block
|
||||
|
||||
%p.hint= t('.hint')
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :domain, wrapper: :with_label, label: t('admin.domain_blocks.domain'), hint: t('.hint'), required: true
|
||||
|
||||
= f.input :domain, placeholder: t('admin.domain_blocks.domain')
|
||||
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| t(".severity.#{type}") }
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| t(".severity.#{type}") }, hint: t('.severity.desc_html')
|
||||
|
||||
%p.hint= t('.severity.desc_html')
|
||||
|
||||
= f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint')
|
||||
.fields-group
|
||||
= f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint')
|
||||
|
||||
.actions
|
||||
= f.button :button, t('.create'), type: :submit
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
= simple_form_for @email_domain_block, url: admin_email_domain_blocks_path do |f|
|
||||
= render 'shared/error_messages', object: @email_domain_block
|
||||
|
||||
= f.input :domain, placeholder: t('admin.email_domain_blocks.domain')
|
||||
.fields-group
|
||||
= f.input :domain, wrapper: :with_label, label: t('admin.email_domain_blocks.domain')
|
||||
|
||||
.actions
|
||||
= f.button :button, t('.create'), type: :submit
|
||||
|
|
|
@ -2,24 +2,37 @@
|
|||
= t('admin.settings.title')
|
||||
|
||||
= simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch } do |f|
|
||||
.actions.actions--top
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
|
||||
.fields-group
|
||||
= f.input :site_title, placeholder: t('admin.settings.site_title')
|
||||
= f.input :site_short_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_short_description.title'), hint: t('admin.settings.site_short_description.desc_html'), input_html: { rows: 2 }
|
||||
= f.input :site_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description.title'), hint: t('admin.settings.site_description.desc_html'), input_html: { rows: 4 }
|
||||
= f.input :site_contact_username, placeholder: t('admin.settings.contact_information.username')
|
||||
= f.input :site_contact_email, placeholder: t('admin.settings.contact_information.email')
|
||||
|
||||
%hr/
|
||||
= f.input :site_title, wrapper: :with_label, label: t('admin.settings.site_title')
|
||||
|
||||
.fields-group
|
||||
= f.input :theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false
|
||||
= f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: t('admin.settings.thumbnail.desc_html')
|
||||
= f.input :hero, as: :file, wrapper: :with_block_label, label: t('admin.settings.hero.title'), hint: t('admin.settings.hero.desc_html')
|
||||
|
||||
%hr/
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :site_contact_username, wrapper: :with_label, label: t('admin.settings.contact_information.username')
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :site_contact_email, wrapper: :with_label, label: t('admin.settings.contact_information.email')
|
||||
|
||||
.fields-group
|
||||
= f.input :site_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description.title'), hint: t('admin.settings.site_description.desc_html'), input_html: { rows: 4 }
|
||||
|
||||
.fields-group
|
||||
= f.input :site_short_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_short_description.title'), hint: t('admin.settings.site_short_description.desc_html'), input_html: { rows: 2 }
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: t('admin.settings.thumbnail.desc_html')
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :hero, as: :file, wrapper: :with_block_label, label: t('admin.settings.hero.title'), hint: t('admin.settings.hero.desc_html')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.fields-group
|
||||
= f.input :bootstrap_timeline_accounts, wrapper: :with_block_label, label: t('admin.settings.bootstrap_timeline_accounts.title'), hint: t('admin.settings.bootstrap_timeline_accounts.desc_html')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.fields-group
|
||||
= f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html')
|
||||
|
@ -36,27 +49,6 @@
|
|||
.fields-group
|
||||
= f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html')
|
||||
|
||||
.fields-group
|
||||
= f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 }
|
||||
|
||||
%hr/
|
||||
|
||||
.fields-group
|
||||
= f.input :min_invite_role, wrapper: :with_label, collection: %i(disabled user moderator admin), label: t('admin.settings.registrations.min_invite_role.title'), label_method: lambda { |role| role == :disabled ? t('admin.settings.registrations.min_invite_role.disabled') : t("admin.accounts.roles.#{role}") }, as: :radio_buttons, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
||||
|
||||
%hr/
|
||||
|
||||
.fields-group
|
||||
= f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 }
|
||||
= f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 }
|
||||
= f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html')
|
||||
%hr/
|
||||
|
||||
.fields-group
|
||||
= f.input :bootstrap_timeline_accounts, wrapper: :with_block_label, label: t('admin.settings.bootstrap_timeline_accounts.title'), hint: t('admin.settings.bootstrap_timeline_accounts.desc_html')
|
||||
|
||||
%hr/
|
||||
|
||||
.fields-group
|
||||
= f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html')
|
||||
|
||||
|
@ -66,5 +58,16 @@
|
|||
.fields-group
|
||||
= f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.fields-group
|
||||
= f.input :min_invite_role, wrapper: :with_label, collection: %i(disabled user moderator admin), label: t('admin.settings.registrations.min_invite_role.title'), label_method: lambda { |role| role == :disabled ? t('admin.settings.registrations.min_invite_role.disabled') : t("admin.accounts.roles.#{role}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
||||
|
||||
.fields-group
|
||||
= f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 }
|
||||
= f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 }
|
||||
= f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 }
|
||||
= f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html')
|
||||
|
||||
.actions
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
= msg
|
||||
%br
|
||||
|
||||
= f.input :email
|
||||
.fields-group
|
||||
= f.input :email, wrapper: :with_label, required: true, hint: false
|
||||
|
||||
.actions
|
||||
= f.submit t('auth.confirm_email'), class: 'button'
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
= simple_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f|
|
||||
= render 'shared/error_messages', object: resource
|
||||
|
||||
= f.input :email, autofocus: true, required: true, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
|
||||
.fields-group
|
||||
= f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
|
||||
|
||||
.actions
|
||||
= f.button :button, t('auth.resend_confirmation'), type: :submit
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
- if !use_seamless_external_login? || resource.encrypted_password.present?
|
||||
= f.input :reset_password_token, as: :hidden
|
||||
|
||||
= f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
|
||||
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
|
||||
.fields-group
|
||||
= f.input :password, wrapper: :with_label, autofocus: true, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }, required: true
|
||||
.fields-group
|
||||
= f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }, required: true
|
||||
|
||||
.actions
|
||||
= f.button :button, t('auth.set_new_password'), type: :submit
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f|
|
||||
= render 'shared/error_messages', object: resource
|
||||
|
||||
= f.input :email, autofocus: true, required: true, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
|
||||
.fields-group
|
||||
= f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
|
||||
|
||||
.actions
|
||||
= f.button :button, t('auth.reset_password'), type: :submit
|
||||
|
|
|
@ -1,24 +1,32 @@
|
|||
- content_for :page_title do
|
||||
= t('auth.security')
|
||||
|
||||
%h4= t('auth.change_password')
|
||||
= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'auth_edit' }) do |f|
|
||||
= render 'shared/error_messages', object: resource
|
||||
|
||||
- if !use_seamless_external_login? || resource.encrypted_password.present?
|
||||
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
|
||||
= f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
|
||||
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
|
||||
= f.input :current_password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }
|
||||
.fields-group
|
||||
= f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, hint: false
|
||||
|
||||
.fields-group
|
||||
= f.input :password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }, hint: false
|
||||
|
||||
.fields-group
|
||||
= f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
|
||||
|
||||
.fields-group
|
||||
= f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true
|
||||
|
||||
.actions
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
- else
|
||||
%p.hint= t('users.seamless_external_login')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
= render 'sessions'
|
||||
|
||||
- if open_deletion?
|
||||
|
||||
%hr.spacer/
|
||||
%h4= t('auth.delete_account')
|
||||
%p.muted-hint= t('auth.delete_account_html', path: settings_delete_path)
|
||||
|
|
|
@ -13,18 +13,22 @@
|
|||
= render 'application/card', account: @invite.user.account
|
||||
|
||||
= f.simple_fields_for :account do |ff|
|
||||
.input-with-append
|
||||
= ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off' }
|
||||
.append
|
||||
= "@#{site_hostname}"
|
||||
.fields-group
|
||||
= ff.input :username, wrapper: :with_label, autofocus: true, label: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off' }, append: "@#{site_hostname}", hint: t('simple_form.hints.defaults.username', domain: site_hostname)
|
||||
|
||||
.fields-group
|
||||
= f.input :email, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }
|
||||
|
||||
.fields-group
|
||||
= f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
|
||||
.fields-group
|
||||
= f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
|
||||
|
||||
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }
|
||||
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
|
||||
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
|
||||
= f.input :invite_code, as: :hidden
|
||||
|
||||
%p.hint= t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path)
|
||||
|
||||
.actions
|
||||
= f.button :button, t('auth.register'), type: :submit
|
||||
|
||||
%p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path)
|
||||
.form-footer= render 'auth/shared/links'
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
= render partial: 'shared/og'
|
||||
|
||||
= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
|
||||
- if use_seamless_external_login?
|
||||
= f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.username_or_email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }
|
||||
- else
|
||||
= f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
|
||||
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
|
||||
.fields-group
|
||||
- if use_seamless_external_login?
|
||||
= f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false
|
||||
- else
|
||||
= f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
|
||||
.fields-group
|
||||
= f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false
|
||||
|
||||
.actions
|
||||
= f.button :button, t('auth.login'), type: :submit
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
= simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
|
||||
%p.hint{ style: 'margin-bottom: 25px' }= t('simple_form.hints.sessions.otp')
|
||||
|
||||
= f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true
|
||||
.fields-group
|
||||
= f.input :otp_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, autofocus: true
|
||||
|
||||
.actions
|
||||
= f.button :button, t('auth.login'), type: :submit
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
.fields-group
|
||||
= f.input :phrase, as: :string, wrapper: :with_block_label
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :phrase, as: :string, wrapper: :with_label, hint: false
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
|
||||
|
||||
.fields-group
|
||||
= f.input :context, wrapper: :with_block_label, collection: CustomFilter::VALID_CONTEXTS, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: lambda { |context| I18n.t("filters.contexts.#{context}") }, include_blank: false
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.fields-group
|
||||
= f.input :irreversible, wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= f.input :whole_word, wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
= simple_form_for(@invite, url: controller.is_a?(Admin::InvitesController) ? admin_invites_path : invites_path) do |f|
|
||||
= render 'shared/error_messages', object: @invite
|
||||
|
||||
.fields-group
|
||||
= f.input :max_uses, wrapper: :with_label, collection: [1, 5, 10, 25, 50, 100], label_method: lambda { |num| I18n.t('invites.max_uses', count: num) }, prompt: I18n.t('invites.max_uses_prompt')
|
||||
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :max_uses, wrapper: :with_label, collection: [1, 5, 10, 25, 50, 100], label_method: lambda { |num| I18n.t('invites.max_uses', count: num) }, prompt: I18n.t('invites.max_uses_prompt')
|
||||
.fields-row__column.fields-row__column-6.fields-group
|
||||
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
|
||||
|
||||
.fields-group
|
||||
= f.input :autofollow, wrapper: :with_label
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
= render 'form'
|
||||
|
||||
%hr/
|
||||
%hr.spacer/
|
||||
|
||||
%table.table
|
||||
%thead
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
.fields-group
|
||||
= f.input :name, placeholder: t('activerecord.attributes.doorkeeper/application.name')
|
||||
= f.input :website, placeholder: t('activerecord.attributes.doorkeeper/application.website')
|
||||
= f.input :name, wrapper: :with_label, label: t('activerecord.attributes.doorkeeper/application.name')
|
||||
|
||||
.fields-group
|
||||
= f.input :website, wrapper: :with_label, label: t('activerecord.attributes.doorkeeper/application.website')
|
||||
|
||||
.fields-group
|
||||
= f.input :redirect_uri, wrapper: :with_block_label, label: t('activerecord.attributes.doorkeeper/application.redirect_uri'), hint: t('doorkeeper.applications.help.redirect_uri')
|
||||
|
|
|
@ -2,10 +2,8 @@
|
|||
= t('settings.import')
|
||||
|
||||
= simple_form_for @import, url: settings_import_path do |f|
|
||||
%p.hint= t('imports.preface')
|
||||
|
||||
.field-group
|
||||
= f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
||||
= f.input :type, collection: Import.types.keys, wrapper: :with_block_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, hint: t('imports.preface')
|
||||
|
||||
.field-group
|
||||
= f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data')
|
||||
|
|
|
@ -1,29 +1,32 @@
|
|||
- content_for :page_title do
|
||||
= t('settings.preferences')
|
||||
|
||||
%ul.quick-nav
|
||||
%li= link_to t('preferences.languages'), '#settings_languages'
|
||||
%li= link_to t('preferences.publishing'), '#settings_publishing'
|
||||
%li= link_to t('preferences.other'), '#settings_other'
|
||||
%li= link_to t('preferences.web'), '#settings_web'
|
||||
|
||||
= simple_form_for current_user, url: settings_preferences_path, html: { method: :put } do |f|
|
||||
= render 'shared/error_messages', object: current_user
|
||||
|
||||
.actions.actions--top
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
|
||||
%h4= t 'preferences.languages'
|
||||
.fields-row#settings_languages
|
||||
.fields-group.fields-row__column.fields-row__column-6
|
||||
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, selected: I18n.locale
|
||||
.fields-group.fields-row__column.fields-row__column-6
|
||||
= f.input :setting_default_language, collection: [nil] + filterable_languages.sort, wrapper: :with_label, label_method: lambda { |locale| locale.nil? ? I18n.t('statuses.language_detection') : human_locale(locale) }, required: false, include_blank: false
|
||||
|
||||
.fields-group
|
||||
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, selected: I18n.locale
|
||||
|
||||
= f.input :setting_default_language, collection: [nil] + filterable_languages.sort, wrapper: :with_label, label_method: lambda { |locale| locale.nil? ? I18n.t('statuses.language_detection') : human_locale(locale) }, required: false, include_blank: false
|
||||
|
||||
= f.input :chosen_languages, collection: filterable_languages.sort, wrapper: :with_block_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
||||
|
||||
%h4= t 'preferences.publishing'
|
||||
%hr#settings_publishing/
|
||||
|
||||
.fields-group
|
||||
= f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
||||
= f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_floating_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
||||
|
||||
= f.input :setting_default_sensitive, as: :boolean, wrapper: :with_label
|
||||
|
||||
%h4= t 'preferences.other'
|
||||
%hr#settings_other/
|
||||
|
||||
.fields-group
|
||||
= f.input :setting_noindex, as: :boolean, wrapper: :with_label
|
||||
|
@ -31,12 +34,13 @@
|
|||
.fields-group
|
||||
= f.input :setting_hide_network, as: :boolean, wrapper: :with_label
|
||||
|
||||
%h4= t 'preferences.web'
|
||||
%hr#settings_web/
|
||||
|
||||
- if Themes.instance.names.size > 1
|
||||
.fields-group
|
||||
= f.input :setting_theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_block_label, include_blank: false
|
||||
|
||||
.fields-group
|
||||
- if Themes.instance.names.size > 1
|
||||
= f.input :setting_theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false
|
||||
|
||||
= f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_delete_modal, as: :boolean, wrapper: :with_label
|
||||
|
|
|
@ -4,16 +4,21 @@
|
|||
= simple_form_for @account, url: settings_profile_path, html: { method: :put } do |f|
|
||||
= render 'shared/error_messages', object: @account
|
||||
|
||||
.fields-group
|
||||
= f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name'), hint: t('simple_form.hints.defaults.display_name', count: 30 - @account.display_name.size).html_safe
|
||||
= f.input :note, placeholder: t('simple_form.labels.defaults.note'), hint: t('simple_form.hints.defaults.note', count: 160 - @account.note.size).html_safe
|
||||
.fields-row
|
||||
.fields-row__column.fields-group.fields-row__column-6
|
||||
= f.input :display_name, wrapper: :with_label, hint: t('simple_form.hints.defaults.display_name', count: 30 - @account.display_name.size).html_safe
|
||||
= f.input :note, wrapper: :with_label, hint: t('simple_form.hints.defaults.note', count: 160 - @account.note.size).html_safe
|
||||
|
||||
= render 'application/card', account: @account
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6
|
||||
= render 'application/card', account: @account
|
||||
|
||||
.fields-group
|
||||
= f.input :avatar, wrapper: :with_label, input_html: { accept: AccountAvatar::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.avatar', dimensions: '400x400', size: number_to_human_size(AccountAvatar::LIMIT))
|
||||
.fields-row__column.fields-group.fields-row__column-6
|
||||
= f.input :avatar, wrapper: :with_label, input_html: { accept: AccountAvatar::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.avatar', dimensions: '400x400', size: number_to_human_size(AccountAvatar::LIMIT))
|
||||
|
||||
= f.input :header, wrapper: :with_label, input_html: { accept: AccountHeader::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.header', dimensions: '1500x500', size: number_to_human_size(AccountHeader::LIMIT))
|
||||
= f.input :header, wrapper: :with_label, input_html: { accept: AccountHeader::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.header', dimensions: '1500x500', size: number_to_human_size(AccountHeader::LIMIT))
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
.fields-group
|
||||
= f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked')
|
||||
|
@ -21,15 +26,27 @@
|
|||
.fields-group
|
||||
= f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot')
|
||||
|
||||
.fields-group
|
||||
.input.with_block_label
|
||||
%label= t('simple_form.labels.defaults.fields')
|
||||
%span.hint= t('simple_form.hints.defaults.fields')
|
||||
%hr.spacer/
|
||||
|
||||
= f.simple_fields_for :fields do |fields_f|
|
||||
.row
|
||||
= fields_f.input :name, placeholder: t('simple_form.labels.account.fields.name')
|
||||
= fields_f.input :value, placeholder: t('simple_form.labels.account.fields.value')
|
||||
.fields-row
|
||||
.fields-row__column.fields-group.fields-row__column-6
|
||||
.input.with_block_label
|
||||
%label= t('simple_form.labels.defaults.fields')
|
||||
%span.hint= t('simple_form.hints.defaults.fields')
|
||||
|
||||
= f.simple_fields_for :fields do |fields_f|
|
||||
.row
|
||||
= fields_f.input :name, placeholder: t('simple_form.labels.account.fields.name')
|
||||
= fields_f.input :value, placeholder: t('simple_form.labels.account.fields.value')
|
||||
|
||||
.fields-row__column.fields-group.fields-row__column-6
|
||||
%h6= t('verification.verification')
|
||||
%p.hint= t('verification.explanation_html')
|
||||
|
||||
.input-copy
|
||||
.input-copy__wrapper
|
||||
%input{ type: :text, maxlength: '999', spellcheck: 'false', readonly: 'true', value: link_to('Mastodon', ActivityPub::TagManager.instance.url_for(@account), rel: 'me').to_str }
|
||||
%button{ type: :button }= t('generic.copy')
|
||||
|
||||
.actions
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
|
@ -38,3 +55,9 @@
|
|||
|
||||
%h6= t('auth.migrate_account')
|
||||
%p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path)
|
||||
|
||||
- if open_deletion?
|
||||
%hr.spacer/
|
||||
|
||||
%h6= t('auth.delete_account')
|
||||
%p.muted-hint= t('auth.delete_account_html', path: settings_delete_path)
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
%p.hint= t('two_factor_authentication.manual_instructions')
|
||||
%samp.qr-alternative__code= current_user.otp_secret.scan(/.{4}/).join(' ')
|
||||
|
||||
= f.input :code, hint: t('two_factor_authentication.code_hint'), placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }
|
||||
.fields-group
|
||||
= f.input :code, wrapper: :with_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true
|
||||
|
||||
.actions
|
||||
= f.button :button, t('two_factor_authentication.enable'), type: :submit
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
%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'), input_html: { :autocomplete => 'off' }
|
||||
= f.input :code, wrapper: :with_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true
|
||||
|
||||
.actions
|
||||
= f.button :button, t('two_factor_authentication.disable'), type: :submit
|
||||
|
|
20
app/workers/verify_account_links_worker.rb
Normal file
20
app/workers/verify_account_links_worker.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class VerifyAccountLinksWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull', retry: false, unique: :until_executed
|
||||
|
||||
def perform(account_id)
|
||||
account = Account.find(account_id)
|
||||
|
||||
account.fields.each do |field|
|
||||
next unless !field.verified? && field.verifiable?
|
||||
VerifyLinkService.new.call(field)
|
||||
end
|
||||
|
||||
account.save! if account.changed?
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
|
@ -1,4 +1,15 @@
|
|||
# Use this setup block to configure all options available in SimpleForm.
|
||||
|
||||
module AppendComponent
|
||||
def append(wrapper_options = nil)
|
||||
@append ||= begin
|
||||
options[:append].to_s.html_safe if options[:append].present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SimpleForm.include_component(AppendComponent)
|
||||
|
||||
SimpleForm.setup do |config|
|
||||
# Wrappers are used by the form builder to generate a
|
||||
# complete input. You can remove any component from the
|
||||
|
@ -52,6 +63,22 @@ SimpleForm.setup do |config|
|
|||
|
||||
config.wrappers :with_label, class: [:input, :with_label], hint_class: :field_with_hint, error_class: :field_with_errors do |b|
|
||||
b.use :html5
|
||||
|
||||
b.wrapper tag: :div, class: :label_input do |ba|
|
||||
ba.use :label
|
||||
|
||||
ba.wrapper tag: :div, class: :label_input__wrapper do |bb|
|
||||
bb.use :input
|
||||
bb.optional :append, wrap_with: { tag: :div, class: 'label_input__append' }
|
||||
end
|
||||
end
|
||||
|
||||
b.use :hint, wrap_with: { tag: :span, class: :hint }
|
||||
b.use :error, wrap_with: { tag: :span, class: :error }
|
||||
end
|
||||
|
||||
config.wrappers :with_floating_label, class: [:input, :with_floating_label], hint_class: :field_with_hint, error_class: :field_with_errors do |b|
|
||||
b.use :html5
|
||||
b.use :label_input, wrap_with: { tag: :div, class: :label_input }
|
||||
b.use :hint, wrap_with: { tag: :span, class: :hint }
|
||||
b.use :error, wrap_with: { tag: :span, class: :error }
|
||||
|
@ -111,7 +138,7 @@ SimpleForm.setup do |config|
|
|||
# config.item_wrapper_class = nil
|
||||
|
||||
# How the label text should be generated altogether with the required text.
|
||||
# config.label_text = lambda { |label, required, explicit_label| "#{required} #{label}" }
|
||||
config.label_text = lambda { |label, required, explicit_label| "#{label} #{required}" }
|
||||
|
||||
# You can define the class to use on all labels. Default is nil.
|
||||
# config.label_class = nil
|
||||
|
|
|
@ -48,6 +48,7 @@ en:
|
|||
other: Followers
|
||||
following: Following
|
||||
joined: Joined %{date}
|
||||
link_verified_on: Ownership of this link was checked on %{date}
|
||||
media: Media
|
||||
moved_html: "%{name} has moved to %{new_profile_link}:"
|
||||
network_hidden: This information is not available
|
||||
|
@ -460,7 +461,7 @@ en:
|
|||
warning: Be very careful with this data. Never share it with anyone!
|
||||
your_token: Your access token
|
||||
auth:
|
||||
agreement_html: By signing up you agree to follow <a href="%{rules_path}">the rules of the instance</a> and <a href="%{terms_path}">our terms of service</a>.
|
||||
agreement_html: By clicking "Sign up" below you agree to follow <a href="%{rules_path}">the rules of the instance</a> and <a href="%{terms_path}">our terms of service</a>.
|
||||
change_password: Password
|
||||
confirm_email: Confirm email
|
||||
delete_account: Delete account
|
||||
|
@ -921,3 +922,6 @@ en:
|
|||
otp_lost_help_html: If you lost access to both, you may get in touch with %{email}
|
||||
seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available.
|
||||
signed_in_as: 'Signed in as:'
|
||||
verification:
|
||||
explanation_html: 'You can <strong>verify yourself as the owner of the links in your profile metadata</strong>. For that, the linked website must contain a link back to your Mastodon profile. The link back <strong>must</strong> have a <code>rel="me"</code> attribute. The text content of the link does not matter. Here is an example:'
|
||||
verification: Verification
|
||||
|
|
|
@ -11,6 +11,7 @@ en:
|
|||
display_name:
|
||||
one: <span class="name-counter">1</span> character left
|
||||
other: <span class="name-counter">%{count}</span> characters left
|
||||
email: You will be sent a confirmation e-mail
|
||||
fields: You can have up to 4 items displayed as a table on your profile
|
||||
header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
|
||||
inbox_url: Copy the URL from the frontpage of the relay you want to use
|
||||
|
@ -20,12 +21,14 @@ en:
|
|||
note:
|
||||
one: <span class="note-counter">1</span> character left
|
||||
other: <span class="note-counter">%{count}</span> characters left
|
||||
password: Use at least 8 characters
|
||||
phrase: Will be matched regardless of casing in text or content warning of a toot
|
||||
scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones.
|
||||
setting_default_language: The language of your toots can be detected automatically, but it's not always accurate
|
||||
setting_hide_network: Who you follow and who follows you will not be shown on your profile
|
||||
setting_noindex: Affects your public profile and status pages
|
||||
setting_theme: Affects how Mastodon looks when you're logged in from any device.
|
||||
username: Your username will be unique on %{domain}
|
||||
whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word
|
||||
imports:
|
||||
data: CSV file exported from another Mastodon instance
|
||||
|
|
51
spec/services/verify_link_service_spec.rb
Normal file
51
spec/services/verify_link_service_spec.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe VerifyLinkService, type: :service do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:account) { Fabricate(:account, username: 'alice') }
|
||||
let(:field) { Account::Field.new(account, 'name' => 'Website', 'value' => 'http://example.com') }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'http://example.com').to_return(status: 200, body: html)
|
||||
subject.call(field)
|
||||
end
|
||||
|
||||
context 'when a link contains an <a> back' do
|
||||
let(:html) do
|
||||
<<-HTML
|
||||
<!doctype html>
|
||||
<body>
|
||||
<a href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="me">Follow me on Mastodon</a>
|
||||
</body>
|
||||
HTML
|
||||
end
|
||||
|
||||
it 'marks the field as verified' do
|
||||
expect(field.verified?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a link contains a <link> back' do
|
||||
let(:html) do
|
||||
<<-HTML
|
||||
<!doctype html>
|
||||
<head>
|
||||
<link type="text/html" href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="me" />
|
||||
</head>
|
||||
HTML
|
||||
end
|
||||
|
||||
it 'marks the field as verified' do
|
||||
expect(field.verified?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a link does not contain a link back' do
|
||||
let(:html) { '' }
|
||||
|
||||
it 'marks the field as verified' do
|
||||
expect(field.verified?).to be false
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue