Merge commit '1cb978bcc3d291a045f367e072ca0af1a1c4dbbc' into glitch-soc/merge-upstream
This commit is contained in:
commit
10404aece8
|
@ -532,7 +532,7 @@ GEM
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
private_address_check (0.5.0)
|
private_address_check (0.5.0)
|
||||||
public_suffix (5.0.3)
|
public_suffix (5.0.3)
|
||||||
puma (6.3.0)
|
puma (6.3.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.0)
|
pundit (2.3.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
|
|
@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
params.require(:account).permit(:discoverable, :locked, :hide_collections, settings: UserSettings.keys)
|
params.require(:account).permit(:discoverable, :unlocked, :show_collections, settings: UserSettings.keys)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
|
|
|
@ -8,6 +8,7 @@ import classNames from 'classnames';
|
||||||
import api from 'mastodon/api';
|
import api from 'mastodon/api';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
legal: { id: 'report.categories.legal', defaultMessage: 'Legal' },
|
||||||
other: { id: 'report.categories.other', defaultMessage: 'Other' },
|
other: { id: 'report.categories.other', defaultMessage: 'Other' },
|
||||||
spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
|
spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
|
||||||
violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
|
violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
|
||||||
|
@ -150,6 +151,7 @@ class ReportReasonSelector extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='report-reason-selector'>
|
<div className='report-reason-selector'>
|
||||||
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
|
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
|
||||||
|
<Category id='legal' text={intl.formatMessage(messages.legal)} selected={category === 'legal'} onSelect={this.handleSelect} disabled={disabled} />
|
||||||
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
|
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
|
||||||
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
|
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
|
||||||
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}
|
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useMemo, useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
|
// About two lines on desktop
|
||||||
|
const VISIBLE_HASHTAGS = 7;
|
||||||
|
|
||||||
|
export const HashtagBar = ({ hashtags, text }) => {
|
||||||
|
const renderedHashtags = useMemo(() => {
|
||||||
|
const body = domParser.parseFromString(text, 'text/html').documentElement;
|
||||||
|
return [].filter.call(body.querySelectorAll('a[href]'), link => link.textContent[0] === '#' || (link.previousSibling?.textContent?.[link.previousSibling.textContent.length - 1] === '#')).map(node => node.textContent);
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
|
const invisibleHashtags = useMemo(() => (
|
||||||
|
hashtags.filter(hashtag => !renderedHashtags.some(textContent => textContent.localeCompare(`#${hashtag.get('name')}`, undefined, { sensitivity: 'accent' }) === 0 || textContent.localeCompare(hashtag.get('name'), undefined, { sensitivity: 'accent' }) === 0))
|
||||||
|
), [hashtags, renderedHashtags]);
|
||||||
|
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const handleClick = useCallback(() => setExpanded(true), []);
|
||||||
|
|
||||||
|
if (invisibleHashtags.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const revealedHashtags = expanded ? invisibleHashtags : invisibleHashtags.take(VISIBLE_HASHTAGS);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='hashtag-bar'>
|
||||||
|
{revealedHashtags.map(hashtag => (
|
||||||
|
<Link key={hashtag.get('name')} to={`/tags/${hashtag.get('name')}`}>
|
||||||
|
#{hashtag.get('name')}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!expanded && invisibleHashtags.size > VISIBLE_HASHTAGS && <button className='link-button' onClick={handleClick}><FormattedMessage id='hashtags.and_other' defaultMessage='…and {count, plural, other {# more}}' values={{ count: invisibleHashtags.size - VISIBLE_HASHTAGS }} /></button>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
HashtagBar.propTypes = {
|
||||||
|
hashtags: ImmutablePropTypes.list,
|
||||||
|
text: PropTypes.string,
|
||||||
|
};
|
|
@ -22,6 +22,7 @@ import { displayMedia } from '../initial_state';
|
||||||
import { Avatar } from './avatar';
|
import { Avatar } from './avatar';
|
||||||
import { AvatarOverlay } from './avatar_overlay';
|
import { AvatarOverlay } from './avatar_overlay';
|
||||||
import { DisplayName } from './display_name';
|
import { DisplayName } from './display_name';
|
||||||
|
import { HashtagBar } from './hashtag_bar';
|
||||||
import { RelativeTimestamp } from './relative_timestamp';
|
import { RelativeTimestamp } from './relative_timestamp';
|
||||||
import StatusActionBar from './status_action_bar';
|
import StatusActionBar from './status_action_bar';
|
||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
|
@ -580,6 +581,8 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
{media}
|
{media}
|
||||||
|
|
||||||
|
<HashtagBar hashtags={status.get('tags')} text={status.get('content')} />
|
||||||
|
|
||||||
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
|
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import { AnimatedNumber } from 'mastodon/components/animated_number';
|
import { AnimatedNumber } from 'mastodon/components/animated_number';
|
||||||
import EditedTimestamp from 'mastodon/components/edited_timestamp';
|
import EditedTimestamp from 'mastodon/components/edited_timestamp';
|
||||||
|
import { HashtagBar } from 'mastodon/components/hashtag_bar';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
||||||
|
|
||||||
|
@ -314,6 +315,8 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
{media}
|
{media}
|
||||||
|
|
||||||
|
<HashtagBar hashtags={status.get('tags')} text={status.get('content')} />
|
||||||
|
|
||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
||||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||||
|
|
|
@ -300,6 +300,7 @@
|
||||||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} posts}} today",
|
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} posts}} today",
|
||||||
"hashtag.follow": "Follow hashtag",
|
"hashtag.follow": "Follow hashtag",
|
||||||
"hashtag.unfollow": "Unfollow hashtag",
|
"hashtag.unfollow": "Unfollow hashtag",
|
||||||
|
"hashtags.and_other": "…and {count, plural, other {# more}}",
|
||||||
"home.actions.go_to_explore": "See what's trending",
|
"home.actions.go_to_explore": "See what's trending",
|
||||||
"home.actions.go_to_suggestions": "Find people to follow",
|
"home.actions.go_to_suggestions": "Find people to follow",
|
||||||
"home.column_settings.basic": "Basic",
|
"home.column_settings.basic": "Basic",
|
||||||
|
@ -532,6 +533,7 @@
|
||||||
"reply_indicator.cancel": "Cancel",
|
"reply_indicator.cancel": "Cancel",
|
||||||
"report.block": "Block",
|
"report.block": "Block",
|
||||||
"report.block_explanation": "You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.",
|
"report.block_explanation": "You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.",
|
||||||
|
"report.categories.legal": "Legal",
|
||||||
"report.categories.other": "Other",
|
"report.categories.other": "Other",
|
||||||
"report.categories.spam": "Spam",
|
"report.categories.spam": "Spam",
|
||||||
"report.categories.violation": "Content violates one or more server rules",
|
"report.categories.violation": "Content violates one or more server rules",
|
||||||
|
|
|
@ -1111,7 +1111,8 @@ body > [data-popper-placement] {
|
||||||
.audio-player,
|
.audio-player,
|
||||||
.attachment-list,
|
.attachment-list,
|
||||||
.picture-in-picture-placeholder,
|
.picture-in-picture-placeholder,
|
||||||
.status-card {
|
.status-card,
|
||||||
|
.hashtag-bar {
|
||||||
margin-inline-start: $thread-margin;
|
margin-inline-start: $thread-margin;
|
||||||
width: calc(100% - ($thread-margin));
|
width: calc(100% - ($thread-margin));
|
||||||
}
|
}
|
||||||
|
@ -9256,3 +9257,26 @@ noscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hashtag-bar {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 14px;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba($highlight-text-color, 0.2);
|
||||||
|
color: $highlight-text-color;
|
||||||
|
padding: 0.4em 0.6em;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background: rgba($highlight-text-color, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,8 +2,14 @@
|
||||||
|
|
||||||
class CacheBuster
|
class CacheBuster
|
||||||
def initialize(options = {})
|
def initialize(options = {})
|
||||||
@secret_header = options[:secret_header] || 'Secret-Header'
|
ActiveSupport::Deprecation.warn('Default values for the cache buster secret header name and values will be removed in Mastodon 4.3. Please set them explicitely if you rely on those.') unless options[:http_method] || (options[:secret] && options[:secret_header])
|
||||||
@secret = options[:secret] || 'True'
|
|
||||||
|
@secret_header = options[:secret_header] ||
|
||||||
|
(options[:http_method] ? nil : 'Secret-Header')
|
||||||
|
@secret = options[:secret] ||
|
||||||
|
(options[:http_method] ? nil : 'True')
|
||||||
|
|
||||||
|
@http_method = options[:http_method] || 'GET'
|
||||||
end
|
end
|
||||||
|
|
||||||
def bust(url)
|
def bust(url)
|
||||||
|
@ -21,8 +27,9 @@ class CacheBuster
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_request(url, http_client)
|
def build_request(url, http_client)
|
||||||
Request.new(:get, url, http_client: http_client).tap do |request|
|
request = Request.new(@http_method.downcase.to_sym, url, http_client: http_client)
|
||||||
request.add_headers(@secret_header => @secret)
|
request.add_headers(@secret_header => @secret) if @secret_header.present? && @secret && !@secret.empty?
|
||||||
end
|
|
||||||
|
request
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -117,7 +117,7 @@ class Request
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
begin
|
begin
|
||||||
response = http_client.public_send(@verb, @url.to_s, @options.merge(headers: headers))
|
response = http_client.request(@verb, @url.to_s, @options.merge(headers: headers))
|
||||||
rescue => e
|
rescue => e
|
||||||
raise e.class, "#{e.message} on #{@url}", e.backtrace[0]
|
raise e.class, "#{e.message} on #{@url}", e.backtrace[0]
|
||||||
end
|
end
|
||||||
|
|
|
@ -43,6 +43,9 @@ class VideoMetadataExtractor
|
||||||
@height = video_stream[:height]
|
@height = video_stream[:height]
|
||||||
@frame_rate = video_stream[:avg_frame_rate] == '0/0' ? nil : Rational(video_stream[:avg_frame_rate])
|
@frame_rate = video_stream[:avg_frame_rate] == '0/0' ? nil : Rational(video_stream[:avg_frame_rate])
|
||||||
@r_frame_rate = video_stream[:r_frame_rate] == '0/0' ? nil : Rational(video_stream[:r_frame_rate])
|
@r_frame_rate = video_stream[:r_frame_rate] == '0/0' ? nil : Rational(video_stream[:r_frame_rate])
|
||||||
|
# For some video streams the frame_rate reported by `ffprobe` will be 0/0, but for these streams we
|
||||||
|
# should use `r_frame_rate` instead. Video screencast generated by Gnome Screencast have this issue.
|
||||||
|
@frame_rate ||= @r_frame_rate
|
||||||
end
|
end
|
||||||
|
|
||||||
if (audio_stream = audio_streams.first)
|
if (audio_stream = audio_streams.first)
|
||||||
|
|
|
@ -440,8 +440,21 @@ class Account < ApplicationRecord
|
||||||
EntityCache.instance.mention(username, domain)
|
EntityCache.instance.mention(username, domain)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def inverse_alias(key, original_key)
|
||||||
|
define_method("#{key}=") do |value|
|
||||||
|
public_send("#{original_key}=", !ActiveModel::Type::Boolean.new.cast(value))
|
||||||
|
end
|
||||||
|
|
||||||
|
define_method(key) do
|
||||||
|
!public_send(original_key)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
inverse_alias :show_collections, :hide_collections
|
||||||
|
inverse_alias :unlocked, :locked
|
||||||
|
|
||||||
def emojis
|
def emojis
|
||||||
@emojis ||= CustomEmoji.from_text(emojifiable_text, domain)
|
@emojis ||= CustomEmoji.from_text(emojifiable_text, domain)
|
||||||
end
|
end
|
||||||
|
|
|
@ -48,7 +48,10 @@ class Report < ApplicationRecord
|
||||||
|
|
||||||
validate :validate_rule_ids
|
validate :validate_rule_ids
|
||||||
|
|
||||||
# entries here needs to be kept in sync with app/javascript/mastodon/features/notifications/components/report.jsx
|
# entries here need to be kept in sync with the front-end:
|
||||||
|
# - app/javascript/mastodon/features/notifications/components/report.jsx
|
||||||
|
# - app/javascript/mastodon/features/report/category.jsx
|
||||||
|
# - app/javascript/mastodon/components/admin/ReportReasonSelector.jsx
|
||||||
enum category: {
|
enum category: {
|
||||||
other: 0,
|
other: 0,
|
||||||
spam: 1_000,
|
spam: 1_000,
|
||||||
|
|
|
@ -410,13 +410,25 @@ class Status < ApplicationRecord
|
||||||
|
|
||||||
account_ids.uniq!
|
account_ids.uniq!
|
||||||
|
|
||||||
|
status_ids = cached_items.map { |item| item.reblog? ? item.reblog_of_id : item.id }.uniq
|
||||||
|
|
||||||
return if account_ids.empty?
|
return if account_ids.empty?
|
||||||
|
|
||||||
accounts = Account.where(id: account_ids).includes(:account_stat, :user).index_by(&:id)
|
accounts = Account.where(id: account_ids).includes(:account_stat, :user).index_by(&:id)
|
||||||
|
|
||||||
|
status_stats = StatusStat.where(status_id: status_ids).index_by(&:status_id)
|
||||||
|
|
||||||
cached_items.each do |item|
|
cached_items.each do |item|
|
||||||
item.account = accounts[item.account_id]
|
item.account = accounts[item.account_id]
|
||||||
item.reblog.account = accounts[item.reblog.account_id] if item.reblog?
|
item.reblog.account = accounts[item.reblog.account_id] if item.reblog?
|
||||||
|
|
||||||
|
if item.reblog?
|
||||||
|
status_stat = status_stats[item.reblog.id]
|
||||||
|
item.reblog.status_stat = status_stat if status_stat.present?
|
||||||
|
else
|
||||||
|
status_stat = status_stats[item.id]
|
||||||
|
item.status_stat = status_stat if status_stat.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
= f.input :discoverable, as: :boolean, wrapper: :with_label, recommended: true
|
= f.input :discoverable, as: :boolean, wrapper: :with_label, recommended: true
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :locked, as: :boolean, wrapper: :with_label
|
= f.input :unlocked, as: :boolean, wrapper: :with_label
|
||||||
|
|
||||||
%h4= t('privacy.search')
|
%h4= t('privacy.search')
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
%p.lead= t('privacy.privacy_hint_html')
|
%p.lead= t('privacy.privacy_hint_html')
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :hide_collections, as: :boolean, wrapper: :with_label
|
= f.input :show_collections, as: :boolean, wrapper: :with_label
|
||||||
|
|
||||||
= f.simple_fields_for :settings, current_user.settings do |ff|
|
= f.simple_fields_for :settings, current_user.settings do |ff|
|
||||||
.fields-group
|
.fields-group
|
||||||
|
|
|
@ -51,6 +51,7 @@ require_relative '../lib/rails/engine_extensions'
|
||||||
require_relative '../lib/active_record/database_tasks_extensions'
|
require_relative '../lib/active_record/database_tasks_extensions'
|
||||||
require_relative '../lib/active_record/batches'
|
require_relative '../lib/active_record/batches'
|
||||||
require_relative '../lib/simple_navigation/item_extensions'
|
require_relative '../lib/simple_navigation/item_extensions'
|
||||||
|
require_relative '../lib/http_extensions'
|
||||||
|
|
||||||
Dotenv::Railtie.load
|
Dotenv::Railtie.load
|
||||||
|
|
||||||
|
|
|
@ -6,5 +6,6 @@ Rails.application.configure do
|
||||||
config.x.cache_buster = {
|
config.x.cache_buster = {
|
||||||
secret_header: ENV['CACHE_BUSTER_SECRET_HEADER'],
|
secret_header: ENV['CACHE_BUSTER_SECRET_HEADER'],
|
||||||
secret: ENV['CACHE_BUSTER_SECRET'],
|
secret: ENV['CACHE_BUSTER_SECRET'],
|
||||||
|
http_method: ENV['CACHE_BUSTER_HTTP_METHOD'] || 'GET',
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,9 +6,9 @@ en:
|
||||||
discoverable: Your public posts and profile may be featured or recommended in various areas of Mastodon and your profile may be suggested to other users.
|
discoverable: Your public posts and profile may be featured or recommended in various areas of Mastodon and your profile may be suggested to other users.
|
||||||
display_name: Your full name or your fun name.
|
display_name: Your full name or your fun name.
|
||||||
fields: Your homepage, pronouns, age, anything you want.
|
fields: Your homepage, pronouns, age, anything you want.
|
||||||
hide_collections: People will not be able to browse through your follows and followers. People that you follow will see that you follow them regardless.
|
|
||||||
locked: People will request to follow you and you will be able to either accept or reject new followers.
|
|
||||||
note: 'You can @mention other people or #hashtags.'
|
note: 'You can @mention other people or #hashtags.'
|
||||||
|
show_collections: People will be able to browse through your follows and followers. People that you follow will see that you follow them regardless.
|
||||||
|
unlocked: People will be able to follow you without requesting approval. Uncheck if you want to review follow requests and chose whether to accept or reject new followers.
|
||||||
account_alias:
|
account_alias:
|
||||||
acct: Specify the username@domain of the account you want to move from
|
acct: Specify the username@domain of the account you want to move from
|
||||||
account_migration:
|
account_migration:
|
||||||
|
@ -143,8 +143,8 @@ en:
|
||||||
fields:
|
fields:
|
||||||
name: Label
|
name: Label
|
||||||
value: Content
|
value: Content
|
||||||
hide_collections: Hide follows and followers from profile
|
show_collections: Show follows and followers on profile
|
||||||
locked: Manually review new followers
|
unlocked: Automatically accept new followers
|
||||||
account_alias:
|
account_alias:
|
||||||
acct: Handle of the old account
|
acct: Handle of the old account
|
||||||
account_migration:
|
account_migration:
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Monkey patching until https://github.com/httprb/http/pull/757 is merged
|
||||||
|
unless HTTP::Request::METHODS.include?(:purge)
|
||||||
|
module HTTP
|
||||||
|
class Request
|
||||||
|
METHODS = METHODS.dup.push(:purge).freeze
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,12 +13,17 @@ RSpec.describe CacheConcern do
|
||||||
def empty_relation
|
def empty_relation
|
||||||
render plain: cache_collection(Status.none, Status).size
|
render plain: cache_collection(Status.none, Status).size
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def account_statuses_favourites
|
||||||
|
render plain: cache_collection(Status.where(account_id: params[:id]), Status).map(&:favourites_count)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
routes.draw do
|
routes.draw do
|
||||||
get 'empty_array' => 'anonymous#empty_array'
|
get 'empty_array' => 'anonymous#empty_array'
|
||||||
post 'empty_relation' => 'anonymous#empty_relation'
|
get 'empty_relation' => 'anonymous#empty_relation'
|
||||||
|
get 'account_statuses_favourites' => 'anonymous#account_statuses_favourites'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -36,5 +41,20 @@ RSpec.describe CacheConcern do
|
||||||
expect(response.body).to eq '0'
|
expect(response.body).to eq '0'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when given a collection of statuses' do
|
||||||
|
let!(:account) { Fabricate(:account) }
|
||||||
|
let!(:status) { Fabricate(:status, account: account) }
|
||||||
|
|
||||||
|
it 'correctly updates with new interactions' do
|
||||||
|
get :account_statuses_favourites, params: { id: account.id }
|
||||||
|
expect(response.body).to eq '[0]'
|
||||||
|
|
||||||
|
FavouriteService.new.call(account, status)
|
||||||
|
|
||||||
|
get :account_statuses_favourites, params: { id: account.id }
|
||||||
|
expect(response.body).to eq '[1]'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe CacheBuster do
|
||||||
|
subject { described_class.new(secret_header: secret_header, secret: secret, http_method: http_method) }
|
||||||
|
|
||||||
|
let(:secret_header) { nil }
|
||||||
|
let(:secret) { nil }
|
||||||
|
let(:http_method) { nil }
|
||||||
|
|
||||||
|
let(:purge_url) { 'https://example.com/test_purge' }
|
||||||
|
|
||||||
|
describe '#bust' do
|
||||||
|
shared_examples 'makes_request' do
|
||||||
|
it 'makes an HTTP purging request' do
|
||||||
|
method = http_method&.to_sym || :get
|
||||||
|
stub_request(method, purge_url).to_return(status: 200)
|
||||||
|
|
||||||
|
subject.bust(purge_url)
|
||||||
|
|
||||||
|
test_request = a_request(method, purge_url)
|
||||||
|
|
||||||
|
test_request = test_request.with(headers: { secret_header => secret }) if secret && secret_header
|
||||||
|
|
||||||
|
expect(test_request).to have_been_made.once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when using default options' do
|
||||||
|
include_examples 'makes_request'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when specifying a secret header' do
|
||||||
|
let(:secret_header) { 'X-Purge-Secret' }
|
||||||
|
let(:secret) { SecureRandom.hex(20) }
|
||||||
|
|
||||||
|
include_examples 'makes_request'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when specifying a PURGE method' do
|
||||||
|
let(:http_method) { 'purge' }
|
||||||
|
|
||||||
|
context 'when not using headers' do
|
||||||
|
include_examples 'makes_request'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when specifying a secret header' do
|
||||||
|
let(:secret_header) { 'X-Purge-Secret' }
|
||||||
|
let(:secret) { SecureRandom.hex(20) }
|
||||||
|
|
||||||
|
include_examples 'makes_request'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
18
yarn.lock
18
yarn.lock
|
@ -1677,9 +1677,9 @@
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||||
|
|
||||||
"@material-design-icons/svg@^0.14.10":
|
"@material-design-icons/svg@^0.14.10":
|
||||||
version "0.14.10"
|
version "0.14.11"
|
||||||
resolved "https://registry.yarnpkg.com/@material-design-icons/svg/-/svg-0.14.10.tgz#25804b66d0740b0bf8d6841fa343dfdd60f22e82"
|
resolved "https://registry.yarnpkg.com/@material-design-icons/svg/-/svg-0.14.11.tgz#f90a2c8de801523c3b17e606c89313121c8bb3b4"
|
||||||
integrity sha512-rXxfqj5Su8i51aG8s8QRIe7mX1gB+C/ZCroLu3JvIsO3+Vx6PcWP97HLwIl7AQH/jYIHQlKq0E6OMqU91u5fCg==
|
integrity sha512-jpAksWZIVLB5/qTAeqANns7pH/faIQR3jgV2yROUNKZkzpJ428h7e1/byJB+rFZNI0hgZpY9nOVMLhc1J41HtA==
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
|
@ -9199,9 +9199,9 @@ pg-types@^4.0.1:
|
||||||
postgres-range "^1.1.1"
|
postgres-range "^1.1.1"
|
||||||
|
|
||||||
pg@^8.5.0:
|
pg@^8.5.0:
|
||||||
version "8.11.2"
|
version "8.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.2.tgz#1a23f6de7bfb65ba56e4dd15df96668d319900c4"
|
resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.3.tgz#d7db6e3fe268fcedd65b8e4599cda0b8b4bf76cb"
|
||||||
integrity sha512-l4rmVeV8qTIrrPrIR3kZQqBgSN93331s9i6wiUiLOSk0Q7PmUxZD/m1rQI622l3NfqBby9Ar5PABfS/SulfieQ==
|
integrity sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-writer "2.0.0"
|
buffer-writer "2.0.0"
|
||||||
packet-reader "1.0.0"
|
packet-reader "1.0.0"
|
||||||
|
@ -9564,9 +9564,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||||
|
|
||||||
postcss@^8.2.15, postcss@^8.4.24, postcss@^8.4.25:
|
postcss@^8.2.15, postcss@^8.4.24, postcss@^8.4.25:
|
||||||
version "8.4.27"
|
version "8.4.28"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.28.tgz#c6cc681ed00109072816e1557f889ef51cf950a5"
|
||||||
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
|
integrity sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid "^3.3.6"
|
nanoid "^3.3.6"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
|
|
Loading…
Reference in New Issue