diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml
index e13d227bd..51bde39bc 100644
--- a/.github/workflows/lint-css.yml
+++ b/.github/workflows/lint-css.yml
@@ -48,4 +48,4 @@ jobs:
- run: echo "::add-matcher::.github/stylelint-matcher.json"
- name: Stylelint
- run: yarn test:lint:sass
+ run: yarn lint:sass
diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml
index 7700e4851..547035ab3 100644
--- a/.github/workflows/lint-js.yml
+++ b/.github/workflows/lint-js.yml
@@ -48,7 +48,7 @@ jobs:
run: yarn --frozen-lockfile
- name: ESLint
- run: yarn test:lint:js --max-warnings 0
+ run: yarn lint:js --max-warnings 0
- name: Typecheck
- run: yarn test:typecheck
+ run: yarn typecheck
diff --git a/.github/workflows/lint-json.yml b/.github/workflows/lint-json.yml
index 98f101ad9..7dfc0e058 100644
--- a/.github/workflows/lint-json.yml
+++ b/.github/workflows/lint-json.yml
@@ -40,4 +40,4 @@ jobs:
run: yarn --frozen-lockfile
- name: Prettier
- run: yarn prettier --check "**/*.json"
+ run: yarn lint:json
diff --git a/.github/workflows/lint-md.yml b/.github/workflows/lint-md.yml
index 6f76dd60c..b489ce968 100644
--- a/.github/workflows/lint-md.yml
+++ b/.github/workflows/lint-md.yml
@@ -5,6 +5,7 @@ on:
- 'dependabot/**'
paths:
- '.github/workflows/lint-md.yml'
+ - '.nvmrc'
- '.prettier*'
- '**/*.md'
- '!AUTHORS.md'
@@ -14,6 +15,7 @@ on:
pull_request:
paths:
- '.github/workflows/lint-md.yml'
+ - '.nvmrc'
- '.prettier*'
- '**/*.md'
- '!AUTHORS.md'
@@ -32,9 +34,10 @@ jobs:
uses: actions/setup-node@v3
with:
cache: yarn
+ node-version-file: '.nvmrc'
- name: Install all yarn packages
run: yarn --frozen-lockfile
- name: Prettier
- run: yarn prettier --check "**/*.md"
+ run: yarn lint:md
diff --git a/.github/workflows/lint-yml.yml b/.github/workflows/lint-yml.yml
index 6f79babcf..d77451ee6 100644
--- a/.github/workflows/lint-yml.yml
+++ b/.github/workflows/lint-yml.yml
@@ -42,4 +42,4 @@ jobs:
run: yarn --frozen-lockfile
- name: Prettier
- run: yarn prettier --check "**/*.{yml,yaml}"
+ run: yarn lint:yml
diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml
index 1c4958550..32e21d23c 100644
--- a/.github/workflows/test-js.yml
+++ b/.github/workflows/test-js.yml
@@ -44,4 +44,4 @@ jobs:
run: yarn --frozen-lockfile
- name: Jest testing
- run: yarn test:jest --reporters github-actions summary
+ run: yarn jest --reporters github-actions summary
diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb
index 9034e8a2f..c55500f76 100644
--- a/app/controllers/api/v1/conversations_controller.rb
+++ b/app/controllers/api/v1/conversations_controller.rb
@@ -11,7 +11,7 @@ class Api::V1::ConversationsController < Api::BaseController
def index
@conversations = paginated_conversations
- render json: @conversations, each_serializer: REST::ConversationSerializer
+ render json: @conversations, each_serializer: REST::ConversationSerializer, relationships: StatusRelationshipsPresenter.new(@conversations.map(&:last_status), current_user&.account_id)
end
def read
@@ -32,7 +32,20 @@ class Api::V1::ConversationsController < Api::BaseController
def paginated_conversations
AccountConversation.where(account: current_account)
- .to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
+ .includes(
+ account: :account_stat,
+ last_status: [
+ :media_attachments,
+ :preview_cards,
+ :status_stat,
+ :tags,
+ {
+ active_mentions: [account: :account_stat],
+ account: :account_stat,
+ },
+ ]
+ )
+ .to_a_paginated_by_id(limit_param(LIMIT), **params_slice(:max_id, :since_id, :min_id))
end
def insert_pagination_headers
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 014e3a3f9..3148756b7 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -170,11 +170,11 @@ module ApplicationHelper
end
def storage_host
- URI::HTTPS.build(host: storage_host_name).to_s
+ "https://#{storage_host_var}"
end
def storage_host?
- storage_host_name.present?
+ storage_host_var.present?
end
def quote_wrap(text, line_width: 80, break_sequence: "\n")
@@ -235,7 +235,7 @@ module ApplicationHelper
private
- def storage_host_name
+ def storage_host_var
ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil)
end
end
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index 61062fd2c..3232e12a2 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -6,7 +6,7 @@ import { unescapeHTML } from '../../utils/html';
const domParser = new DOMParser();
-const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
+const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji;
return obj;
}, {});
@@ -20,7 +20,7 @@ export function searchTextFromRawStatus (status) {
export function normalizeAccount(account) {
account = { ...account };
- const emojiMap = makeEmojiMap(account);
+ const emojiMap = makeEmojiMap(account.emojis);
const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
@@ -86,7 +86,7 @@ export function normalizeStatus(status, normalOldStatus) {
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/
/g, '\n').replace(/<\/p>
/g, '\n\n');
- const emojiMap = makeEmojiMap(normalStatus);
+ const emojiMap = makeEmojiMap(normalStatus.emojis);
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
@@ -97,22 +97,48 @@ export function normalizeStatus(status, normalOldStatus) {
return normalStatus;
}
+export function normalizeStatusTranslation(translation, status) {
+ const emojiMap = makeEmojiMap(status.get('emojis').toJS());
+
+ const normalTranslation = {
+ detected_source_language: translation.detected_source_language,
+ language: translation.language,
+ provider: translation.provider,
+ contentHtml: emojify(translation.content, emojiMap),
+ spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
+ spoiler_text: translation.spoiler_text,
+ };
+
+ return normalTranslation;
+}
+
export function normalizePoll(poll) {
const normalPoll = { ...poll };
- const emojiMap = makeEmojiMap(normalPoll);
+ const emojiMap = makeEmojiMap(poll.emojis);
normalPoll.options = poll.options.map((option, index) => ({
...option,
voted: poll.own_votes && poll.own_votes.includes(index),
- title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
+ titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
}));
return normalPoll;
}
+export function normalizePollOptionTranslation(translation, poll) {
+ const emojiMap = makeEmojiMap(poll.get('emojis').toJS());
+
+ const normalTranslation = {
+ ...translation,
+ titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap),
+ };
+
+ return normalTranslation;
+}
+
export function normalizeAnnouncement(announcement) {
const normalAnnouncement = { ...announcement };
- const emojiMap = makeEmojiMap(normalAnnouncement);
+ const emojiMap = makeEmojiMap.emojis(normalAnnouncement);
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 84a1271b8..3aed80735 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -343,7 +343,8 @@ export const translateStatusFail = (id, error) => ({
error,
});
-export const undoStatusTranslation = id => ({
+export const undoStatusTranslation = (id, pollId) => ({
type: STATUS_TRANSLATE_UNDO,
id,
+ pollId,
});
diff --git a/app/javascript/mastodon/components/media_attachments.jsx b/app/javascript/mastodon/components/media_attachments.jsx
index d2f171243..7b945a0ea 100644
--- a/app/javascript/mastodon/components/media_attachments.jsx
+++ b/app/javascript/mastodon/components/media_attachments.jsx
@@ -51,8 +51,9 @@ export default class MediaAttachments extends ImmutablePureComponent {
};
render () {
- const { status, lang, width, height } = this.props;
+ const { status, width, height } = this.props;
const mediaAttachments = status.get('media_attachments');
+ const language = status.getIn(['language', 'translation']) || status.get('language') || this.props.lang;
if (mediaAttachments.size === 0) {
return null;
@@ -60,14 +61,15 @@ export default class MediaAttachments extends ImmutablePureComponent {
if (mediaAttachments.getIn([0, 'type']) === 'audio') {
const audio = mediaAttachments.get(0);
+ const description = audio.getIn(['translation', 'description']) || audio.get('description');
return (
{Component => (
@@ -90,8 +93,8 @@ export default class MediaAttachments extends ImmutablePureComponent {
frameRate={video.getIn(['meta', 'original', 'frame_rate'])}
blurhash={video.get('blurhash')}
src={video.get('url')}
- alt={video.get('description')}
- lang={lang || status.get('language')}
+ alt={description}
+ lang={language}
width={width}
height={height}
inline
@@ -107,7 +110,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
{Component => (
ALT);
}
+ const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
+
if (attachment.get('type') === 'unknown') {
return (
-
+
@@ -182,7 +184,7 @@ class Poll extends ImmutablePureComponent {
{!!voted &&
diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx
index 3f3c292ea..8f188a638 100644
--- a/app/javascript/mastodon/components/status.jsx
+++ b/app/javascript/mastodon/components/status.jsx
@@ -27,12 +27,18 @@ import { RelativeTimestamp } from './relative_timestamp';
import StatusActionBar from './status_action_bar';
import StatusContent from './status_content';
+const domParser = new DOMParser();
+
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']);
+ const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text');
+ const contentHtml = status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
+ const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent;
+
const values = [
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
- status.get('spoiler_text') && status.get('hidden') ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length),
+ spoilerText && status.get('hidden') ? spoilerText : contentText,
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
status.getIn(['account', 'acct']),
];
@@ -199,12 +205,14 @@ class Status extends ImmutablePureComponent {
handleOpenVideo = (options) => {
const status = this._properStatus();
- this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), status.get('language'), options);
+ const lang = status.getIn(['translation', 'language']) || status.get('language');
+ this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, options);
};
handleOpenMedia = (media, index) => {
const status = this._properStatus();
- this.props.onOpenMedia(status.get('id'), media, index, status.get('language'));
+ const lang = status.getIn(['translation', 'language']) || status.get('language');
+ this.props.onOpenMedia(status.get('id'), media, index, lang);
};
handleHotkeyOpenMedia = e => {
@@ -214,7 +222,7 @@ class Status extends ImmutablePureComponent {
e.preventDefault();
if (status.get('media_attachments').size > 0) {
- const lang = status.get('language');
+ const lang = status.getIn(['translation', 'language']) || status.get('language');
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, { startTime: 0 });
} else {
@@ -420,6 +428,8 @@ class Status extends ImmutablePureComponent {
if (pictureInPicture.get('inUse')) {
media = ;
} else if (status.get('media_attachments').size > 0) {
+ const language = status.getIn(['translation', 'language']) || status.get('language');
+
if (this.props.muted) {
media = (
{Component => (
@@ -465,8 +477,8 @@ class Status extends ImmutablePureComponent {
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
- alt={attachment.get('description')}
- lang={status.get('language')}
+ alt={description}
+ lang={language}
inline
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
@@ -483,7 +495,7 @@ class Status extends ImmutablePureComponent {
{Component => (
0 && targetLanguages?.includes(contentLocale);
+ 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.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
- const spoilerContent = { __html: status.get('spoilerHtml') };
- const lang = status.get('translation') ? intl.locale : status.get('language');
+ const content = { __html: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') };
+ const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
+ const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
@@ -253,7 +253,7 @@ class StatusContent extends PureComponent {
);
const poll = !!status.get('poll') && (
-
+
);
if (status.get('spoiler_text').length > 0) {
@@ -274,24 +274,24 @@ class StatusContent extends PureComponent {
return (
-
+
{' '}
{mentionsPlaceholder}
-
+
{!hidden && poll}
- {!hidden && translateButton}
+ {translateButton}
);
} else if (this.props.onClick) {
return (
<>
-
+
{poll}
{translateButton}
@@ -303,7 +303,7 @@ class StatusContent extends PureComponent {
} else {
return (
-
+
{poll}
{translateButton}
diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx
index 3026dde0a..6167b404f 100644
--- a/app/javascript/mastodon/containers/status_container.jsx
+++ b/app/javascript/mastodon/containers/status_container.jsx
@@ -180,7 +180,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
onTranslate (status) {
if (status.get('translation')) {
- dispatch(undoStatusTranslation(status.get('id')));
+ dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
} else {
dispatch(translateStatus(status.get('id')));
}
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx
index 187e04ad1..83a566710 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.jsx
+++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx
@@ -133,17 +133,20 @@ class DetailedStatus extends ImmutablePureComponent {
outerStyle.height = `${this.state.height}px`;
}
+ const language = status.getIn(['translation', 'language']) || status.get('language');
+
if (pictureInPicture.get('inUse')) {
media =
;
} else if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
+ const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
media = (