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
master
Eugen Rochko 6 years ago committed by GitHub
parent f8b54d229f
commit f4d549d300
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      app/helpers/home_helper.rb
  2. 15
      app/javascript/mastodon/features/account/components/header.js
  3. 27
      app/javascript/packs/public.js
  4. 14
      app/javascript/styles/mastodon/accounts.scss
  5. 84
      app/javascript/styles/mastodon/admin.scss
  6. 7
      app/javascript/styles/mastodon/basics.scss
  7. 7
      app/javascript/styles/mastodon/boost.scss
  8. 9
      app/javascript/styles/mastodon/components.scss
  9. 8
      app/javascript/styles/mastodon/containers.scss
  10. 361
      app/javascript/styles/mastodon/forms.scss
  11. 1
      app/lib/activitypub/activity/update.rb
  12. 54
      app/models/account.rb
  13. 4
      app/serializers/rest/account_serializer.rb
  14. 5
      app/services/activitypub/process_account_service.rb
  15. 2
      app/services/fetch_link_card_service.rb
  16. 5
      app/services/update_account_service.rb
  17. 32
      app/services/verify_link_service.rb
  18. 11
      app/views/about/_registration.html.haml
  19. 7
      app/views/accounts/_bio.html.haml
  20. 11
      app/views/admin/change_emails/show.html.haml
  21. 5
      app/views/admin/custom_emojis/new.html.haml
  22. 13
      app/views/admin/domain_blocks/new.html.haml
  23. 3
      app/views/admin/email_domain_blocks/new.html.haml
  24. 63
      app/views/admin/settings/edit.html.haml
  25. 3
      app/views/auth/confirmations/finish_signup.html.haml
  26. 3
      app/views/auth/confirmations/new.html.haml
  27. 6
      app/views/auth/passwords/edit.html.haml
  28. 3
      app/views/auth/passwords/new.html.haml
  29. 20
      app/views/auth/registrations/edit.html.haml
  30. 22
      app/views/auth/registrations/new.html.haml
  31. 12
      app/views/auth/sessions/new.html.haml
  32. 3
      app/views/auth/sessions/two_factor.html.haml
  33. 12
      app/views/filters/_fields.html.haml
  34. 8
      app/views/invites/_form.html.haml
  35. 2
      app/views/invites/index.html.haml
  36. 6
      app/views/settings/applications/_fields.html.haml
  37. 4
      app/views/settings/imports/show.html.haml
  38. 34
      app/views/settings/preferences/show.html.haml
  39. 53
      app/views/settings/profiles/show.html.haml
  40. 3
      app/views/settings/two_factor_authentication/confirmations/new.html.haml
  41. 2
      app/views/settings/two_factor_authentications/show.html.haml
  42. 20
      app/workers/verify_account_links_worker.rb
  43. 29
      config/initializers/simple_form.rb
  44. 6
      config/locales/en.yml
  45. 3
      config/locales/simple_form.en.yml
  46. 51
      spec/services/verify_link_service_spec.rb

@ -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,20 +74,24 @@
a {
border: 0;
padding: 15px 35px;
}
}
&.selected {
color: $primary-text-color;
background-color: $ui-highlight-color;
border-bottom: 0;
border-radius: 0;
.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%);
}
}
&:hover {
background-color: lighten($ui-highlight-color, 5%);
}
}
}
& > ul > .simple-navigation-active-leaf a {
border-radius: 4px 0 0 4px;
}
}
.content-wrapper {
@ -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,49 +196,70 @@ code {
}
}
.required abbr {
text-decoration: none;
color: lighten($error-value-color, 12%);
}
.fields-group {
margin-bottom: 25px;
}
.input.radio_buttons .radio label {
margin-bottom: 5px;
font-family: inherit;
font-size: 14px;
color: $primary-text-color;
display: block;
width: auto;
.input:last-child {
margin-bottom: 0;
}
}
.input.boolean {
margin-bottom: 5px;
.fields-row {
display: flex;
margin: 0 -10px;
padding-top: 5px;
margin-bottom: 25px;
label {
font-family: inherit;
font-size: 14px;
color: $primary-text-color;
display: block;
width: auto;
.input {
max-width: none;
}
label.checkbox {
position: relative;
padding-left: 25px;
&__column {
box-sizing: border-box;
padding: 0 10px;
flex: 1 1 auto;
min-height: 1px;
&-6 {
max-width: 50%;
}
}
input[type=checkbox] {
position: absolute;
left: 0;
top: 5px;
margin: 0;
.fields-group:last-child,
.fields-row__column.fields-group {
margin-bottom: 0;
}
.hint {
padding-left: 25px;
margin-left: 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 {
margin-bottom: 5px;
font-family: inherit;
font-size: 14px;
color: $primary-text-color;
display: block;
width: auto;
}
.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
DEFAULT_FIELDS_SIZE = 4
def build_fields
return if fields.size >= 4
return if fields.size >= DEFAULT_FIELDS_SIZE
tmp = self[:fields] || []
(DEFAULT_FIELDS_SIZE - tmp.size).times do
tmp << { name: '', value: '' }
end
raw_fields = self[:fields] || []
add_fields = 4 - raw_fields.size
add_fields.times { raw_fields << { name: '', value: '' } }
self.fields = raw_fields
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, 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 initialize(account, attr)
@account = account
@name = attr['name'].strip[0, 255]
@value = attr['value'].strip[0, 255]
@errors = {}
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

@ -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_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
.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 }
= 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/
.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 :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')
= 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/
%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')
@ -37,34 +50,24 @@
= 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/
= 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')
.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/
= f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html')
.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')
= 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/
%hr.spacer/
.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')
= 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 :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html')
.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')
= 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?</