diff --git a/app/javascript/mastodon/features/compose/redesign/hints.tsx b/app/javascript/mastodon/features/compose/redesign/hints.tsx
new file mode 100644
index 00000000000..60ae65b6f7a
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/redesign/hints.tsx
@@ -0,0 +1,145 @@
+import type React from 'react';
+import { useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { WarningIcon } from '@phosphor-icons/react';
+
+import { changeComposeLanguage } from '@/mastodon/actions/compose';
+import { Callout } from '@/mastodon/components/callout/redesign';
+import { useDismissible } from '@/mastodon/hooks/useDismissible';
+import { selectAccountStatus } from '@/mastodon/selectors/statuses';
+import {
+ createAppSelector,
+ useAppDispatch,
+ useAppSelector,
+} from '@/mastodon/store';
+
+import { useLanguageGuess, useLanguages } from './hooks';
+import { selectComposeAttachments } from './selectors';
+
+const selectComposeAttachmentsWithoutAlt = createAppSelector(
+ [selectComposeAttachments],
+ (attachments) => ({
+ count: attachments.length,
+ missingAlt: attachments.some((attachment) => !attachment.description),
+ }),
+);
+
+const selectIsFollowersReply = createAppSelector(
+ [
+ (state) =>
+ selectAccountStatus(
+ state,
+ state.compose.get('in_reply_to') as null | string,
+ ),
+ ],
+ (status) => (status?.visibility === 'private' ? status.account.acct : null),
+);
+
+export const ComposeHints = () => {
+ const { count, missingAlt } = useAppSelector(
+ selectComposeAttachmentsWithoutAlt,
+ );
+ const replyFollowersHandle = useAppSelector(selectIsFollowersReply);
+
+ const lang = useAppSelector(
+ (state) => state.compose.get('language') as string,
+ );
+ const guess = useLanguageGuess();
+ const isDifferentLanguage = lang && guess && lang !== guess;
+
+ const messages: React.ReactNode[] = [];
+
+ if (replyFollowersHandle) {
+ messages.push(
+ defaultWrapper(
+ ,
+ ),
+ );
+ }
+
+ if (isDifferentLanguage) {
+ messages.push();
+ }
+
+ if (missingAlt && count === 1) {
+ messages.push(
+ defaultWrapper(
+ ,
+ ),
+ );
+ } else if (missingAlt) {
+ messages.push(
+ defaultWrapper(
+ ,
+ ),
+ );
+ }
+
+ if (messages.length === 0) {
+ return null;
+ }
+
+ return
{messages}
;
+};
+
+const defaultWrapper = (children: React.ReactNode) => (
+ {children}
+);
+
+const LanguageHint: React.FC<{ guess: string }> = ({ guess }) => {
+ const languages = useLanguages();
+ const language = languages.find(([lang]) => lang === guess);
+
+ const { wasDismissed, dismiss } = useDismissible('compose_language_hint');
+
+ const dispatch = useAppDispatch();
+ const handleChange = useCallback(() => {
+ dispatch(changeComposeLanguage(guess));
+ }, [dispatch, guess]);
+
+ if (!language || wasDismissed) {
+ return null;
+ }
+
+ return (
+
+ }
+ secondaryActionClick={dismiss}
+ secondaryActionText={
+
+ }
+ >
+
+
+ );
+};
diff --git a/app/javascript/mastodon/features/compose/redesign/hooks.ts b/app/javascript/mastodon/features/compose/redesign/hooks.ts
new file mode 100644
index 00000000000..d3c1ce14f06
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/redesign/hooks.ts
@@ -0,0 +1,41 @@
+import { useEffect, useState } from 'react';
+
+import type { InitialStateLanguage } from '@/mastodon/initial_state';
+import { languages } from '@/mastodon/initial_state';
+import { useAppSelector } from '@/mastodon/store';
+
+const emptyArray: InitialStateLanguage[] = [];
+
+export function useLanguages() {
+ return languages ?? emptyArray;
+}
+
+export function useLanguageGuess() {
+ const text = useAppSelector((state) => state.compose.get('text') as string);
+ const [guess, setGuess] = useState('');
+
+ useEffect(() => {
+ void import('../util/language_detection').then(({ debouncedGuess }) => {
+ if (text.length > 20) {
+ debouncedGuess(text, setGuess);
+ } else {
+ debouncedGuess.cancel();
+ }
+ });
+ }, [text]);
+
+ // Keeping track of the previous render's text length here
+ // to be able to reset the guess when the text length drops
+ // below the threshold needed to make a guess
+ const isLongText = text.length > 20;
+ const [wasLongText, setWasLongText] = useState(() => isLongText);
+ if (wasLongText !== isLongText) {
+ setWasLongText(isLongText);
+
+ if (wasLongText) {
+ setGuess('');
+ }
+ }
+
+ return guess;
+}
diff --git a/app/javascript/mastodon/features/compose/redesign/index.tsx b/app/javascript/mastodon/features/compose/redesign/index.tsx
index 1aad10855aa..8095836a841 100644
--- a/app/javascript/mastodon/features/compose/redesign/index.tsx
+++ b/app/javascript/mastodon/features/compose/redesign/index.tsx
@@ -30,6 +30,7 @@ import { ComposeAttachments } from './attachments';
import type { OnEmojiPick } from './emoji';
import { ComposeFooter } from './footer';
import { ComposeFormHeader } from './header';
+import { ComposeHints } from './hints';
import { LanguageButton } from './language';
import { ComposeReply } from './reply';
import {
@@ -140,6 +141,8 @@ export const RedesignComposeForm: React.FC = ({
+
+
);
diff --git a/app/javascript/mastodon/features/compose/redesign/language.tsx b/app/javascript/mastodon/features/compose/redesign/language.tsx
index 374b49f4809..ab57a404572 100644
--- a/app/javascript/mastodon/features/compose/redesign/language.tsx
+++ b/app/javascript/mastodon/features/compose/redesign/language.tsx
@@ -1,5 +1,5 @@
import type React from 'react';
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useCallback, useRef, useState } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -12,6 +12,7 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { LanguageDropdownMenu } from '../components/language_dropdown';
+import { useLanguageGuess } from './hooks';
import classes from './styles.module.scss';
export const LanguageButton: React.FC = () => {
@@ -96,33 +97,3 @@ export const LanguageDropdown: React.FC<{ onClose: () => void }> = ({
/>
);
};
-
-function useLanguageGuess() {
- const text = useAppSelector((state) => state.compose.get('text') as string);
- const [guess, setGuess] = useState('');
-
- useEffect(() => {
- void import('../util/language_detection').then(({ debouncedGuess }) => {
- if (text.length > 20) {
- debouncedGuess(text, setGuess);
- } else {
- debouncedGuess.cancel();
- }
- });
- }, [text]);
-
- // Keeping track of the previous render's text length here
- // to be able to reset the guess when the text length drops
- // below the threshold needed to make a guess
- const isLongText = text.length > 20;
- const [wasLongText, setWasLongText] = useState(() => isLongText);
- if (wasLongText !== isLongText) {
- setWasLongText(isLongText);
-
- if (wasLongText) {
- setGuess('');
- }
- }
-
- return guess;
-}
diff --git a/app/javascript/mastodon/initial_state.ts b/app/javascript/mastodon/initial_state.ts
index 29bad1d6729..d452ad88614 100644
--- a/app/javascript/mastodon/initial_state.ts
+++ b/app/javascript/mastodon/initial_state.ts
@@ -1,7 +1,11 @@
import type { ApiAnnualReportState } from './api/annual_report';
import type { ApiAccountJSON } from './api_types/accounts';
-type InitialStateLanguage = [code: string, name: string, localName: string];
+export type InitialStateLanguage = [
+ code: string,
+ name: string,
+ localName: string,
+];
interface InitialStateMeta {
access_token: string;
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index e88f0c730d2..544c131701e 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -494,6 +494,12 @@
"compose.discoverable": "Discoverable in public feeds & search results",
"compose.error.blank_post": "Post can't be blank.",
"compose.expand": "Show composer",
+ "compose.hints.followers-reply": "You're replying to a followers-only post. People not following {user} might see your reply without the context of what you’re replying to.",
+ "compose.hints.language": "Change this post’s language to {language}?",
+ "compose.hints.language.change": "Change",
+ "compose.hints.language.dismiss": "Dismiss",
+ "compose.hints.missing-alt": "Your attachment is missing alt text.",
+ "compose.hints.missing-alt-many": "One or more of your attachments are missing alt text.",
"compose.language.change": "Change language",
"compose.language.search": "Search languages...",
"compose.message.notice": "Messages are not end-to-end encrypted",