M CHANGELOG.md => CHANGELOG.md +26 -7
@@ 8,6 8,9 @@ The following changelog entries focus on changes visible to users, administrator
### Added
+- **Add “Privacy and reach” tab in profile settings** ([Gargron](https://github.com/mastodon/mastodon/pull/26484), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26508))
+ This reorganized scattered privacy and reach settings to a single place, as well as improve their wording.
+- **Add display of out-of-band hashtags in the web interface** ([Gargron](https://github.com/mastodon/mastodon/pull/26492), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26497), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26506), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26525))
- **Add role badges to the web interface** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25649), [Gargron](https://github.com/mastodon/mastodon/pull/26281))
- **Add ability to pick domains to forward reports to using the `forward_to_domains` parameter in `POST /api/v1/reports`** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25866))
The `forward_to_domains` REST API parameter is a list of strings. If it is empty or omitted, the previous behavior is maintained.
@@ 23,8 26,18 @@ The following changelog entries focus on changes visible to users, administrator
- **Add optional hCaptcha support** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25019), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25057), [Gargron](https://github.com/mastodon/mastodon/pull/25395), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26388))
- **Add lines to threads in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24549), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24677), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24696), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24711), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24713), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24715), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24800), [teeerevor](https://github.com/mastodon/mastodon/pull/25706), [renchap](https://github.com/mastodon/mastodon/pull/25807))
- **Add new onboarding flow to web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24619), [Gargron](https://github.com/mastodon/mastodon/pull/24646), [Gargron](https://github.com/mastodon/mastodon/pull/24705), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24872), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24883), [Gargron](https://github.com/mastodon/mastodon/pull/24954), [stevenjlm](https://github.com/mastodon/mastodon/pull/24959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25010), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25275), [Gargron](https://github.com/mastodon/mastodon/pull/25559), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25561))
+- **Add `S3_DISABLE_CHECKSUM_MODE` environment variable for compatibility with some S3-compatible providers** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26435))
+- **Add auto-refresh of accounts we get new messages/edits of** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26510))
+- **Add Elasticsearch cluster health check and indexes mismatch check to dashboard** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26448))
+- Add support for `indexable` attribute on remote actors ([Gargron](https://github.com/mastodon/mastodon/pull/26485))
+- Add `DELETE /api/v1/profile/avatar` and `DELETE /api/v1/profile/header` to the REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25124), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26573))
+- Add `ES_PRESET` option to customize numbers of shards and replicas ([Gargron](https://github.com/mastodon/mastodon/pull/26483), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26489))
+ This can have a value of `single_node_cluster` (default), `small_cluster` (uses one replica) or `large_cluster` (uses one replica and a higher number of shards).
+- Add missing `instances` option to `tootctl search deploy` ([tribela](https://github.com/mastodon/mastodon/pull/26461))
+- Add `CACHE_BUSTER_HTTP_METHOD` environment variable ([renchap](https://github.com/mastodon/mastodon/pull/26528), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26542))
+- Add support for `DB_PASS` when using `DATABASE_URL` ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26295))
- Add `GET /api/v1/instance/languages` to REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24443))
-- Add primary key to `preview_cards_statuses` join table ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25243), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26384))
+- Add primary key to `preview_cards_statuses` join table ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25243), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26384), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26447))
- Add client-side timeout on resend confirmation button ([Gargron](https://github.com/mastodon/mastodon/pull/26300))
- Add published date and author to news on the explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26155))
- Add `lang` attribute to various UI components ([c960657](https://github.com/mastodon/mastodon/pull/23869), [c960657](https://github.com/mastodon/mastodon/pull/23891), [c960657](https://github.com/mastodon/mastodon/pull/26111), [c960657](https://github.com/mastodon/mastodon/pull/26149))
@@ 43,7 56,7 @@ The following changelog entries focus on changes visible to users, administrator
- Add unsubscribe link and headers to e-mails ([Gargron](https://github.com/mastodon/mastodon/pull/25378), [c960657](https://github.com/mastodon/mastodon/pull/26085))
- Add logging of websocket send errors ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25280))
- Add time zone preference ([Gargron](https://github.com/mastodon/mastodon/pull/25342), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26025))
-- Add `legal` as report category ([Gargron](https://github.com/mastodon/mastodon/pull/23941), [renchap](https://github.com/mastodon/mastodon/pull/25400))
+- Add `legal` as report category ([Gargron](https://github.com/mastodon/mastodon/pull/23941), [renchap](https://github.com/mastodon/mastodon/pull/25400), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26509))
- Add `data-nosnippet` so Google doesn't use trending posts in snippets for `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25279))
- Add card with who invited you to join when displaying rules on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23475))
- Add missing primary keys to `accounts_tags` and `statuses_tags` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25210))
@@ 80,11 93,12 @@ The following changelog entries focus on changes visible to users, administrator
### Changed
+- **Change hashtags to be displayed separately when they are the last line of a post** ([renchap](https://github.com/mastodon/mastodon/pull/26499))
- **Change reblogs to be excluded from "Posts and replies" tab in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26302))
-- **Change interaction modal in web interface** ([Gargron, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26075), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26269), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26268), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26267))
+- **Change interaction modal in web interface** ([Gargron, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26075), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26269), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26268), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26267), [mgmn](https://github.com/mastodon/mastodon/pull/26459))
- **Change design of link previews in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26136), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26151), [Gargron](https://github.com/mastodon/mastodon/pull/26153), [Gargron](https://github.com/mastodon/mastodon/pull/26250), [Gargron](https://github.com/mastodon/mastodon/pull/26287), [Gargron](https://github.com/mastodon/mastodon/pull/26286), [c960657](https://github.com/mastodon/mastodon/pull/26184))
- **Change "direct message" nomenclature to "private mention" in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24248))
-- **Change translation feature to cover Content Warnings, poll options and media descriptions** ([c960657](https://github.com/mastodon/mastodon/pull/24175), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25251), [c960657](https://github.com/mastodon/mastodon/pull/26168))
+- **Change translation feature to cover Content Warnings, poll options and media descriptions** ([c960657](https://github.com/mastodon/mastodon/pull/24175), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25251), [c960657](https://github.com/mastodon/mastodon/pull/26168), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26452))
- **Change account search to match by text when opted-in** ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25599), [Gargron](https://github.com/mastodon/mastodon/pull/26378))
- **Change import feature to be clearer, less error-prone and more reliable** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21054), [mgmn](https://github.com/mastodon/mastodon/pull/24874))
- **Change local and federated timelines to be in a single “Live feeds” column** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25641), [Gargron](https://github.com/mastodon/mastodon/pull/25683), [mgmn](https://github.com/mastodon/mastodon/pull/25694), [Plastikmensch](https://github.com/mastodon/mastodon/pull/26247))
@@ 97,7 111,9 @@ The following changelog entries focus on changes visible to users, administrator
- **Change replica support to native Rails adapter** ([krainboltgreene](https://github.com/mastodon/mastodon/pull/25693), [Gargron](https://github.com/mastodon/mastodon/pull/25849), [Gargron](https://github.com/mastodon/mastodon/pull/25874), [Gargron](https://github.com/mastodon/mastodon/pull/25851), [Gargron](https://github.com/mastodon/mastodon/pull/25977), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26074), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26326), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26386))
This is a breaking change, dropping `makara` support, and requiring you to update your database configuration if you are using replicas.
To tell Mastodon to use a read replica, you can either set the `REPLICA_DB_NAME` environment variable (along with `REPLICA_DB_USER`, `REPLICA_DB_PASS`, `REPLICA_DB_HOST`, and `REPLICA_DB_PORT`, if they differ from the primary database), or the `REPLICA_DATABASE_URL` environment variable if your configuration is based on `DATABASE_URL`.
-- Change header of hashtag timelines in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26362))
+- Change follow recommendation materialized view to be faster in most cases ([renchap, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26545))
+- Change `robots.txt` to block GPTBot ([Foritus](https://github.com/mastodon/mastodon/pull/26396))
+- Change header of hashtag timelines in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26362), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26416))
- Change streaming `/metrics` to include additional metrics ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26299))
- Change indexing frequency from 5 minutes to 1 minute, add locks to schedulers ([Gargron](https://github.com/mastodon/mastodon/pull/26304))
- Change column link to add a better keyboard focus indicator ([teeerevor](https://github.com/mastodon/mastodon/pull/26278))
@@ 114,7 130,7 @@ The following changelog entries focus on changes visible to users, administrator
- Change header backgrounds to use fewer different colors in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25577))
- Change files to be deleted in batches instead of one-by-one ([Gargron](https://github.com/mastodon/mastodon/pull/23302), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25586), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25587))
- Change emoji picker icon ([iparr](https://github.com/mastodon/mastodon/pull/25479))
-- Change edit profile page ([Gargron](https://github.com/mastodon/mastodon/pull/25413))
+- Change edit profile page ([Gargron](https://github.com/mastodon/mastodon/pull/25413), [c960657](https://github.com/mastodon/mastodon/pull/26538))
- Change "bot" label to "automated" ([Gargron](https://github.com/mastodon/mastodon/pull/25356))
- Change design of dropdowns in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25107))
- Change wording of “Content cache retention period” setting to highlight destructive implications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23261))
@@ 172,6 188,9 @@ The following changelog entries focus on changes visible to users, administrator
- **Fix being unable to load past a full page of filtered posts in Home timeline** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24930))
- **Fix log-in flow when involving both OAuth and external authentication** ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24073))
- **Fix broken links in account gallery** ([c960657](https://github.com/mastodon/mastodon/pull/24218))
+- **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392))
+- Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500))
+- Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409))
- Fix adding column with default value taking longer on Postgres >= 11 ([Gargron](https://github.com/mastodon/mastodon/pull/26375))
- Fix light theme select option for hashtags ([teeerevor](https://github.com/mastodon/mastodon/pull/26311))
- Fix AVIF attachments ([c960657](https://github.com/mastodon/mastodon/pull/26264))
@@ 189,7 208,7 @@ The following changelog entries focus on changes visible to users, administrator
- Fix for "follows you" indicator in light web UI not readable ([vmstan](https://github.com/mastodon/mastodon/pull/25993))
- Fix incorrect line break between icon and number of reposts & favourites ([edent](https://github.com/mastodon/mastodon/pull/26004))
- Fix sounds not being loaded from assets host ([Signez](https://github.com/mastodon/mastodon/pull/25931))
-- Fix buttons showing inconsistent styles ([teeerevor](https://github.com/mastodon/mastodon/pull/25903), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25965), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26341))
+- Fix buttons showing inconsistent styles ([teeerevor](https://github.com/mastodon/mastodon/pull/25903), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25965), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26341), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26482))
- Fix trend calculation working on too many items at a time ([Gargron](https://github.com/mastodon/mastodon/pull/25835))
- Fix dropdowns being disabled for logged out users in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25964))
- Fix explore page being inaccessible when opted-out of trends in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25716))
M app/chewy/instances_index.rb => app/chewy/instances_index.rb +1 -1
@@ 6,7 6,7 @@ class InstancesIndex < Chewy::Index
index_scope ::Instance.searchable
root date_detection: false do
- field :domain, type: 'text', index_prefixes: { min_chars: 1 }
+ field :domain, type: 'text', index_prefixes: { min_chars: 1, max_chars: 5 }
field :accounts_count, type: 'long'
end
end
R app/controllers/api/v1/profiles_controller.rb => app/controllers/api/v1/profile/avatars_controller.rb +2 -18
@@ 1,29 1,13 @@
# frozen_string_literal: true
-class Api::V1::ProfilesController < Api::BaseController
+class Api::V1::Profile::AvatarsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user!
- before_action :set_image
- before_action :validate_image_param
def destroy
@account = current_account
- UpdateAccountService.new.call(@account, { @image => nil }, raise_error: true)
+ UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end
-
- private
-
- def set_image
- @image = params[:image]
- end
-
- def validate_image_param
- raise(Mastodon::InvalidParameterError, 'Image must be either "avatar" or "header"') unless valid_image?
- end
-
- def valid_image?
- %w(avatar header).include?(@image)
- end
end
A => +13 -0
@@ 0,0 1,13 @@
# frozen_string_literal: true
class Api::V1::Profile::HeadersController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user!
def destroy
@account = current_account
UpdateAccountService.new.call(@account, { header: nil }, raise_error: true)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end
end
A app/javascript/mastodon/components/__tests__/hashtag_bar.tsx => app/javascript/mastodon/components/__tests__/hashtag_bar.tsx +184 -0
@@ 0,0 1,184 @@
+import { fromJS } from 'immutable';
+
+import type { StatusLike } from '../hashtag_bar';
+import { computeHashtagBarForStatus } from '../hashtag_bar';
+
+function createStatus(
+ content: string,
+ hashtags: string[],
+ hasMedia = false,
+ spoilerText?: string,
+) {
+ return fromJS({
+ tags: hashtags.map((name) => ({ name })),
+ contentHtml: content,
+ media_attachments: hasMedia ? ['fakeMedia'] : [],
+ spoiler_text: spoilerText,
+ }) as unknown as StatusLike; // need to force the type here, as it is not properly defined
+}
+
+describe('computeHashtagBarForStatus', () => {
+ it('does nothing when there are no tags', () => {
+ const status = createStatus('<p>Simple text</p>', []);
+
+ const { hashtagsInBar, statusContentProps } =
+ computeHashtagBarForStatus(status);
+
+ expect(hashtagsInBar).toEqual([]);
+ expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+ `"<p>Simple text</p>"`,
+ );
+ });
+
+ it('displays out of band hashtags in the bar', () => {
+ const status = createStatus(
+ '<p>Simple text <a href="test">#hashtag</a></p>',
+ ['hashtag', 'test'],
+ );
+
+ const { hashtagsInBar, statusContentProps } =
+ computeHashtagBarForStatus(status);
+
+ expect(hashtagsInBar).toEqual(['test']);
+ expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+ `"<p>Simple text <a href="test">#hashtag</a></p>"`,
+ );
+ });
+
+ it('extract tags from the last line', () => {
+ const status = createStatus(
+ '<p>Simple text</p><p><a href="test">#hashtag</a></p>',
+ ['hashtag'],
+ );
+
+ const { hashtagsInBar, statusContentProps } =
+ computeHashtagBarForStatus(status);
+
+ expect(hashtagsInBar).toEqual(['hashtag']);
+ expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+ `"<p>Simple text</p>"`,
+ );
+ });
+
+ it('does not include tags from content', () => {
+ const status = createStatus(
+ '<p>Simple text with a <a href="test">#hashtag</a></p><p><a href="test">#hashtag</a></p>',
+ ['hashtag'],
+ );
+
+ const { hashtagsInBar, statusContentProps } =
+ computeHashtagBarForStatus(status);
+
+ expect(hashtagsInBar).toEqual([]);
+ expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+ `"<p>Simple text with a <a href="test">#hashtag</a></p>"`,
+ );
+ });
+
+ it('works with one line status and hashtags', () => {
+ const status = createStatus(
+ '<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>',
+ ['hashtag', 'test'],
+ );
+
+ const { hashtagsInBar, statusContentProps } =
+ computeHashtagBarForStatus(status);
+
+ expect(hashtagsInBar).toEqual([]);
+ expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+ `"<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>"`,
+ );
+ });
+
+ it('de-duplicate accentuated characters with case differences', () => {
+ const status = createStatus(
+ '<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
+ ['éaa'],
+ );
+
+ 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>',
+ ['éaa'],
+ );
+
+ const { hashtagsInBar, statusContentProps } =
+ computeHashtagBarForStatus(status);
+
+ expect(hashtagsInBar).toEqual([]);
+ expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+ `"<p>Text <a href="test">#Éaa</a></p>"`,
+ );
+ });
+
+ it('does not modify a status with a line of hashtags only', () => {
+ const status = createStatus(
+ '<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
+ ['test', 'hashtag'],
+ );
+
+ const { hashtagsInBar, statusContentProps } =
+ computeHashtagBarForStatus(status);
+
+ expect(hashtagsInBar).toEqual([]);
+ expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+ `"<p><a href="test">#test</a> <a href="test">#hashtag</a></p>"`,
+ );
+ });
+
+ it('puts the hashtags in the bar if a status content has hashtags in the only line and has a media', () => {
+ const status = createStatus(
+ '<p>This is my content! <a href="test">#hashtag</a></p>',
+ ['hashtag'],
+ true,
+ );
+
+ const { hashtagsInBar, statusContentProps } =
+ computeHashtagBarForStatus(status);
+
+ expect(hashtagsInBar).toEqual([]);
+ expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+ `"<p>This is my content! <a href="test">#hashtag</a></p>"`,
+ );
+ });
+
+ it('puts the hashtags in the bar if a status content is only hashtags and has a media', () => {
+ const status = createStatus(
+ '<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
+ ['test', 'hashtag'],
+ true,
+ );
+
+ const { hashtagsInBar, statusContentProps } =
+ computeHashtagBarForStatus(status);
+
+ expect(hashtagsInBar).toEqual(['test', 'hashtag']);
+ expect(statusContentProps.statusContent).toMatchInlineSnapshot(`""`);
+ });
+
+ it('does not use the hashtag bar if the status content is only hashtags, has a CW and a media', () => {
+ const status = createStatus(
+ '<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
+ ['test', 'hashtag'],
+ true,
+ 'My CW text',
+ );
+
+ const { hashtagsInBar, statusContentProps } =
+ computeHashtagBarForStatus(status);
+
+ expect(hashtagsInBar).toEqual([]);
+ expect(statusContentProps.statusContent).toMatchInlineSnapshot(
+ `"<p><a href="test">#test</a> <a href="test">#hashtag</a></p>"`,
+ );
+ });
+});
D app/javascript/mastodon/components/hashtag_bar.jsx => app/javascript/mastodon/components/hashtag_bar.jsx +0 -50
@@ 1,50 0,0 @@
-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,
-};
A app/javascript/mastodon/components/hashtag_bar.tsx => app/javascript/mastodon/components/hashtag_bar.tsx +222 -0
@@ 0,0 1,222 @@
+import { useState, useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import type { List, Record } from 'immutable';
+
+import { groupBy, minBy } from 'lodash';
+
+import { getStatusContent } from './status_content';
+
+// About two lines on desktop
+const VISIBLE_HASHTAGS = 7;
+
+// Those types are not correct, they need to be replaced once this part of the state is typed
+export type TagLike = Record<{ name: string }>;
+export type StatusLike = Record<{
+ tags: List<TagLike>;
+ contentHTML: string;
+ media_attachments: List<unknown>;
+ spoiler_text?: string;
+}>;
+
+function normalizeHashtag(hashtag: string) {
+ if (hashtag && hashtag.startsWith('#')) return hashtag.slice(1);
+ else return hashtag;
+}
+
+function isNodeLinkHashtag(element: Node): element is HTMLLinkElement {
+ return (
+ element instanceof HTMLAnchorElement &&
+ // it may be a <a> starting with a hashtag
+ (element.textContent?.[0] === '#' ||
+ // or a #<a>
+ element.previousSibling?.textContent?.[
+ element.previousSibling.textContent.length - 1
+ ] === '#')
+ );
+}
+
+/**
+ * Removes duplicates from an hashtag list, case-insensitive, keeping only the best one
+ * "Best" here is defined by the one with the more casing difference (ie, the most camel-cased one)
+ * @param hashtags The list of hashtags
+ * @returns The input hashtags, but with only 1 occurence of each (case-insensitive)
+ */
+function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
+ const groups = groupBy(hashtags, (tag) =>
+ tag.normalize('NFKD').toLowerCase(),
+ );
+
+ return Object.values(groups).map((tags) => {
+ if (tags.length === 1) return tags[0];
+
+ // The best match is the one where we have the less difference between upper and lower case letter count
+ const best = minBy(tags, (tag) => {
+ const upperCase = Array.from(tag).reduce(
+ (acc, char) => (acc += char.toUpperCase() === char ? 1 : 0),
+ 0,
+ );
+
+ const lowerCase = tag.length - upperCase;
+
+ return Math.abs(lowerCase - upperCase);
+ });
+
+ return best ?? tags[0];
+ });
+}
+
+// Create the collator once, this is much more efficient
+const collator = new Intl.Collator(undefined, { sensitivity: 'accent' });
+function localeAwareInclude(collection: string[], value: string) {
+ return collection.find((item) => collator.compare(item, value) === 0);
+}
+
+// We use an intermediate function here to make it easier to test
+export function computeHashtagBarForStatus(status: StatusLike): {
+ statusContentProps: { statusContent: string };
+ hashtagsInBar: string[];
+} {
+ let statusContent = getStatusContent(status);
+
+ const tagNames = status
+ .get('tags')
+ .map((tag) => tag.get('name'))
+ .toJS();
+
+ // this is returned if we stop the processing early, it does not change what is displayed
+ const defaultResult = {
+ statusContentProps: { statusContent },
+ hashtagsInBar: [],
+ };
+
+ // return early if this status does not have any tags
+ if (tagNames.length === 0) return defaultResult;
+
+ const template = document.createElement('template');
+ template.innerHTML = statusContent.trim();
+
+ const lastChild = template.content.lastChild;
+
+ if (!lastChild) return defaultResult;
+
+ template.content.removeChild(lastChild);
+ const contentWithoutLastLine = template;
+
+ // First, try to parse
+ const contentHashtags = Array.from(
+ contentWithoutLastLine.content.querySelectorAll<HTMLLinkElement>('a[href]'),
+ ).reduce<string[]>((result, link) => {
+ if (isNodeLinkHashtag(link)) {
+ if (link.textContent) result.push(normalizeHashtag(link.textContent));
+ }
+ return result;
+ }, []);
+
+ // Now we parse the last line, and try to see if it only contains hashtags
+ const lastLineHashtags: string[] = [];
+ // try to see if the last line is only hashtags
+ let onlyHashtags = true;
+
+ Array.from(lastChild.childNodes).forEach((node) => {
+ if (isNodeLinkHashtag(node) && node.textContent) {
+ const normalized = normalizeHashtag(node.textContent);
+
+ if (!localeAwareInclude(tagNames, normalized)) {
+ // stop here, this is not a real hashtag, so consider it as text
+ onlyHashtags = false;
+ return;
+ }
+
+ if (!localeAwareInclude(contentHashtags, normalized))
+ // only add it if it does not appear in the rest of the content
+ lastLineHashtags.push(normalized);
+ } else if (node.nodeType !== Node.TEXT_NODE || node.nodeValue?.trim()) {
+ // not a space
+ onlyHashtags = false;
+ }
+ });
+
+ 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 isOnlyOneLine = contentWithoutLastLine.content.childElementCount === 0;
+ const hasMedia = status.get('media_attachments').size > 0;
+ const hasSpoiler = !!status.get('spoiler_text');
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- due to https://github.com/microsoft/TypeScript/issues/9998
+ if (onlyHashtags && ((hasMedia && !hasSpoiler) || !isOnlyOneLine)) {
+ // if the last line only contains hashtags, and we either:
+ // - have other content in the status
+ // - dont have other content, but a media and no CW. If it has a CW, then we do not remove the content to avoid having an empty content behind the CW button
+ statusContent = contentWithoutLastLine.innerHTML;
+ // and add the tags to the bar
+ hashtagsInBar.push(...lastLineHashtags);
+ }
+
+ return {
+ statusContentProps: { statusContent },
+ hashtagsInBar: uniqueHashtagsWithCaseHandling(hashtagsInBar),
+ };
+}
+
+/**
+ * This function will process a status to, at the same time (avoiding parsing it twice):
+ * - build the HashtagBar for this status
+ * - remove the last-line hashtags from the status content
+ * @param status The status to process
+ * @returns Props to be passed to the <StatusContent> component, and the hashtagBar to render
+ */
+export function getHashtagBarForStatus(status: StatusLike) {
+ const { statusContentProps, hashtagsInBar } =
+ computeHashtagBarForStatus(status);
+
+ return {
+ statusContentProps,
+ hashtagBar: <HashtagBar hashtags={hashtagsInBar} />,
+ };
+}
+
+const HashtagBar: React.FC<{
+ hashtags: string[];
+}> = ({ hashtags }) => {
+ const [expanded, setExpanded] = useState(false);
+ const handleClick = useCallback(() => {
+ setExpanded(true);
+ }, []);
+
+ if (hashtags.length === 0) {
+ return null;
+ }
+
+ const revealedHashtags = expanded
+ ? hashtags
+ : hashtags.slice(0, VISIBLE_HASHTAGS - 1);
+
+ return (
+ <div className='hashtag-bar'>
+ {revealedHashtags.map((hashtag) => (
+ <Link key={hashtag} to={`/tags/${hashtag}`}>
+ #{hashtag}
+ </Link>
+ ))}
+
+ {!expanded && hashtags.length > VISIBLE_HASHTAGS && (
+ <button className='link-button' onClick={handleClick}>
+ <FormattedMessage
+ id='hashtags.and_other'
+ defaultMessage='…and {count, plural, other {# more}}'
+ values={{ count: hashtags.length - VISIBLE_HASHTAGS }}
+ />
+ </button>
+ )}
+ </div>
+ );
+};
M app/javascript/mastodon/components/status.jsx => app/javascript/mastodon/components/status.jsx +5 -2
@@ 22,7 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 { getHashtagBarForStatus } from './hashtag_bar';
import { RelativeTimestamp } from './relative_timestamp';
import StatusActionBar from './status_action_bar';
import StatusContent from './status_content';
@@ 545,6 545,8 @@ class Status extends ImmutablePureComponent {
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
+ const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
+
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
@@ 577,11 579,12 @@ class Status extends ImmutablePureComponent {
onTranslate={this.handleTranslate}
collapsible
onCollapsedToggle={this.handleCollapsedToggle}
+ {...statusContentProps}
/>
{media}
- <HashtagBar hashtags={status.get('tags')} text={status.get('content')} />
+ {hashtagBar}
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
</div>
M app/javascript/mastodon/components/status_content.jsx => app/javascript/mastodon/components/status_content.jsx +12 -2
@@ 15,6 15,15 @@ import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_s
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
+/**
+ *
+ * @param {any} status
+ * @returns {string}
+ */
+export function getStatusContent(status) {
+ return status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
+}
+
class TranslateButton extends PureComponent {
static propTypes = {
@@ 65,6 74,7 @@ class StatusContent extends PureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
+ statusContent: PropTypes.string,
expanded: PropTypes.bool,
onExpandedToggle: PropTypes.func,
onTranslate: PropTypes.func,
@@ 225,7 235,7 @@ class StatusContent extends PureComponent {
};
render () {
- const { status, intl } = this.props;
+ const { status, intl, statusContent } = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed');
@@ 233,7 243,7 @@ class StatusContent extends PureComponent {
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
- const content = { __html: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') };
+ const content = { __html: statusContent ?? getStatusContent(status) };
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', {
M app/javascript/mastodon/features/status/components/detailed_status.jsx => app/javascript/mastodon/features/status/components/detailed_status.jsx +5 -2
@@ 10,7 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 { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
@@ 292,6 292,8 @@ class DetailedStatus extends ImmutablePureComponent {
);
}
+ const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
+
return (
<div style={outerStyle}>
<div ref={this.setRef} className={classNames('detailed-status', { compact })}>
@@ 311,11 313,12 @@ class DetailedStatus extends ImmutablePureComponent {
expanded={!status.get('hidden')}
onExpandedToggle={this.handleExpandedToggle}
onTranslate={this.handleTranslate}
+ {...statusContentProps}
/>
{media}
- <HashtagBar hashtags={status.get('tags')} text={status.get('content')} />
+ {hashtagBar}
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
M app/lib/activitypub/activity/create.rb => app/lib/activitypub/activity/create.rb +2 -0
@@ 4,6 4,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
include FormattingHelper
def perform
+ @account.schedule_refresh_if_stale!
+
dereference_object!
case @object['type']
M app/lib/activitypub/activity/update.rb => app/lib/activitypub/activity/update.rb +2 -0
@@ 2,6 2,8 @@
class ActivityPub::Activity::Update < ActivityPub::Activity
def perform
+ @account.schedule_refresh_if_stale!
+
dereference_object!
if equals_or_includes_any?(@object['type'], %w(Application Group Organization Person Service))
M app/lib/admin/system_check/elasticsearch_check.rb => app/lib/admin/system_check/elasticsearch_check.rb +58 -3
@@ 1,6 1,13 @@
# frozen_string_literal: true
class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
+ INDEXES = [
+ InstancesIndex,
+ AccountsIndex,
+ TagsIndex,
+ StatusesIndex,
+ ].freeze
+
def skip?
!current_user.can?(:view_devops)
end
@@ 8,11 15,15 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
def pass?
return true unless Chewy.enabled?
- running_version.present? && compatible_version?
+ running_version.present? && compatible_version? && cluster_health['status'] == 'green' && indexes_match? && preset_matches?
+ rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error
+ false
end
def message
- if running_version.present?
+ if running_version.blank?
+ Admin::SystemCheck::Message.new(:elasticsearch_running_check)
+ elsif !compatible_version?
Admin::SystemCheck::Message.new(
:elasticsearch_version_check,
I18n.t(
@@ 21,13 32,32 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
required_version: required_version
)
)
+ elsif !indexes_match?
+ Admin::SystemCheck::Message.new(
+ :elasticsearch_index_mismatch,
+ mismatched_indexes.join(' ')
+ )
+ elsif cluster_health['status'] == 'red'
+ 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'
+ Admin::SystemCheck::Message.new(:elasticsearch_reset_chewy)
+ elsif cluster_health['status'] == 'yellow'
+ Admin::SystemCheck::Message.new(:elasticsearch_health_yellow)
else
- Admin::SystemCheck::Message.new(:elasticsearch_running_check)
+ Admin::SystemCheck::Message.new(:elasticsearch_preset, nil, 'https://docs.joinmastodon.org/admin/optional/elasticsearch/#scaling')
end
+ rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error
+ Admin::SystemCheck::Message.new(:elasticsearch_running_check)
end
private
+ def cluster_health
+ @cluster_health ||= Chewy.client.cluster.health
+ end
+
def running_version
@running_version ||= begin
Chewy.client.info['version']['number']
@@ 49,5 79,30 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
Gem::Version.new(running_version) >= Gem::Version.new(required_version) ||
Gem::Version.new(compatible_wire_version) >= Gem::Version.new(required_version)
+ rescue ArgumentError
+ false
+ end
+
+ def mismatched_indexes
+ @mismatched_indexes ||= INDEXES.filter_map do |klass|
+ klass.index_name if Chewy.client.indices.get_mapping[klass.index_name]&.deep_symbolize_keys != klass.mappings_hash
+ end
+ end
+
+ def indexes_match?
+ mismatched_indexes.empty?
+ end
+
+ def es_preset
+ ENV.fetch('ES_PRESET', 'single_node_cluster')
+ end
+
+ def preset_matches?
+ case es_preset
+ when 'single_node_cluster'
+ cluster_health['number_of_nodes'] == 1
+ else
+ cluster_health['number_of_nodes'] > 1
+ end
end
end
M app/models/account.rb => app/models/account.rb +8 -0
@@ 63,6 63,8 @@ class Account < ApplicationRecord
trust_level
)
+ BACKGROUND_REFRESH_INTERVAL = 1.week.freeze
+
USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i
MENTION_RE = %r{(?<=^|[^/[:word:]])@((#{USERNAME_RE})(?:@[[:word:].-]+[[:word:]]+)?)}i
URL_PREFIX_RE = %r{\Ahttp(s?)://[^/]+}
@@ 213,6 215,12 @@ class Account < ApplicationRecord
last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago
end
+ def schedule_refresh_if_stale!
+ return unless last_webfingered_at.present? && last_webfingered_at <= BACKGROUND_REFRESH_INTERVAL.ago
+
+ AccountRefreshWorker.perform_in(rand(6.hours.to_i), id)
+ end
+
def refresh!
ResolveAccountService.new.call(acct) unless local?
end
A app/workers/account_refresh_worker.rb => app/workers/account_refresh_worker.rb +14 -0
@@ 0,0 1,14 @@
+# frozen_string_literal: true
+
+class AccountRefreshWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: 'pull', retry: 3, dead: false, lock: :until_executed, lock_ttl: 1.day.to_i
+
+ def perform(account_id)
+ account = Account.find_by(id: account_id)
+ return if account.nil? || account.last_webfingered_at > Account::BACKGROUND_REFRESH_INTERVAL.ago
+
+ ResolveAccountService.new.call(account)
+ end
+end
M config/locales/en.yml => config/locales/en.yml +14 -0
@@ 814,6 814,20 @@ en:
system_checks:
database_schema_check:
message_html: There are pending database migrations. Please run them to ensure the application behaves as expected
+ elasticsearch_health_red:
+ message_html: Elasticsearch cluster is unhealthy (red status), search features are unavailable
+ elasticsearch_health_yellow:
+ message_html: Elasticsearch cluster is unhealthy (yellow status), you may want to investigate the reason
+ elasticsearch_index_mismatch:
+ message_html: Elasticsearch index mappings are outdated. Please run <code>tootctl search deploy --only=%{value}</code>
+ elasticsearch_preset:
+ action: See documentation
+ message_html: Your Elasticsearch cluster has more than one node, but Mastodon is not configured to use them.
+ elasticsearch_preset_single_node:
+ action: See documentation
+ message_html: Your Elasticsearch cluster has only one node, <code>ES_PRESET</code> should be set to <code>single_node_cluster</code>.
+ elasticsearch_reset_chewy:
+ message_html: Your Elasticsearch system index is outdated due to a setting change. Please run <code>tootctl search deploy --reset-chewy</code> to update it.
elasticsearch_running_check:
message_html: Could not connect to Elasticsearch. Please check that it is running, or disable full-text search
elasticsearch_version_check:
M config/routes/api.rb => config/routes/api.rb +5 -1
@@ 96,7 96,11 @@ namespace :api, format: false do
resources :filters, only: [:index, :create, :show, :update, :destroy]
resources :endorsements, only: [:index]
resources :markers, only: [:index, :create]
- resources :profile, only: :destroy, param: :image, controller: 'profiles'
+
+ namespace :profile do
+ resource :avatar, only: :destroy
+ resource :header, only: :destroy
+ end
namespace :apps do
get :verify_credentials, to: 'credentials#show'
M lib/mastodon/version.rb => lib/mastodon/version.rb +1 -1
@@ 17,7 17,7 @@ module Mastodon
end
def flags
- ENV.fetch('MASTODON_VERSION_FLAGS', '-beta1')
+ ENV.fetch('MASTODON_VERSION_FLAGS', '-beta2')
end
def suffix
M spec/lib/admin/system_check/elasticsearch_check_spec.rb => spec/lib/admin/system_check/elasticsearch_check_spec.rb +32 -3
@@ 11,7 11,25 @@ describe Admin::SystemCheck::ElasticsearchCheck do
describe 'pass?' do
context 'when chewy is enabled' do
- before { allow(Chewy).to receive(:enabled?).and_return(true) }
+ before do
+ allow(Chewy).to receive(:enabled?).and_return(true)
+ allow(Chewy.client.cluster).to receive(:health).and_return({ 'status' => 'green', 'number_of_nodes' => 1 })
+ allow(Chewy.client.indices).to receive(:get_mapping).and_return({
+ AccountsIndex.index_name => AccountsIndex.mappings_hash.deep_stringify_keys,
+ StatusesIndex.index_name => StatusesIndex.mappings_hash.deep_stringify_keys,
+ InstancesIndex.index_name => InstancesIndex.mappings_hash.deep_stringify_keys,
+ TagsIndex.index_name => TagsIndex.mappings_hash.deep_stringify_keys,
+ })
+ allow(Chewy.client.indices).to receive(:get_settings).and_return({
+ 'chewy_specifications' => {
+ 'settings' => {
+ 'index' => {
+ 'number_of_replicas' => 0,
+ },
+ },
+ },
+ })
+ end
context 'when running version is present and high enough' do
before do
@@ 67,8 85,19 @@ describe Admin::SystemCheck::ElasticsearchCheck do
end
describe 'message' do
+ before do
+ allow(Chewy).to receive(:enabled?).and_return(true)
+ allow(Chewy.client.cluster).to receive(:health).and_return({ 'status' => 'green', 'number_of_nodes' => 1 })
+ allow(Chewy.client.indices).to receive(:get_mapping).and_return({
+ AccountsIndex.index_name => AccountsIndex.mappings_hash.deep_stringify_keys,
+ StatusesIndex.index_name => StatusesIndex.mappings_hash.deep_stringify_keys,
+ InstancesIndex.index_name => InstancesIndex.mappings_hash.deep_stringify_keys,
+ TagsIndex.index_name => TagsIndex.mappings_hash.deep_stringify_keys,
+ })
+ end
+
context 'when running version is present' do
- before { allow(Chewy.client).to receive(:info).and_return({ 'version' => { 'number' => '999.99.9' } }) }
+ before { allow(Chewy.client).to receive(:info).and_return({ 'version' => { 'number' => '1.2.3' } }) }
it 'sends class name symbol to message instance' do
allow(Admin::SystemCheck::Message).to receive(:new)
@@ 77,7 106,7 @@ describe Admin::SystemCheck::ElasticsearchCheck do
check.message
expect(Admin::SystemCheck::Message).to have_received(:new)
- .with(:elasticsearch_version_check, 'Elasticsearch 999.99.9 is running while 7.x is required')
+ .with(:elasticsearch_version_check, 'Elasticsearch 1.2.3 is running while 7.x is required')
end
end
M spec/requests/api/v1/profiles_spec.rb => spec/requests/api/v1/profiles_spec.rb +0 -14
@@ 94,19 94,5 @@ RSpec.describe 'Deleting profile images' do
expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id)
end
end
-
- context 'when provided picture value is invalid' do
- it 'returns http bad request' do
- delete '/api/v1/profile/invalid', headers: headers
-
- expect(response).to have_http_status(400)
- end
-
- it 'does not queue up an account update distribution' do
- delete '/api/v1/profile/invalid', headers: headers
-
- expect(ActivityPub::UpdateDistributionWorker).to_not have_received(:perform_async).with(account.id)
- end
- end
end
end