~cytrogen/masto-fe

30ad9d976b2c2905f951aa4e9f34c3d6660f809b — Claire 2 years ago ed15893 + c0fa85b
Merge pull request #2272 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes
38 files changed, 270 insertions(+), 252 deletions(-)

M app/controllers/api/v2/search_controller.rb
M app/javascript/flavours/glitch/components/poll.jsx
M app/javascript/flavours/glitch/components/status_content.jsx
M app/javascript/flavours/glitch/features/account/components/header.jsx
M app/javascript/flavours/glitch/features/firehose/index.jsx
M app/javascript/flavours/glitch/features/public_timeline/index.jsx
M app/javascript/flavours/glitch/features/status/index.jsx
M app/javascript/flavours/glitch/styles/admin.scss
M app/javascript/flavours/glitch/styles/components/misc.scss
M app/javascript/flavours/glitch/styles/components/modal.scss
M app/javascript/flavours/glitch/styles/dashboard.scss
M app/javascript/flavours/glitch/styles/forms.scss
M app/javascript/flavours/glitch/styles/mastodon-light/diff.scss
M app/javascript/flavours/glitch/styles/mastodon-light/variables.scss
M app/javascript/flavours/glitch/styles/variables.scss
M app/javascript/mastodon/components/poll.jsx
M app/javascript/mastodon/components/status_content.jsx
M app/javascript/mastodon/features/account/components/header.jsx
M app/javascript/mastodon/features/firehose/index.jsx
M app/javascript/mastodon/features/public_timeline/index.jsx
M app/javascript/mastodon/features/status/index.jsx
M app/javascript/mastodon/locales/en.json
M app/javascript/styles/mastodon-light/diff.scss
M app/javascript/styles/mastodon-light/variables.scss
M app/javascript/styles/mastodon/admin.scss
M app/javascript/styles/mastodon/components.scss
M app/javascript/styles/mastodon/dashboard.scss
M app/javascript/styles/mastodon/forms.scss
M app/javascript/styles/mastodon/variables.scss
M app/lib/scope_parser.rb
M app/services/search_service.rb
M app/workers/account_deletion_worker.rb
M config/routes.rb
A db/migrate/20230702131023_add_superapp_index_to_applications.rb
A db/migrate/20230702151753_add_index_user_on_unconfirmed_email.rb
M db/schema.rb
M spec/controllers/api/v2/search_controller_spec.rb
M spec/services/search_service_spec.rb
M app/controllers/api/v2/search_controller.rb => app/controllers/api/v2/search_controller.rb +2 -2
@@ 34,11 34,11 @@ class Api::V2::SearchController < Api::BaseController
      params[:q],
      current_account,
      limit_param(RESULTS_LIMIT),
      search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed))
      search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed), following: truthy_param?(:following))
    )
  end

  def search_params
    params.permit(:type, :offset, :min_id, :max_id, :account_id)
    params.permit(:type, :offset, :min_id, :max_id, :account_id, :following)
  end
end

