Merge remote-tracking branch 'parent/main' into kb_migration
This commit is contained in:
commit
adfa3524fc
38 changed files with 294 additions and 184 deletions
|
@ -21,6 +21,7 @@ module ContextHelper
|
|||
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
|
||||
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
|
||||
indexable: { 'toot' => 'http://joinmastodon.org/ns#', 'indexable' => 'toot:indexable' },
|
||||
memorial: { 'toot' => 'http://joinmastodon.org/ns#', 'memorial' => 'toot:memorial' },
|
||||
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
||||
emoji_reactions: { 'fedibird' => 'http://fedibird.com/ns#', 'emojiReactions' => { '@id' => 'fedibird:emojiReactions', '@type' => '@id' } },
|
||||
searchable_by: { 'fedibird' => 'http://fedibird.com/ns#', 'searchableBy' => { '@id' => 'fedibird:searchableBy', '@type' => '@id' } },
|
||||
|
|
|
@ -188,6 +188,7 @@ module LanguagesHelper
|
|||
|
||||
ISO_639_3 = {
|
||||
ast: ['Asturian', 'Asturianu'].freeze,
|
||||
chr: ['Cherokee', 'ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ'].freeze,
|
||||
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
|
||||
cnr: ['Montenegrin', 'crnogorski'].freeze,
|
||||
jbo: ['Lojban', 'la .lojban.'].freeze,
|
||||
|
@ -200,6 +201,7 @@ module LanguagesHelper
|
|||
smj: ['Lule Sami', 'Julevsámegiella'].freeze,
|
||||
szl: ['Silesian', 'ślůnsko godka'].freeze,
|
||||
tok: ['Toki Pona', 'toki pona'].freeze,
|
||||
xal: ['Kalmyk', 'Хальмг келн'].freeze,
|
||||
zba: ['Balaibalan', 'باليبلن'].freeze,
|
||||
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
|
||||
}.freeze
|
||||
|
|
|
@ -105,6 +105,21 @@ describe('computeHashtagBarForStatus', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('handles server-side normalized tags with accentuated characters', () => {
|
||||
const status = createStatus(
|
||||
'<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
|
||||
['eaa'], // The server may normalize the hashtags in the `tags` attribute
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual(['Éaa']);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
`"<p>Text</p>"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not display in bar a hashtag in content with a case difference', () => {
|
||||
const status = createStatus(
|
||||
'<p>Text <a href="test">#Éaa</a></p><p><a href="test">#éaa</a></p>',
|
||||
|
|
|
@ -23,8 +23,9 @@ export type StatusLike = Record<{
|
|||
}>;
|
||||
|
||||
function normalizeHashtag(hashtag: string) {
|
||||
if (hashtag && hashtag.startsWith('#')) return hashtag.slice(1);
|
||||
else return hashtag;
|
||||
return (
|
||||
hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
|
||||
).normalize('NFKC');
|
||||
}
|
||||
|
||||
function isNodeLinkHashtag(element: Node): element is HTMLLinkElement {
|
||||
|
@ -70,9 +71,16 @@ function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
|
|||
}
|
||||
|
||||
// Create the collator once, this is much more efficient
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'accent' });
|
||||
const collator = new Intl.Collator(undefined, {
|
||||
sensitivity: 'base', // we use this to emulate the ASCII folding done on the server-side, hopefuly more efficiently
|
||||
});
|
||||
|
||||
function localeAwareInclude(collection: string[], value: string) {
|
||||
return collection.find((item) => collator.compare(item, value) === 0);
|
||||
const normalizedValue = value.normalize('NFKC');
|
||||
|
||||
return !!collection.find(
|
||||
(item) => collator.compare(item.normalize('NFKC'), normalizedValue) === 0,
|
||||
);
|
||||
}
|
||||
|
||||
// We use an intermediate function here to make it easier to test
|
||||
|
@ -121,11 +129,13 @@ export function computeHashtagBarForStatus(status: StatusLike): {
|
|||
// try to see if the last line is only hashtags
|
||||
let onlyHashtags = true;
|
||||
|
||||
const normalizedTagNames = tagNames.map((tag) => tag.normalize('NFKC'));
|
||||
|
||||
Array.from(lastChild.childNodes).forEach((node) => {
|
||||
if (isNodeLinkHashtag(node) && node.textContent) {
|
||||
const normalized = normalizeHashtag(node.textContent);
|
||||
|
||||
if (!localeAwareInclude(tagNames, normalized)) {
|
||||
if (!localeAwareInclude(normalizedTagNames, normalized)) {
|
||||
// stop here, this is not a real hashtag, so consider it as text
|
||||
onlyHashtags = false;
|
||||
return;
|
||||
|
@ -140,12 +150,14 @@ export function computeHashtagBarForStatus(status: StatusLike): {
|
|||
}
|
||||
});
|
||||
|
||||
const hashtagsInBar = tagNames.filter(
|
||||
(tag) =>
|
||||
// the tag does not appear at all in the status content, it is an out-of-band tag
|
||||
!localeAwareInclude(contentHashtags, tag) &&
|
||||
!localeAwareInclude(lastLineHashtags, tag),
|
||||
);
|
||||
const hashtagsInBar = tagNames.filter((tag) => {
|
||||
const normalizedTag = tag.normalize('NFKC');
|
||||
// the tag does not appear at all in the status content, it is an out-of-band tag
|
||||
return (
|
||||
!localeAwareInclude(contentHashtags, normalizedTag) &&
|
||||
!localeAwareInclude(lastLineHashtags, normalizedTag)
|
||||
);
|
||||
});
|
||||
|
||||
const isOnlyOneLine = contentWithoutLastLine.content.childElementCount === 0;
|
||||
const hasMedia = status.get('media_attachments').size > 0;
|
||||
|
@ -204,7 +216,7 @@ const HashtagBar: React.FC<{
|
|||
<div className='hashtag-bar'>
|
||||
{revealedHashtags.map((hashtag) => (
|
||||
<Link key={hashtag} to={`/tags/${hashtag}`}>
|
||||
#{hashtag}
|
||||
#<span>{hashtag}</span>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
|
|
|
@ -583,6 +583,7 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
const expanded = !status.get('hidden')
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
|
@ -611,7 +612,7 @@ class Status extends ImmutablePureComponent {
|
|||
<StatusContent
|
||||
status={status}
|
||||
onClick={this.handleClick}
|
||||
expanded={!status.get('hidden')}
|
||||
expanded={expanded}
|
||||
onExpandedToggle={this.handleExpandedToggle}
|
||||
onTranslate={this.handleTranslate}
|
||||
collapsible
|
||||
|
@ -621,7 +622,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
{(!isCardMediaWithSensitive || !status.get('hidden')) && media}
|
||||
|
||||
{hashtagBar}
|
||||
{expanded && hashtagBar}
|
||||
|
||||
{emojiReactionsBar}
|
||||
|
||||
|
|
|
@ -108,10 +108,10 @@ class Results extends PureComponent {
|
|||
return (
|
||||
<>
|
||||
<div className='account__section-headline'>
|
||||
<button onClick={this.handleSelectAll} className={type === 'all' && 'active'}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
|
||||
<button onClick={this.handleSelectAccounts} className={type === 'accounts' && 'active'}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
|
||||
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' && 'active'}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
|
||||
<button onClick={this.handleSelectStatuses} className={type === 'statuses' && 'active'}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
|
||||
<button onClick={this.handleSelectAll} className={type === 'all' ? 'active' : undefined}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
|
||||
<button onClick={this.handleSelectAccounts} className={type === 'accounts' ? 'active' : undefined}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
|
||||
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' ? 'active' : undefined}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
|
||||
<button onClick={this.handleSelectStatuses} className={type === 'statuses' ? 'active' : undefined}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
|
||||
</div>
|
||||
|
||||
<div className='explore__search-results'>
|
||||
|
|
|
@ -372,6 +372,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
const expanded = !status.get('hidden')
|
||||
|
||||
return (
|
||||
<div style={outerStyle}>
|
||||
|
@ -397,7 +398,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
|
||||
{(!isCardMediaWithSensitive || !status.get('hidden')) && media}
|
||||
|
||||
{hashtagBar}
|
||||
{expanded && hashtagBar}
|
||||
|
||||
{emojiReactionsBar}
|
||||
|
||||
|
|
|
@ -609,7 +609,7 @@ class Status extends ImmutablePureComponent {
|
|||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType='thread'
|
||||
previousId={i > 0 && list.get(i - 1)}
|
||||
previousId={i > 0 ? list.get(i - 1) : undefined}
|
||||
nextId={list.get(i + 1) || (ancestors && statusId)}
|
||||
rootId={statusId}
|
||||
/>
|
||||
|
|
|
@ -123,7 +123,10 @@ export default class ModalRoot extends PureComponent {
|
|||
{visible && (
|
||||
<>
|
||||
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
|
||||
{(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />}
|
||||
{(SpecificComponent) => {
|
||||
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
|
||||
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />
|
||||
}}
|
||||
</BundleContainer>
|
||||
|
||||
<Helmet>
|
||||
|
|
|
@ -5338,6 +5338,7 @@ a.status-card {
|
|||
|
||||
&.active {
|
||||
transform: rotate(90deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -8898,6 +8899,44 @@ noscript {
|
|||
}
|
||||
}
|
||||
|
||||
&__choices {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
|
||||
&__choice {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $darker-text-color;
|
||||
margin-bottom: 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint - 1px) {
|
||||
&__choices {
|
||||
flex-direction: column;
|
||||
|
||||
&__choice {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-button {
|
||||
font-size: inherit;
|
||||
display: inline;
|
||||
|
@ -9620,16 +9659,15 @@ noscript {
|
|||
|
||||
a {
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
background: rgba($highlight-text-color, 0.2);
|
||||
color: $highlight-text-color;
|
||||
padding: 0.4em 0.6em;
|
||||
color: $dark-text-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: rgba($highlight-text-color, 0.3);
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
|
|||
Admin::SystemCheck::Message.new(:elasticsearch_health_red)
|
||||
elsif cluster_health['number_of_nodes'] < 2 && es_preset != 'single_node_cluster'
|
||||
Admin::SystemCheck::Message.new(:elasticsearch_preset_single_node, nil, 'https://docs.joinmastodon.org/admin/optional/elasticsearch/#scaling')
|
||||
elsif Chewy.client.indices.get_settings['chewy_specifications'].dig('settings', 'index', 'number_of_replicas')&.to_i&.positive? && es_preset == 'single_node_cluster'
|
||||
elsif Chewy.client.indices.get_settings[Chewy::Stash::Specification.index_name]&.dig('settings', 'index', 'number_of_replicas')&.to_i&.positive? && es_preset == 'single_node_cluster'
|
||||
Admin::SystemCheck::Message.new(:elasticsearch_reset_chewy)
|
||||
elsif cluster_health['status'] == 'yellow'
|
||||
Admin::SystemCheck::Message.new(:elasticsearch_health_yellow)
|
||||
|
|
|
@ -68,8 +68,8 @@ class Importer::BaseImporter
|
|||
|
||||
protected
|
||||
|
||||
def in_work_unit(*args, &block)
|
||||
work_unit = Concurrent::Promises.future_on(@executor, *args, &block)
|
||||
def in_work_unit(...)
|
||||
work_unit = Concurrent::Promises.future_on(@executor, ...)
|
||||
|
||||
work_unit.on_fulfillment!(&@on_progress)
|
||||
work_unit.on_rejection!(&@on_failure)
|
||||
|
|
|
@ -46,11 +46,11 @@ class PublicFeed
|
|||
end
|
||||
|
||||
def local_only?
|
||||
options[:local]
|
||||
options[:local] && !options[:remote]
|
||||
end
|
||||
|
||||
def remote_only?
|
||||
options[:remote]
|
||||
options[:remote] && !options[:local]
|
||||
end
|
||||
|
||||
def hide_local_users?
|
||||
|
|
|
@ -8,13 +8,13 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
|||
|
||||
context_extensions :manually_approves_followers, :featured, :also_known_as,
|
||||
:moved_to, :property_value, :discoverable, :olm, :suspended, :searchable_by, :subscribable_by,
|
||||
:other_setting
|
||||
:other_setting, :memorial
|
||||
|
||||
attributes :id, :type, :following, :followers,
|
||||
:inbox, :outbox, :featured, :featured_tags,
|
||||
:preferred_username, :name, :summary,
|
||||
:url, :manually_approves_followers,
|
||||
:discoverable, :published, :searchable_by, :subscribable_by, :other_setting
|
||||
:discoverable, :published, :searchable_by, :subscribable_by, :other_setting, :memorial
|
||||
|
||||
has_one :public_key, serializer: ActivityPub::PublicKeySerializer
|
||||
|
||||
|
|
|
@ -147,7 +147,7 @@ class AccountSearchService < BaseService
|
|||
multi_match: {
|
||||
query: @query,
|
||||
type: 'bool_prefix',
|
||||
fields: %w(username username.* display_name display_name.*),
|
||||
fields: %w(username^2 username.*^2 display_name display_name.*),
|
||||
},
|
||||
}
|
||||
end
|
||||
|
|
|
@ -127,6 +127,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
@account.searchability = searchability_from_audience
|
||||
@account.dissubscribable = !subscribable(@account.note)
|
||||
@account.settings = other_settings
|
||||
@account.memorial = @json['memorial'] || false
|
||||
end
|
||||
|
||||
def valid_account?
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue