Merge branch 'main' into glitch-soc/merge-upstream
This commit is contained in:
@@ -129,13 +129,13 @@ export function resetCompose() {
|
||||
};
|
||||
}
|
||||
|
||||
export const focusCompose = (routerHistory, defaultText) => dispatch => {
|
||||
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: COMPOSE_FOCUS,
|
||||
defaultText,
|
||||
});
|
||||
|
||||
ensureComposeIsVisible(routerHistory);
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
};
|
||||
|
||||
export function mentionCompose(account, routerHistory) {
|
||||
|
@@ -16,9 +16,11 @@ export const ExplorePrompt = () => (
|
||||
<h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
|
||||
<p><FormattedMessage id='home.explore_prompt.body' defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" /></p>
|
||||
|
||||
<div className='dismissable-banner__message__actions'>
|
||||
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
|
||||
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
|
||||
<div className='dismissable-banner__message__actions__wrapper'>
|
||||
<div className='dismissable-banner__message__actions'>
|
||||
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
|
||||
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
|
||||
</div>
|
||||
</div>
|
||||
</DismissableBanner>
|
||||
);
|
||||
);
|
||||
|
@@ -33,9 +33,11 @@ const messages = defineMessages({
|
||||
|
||||
const getHomeFeedSpeed = createSelector([
|
||||
state => state.getIn(['timelines', 'home', 'items'], ImmutableList()),
|
||||
state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()),
|
||||
state => state.get('statuses'),
|
||||
], (statusIds, statusMap) => {
|
||||
const statuses = statusIds.map(id => statusMap.get(id)).filter(status => status.get('account') !== me).take(20);
|
||||
], (statusIds, pendingStatusIds, statusMap) => {
|
||||
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
|
||||
const statuses = recentStatusIds.map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
|
||||
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
|
||||
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
|
||||
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
||||
@@ -46,9 +48,14 @@ const getHomeFeedSpeed = createSelector([
|
||||
};
|
||||
});
|
||||
|
||||
const homeTooSlow = createSelector(getHomeFeedSpeed, speed =>
|
||||
speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|
||||
|| (Date.now() - speed.newest) > (1000 * 3600) // If the most recent post is from over an hour ago
|
||||
const homeTooSlow = createSelector([
|
||||
state => state.getIn(['timelines', 'home', 'isLoading']),
|
||||
state => state.getIn(['timelines', 'home', 'isPartial']),
|
||||
getHomeFeedSpeed,
|
||||
], (isLoading, isPartial, speed) =>
|
||||
!isLoading && !isPartial // Only if the home feed has finished loading
|
||||
&& (speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|
||||
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
|
||||
@@ -10,6 +10,7 @@ import { connect } from 'react-redux';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { fetchServer } from 'mastodon/actions/server';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
|
||||
import { registrationsOpen, me } from 'mastodon/initial_state';
|
||||
|
||||
@@ -21,6 +22,10 @@ const Account = connect(state => ({
|
||||
</Link>
|
||||
));
|
||||
|
||||
const messages = defineMessages({
|
||||
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
|
||||
});
|
||||
@@ -44,7 +49,8 @@ class Header extends PureComponent {
|
||||
openClosedRegistrationsModal: PropTypes.func,
|
||||
location: PropTypes.object,
|
||||
signupUrl: PropTypes.string.isRequired,
|
||||
dispatchServer: PropTypes.func
|
||||
dispatchServer: PropTypes.func,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
@@ -54,14 +60,15 @@ class Header extends PureComponent {
|
||||
|
||||
render () {
|
||||
const { signedIn } = this.context.identity;
|
||||
const { location, openClosedRegistrationsModal, signupUrl } = this.props;
|
||||
const { location, openClosedRegistrationsModal, signupUrl, intl } = this.props;
|
||||
|
||||
let content;
|
||||
|
||||
if (signedIn) {
|
||||
content = (
|
||||
<>
|
||||
{location.pathname !== '/publish' && <Link to='/publish' className='button'><FormattedMessage id='compose_form.publish_form' defaultMessage='Publish' /></Link>}
|
||||
{location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' /></Link>}
|
||||
{location.pathname !== '/publish' && <Link to='/publish' className='button button-secondary'><FormattedMessage id='compose_form.publish_form' defaultMessage='New post' /></Link>}
|
||||
<Account />
|
||||
</>
|
||||
);
|
||||
@@ -84,6 +91,7 @@ class Header extends PureComponent {
|
||||
|
||||
content = (
|
||||
<>
|
||||
{location.pathname !== '/search' && <Link to='/search' className='button button-secondary' aria-label={intl.formatMessage(messages.search)}><Icon id='search' /></Link>}
|
||||
{signupButton}
|
||||
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
|
||||
</>
|
||||
@@ -106,4 +114,4 @@ class Header extends PureComponent {
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header));
|
||||
export default injectIntl(withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)));
|
||||
|
@@ -147,7 +147,7 @@
|
||||
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
||||
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
||||
"compose_form.publish": "Publish",
|
||||
"compose_form.publish_form": "Publish",
|
||||
"compose_form.publish_form": "New post",
|
||||
"compose_form.publish_loud": "{publish}!",
|
||||
"compose_form.save_changes": "Save changes",
|
||||
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
|
||||
|
@@ -133,12 +133,13 @@
|
||||
color: $darker-text-color;
|
||||
background: transparent;
|
||||
padding: 6px 17px;
|
||||
border: 1px solid $ui-primary-color;
|
||||
border: 1px solid lighten($ui-base-color, 12%);
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: lighten($ui-primary-color, 4%);
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border-color: lighten($ui-base-color, 16%);
|
||||
color: lighten($darker-text-color, 4%);
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -3146,7 +3147,7 @@ $ui-header-height: 55px;
|
||||
.column-back-button {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
background: $ui-base-color;
|
||||
border-radius: 4px 4px 0 0;
|
||||
color: $highlight-text-color;
|
||||
cursor: pointer;
|
||||
@@ -3154,6 +3155,7 @@ $ui-header-height: 55px;
|
||||
font-size: 16px;
|
||||
line-height: inherit;
|
||||
border: 0;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
text-align: unset;
|
||||
padding: 15px;
|
||||
margin: 0;
|
||||
@@ -3166,7 +3168,7 @@ $ui-header-height: 55px;
|
||||
}
|
||||
|
||||
.column-header__back-button {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
background: $ui-base-color;
|
||||
border: 0;
|
||||
font-family: inherit;
|
||||
color: $highlight-text-color;
|
||||
@@ -3201,7 +3203,7 @@ $ui-header-height: 55px;
|
||||
padding: 15px;
|
||||
position: absolute;
|
||||
inset-inline-end: 0;
|
||||
top: -48px;
|
||||
top: -50px;
|
||||
}
|
||||
|
||||
.react-toggle {
|
||||
@@ -3882,7 +3884,8 @@ a.status-card.compact:hover {
|
||||
.column-header {
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
background: $ui-base-color;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
border-radius: 4px 4px 0 0;
|
||||
flex: 0 0 auto;
|
||||
cursor: pointer;
|
||||
@@ -3937,7 +3940,7 @@ a.status-card.compact:hover {
|
||||
}
|
||||
|
||||
.column-header__button {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
background: $ui-base-color;
|
||||
border: 0;
|
||||
color: $darker-text-color;
|
||||
cursor: pointer;
|
||||
@@ -3945,16 +3948,15 @@ a.status-card.compact:hover {
|
||||
padding: 0 15px;
|
||||
|
||||
&:hover {
|
||||
color: lighten($darker-text-color, 7%);
|
||||
color: lighten($darker-text-color, 4%);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $primary-text-color;
|
||||
background: lighten($ui-base-color, 8%);
|
||||
background: lighten($ui-base-color, 4%);
|
||||
|
||||
&:hover {
|
||||
color: $primary-text-color;
|
||||
background: lighten($ui-base-color, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3968,6 +3970,7 @@ a.status-card.compact:hover {
|
||||
max-height: 70vh;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
color: $darker-text-color;
|
||||
transition: max-height 150ms ease-in-out, opacity 300ms linear;
|
||||
opacity: 1;
|
||||
@@ -3987,13 +3990,13 @@ a.status-card.compact:hover {
|
||||
height: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-top: 1px solid lighten($ui-base-color, 12%);
|
||||
border-top: 1px solid lighten($ui-base-color, 8%);
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.column-header__collapsible-inner {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
background: $ui-base-color;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
@@ -4406,17 +4409,13 @@ a.status-card.compact:hover {
|
||||
color: $primary-text-color;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
background-color: $base-overlay-background;
|
||||
text-transform: uppercase;
|
||||
background-color: rgba($black, 0.45);
|
||||
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 4px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-toggle {
|
||||
@@ -4476,6 +4475,7 @@ a.status-card.compact:hover {
|
||||
|
||||
.follow_requests-unlocked_explanation {
|
||||
background: darken($ui-base-color, 4%);
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
contain: initial;
|
||||
flex-grow: 0;
|
||||
}
|
||||
@@ -6160,6 +6160,7 @@ a.status-card.compact:hover {
|
||||
display: block;
|
||||
color: $white;
|
||||
background: rgba($black, 0.65);
|
||||
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
@@ -6837,24 +6838,6 @@ a.status-card.compact:hover {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.directory__section-headline {
|
||||
background: darken($ui-base-color, 2%);
|
||||
border-bottom-color: transparent;
|
||||
|
||||
a,
|
||||
button {
|
||||
&.active {
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-color: transparent transparent darken($ui-base-color, 7%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
@@ -7369,7 +7352,6 @@ noscript {
|
||||
|
||||
.account__header {
|
||||
overflow: hidden;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
|
||||
&.inactive {
|
||||
opacity: 0.5;
|
||||
@@ -7391,6 +7373,7 @@ noscript {
|
||||
height: 145px;
|
||||
position: relative;
|
||||
background: darken($ui-base-color, 4%);
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
@@ -7404,7 +7387,7 @@ noscript {
|
||||
&__bar {
|
||||
position: relative;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 12%);
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
@@ -7413,7 +7396,7 @@ noscript {
|
||||
|
||||
.account__avatar {
|
||||
background: darken($ui-base-color, 8%);
|
||||
border: 2px solid lighten($ui-base-color, 4%);
|
||||
border: 2px solid $ui-base-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8785,9 +8768,18 @@ noscript {
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 30px;
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.button-tertiary {
|
||||
|
111
app/lib/attachment_batch.rb
Normal file
111
app/lib/attachment_batch.rb
Normal file
@@ -0,0 +1,111 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AttachmentBatch
|
||||
# Maximum amount of objects you can delete in an S3 API call. It's
|
||||
# important to remember that this does not correspond to the number
|
||||
# of records in the batch, since records can have multiple attachments
|
||||
LIMIT = 1_000
|
||||
|
||||
# Attributes generated and maintained by Paperclip (not all of them
|
||||
# are always used on every class, however)
|
||||
NULLABLE_ATTRIBUTES = %w(
|
||||
file_name
|
||||
content_type
|
||||
file_size
|
||||
fingerprint
|
||||
created_at
|
||||
updated_at
|
||||
).freeze
|
||||
|
||||
# Styles that are always present even when not explicitly defined
|
||||
BASE_STYLES = %i(original).freeze
|
||||
|
||||
attr_reader :klass, :records, :storage_mode
|
||||
|
||||
def initialize(klass, records)
|
||||
@klass = klass
|
||||
@records = records
|
||||
@storage_mode = Paperclip::Attachment.default_options[:storage]
|
||||
@attachment_names = klass.attachment_definitions.keys
|
||||
end
|
||||
|
||||
def delete
|
||||
remove_files
|
||||
batch.delete_all
|
||||
end
|
||||
|
||||
def clear
|
||||
remove_files
|
||||
batch.update_all(nullified_attributes) # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def batch
|
||||
klass.where(id: records.map(&:id))
|
||||
end
|
||||
|
||||
def remove_files
|
||||
keys = []
|
||||
|
||||
logger.debug { "Preparing to delete attachments for #{records.size} records" }
|
||||
|
||||
records.each do |record|
|
||||
@attachment_names.each do |attachment_name|
|
||||
attachment = record.public_send(attachment_name)
|
||||
styles = BASE_STYLES | attachment.styles.keys
|
||||
|
||||
next if attachment.blank?
|
||||
|
||||
styles.each do |style|
|
||||
case @storage_mode
|
||||
when :s3
|
||||
logger.debug { "Adding #{attachment.path(style)} to batch for deletion" }
|
||||
keys << attachment.style_name_as_path(style)
|
||||
when :filesystem
|
||||
logger.debug { "Deleting #{attachment.path(style)}" }
|
||||
path = attachment.path(style)
|
||||
FileUtils.remove_file(path, true)
|
||||
|
||||
begin
|
||||
FileUtils.rmdir(File.dirname(path), parents: true)
|
||||
rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR, Errno::EACCES
|
||||
# Ignore failure to delete a directory, with the same ignored errors
|
||||
# as Paperclip
|
||||
end
|
||||
when :fog
|
||||
logger.debug { "Deleting #{attachment.path(style)}" }
|
||||
attachment.directory.files.new(key: attachment.path(style)).destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return unless storage_mode == :s3
|
||||
|
||||
# We can batch deletes over S3, but there is a limit of how many
|
||||
# objects can be processed at once, so we have to potentially
|
||||
# separate them into multiple calls.
|
||||
|
||||
keys.each_slice(LIMIT) do |keys_slice|
|
||||
logger.debug { "Deleting #{keys_slice.size} objects" }
|
||||
|
||||
bucket.delete_objects(delete: {
|
||||
objects: keys_slice.map { |key| { key: key } },
|
||||
quiet: true,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def bucket
|
||||
@bucket ||= records.first.public_send(@attachment_names.first).s3_bucket
|
||||
end
|
||||
|
||||
def nullified_attributes
|
||||
@attachment_names.flat_map { |attachment_name| NULLABLE_ATTRIBUTES.map { |attribute| "#{attachment_name}_#{attribute}" } & klass.column_names }.index_with(nil)
|
||||
end
|
||||
|
||||
def logger
|
||||
Rails.logger
|
||||
end
|
||||
end
|
@@ -15,15 +15,15 @@ class Vacuum::MediaAttachmentsVacuum
|
||||
private
|
||||
|
||||
def vacuum_cached_files!
|
||||
media_attachments_past_retention_period.find_each do |media_attachment|
|
||||
media_attachment.file.destroy
|
||||
media_attachment.thumbnail.destroy
|
||||
media_attachment.save
|
||||
media_attachments_past_retention_period.find_in_batches do |media_attachments|
|
||||
AttachmentBatch.new(MediaAttachment, media_attachments).clear
|
||||
end
|
||||
end
|
||||
|
||||
def vacuum_orphaned_records!
|
||||
orphaned_media_attachments.in_batches.destroy_all
|
||||
orphaned_media_attachments.find_in_batches do |media_attachments|
|
||||
AttachmentBatch.new(MediaAttachment, media_attachments).delete
|
||||
end
|
||||
end
|
||||
|
||||
def media_attachments_past_retention_period
|
||||
|
@@ -10,14 +10,6 @@ class ClearDomainMediaService < BaseService
|
||||
|
||||
private
|
||||
|
||||
def invalidate_association_caches!(status_ids)
|
||||
# Normally, associated models of a status are immutable (except for accounts)
|
||||
# so they are aggressively cached. After updating the media attachments to no
|
||||
# longer point to a local file, we need to clear the cache to make those
|
||||
# changes appear in the API and UI
|
||||
Rails.cache.delete_multi(status_ids.map { |id| "statuses/#{id}" })
|
||||
end
|
||||
|
||||
def clear_media!
|
||||
clear_account_images!
|
||||
clear_account_attachments!
|
||||
@@ -25,31 +17,21 @@ class ClearDomainMediaService < BaseService
|
||||
end
|
||||
|
||||
def clear_account_images!
|
||||
blocked_domain_accounts.reorder(nil).find_each do |account|
|
||||
account.avatar.destroy if account.avatar&.exists?
|
||||
account.header.destroy if account.header&.exists?
|
||||
account.save
|
||||
blocked_domain_accounts.reorder(nil).find_in_batches do |accounts|
|
||||
AttachmentBatch.new(Account, accounts).clear
|
||||
end
|
||||
end
|
||||
|
||||
def clear_account_attachments!
|
||||
media_from_blocked_domain.reorder(nil).find_in_batches do |attachments|
|
||||
affected_status_ids = []
|
||||
|
||||
attachments.each do |attachment|
|
||||
affected_status_ids << attachment.status_id if attachment.status_id.present?
|
||||
|
||||
attachment.file.destroy if attachment.file&.exists?
|
||||
attachment.type = :unknown
|
||||
attachment.save
|
||||
end
|
||||
|
||||
invalidate_association_caches!(affected_status_ids) unless affected_status_ids.empty?
|
||||
AttachmentBatch.new(MediaAttachment, attachments).clear
|
||||
end
|
||||
end
|
||||
|
||||
def clear_emojos!
|
||||
emojis_from_blocked_domains.destroy_all
|
||||
emojis_from_blocked_domains.find_in_batches do |custom_emojis|
|
||||
AttachmentBatch.new(CustomEmoji, custom_emojis).delete
|
||||
end
|
||||
end
|
||||
|
||||
def blocked_domain
|
||||
|
@@ -1,7 +1,7 @@
|
||||
- content_for :page_title do
|
||||
= t('.title', domain: Addressable::IDNA.to_unicode(@domain_block.domain))
|
||||
|
||||
= simple_form_for @domain_block, url: admin_domain_blocks_path(@domain_block) do |f|
|
||||
= simple_form_for @domain_block, url: admin_domain_blocks_path, method: :post do |f|
|
||||
|
||||
%p.hint= t('.preamble_html', domain: Addressable::IDNA.to_unicode(@domain_block.domain))
|
||||
%ul.hint
|
||||
|
Reference in New Issue
Block a user