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:
		| @@ -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 | ||||
		Reference in New Issue
	
	Block a user