Merge commit 'b91724fb9d0839365391310e20c2589ff6062d4f' into glitch-soc/merge-upstream
Conflicts: - `Vagrantfile`: Upstream bumped a bunch of values, including one that was already bumped by glitch-soc. Took upstream's version. - `lib/paperclip/transcoder.rb`: glitch-soc already had a partial fix for this. Took upstream's version.
This commit is contained in:
		@@ -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>
 | 
			
		||||
      ))}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -546,6 +546,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
    const visibilityIcon = visibilityIconInfo[status.get('visibility')];
 | 
			
		||||
 | 
			
		||||
    const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
 | 
			
		||||
    const expanded = !status.get('hidden')
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <HotKeys handlers={handlers}>
 | 
			
		||||
@@ -574,7 +575,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
            <StatusContent
 | 
			
		||||
              status={status}
 | 
			
		||||
              onClick={this.handleClick}
 | 
			
		||||
              expanded={!status.get('hidden')}
 | 
			
		||||
              expanded={expanded}
 | 
			
		||||
              onExpandedToggle={this.handleExpandedToggle}
 | 
			
		||||
              onTranslate={this.handleTranslate}
 | 
			
		||||
              collapsible
 | 
			
		||||
@@ -584,7 +585,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
            {media}
 | 
			
		||||
 | 
			
		||||
            {hashtagBar}
 | 
			
		||||
            {expanded && hashtagBar}
 | 
			
		||||
 | 
			
		||||
            <StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
 | 
			
		||||
          </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -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'>
 | 
			
		||||
 
 | 
			
		||||
@@ -293,6 +293,7 @@ class DetailedStatus extends ImmutablePureComponent {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
 | 
			
		||||
    const expanded = !status.get('hidden')
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div style={outerStyle}>
 | 
			
		||||
@@ -318,7 +319,7 @@ class DetailedStatus extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
          {media}
 | 
			
		||||
 | 
			
		||||
          {hashtagBar}
 | 
			
		||||
          {expanded && hashtagBar}
 | 
			
		||||
 | 
			
		||||
          <div className='detailed-status__meta'>
 | 
			
		||||
            <a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
 | 
			
		||||
 
 | 
			
		||||
@@ -568,7 +568,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}
 | 
			
		||||
      />
 | 
			
		||||
 
 | 
			
		||||
@@ -115,7 +115,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>
 | 
			
		||||
 
 | 
			
		||||
@@ -9305,16 +9305,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;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user