Merge commit '1cb978bcc3d291a045f367e072ca0af1a1c4dbbc' into glitch-soc/merge-upstream
This commit is contained in:
@@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController
|
||||
private
|
||||
|
||||
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
|
||||
|
||||
def set_account
|
||||
|
@@ -8,6 +8,7 @@ import classNames from 'classnames';
|
||||
import api from 'mastodon/api';
|
||||
|
||||
const messages = defineMessages({
|
||||
legal: { id: 'report.categories.legal', defaultMessage: 'Legal' },
|
||||
other: { id: 'report.categories.other', defaultMessage: 'Other' },
|
||||
spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
|
||||
violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
|
||||
@@ -150,6 +151,7 @@ class ReportReasonSelector extends PureComponent {
|
||||
return (
|
||||
<div className='report-reason-selector'>
|
||||
<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='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} />)}
|
||||
|
50
app/javascript/mastodon/components/hashtag_bar.jsx
Normal file
50
app/javascript/mastodon/components/hashtag_bar.jsx
Normal file
@@ -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 { AvatarOverlay } from './avatar_overlay';
|
||||
import { DisplayName } from './display_name';
|
||||
import { HashtagBar } from './hashtag_bar';
|
||||
import { RelativeTimestamp } from './relative_timestamp';
|
||||
import StatusActionBar from './status_action_bar';
|
||||
import StatusContent from './status_content';
|
||||
@@ -580,6 +581,8 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
{media}
|
||||
|
||||
<HashtagBar hashtags={status.get('tags')} text={status.get('content')} />
|
||||
|
||||
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -10,6 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { AnimatedNumber } from 'mastodon/components/animated_number';
|
||||
import EditedTimestamp from 'mastodon/components/edited_timestamp';
|
||||
import { HashtagBar } from 'mastodon/components/hashtag_bar';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
||||
|
||||
@@ -314,6 +315,8 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||
|
||||
{media}
|
||||
|
||||
<HashtagBar hashtags={status.get('tags')} text={status.get('content')} />
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
<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' />
|
||||
|
@@ -300,6 +300,7 @@
|
||||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} posts}} today",
|
||||
"hashtag.follow": "Follow 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_suggestions": "Find people to follow",
|
||||
"home.column_settings.basic": "Basic",
|
||||
@@ -532,6 +533,7 @@
|
||||
"reply_indicator.cancel": "Cancel",
|
||||
"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.categories.legal": "Legal",
|
||||
"report.categories.other": "Other",
|
||||
"report.categories.spam": "Spam",
|
||||
"report.categories.violation": "Content violates one or more server rules",
|
||||
|
@@ -1111,7 +1111,8 @@ body > [data-popper-placement] {
|
||||
.audio-player,
|
||||
.attachment-list,
|
||||
.picture-in-picture-placeholder,
|
||||
.status-card {
|
||||
.status-card,
|
||||
.hashtag-bar {
|
||||
margin-inline-start: $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
|
||||
def initialize(options = {})
|
||||
@secret_header = options[:secret_header] || 'Secret-Header'
|
||||
@secret = options[:secret] || 'True'
|
||||
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_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
|
||||
|
||||
def bust(url)
|
||||
@@ -21,8 +27,9 @@ class CacheBuster
|
||||
end
|
||||
|
||||
def build_request(url, http_client)
|
||||
Request.new(:get, url, http_client: http_client).tap do |request|
|
||||
request.add_headers(@secret_header => @secret)
|
||||
end
|
||||
request = Request.new(@http_method.downcase.to_sym, url, http_client: http_client)
|
||||
request.add_headers(@secret_header => @secret) if @secret_header.present? && @secret && !@secret.empty?
|
||||
|
||||
request
|
||||
end
|
||||
end
|
||||
|
@@ -117,7 +117,7 @@ class Request
|
||||
|
||||
def perform
|
||||
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
|
||||
raise e.class, "#{e.message} on #{@url}", e.backtrace[0]
|
||||
end
|
||||
|
@@ -43,6 +43,9 @@ class VideoMetadataExtractor
|
||||
@height = video_stream[:height]
|
||||
@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])
|
||||
# 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
|
||||
|
||||
if (audio_stream = audio_streams.first)
|
||||
|
@@ -440,8 +440,21 @@ class Account < ApplicationRecord
|
||||
EntityCache.instance.mention(username, domain)
|
||||
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
|
||||
|
||||
inverse_alias :show_collections, :hide_collections
|
||||
inverse_alias :unlocked, :locked
|
||||
|
||||
def emojis
|
||||
@emojis ||= CustomEmoji.from_text(emojifiable_text, domain)
|
||||
end
|
||||
|
@@ -48,7 +48,10 @@ class Report < ApplicationRecord
|
||||
|
||||
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: {
|
||||
other: 0,
|
||||
spam: 1_000,
|
||||
|
@@ -410,13 +410,25 @@ class Status < ApplicationRecord
|
||||
|
||||
account_ids.uniq!
|
||||
|
||||
status_ids = cached_items.map { |item| item.reblog? ? item.reblog_of_id : item.id }.uniq
|
||||
|
||||
return if account_ids.empty?
|
||||
|
||||
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|
|
||||
item.account = accounts[item.account_id]
|
||||
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
|
||||
|
||||
|
@@ -18,7 +18,7 @@
|
||||
= f.input :discoverable, as: :boolean, wrapper: :with_label, recommended: true
|
||||
|
||||
.fields-group
|
||||
= f.input :locked, as: :boolean, wrapper: :with_label
|
||||
= f.input :unlocked, as: :boolean, wrapper: :with_label
|
||||
|
||||
%h4= t('privacy.search')
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
%p.lead= t('privacy.privacy_hint_html')
|
||||
|
||||
.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|
|
||||
.fields-group
|
||||
|
Reference in New Issue
Block a user