M app/javascript/flavours/glitch/components/poll.jsx => app/javascript/flavours/glitch/components/poll.jsx +9 -4
@@ 131,6 131,10 @@ class Poll extends ImmutablePureComponent {
    this.props.refresh();
  };

  handleReveal = () => {
    this.setState({ revealed: true });
  }

  renderOption (option, optionIndex, showResults) {
    const { poll, lang, disabled, intl } = this.props;
    const pollVotesCount  = poll.get('voters_count') || poll.get('votes_count');


@@ 206,14 210,14 @@ class Poll extends ImmutablePureComponent {

  render () {
    const { poll, intl } = this.props;
    const { expired } = this.state;
    const { revealed, expired } = this.state;

    if (!poll) {
      return null;
    }

    const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
    const showResults   = poll.get('voted') || expired;
    const showResults   = poll.get('voted') || revealed || expired;
    const disabled      = this.props.disabled || Object.entries(this.state.selected).every(item => !item);

    let votesCount = null;


@@ 232,9 236,10 @@ class Poll extends ImmutablePureComponent {

        <div className='poll__footer'>
          {!showResults && <button className='button button-secondary' disabled={disabled || !this.context.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
          {showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
          {!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
          {showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
          {votesCount}
          {poll.get('expires_at') && <span> · {timeRemaining}</span>}
          {poll.get('expires_at') && <> · {timeRemaining}</>}
        </div>
      </div>
    );

M app/javascript/flavours/glitch/components/status_content.jsx => app/javascript/flavours/glitch/components/status_content.jsx +1 -1
@@ 163,7 163,7 @@ class StatusContent extends PureComponent {

      if (mention) {
        link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
        link.setAttribute('title', mention.get('acct'));
        link.setAttribute('title', `@${mention.get('acct')}`);
        if (rewriteMentions !== 'no') {
          while (link.firstChild) link.removeChild(link.firstChild);
          link.appendChild(document.createTextNode('@'));

M app/javascript/flavours/glitch/features/account/components/header.jsx => app/javascript/flavours/glitch/features/account/components/header.jsx +1 -0
@@ 398,6 398,7 @@ class Header extends ImmutablePureComponent {
        <Helmet>
          <title>{titleFromAccount(account)}</title>
          <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
          <link rel='canonical' href={account.get('url')} />
        </Helmet>
      </div>
    );

M app/javascript/flavours/glitch/features/firehose/index.jsx => app/javascript/flavours/glitch/features/firehose/index.jsx +14 -13
@@ 103,7 103,7 @@ const Firehose = ({ feedType, multiColumn }) => {
    (maxId) => {
      switch(feedType) {
      case 'community':
        dispatch(expandCommunityTimeline({ onlyMedia }));
        dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
        break;
      case 'public':
        dispatch(expandPublicTimeline({ maxId, onlyMedia, allowLocalOnly }));


@@ 154,12 154,13 @@ const Firehose = ({ feedType, multiColumn }) => {
      />
    </DismissableBanner>
  ) : (
   <DismissableBanner id='public_timeline'>
     <FormattedMessage
       id='dismissable_banner.public_timeline'
       defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.'
     />
   </DismissableBanner>
    <DismissableBanner id='public_timeline'>
      <FormattedMessage
        id='dismissable_banner.public_timeline'
        defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.'
        values={{ domain }}
      />
    </DismissableBanner>
  );

  const emptyMessage = feedType === 'community' ? (


@@ 168,10 169,10 @@ const Firehose = ({ feedType, multiColumn }) => {
      defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
    />
  ) : (
   <FormattedMessage
     id='empty_column.public'
     defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
   />
    <FormattedMessage
      id='empty_column.public'
      defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
    />
  );

  return (


@@ 190,11 191,11 @@ const Firehose = ({ feedType, multiColumn }) => {
      <div className='scrollable scrollable--flex'>
        <div className='account__section-headline'>
          <NavLink exact to='/public/local'>
            <FormattedMessage tagName='div' id='firehose.local' defaultMessage='Local' />
            <FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
          </NavLink>

          <NavLink exact to='/public/remote'>
            <FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Remote' />
            <FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
          </NavLink>

          <NavLink exact to='/public'>

M app/javascript/flavours/glitch/features/public_timeline/index.jsx => app/javascript/flavours/glitch/features/public_timeline/index.jsx +2 -1
@@ 13,6 13,7 @@ import { expandPublicTimeline } from 'flavours/glitch/actions/timelines';
import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header';
import DismissableBanner from 'flavours/glitch/components/dismissable_banner';
import { domain } from 'flavours/glitch/initial_state';
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';

import ColumnSettingsContainer from './containers/column_settings_container';


@@ 147,7 148,7 @@ class PublicTimeline extends PureComponent {
        </ColumnHeader>

        <StatusListContainer
          prepend={<DismissableBanner id='public_timeline'><FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.' /></DismissableBanner>}
          prepend={<DismissableBanner id='public_timeline'><FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.' values={{ domain }} /></DismissableBanner>}
          timelineId={`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`}
          onLoadMore={this.handleLoadMore}
          trackScroll={!pinned}

M app/javascript/flavours/glitch/features/status/index.jsx => app/javascript/flavours/glitch/features/status/index.jsx +1 -0
@@ 754,6 754,7 @@ class Status extends ImmutablePureComponent {
        <Helmet>
          <title>{titleFromStatus(intl, status)}</title>
          <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
          <link rel='canonical' href={status.get('url')} />
        </Helmet>
      </Column>
    );

M app/javascript/flavours/glitch/styles/admin.scss => app/javascript/flavours/glitch/styles/admin.scss +1 -12
@@ 128,7 128,6 @@ $content-width: 840px;
        }

        &.selected {
          background: darken($ui-base-color, 2%);
          border-radius: 4px 0 0;
        }
      }


@@ 146,13 145,9 @@ $content-width: 840px;

      .simple-navigation-active-leaf a {
        color: $primary-text-color;
        background-color: darken($ui-highlight-color, 2%);
        background-color: $ui-highlight-color;
        border-bottom: 0;
        border-radius: 0;

        &:hover {
          background-color: $ui-highlight-color;
        }
      }
    }



@@ 246,12 241,6 @@ $content-width: 840px;
            font-weight: 700;
            color: $primary-text-color;
            background: $ui-highlight-color;

            &:hover,
            &:focus,
            &:active {
              background: lighten($ui-highlight-color, 4%);
            }
          }
        }
      }

M app/javascript/flavours/glitch/styles/components/misc.scss => app/javascript/flavours/glitch/styles/components/misc.scss +13 -34
@@ 38,11 38,11 @@
}

.button {
  background-color: darken($ui-highlight-color, 3%);
  background-color: $ui-button-background-color;
  border: 10px none;
  border-radius: 4px;
  box-sizing: border-box;
  color: $primary-text-color;
  color: $ui-button-color;
  cursor: pointer;
  display: inline-block;
  font-family: inherit;


@@ 62,14 62,14 @@
  &:active,
  &:focus,
  &:hover {
    background-color: $ui-highlight-color;
    background-color: $ui-button-focus-background-color;
  }

  &--destructive {
    &:active,
    &:focus,
    &:hover {
      background-color: $error-red;
      background-color: $ui-button-destructive-focus-background-color;
      transition: none;
    }
  }


@@ 79,43 79,22 @@
    cursor: default;
  }

  &.button-alternative {
    color: $inverted-text-color;
    background: $ui-primary-color;

    &:active,
    &:focus,
    &:hover {
      background-color: lighten($ui-primary-color, 4%);
    }
  }

  &.button-alternative-2 {
    background: $ui-base-lighter-color;

    &:active,
    &:focus,
    &:hover {
      background-color: lighten($ui-base-lighter-color, 4%);
    }
  }

  &.button-secondary {
    font-size: 16px;
    line-height: 36px;
    height: auto;
    color: $darker-text-color;
    color: $ui-button-secondary-color;
    text-transform: none;
    background: transparent;
    padding: 6px 17px;
    border: 1px solid lighten($ui-base-color, 12%);
    border: 1px solid $ui-button-secondary-border-color;

    &:active,
    &:focus,
    &:hover {
      background: lighten($ui-base-color, 4%);
      border-color: lighten($ui-base-color, 16%);
      color: lighten($darker-text-color, 4%);
      border-color: $ui-button-secondary-focus-background-color;
      color: $ui-button-secondary-focus-color;
      background-color: $ui-button-secondary-focus-background-color;
      text-decoration: none;
    }



@@ 127,14 106,14 @@
  &.button-tertiary {
    background: transparent;
    padding: 6px 17px;
    color: $highlight-text-color;
    border: 1px solid $highlight-text-color;
    color: $ui-button-tertiary-color;
    border: 1px solid $ui-button-tertiary-border-color;

    &:active,
    &:focus,
    &:hover {
      background: $ui-highlight-color;
      color: $primary-text-color;
      background-color: $ui-button-tertiary-focus-background-color;
      color: $ui-button-tertiary-focus-color;
      border: 0;
      padding: 7px 18px;
    }

M app/javascript/flavours/glitch/styles/components/modal.scss => app/javascript/flavours/glitch/styles/components/modal.scss +4 -4
@@ 718,15 718,15 @@
  }

  .button.button-secondary {
    border-color: $inverted-text-color;
    color: $inverted-text-color;
    border-color: $ui-button-secondary-border-color;
    color: $ui-button-secondary-color;
    flex: 0 0 auto;

    &:hover,
    &:focus,
    &:active {
      border-color: lighten($inverted-text-color, 15%);
      color: lighten($inverted-text-color, 15%);
      border-color: $ui-button-secondary-focus-background-color;
      color: $ui-button-secondary-focus-color;
    }
  }


M app/javascript/flavours/glitch/styles/dashboard.scss => app/javascript/flavours/glitch/styles/dashboard.scss +2 -2
@@ 81,7 81,7 @@
    display: flex;
    align-items: baseline;
    border-radius: 4px;
    background: darken($ui-highlight-color, 2%);
    background: $ui-button-background-color;
    color: $primary-text-color;
    transition: all 100ms ease-in;
    font-size: 14px;


@@ 94,7 94,7 @@
    &:active,
    &:focus,
    &:hover {
      background-color: $ui-highlight-color;
      background-color: $ui-button-focus-background-color;
      transition: all 200ms ease-out;
    }


M app/javascript/flavours/glitch/styles/forms.scss => app/javascript/flavours/glitch/styles/forms.scss +6 -9
@@ 512,8 512,8 @@ code {
    width: 100%;
    border: 0;
    border-radius: 4px;
    background: darken($ui-highlight-color, 2%);
    color: $primary-text-color;
    background: $ui-button-background-color;
    color: $ui-button-color;
    font-size: 18px;
    line-height: inherit;
    height: auto;


@@ 535,7 535,7 @@ code {
    &:active,
    &:focus,
    &:hover {
      background-color: $ui-highlight-color;
      background-color: $ui-button-focus-background-color;
    }

    &:disabled:hover {


@@ 543,15 543,12 @@ code {
    }

    &.negative {
      background: $error-value-color;

      &:hover {
        background-color: lighten($error-value-color, 5%);
      }
      background: $ui-button-destructive-background-color;

      &:hover,
      &:active,
      &:focus {
        background-color: darken($error-value-color, 5%);
        background-color: $ui-button-destructive-focus-background-color;
      }
    }
  }

M app/javascript/flavours/glitch/styles/mastodon-light/diff.scss => app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +0 -33
@@ 5,19 5,6 @@ html {
  scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
}

// Change the colors of button texts
.button {
  color: $white;

  &.button-alternative-2 {
    color: $white;
  }

  &.button-tertiary {
    color: $highlight-text-color;
  }
}

.simple_form .button.button-tertiary {
  color: $highlight-text-color;
}


@@ 436,26 423,6 @@ html {
  color: $white;
}

.button.button-tertiary {
  &:hover,
  &:focus,
  &:active {
    color: $white;
  }
}

.button.button-secondary {
  border-color: $darker-text-color;
  color: $darker-text-color;

  &:hover,
  &:focus,
  &:active {
    border-color: darken($darker-text-color, 8%);
    color: darken($darker-text-color, 8%);
  }
}

.flash-message.warning {
  color: lighten($gold-star, 16%);
}

M app/javascript/flavours/glitch/styles/mastodon-light/variables.scss => app/javascript/flavours/glitch/styles/mastodon-light/variables.scss +13 -0
@@ 7,6 7,12 @@ $classic-primary-color: #9baec8;
$classic-secondary-color: #d9e1e8;
$classic-highlight-color: #6364ff;

$blurple-600: #563acc; // Iris
$blurple-500: #6364ff; // Brand purple
$blurple-300: #858afa; // Faded Blue
$grey-600: #4e4c5a; // Trout
$grey-100: #dadaf3; // Topaz

// Differences
$success-green: lighten(#3c754d, 8%);



@@ 19,6 25,13 @@ $ui-primary-color: #9bcbed;
$ui-secondary-color: $classic-base-color !default;
$ui-highlight-color: $classic-highlight-color !default;

$ui-button-secondary-color: $grey-600 !default;
$ui-button-secondary-border-color: $grey-600 !default;
$ui-button-secondary-focus-color: $white !default;

$ui-button-tertiary-color: $blurple-500 !default;
$ui-button-tertiary-border-color: $blurple-500 !default;

$primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default;
$highlight-text-color: darken($ui-highlight-color, 8%) !default;

M app/javascript/flavours/glitch/styles/variables.scss => app/javascript/flavours/glitch/styles/variables.scss +29 -4
@@ 1,10 1,18 @@
// Commonly used web colors
$black: #000000; // Black
$white: #ffffff; // White
$success-green: #79bd9a; // Padua
$error-red: #df405a; // Cerise
$warning-red: #ff5050; // Sunset Orange
$gold-star: #ca8f04; // Dark Goldenrod
$red-600: #b7253d !default; // Deep Carmine
$red-500: #df405a !default; // Cerise
$blurple-600: #563acc; // Iris
$blurple-500: #6364ff; // Brand purple
$blurple-300: #858afa; // Faded Blue
$grey-600: #4e4c5a; // Trout
$grey-100: #dadaf3; // Topaz

$success-green: #79bd9a !default; // Padua
$error-red: $red-500 !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod

$red-bookmark: $warning-red;



@@ 31,6 39,22 @@ $ui-base-lighter-color: lighten(
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default;
$ui-button-color: $white !default;
$ui-button-background-color: $blurple-500 !default;
$ui-button-focus-background-color: $blurple-600 !default;

$ui-button-secondary-color: $grey-100 !default;
$ui-button-secondary-border-color: $grey-100 !default;
$ui-button-secondary-focus-background-color: $grey-600 !default;
$ui-button-secondary-focus-color: $white !default;

$ui-button-tertiary-color: $blurple-300 !default;
$ui-button-tertiary-border-color: $blurple-300 !default;
$ui-button-tertiary-focus-background-color: $blurple-600 !default;
$ui-button-tertiary-focus-color: $white !default;

$ui-button-destructive-background-color: $red-500 !default;
$ui-button-destructive-focus-background-color: $red-600 !default;

// Variables for texts
$primary-text-color: $white !default;


@@ 39,6 63,7 @@ $dark-text-color: $ui-base-lighter-color !default;
$secondary-text-color: $ui-secondary-color !default;
$highlight-text-color: lighten($ui-highlight-color, 8%) !default;
$action-button-color: $ui-base-lighter-color !default;
$action-button-focus-color: lighten($ui-base-lighter-color, 4%) !default;
$passive-text-color: $gold-star !default;
$active-passive-text-color: $success-green !default;


M app/javascript/mastodon/components/poll.jsx => app/javascript/mastodon/components/poll.jsx +9 -4
@@ 130,6 130,10 @@ class Poll extends ImmutablePureComponent {
    this.props.refresh();
  };

  handleReveal = () => {
    this.setState({ revealed: true });
  }

  renderOption (option, optionIndex, showResults) {
    const { poll, lang, disabled, intl } = this.props;
    const pollVotesCount  = poll.get('voters_count') || poll.get('votes_count');


@@ 205,14 209,14 @@ class Poll extends ImmutablePureComponent {

  render () {
    const { poll, intl } = this.props;
    const { expired } = this.state;
    const { revealed, expired } = this.state;

    if (!poll) {
      return null;
    }

    const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
    const showResults   = poll.get('voted') || expired;
    const showResults   = poll.get('voted') || revealed || expired;
    const disabled      = this.props.disabled || Object.entries(this.state.selected).every(item => !item);

    let votesCount = null;


@@ 231,9 235,10 @@ class Poll extends ImmutablePureComponent {

        <div className='poll__footer'>
          {!showResults && <button className='button button-secondary' disabled={disabled || !this.context.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
          {showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>}
          {!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
          {showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
          {votesCount}
          {poll.get('expires_at') && <span> · {timeRemaining}</span>}
          {poll.get('expires_at') && <> · {timeRemaining}</>}
        </div>
      </div>
    );

M app/javascript/mastodon/components/status_content.jsx => app/javascript/mastodon/components/status_content.jsx +1 -1
@@ 104,7 104,7 @@ class StatusContent extends PureComponent {

      if (mention) {
        link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
        link.setAttribute('title', mention.get('acct'));
        link.setAttribute('title', `@${mention.get('acct')}`);
        link.setAttribute('href', `/@${mention.get('acct')}`);
      } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
        link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);

M app/javascript/mastodon/features/account/components/header.jsx => app/javascript/mastodon/features/account/components/header.jsx +1 -0
@@ 476,6 476,7 @@ class Header extends ImmutablePureComponent {
        <Helmet>
          <title>{titleFromAccount(account)}</title>
          <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
          <link rel='canonical' href={account.get('url')} />
        </Helmet>
      </div>
    );

M app/javascript/mastodon/features/firehose/index.jsx => app/javascript/mastodon/features/firehose/index.jsx +14 -13
@@ 84,7 84,7 @@ const Firehose = ({ feedType, multiColumn }) => {
    (maxId) => {
      switch(feedType) {
      case 'community':
        dispatch(expandCommunityTimeline({ onlyMedia }));
        dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
        break;
      case 'public':
        dispatch(expandPublicTimeline({ maxId, onlyMedia }));


@@ 135,12 135,13 @@ const Firehose = ({ feedType, multiColumn }) => {
      />
    </DismissableBanner>
  ) : (
   <DismissableBanner id='public_timeline'>
     <FormattedMessage
       id='dismissable_banner.public_timeline'
       defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.'
     />
   </DismissableBanner>
    <DismissableBanner id='public_timeline'>
      <FormattedMessage
        id='dismissable_banner.public_timeline'
        defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.'
        values={{ domain }}
      />
    </DismissableBanner>
  );

  const emptyMessage = feedType === 'community' ? (


@@ 149,10 150,10 @@ const Firehose = ({ feedType, multiColumn }) => {
      defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
    />
  ) : (
   <FormattedMessage
     id='empty_column.public'
     defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
   />
    <FormattedMessage
      id='empty_column.public'
      defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
    />
  );

  return (


@@ 171,11 172,11 @@ const Firehose = ({ feedType, multiColumn }) => {
      <div className='scrollable scrollable--flex'>
        <div className='account__section-headline'>
          <NavLink exact to='/public/local'>
            <FormattedMessage tagName='div' id='firehose.local' defaultMessage='Local' />
            <FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
          </NavLink>

          <NavLink exact to='/public/remote'>
            <FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Remote' />
            <FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
          </NavLink>

          <NavLink exact to='/public'>

M app/javascript/mastodon/features/public_timeline/index.jsx => app/javascript/mastodon/features/public_timeline/index.jsx +2 -1
@@ 8,6 8,7 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';

import DismissableBanner from 'mastodon/components/dismissable_banner';
import { domain } from 'mastodon/initial_state';

import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { connectPublicStream } from '../../actions/streaming';


@@ 143,7 144,7 @@ class PublicTimeline extends PureComponent {
        </ColumnHeader>

        <StatusListContainer
          prepend={<DismissableBanner id='public_timeline'><FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.' /></DismissableBanner>}
          prepend={<DismissableBanner id='public_timeline'><FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.' values={{ domain }} /></DismissableBanner>}
          timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
          onLoadMore={this.handleLoadMore}
          trackScroll={!pinned}

M app/javascript/mastodon/features/status/index.jsx => app/javascript/mastodon/features/status/index.jsx +1 -0
@@ 713,6 713,7 @@ class Status extends ImmutablePureComponent {
        <Helmet>
          <title>{titleFromStatus(intl, status)}</title>
          <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
          <link rel='canonical' href={status.get('url')} />
        </Helmet>
      </Column>
    );

M app/javascript/mastodon/locales/en.json => app/javascript/mastodon/locales/en.json +4 -3
@@ 202,7 202,7 @@
  "dismissable_banner.explore_links": "These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.",
  "dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.",
  "dismissable_banner.explore_tags": "These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.",
  "dismissable_banner.public_timeline": "These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.",
  "dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.",
  "embed.instructions": "Embed this post on your website by copying the code below.",
  "embed.preview": "Here is what it will look like:",
  "emoji_button.activity": "Activity",


@@ 269,8 269,8 @@
  "filter_modal.select_filter.title": "Filter this post",
  "filter_modal.title.status": "Filter a post",
  "firehose.all": "All",
  "firehose.local": "Local",
  "firehose.remote": "Remote",
  "firehose.local": "This server",
  "firehose.remote": "Other servers",
  "follow_request.authorize": "Authorize",
  "follow_request.reject": "Reject",
  "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",


@@ 487,6 487,7 @@
  "picture_in_picture.restore": "Put it back",
  "poll.closed": "Closed",
  "poll.refresh": "Refresh",
  "poll.reveal": "See results",
  "poll.total_people": "{count, plural, one {# person} other {# people}}",
  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
  "poll.vote": "Vote",

M app/javascript/styles/mastodon-light/diff.scss => app/javascript/styles/mastodon-light/diff.scss +0 -33
@@ 5,19 5,6 @@ html {
  scrollbar-color: $ui-base-color rgba($ui-base-color, 0.25);
}

// Change the colors of button texts
.button {
  color: $white;

  &.button-alternative-2 {
    color: $white;
  }

  &.button-tertiary {
    color: $highlight-text-color;
  }
}

.simple_form .button.button-tertiary {
  color: $highlight-text-color;
}


@@ 436,26 423,6 @@ html {
  color: $white;
}

.button.button-tertiary {
  &:hover,
  &:focus,
  &:active {
    color: $white;
  }
}

.button.button-secondary {
  border-color: $darker-text-color;
  color: $darker-text-color;

  &:hover,
  &:focus,
  &:active {
    border-color: darken($darker-text-color, 8%);
    color: darken($darker-text-color, 8%);
  }
}

.flash-message.warning {
  color: lighten($gold-star, 16%);
}

M app/javascript/styles/mastodon-light/variables.scss => app/javascript/styles/mastodon-light/variables.scss +13 -0
@@ 7,6 7,12 @@ $classic-primary-color: #9baec8;
$classic-secondary-color: #d9e1e8;
$classic-highlight-color: #6364ff;

$blurple-600: #563acc; // Iris
$blurple-500: #6364ff; // Brand purple
$blurple-300: #858afa; // Faded Blue
$grey-600: #4e4c5a; // Trout
$grey-100: #dadaf3; // Topaz

// Differences
$success-green: lighten(#3c754d, 8%);



@@ 19,6 25,13 @@ $ui-primary-color: #9bcbed;
$ui-secondary-color: $classic-base-color !default;
$ui-highlight-color: $classic-highlight-color !default;

$ui-button-secondary-color: $grey-600 !default;
$ui-button-secondary-border-color: $grey-600 !default;
$ui-button-secondary-focus-color: $white !default;

$ui-button-tertiary-color: $blurple-500 !default;
$ui-button-tertiary-border-color: $blurple-500 !default;

$primary-text-color: $black !default;
$darker-text-color: $classic-base-color !default;
$highlight-text-color: darken($ui-highlight-color, 8%) !default;

M app/javascript/styles/mastodon/admin.scss => app/javascript/styles/mastodon/admin.scss +1 -12
@@ 128,7 128,6 @@ $content-width: 840px;
        }

        &.selected {
          background: darken($ui-base-color, 2%);
          border-radius: 4px 0 0;
        }
      }


@@ 146,13 145,9 @@ $content-width: 840px;

      .simple-navigation-active-leaf a {
        color: $primary-text-color;
        background-color: darken($ui-highlight-color, 2%);
        background-color: $ui-highlight-color;
        border-bottom: 0;
        border-radius: 0;

        &:hover {
          background-color: $ui-highlight-color;
        }
      }
    }



@@ 246,12 241,6 @@ $content-width: 840px;
            font-weight: 700;
            color: $primary-text-color;
            background: $ui-highlight-color;

            &:hover,
            &:focus,
            &:active {
              background: lighten($ui-highlight-color, 4%);
            }
          }
        }
      }

M app/javascript/styles/mastodon/components.scss => app/javascript/styles/mastodon/components.scss +25 -40
@@ 47,11 47,11 @@
}

.button {
  background-color: darken($ui-highlight-color, 2%);
  background-color: $ui-button-background-color;
  border: 10px none;
  border-radius: 4px;
  box-sizing: border-box;
  color: $primary-text-color;
  color: $ui-button-color;
  cursor: pointer;
  display: inline-block;
  font-family: inherit;


@@ 71,14 71,14 @@
  &:active,
  &:focus,
  &:hover {
    background-color: $ui-highlight-color;
    background-color: $ui-button-focus-background-color;
  }

  &--destructive {
    &:active,
    &:focus,
    &:hover {
      background-color: $error-red;
      background-color: $ui-button-destructive-focus-background-color;
      transition: none;
    }
  }


@@ 108,39 108,18 @@
    outline: 0 !important;
  }

  &.button-alternative {
    color: $inverted-text-color;
    background: $ui-primary-color;

    &:active,
    &:focus,
    &:hover {
      background-color: lighten($ui-primary-color, 4%);
    }
  }

  &.button-alternative-2 {
    background: $ui-base-lighter-color;

    &:active,
    &:focus,
    &:hover {
      background-color: lighten($ui-base-lighter-color, 4%);
    }
  }

  &.button-secondary {
    color: $darker-text-color;
    color: $ui-button-secondary-color;
    background: transparent;
    padding: 6px 17px;
    border: 1px solid lighten($ui-base-color, 12%);
    border: 1px solid $ui-button-secondary-border-color;

    &:active,
    &:focus,
    &:hover {
      background: lighten($ui-base-color, 4%);
      border-color: lighten($ui-base-color, 16%);
      color: lighten($darker-text-color, 4%);
      border-color: $ui-button-secondary-focus-background-color;
      color: $ui-button-secondary-focus-color;
      background-color: $ui-button-secondary-focus-background-color;
      text-decoration: none;
    }



@@ 152,14 131,14 @@
  &.button-tertiary {
    background: transparent;
    padding: 6px 17px;
    color: $highlight-text-color;
    border: 1px solid $highlight-text-color;
    color: $ui-button-tertiary-color;
    border: 1px solid $ui-button-tertiary-border-color;

    &:active,
    &:focus,
    &:hover {
      background: $ui-highlight-color;
      color: $primary-text-color;
      background-color: $ui-button-tertiary-focus-background-color;
      color: $ui-button-tertiary-focus-color;
      border: 0;
      padding: 7px 18px;
    }


@@ 1148,6 1127,8 @@ body > [data-popper-placement] {
  }

  &--in-thread {
    $thread-margin: 46px + 10px;

    border-bottom: 0;

    .status__content,


@@ 1158,8 1139,12 @@ body > [data-popper-placement] {
    .attachment-list,
    .picture-in-picture-placeholder,
    .status-card {
      margin-inline-start: 46px + 10px;
      width: calc(100% - (46px + 10px));
      margin-inline-start: $thread-margin;
      width: calc(100% - ($thread-margin));
    }

    .status__content__read-more-button {
      margin-inline-start: $thread-margin;
    }
  }



@@ 5810,15 5795,15 @@ a.status-card.compact:hover {
  }

  .button.button-secondary {
    border-color: $inverted-text-color;
    color: $inverted-text-color;
    border-color: $ui-button-secondary-border-color;
    color: $ui-button-secondary-color;
    flex: 0 0 auto;

    &:hover,
    &:focus,
    &:active {
      border-color: lighten($inverted-text-color, 15%);
      color: lighten($inverted-text-color, 15%);
      border-color: $ui-button-secondary-focus-background-color;
      color: $ui-button-secondary-focus-color;
    }
  }


M app/javascript/styles/mastodon/dashboard.scss => app/javascript/styles/mastodon/dashboard.scss +2 -2
@@ 81,7 81,7 @@
    display: flex;
    align-items: baseline;
    border-radius: 4px;
    background: darken($ui-highlight-color, 2%);
    background: $ui-button-background-color;
    color: $primary-text-color;
    transition: all 100ms ease-in;
    font-size: 14px;


@@ 94,7 94,7 @@
    &:active,
    &:focus,
    &:hover {
      background-color: $ui-highlight-color;
      background-color: $ui-button-focus-background-color;
      transition: all 200ms ease-out;
    }


M app/javascript/styles/mastodon/forms.scss => app/javascript/styles/mastodon/forms.scss +6 -9
@@ 511,8 511,8 @@ code {
    width: 100%;
    border: 0;
    border-radius: 4px;
    background: darken($ui-highlight-color, 2%);
    color: $primary-text-color;
    background: $ui-button-background-color;
    color: $ui-button-color;
    font-size: 18px;
    line-height: inherit;
    height: auto;


@@ 534,7 534,7 @@ code {
    &:active,
    &:focus,
    &:hover {
      background-color: $ui-highlight-color;
      background-color: $ui-button-focus-background-color;
    }

    &:disabled:hover {


@@ 542,15 542,12 @@ code {
    }

    &.negative {
      background: $error-value-color;

      &:hover {
        background-color: lighten($error-value-color, 5%);
      }
      background: $ui-button-destructive-background-color;

      &:hover,
      &:active,
      &:focus {
        background-color: darken($error-value-color, 5%);
        background-color: $ui-button-destructive-focus-background-color;
      }
    }
  }

M app/javascript/styles/mastodon/variables.scss => app/javascript/styles/mastodon/variables.scss +26 -1
@@ 1,8 1,16 @@
// Commonly used web colors
$black: #000000; // Black
$white: #ffffff; // White
$red-600: #b7253d !default; // Deep Carmine
$red-500: #df405a !default; // Cerise
$blurple-600: #563acc; // Iris
$blurple-500: #6364ff; // Brand purple
$blurple-300: #858afa; // Faded Blue
$grey-600: #4e4c5a; // Trout
$grey-100: #dadaf3; // Topaz

$success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise
$error-red: $red-500 !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod



@@ 31,6 39,22 @@ $ui-base-lighter-color: lighten(
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default;
$ui-button-color: $white !default;
$ui-button-background-color: $blurple-500 !default;
$ui-button-focus-background-color: $blurple-600 !default;

$ui-button-secondary-color: $grey-100 !default;
$ui-button-secondary-border-color: $grey-100 !default;
$ui-button-secondary-focus-background-color: $grey-600 !default;
$ui-button-secondary-focus-color: $white !default;

$ui-button-tertiary-color: $blurple-300 !default;
$ui-button-tertiary-border-color: $blurple-300 !default;
$ui-button-tertiary-focus-background-color: $blurple-600 !default;
$ui-button-tertiary-focus-color: $white !default;

$ui-button-destructive-background-color: $red-500 !default;
$ui-button-destructive-focus-background-color: $red-600 !default;

// Variables for texts
$primary-text-color: $white !default;


@@ 39,6 63,7 @@ $dark-text-color: $ui-base-lighter-color !default;
$secondary-text-color: $ui-secondary-color !default;
$highlight-text-color: lighten($ui-highlight-color, 8%) !default;
$action-button-color: $ui-base-lighter-color !default;
$action-button-focus-color: lighten($ui-base-lighter-color, 4%) !default;
$passive-text-color: $gold-star !default;
$active-passive-text-color: $success-green !default;


M app/lib/scope_parser.rb => app/lib/scope_parser.rb +1 -1
@@ 1,7 1,7 @@
# frozen_string_literal: true

class ScopeParser < Parslet::Parser
  rule(:term)      { match('[a-z]').repeat(1).as(:term) }
  rule(:term)      { match('[a-z_]').repeat(1).as(:term) }
  rule(:colon)     { str(':') }
  rule(:access)    { (str('write') | str('read')).as(:access) }
  rule(:namespace) { str('admin').as(:namespace) }

M app/services/search_service.rb => app/services/search_service.rb +9 -7
@@ 2,12 2,13 @@

class SearchService < BaseService
  def call(query, account, limit, options = {})
    @query   = query&.strip
    @account = account
    @options = options
    @limit   = limit.to_i
    @offset  = options[:type].blank? ? 0 : options[:offset].to_i
    @resolve = options[:resolve] || false
    @query     = query&.strip
    @account   = account
    @options   = options
    @limit     = limit.to_i
    @offset    = options[:type].blank? ? 0 : options[:offset].to_i
    @resolve   = options[:resolve] || false
    @following = options[:following] || false

    default_results.tap do |results|
      next if @query.blank? || @limit.zero?


@@ 31,7 32,8 @@ class SearchService < BaseService
      limit: @limit,
      resolve: @resolve,
      offset: @offset,
      use_searchable_text: true
      use_searchable_text: true,
      following: @following
    )
  end


M app/workers/account_deletion_worker.rb => app/workers/account_deletion_worker.rb +4 -1
@@ 6,9 6,12 @@ class AccountDeletionWorker
  sidekiq_options queue: 'pull', lock: :until_executed

  def perform(account_id, options = {})
    account = Account.find(account_id)
    return unless account.suspended?

    reserve_username = options.with_indifferent_access.fetch(:reserve_username, true)
    skip_activitypub = options.with_indifferent_access.fetch(:skip_activitypub, false)
    DeleteAccountService.new.call(Account.find(account_id), reserve_username: reserve_username, skip_activitypub: skip_activitypub, reserve_email: false)
    DeleteAccountService.new.call(account, reserve_username: reserve_username, skip_activitypub: skip_activitypub, reserve_email: false)
  rescue ActiveRecord::RecordNotFound
    true
  end

M config/routes.rb => config/routes.rb +1 -0
@@ 13,6 13,7 @@ Rails.application.routes.draw do
    /home
    /public
    /public/local
    /public/remote
    /conversations
    /lists/(*any)
    /notifications

A db/migrate/20230702131023_add_superapp_index_to_applications.rb => db/migrate/20230702131023_add_superapp_index_to_applications.rb +9 -0
@@ 0,0 1,9 @@
# frozen_string_literal: true

class AddSuperappIndexToApplications < ActiveRecord::Migration[6.1]
  disable_ddl_transaction!

  def change
    add_index :oauth_applications, :superapp, where: 'superapp = true', algorithm: :concurrently
  end
end

A db/migrate/20230702151753_add_index_user_on_unconfirmed_email.rb => db/migrate/20230702151753_add_index_user_on_unconfirmed_email.rb +9 -0
@@ 0,0 1,9 @@
# frozen_string_literal: true

class AddIndexUserOnUnconfirmedEmail < ActiveRecord::Migration[6.1]
  disable_ddl_transaction!

  def change
    add_index :users, :unconfirmed_email, where: 'unconfirmed_email IS NOT NULL', algorithm: :concurrently
  end
end

M db/schema.rb => db/schema.rb +3 -1
@@ 10,7 10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2023_06_30_145300) do
ActiveRecord::Schema.define(version: 2023_07_02_151753) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"


@@ 700,6 700,7 @@ ActiveRecord::Schema.define(version: 2023_06_30_145300) do
    t.bigint "owner_id"
    t.boolean "confidential", default: true, null: false
    t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type"
    t.index ["superapp"], name: "index_oauth_applications_on_superapp", where: "(superapp = true)"
    t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
  end



@@ 1099,6 1100,7 @@ ActiveRecord::Schema.define(version: 2023_06_30_145300) do
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, opclass: :text_pattern_ops, where: "(reset_password_token IS NOT NULL)"
    t.index ["role_id"], name: "index_users_on_role_id", where: "(role_id IS NOT NULL)"
    t.index ["unconfirmed_email"], name: "index_users_on_unconfirmed_email", where: "(unconfirmed_email IS NOT NULL)"
  end

  create_table "web_push_subscriptions", force: :cascade do |t|

M spec/controllers/api/v2/search_controller_spec.rb => spec/controllers/api/v2/search_controller_spec.rb +30 -3
@@ 14,13 14,40 @@ RSpec.describe Api::V2::SearchController do
    end

    describe 'GET #index' do
      before do
        get :index, params: { q: 'test' }
      end
      let!(:bob)   { Fabricate(:account, username: 'bob_test') }
      let!(:ana)   { Fabricate(:account, username: 'ana_test') }
      let!(:tom)   { Fabricate(:account, username: 'tom_test') }
      let(:params) { { q: 'test' } }

      it 'returns http success' do
        get :index, params: params

        expect(response).to have_http_status(200)
      end

      context 'when searching accounts' do
        let(:params) { { q: 'test', type: 'accounts' } }

        it 'returns all matching accounts' do
          get :index, params: params

          expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(bob.id.to_s, ana.id.to_s, tom.id.to_s)
        end

        context 'with following=true' do
          let(:params) { { q: 'test', type: 'accounts', following: 'true' } }

          before do
            user.account.follow!(ana)
          end

          it 'returns only the followed accounts' do
            get :index, params: params

            expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(ana.id.to_s)
          end
        end
      end
    end
  end


M spec/services/search_service_spec.rb => spec/services/search_service_spec.rb +1 -1
@@ 68,7 68,7 @@ describe SearchService, type: :service do
          allow(AccountSearchService).to receive(:new).and_return(service)

          results = subject.call(query, nil, 10)
          expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false, use_searchable_text: true)
          expect(service).to have_received(:call).with(query, nil, limit: 10, offset: 0, resolve: false, use_searchable_text: true, following: false)
          expect(results).to eq empty_results.merge(accounts: [account])
        end
      end