Profile: Support auto translation in channels (#5891)

Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
Co-authored-by: Dmitry Kabanov <dmitrykabanovdev@gmail.com>
This commit is contained in:
Alexander Zinchuk 2025-06-18 17:40:58 +02:00
parent aea46b1796
commit 9b82953426
21 changed files with 184 additions and 64 deletions

View File

@ -94,6 +94,7 @@ export interface GramJsAppConfig extends LimitsConfig {
freeze_since_date?: number; freeze_since_date?: number;
freeze_until_date?: number; freeze_until_date?: number;
freeze_appeal_url?: string; freeze_appeal_url?: string;
channel_autotranslation_level_min?: number;
stars_stargift_resale_amount_max?: number; stars_stargift_resale_amount_max?: number;
stars_stargift_resale_amount_min?: number; stars_stargift_resale_amount_min?: number;
stars_stargift_resale_commission_permille?: number; stars_stargift_resale_commission_permille?: number;
@ -182,6 +183,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
bandwidthPremiumUploadSpeedup: appConfig.upload_premium_speedup_upload, bandwidthPremiumUploadSpeedup: appConfig.upload_premium_speedup_upload,
bandwidthPremiumDownloadSpeedup: appConfig.upload_premium_speedup_download, bandwidthPremiumDownloadSpeedup: appConfig.upload_premium_speedup_download,
channelRestrictAdsLevelMin: appConfig.channel_restrict_sponsored_level_min, channelRestrictAdsLevelMin: appConfig.channel_restrict_sponsored_level_min,
channelAutoTranslationLevelMin: appConfig.channel_autotranslation_level_min,
paidReactionMaxAmount: appConfig.stars_paid_reaction_amount_max, paidReactionMaxAmount: appConfig.stars_paid_reaction_amount_max,
isChannelRevenueWithdrawalEnabled: appConfig.channel_revenue_withdrawal_enabled, isChannelRevenueWithdrawalEnabled: appConfig.channel_revenue_withdrawal_enabled,
isStarsGiftEnabled: appConfig.stars_gifts_enabled, isStarsGiftEnabled: appConfig.stars_gifts_enabled,

View File

@ -119,6 +119,8 @@ function buildApiChatFieldsFromPeerEntity(
hasGeo: channel?.hasGeo, hasGeo: channel?.hasGeo,
subscriptionUntil: channel?.subscriptionUntilDate, subscriptionUntil: channel?.subscriptionUntilDate,
paidMessagesStars: paidMessagesStars?.toJSNumber(), paidMessagesStars: paidMessagesStars?.toJSNumber(),
level: channel?.level,
hasAutoTranslation: channel?.autotranslation,
...buildApiChatPermissions(peerEntity), ...buildApiChatPermissions(peerEntity),
...buildApiChatRestrictions(peerEntity), ...buildApiChatRestrictions(peerEntity),

View File

@ -2087,3 +2087,16 @@ export async function fetchSponsoredPeer({ query }: { query: string }) {
if (!result || result instanceof GramJs.contacts.SponsoredPeersEmpty) return undefined; if (!result || result instanceof GramJs.contacts.SponsoredPeersEmpty) return undefined;
return buildApiSponsoredPeer(result.peers[0]); return buildApiSponsoredPeer(result.peers[0]);
} }
export function toggleAutoTranslation({
chat, isEnabled,
}: {
chat: ApiChat; isEnabled: boolean;
}) {
return invokeRequest(new GramJs.channels.ToggleAutotranslation({
channel: buildInputChannel(chat.id, chat.accessHash),
enabled: isEnabled,
}), {
shouldReturnTrue: true,
});
}

View File

@ -49,6 +49,8 @@ export interface ApiChat {
isForumAsMessages?: true; isForumAsMessages?: true;
boostLevel?: number; boostLevel?: number;
botVerificationIconId?: string; botVerificationIconId?: string;
hasAutoTranslation?: true;
level?: number;
// Calls // Calls
isCallActive?: boolean; isCallActive?: boolean;

View File

@ -235,6 +235,7 @@ export interface ApiAppConfig {
bandwidthPremiumUploadSpeedup?: number; bandwidthPremiumUploadSpeedup?: number;
bandwidthPremiumDownloadSpeedup?: number; bandwidthPremiumDownloadSpeedup?: number;
channelRestrictAdsLevelMin?: number; channelRestrictAdsLevelMin?: number;
channelAutoTranslationLevelMin?: number;
paidReactionMaxAmount?: number; paidReactionMaxAmount?: number;
isChannelRevenueWithdrawalEnabled?: boolean; isChannelRevenueWithdrawalEnabled?: boolean;
isStarsGiftEnabled?: boolean; isStarsGiftEnabled?: boolean;

View File

@ -2007,4 +2007,5 @@
"ValueGiftSortByPrice" = "Price"; "ValueGiftSortByPrice" = "Price";
"ValueGiftSortByNumber" = "Number"; "ValueGiftSortByNumber" = "Number";
"ResellGiftsNoFound" = "No gifts found"; "ResellGiftsNoFound" = "No gifts found";
"ResellGiftsClearFilters" = "Clear Filters"; "ResellGiftsClearFilters" = "Clear Filters";
"AutomaticTranslation" = "Automatic Translation";

View File

@ -1,6 +1,6 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import { import {
memo, useMemo, useRef, useState, memo, useCallback, useMemo, useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
@ -123,8 +123,8 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
language, language,
detectedChatLanguage, detectedChatLanguage,
doNotTranslate, doNotTranslate,
onTopicSearch,
isAccountFrozen, isAccountFrozen,
onTopicSearch,
}) => { }) => {
const { const {
joinChannel, joinChannel,
@ -239,7 +239,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
handleSearchClick(); handleSearchClick();
}); });
const getTextWithLanguage = useLastCallback((langKey: string, langCode: string) => { const getTextWithLanguage = useCallback((langKey: string, langCode: string) => {
const simplified = langCode.split('-')[0]; const simplified = langCode.split('-')[0];
const translationKey = `TranslateLanguage${simplified.toUpperCase()}`; const translationKey = `TranslateLanguage${simplified.toUpperCase()}`;
const name = lang(translationKey); const name = lang(translationKey);
@ -250,7 +250,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
const translatedNames = new Intl.DisplayNames([language], { type: 'language' }); const translatedNames = new Intl.DisplayNames([language], { type: 'language' });
const translatedName = translatedNames.of(langCode)!; const translatedName = translatedNames.of(langCode)!;
return lang(`${langKey}Other`, translatedName); return lang(`${langKey}Other`, translatedName);
}); }, [language, lang]);
const buttonText = useMemo(() => { const buttonText = useMemo(() => {
if (isTranslating) return lang('ShowOriginalButton'); if (isTranslating) return lang('ShowOriginalButton');

View File

@ -32,6 +32,7 @@ import {
} from '../../global/helpers'; } from '../../global/helpers';
import { import {
selectBot, selectBot,
selectCanTranslateChat,
selectChat, selectChat,
selectChatFullInfo, selectChatFullInfo,
selectChatLastMessage, selectChatLastMessage,
@ -52,6 +53,7 @@ import {
selectTabState, selectTabState,
selectThreadInfo, selectThreadInfo,
selectTopic, selectTopic,
selectTranslationLanguage,
selectUserFullInfo, selectUserFullInfo,
} from '../../global/selectors'; } from '../../global/selectors';
import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll'; import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll';
@ -92,13 +94,13 @@ type OwnProps = {
isComments?: boolean; isComments?: boolean;
canPost: boolean; canPost: boolean;
isReady: boolean; isReady: boolean;
onScrollDownToggle: BooleanToVoidFunction;
onNotchToggle: BooleanToVoidFunction;
withBottomShift?: boolean; withBottomShift?: boolean;
withDefaultBg: boolean; withDefaultBg: boolean;
onIntersectPinnedMessage: OnIntersectPinnedMessage;
isContactRequirePremium?: boolean; isContactRequirePremium?: boolean;
paidMessagesStars?: number; paidMessagesStars?: number;
onScrollDownToggle: BooleanToVoidFunction;
onNotchToggle: BooleanToVoidFunction;
onIntersectPinnedMessage: OnIntersectPinnedMessage;
}; };
type StateProps = { type StateProps = {
@ -137,6 +139,9 @@ type StateProps = {
isChatProtected?: boolean; isChatProtected?: boolean;
hasCustomGreeting?: boolean; hasCustomGreeting?: boolean;
isAppConfigLoaded?: boolean; isAppConfigLoaded?: boolean;
canTranslate?: boolean;
translationLanguage?: string;
shouldAutoTranslate?: boolean;
}; };
const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000; const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000;
@ -196,16 +201,19 @@ const MessageList: FC<OwnProps & StateProps> = ({
areAdsEnabled, areAdsEnabled,
channelJoinInfo, channelJoinInfo,
isChatProtected, isChatProtected,
onIntersectPinnedMessage,
onScrollDownToggle,
onNotchToggle,
isAccountFrozen, isAccountFrozen,
hasCustomGreeting, hasCustomGreeting,
isAppConfigLoaded, isAppConfigLoaded,
canTranslate,
translationLanguage,
shouldAutoTranslate,
onIntersectPinnedMessage,
onScrollDownToggle,
onNotchToggle,
}) => { }) => {
const { const {
loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds, loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds,
loadMessageViews, loadPeerStoriesByIds, loadFactChecks, loadMessageViews, loadPeerStoriesByIds, loadFactChecks, requestChatTranslation,
} = getActions(); } = getActions();
const containerRef = useRef<HTMLDivElement>(); const containerRef = useRef<HTMLDivElement>();
@ -268,6 +276,12 @@ const MessageList: FC<OwnProps & StateProps> = ({
memoFocusingIdRef.current = focusingId; memoFocusingIdRef.current = focusingId;
}, [focusingId]); }, [focusingId]);
// Enable auto translation for the chat if it's available
useEffect(() => {
if (!shouldAutoTranslate || !canTranslate) return;
requestChatTranslation({ chatId, toLanguageCode: translationLanguage });
}, [shouldAutoTranslate, canTranslate, translationLanguage, chatId]);
useNativeCopySelectedMessages(copyMessagesByIds); useNativeCopySelectedMessages(copyMessagesByIds);
const messageGroups = useMemo(() => { const messageGroups = useMemo(() => {
@ -806,6 +820,10 @@ export default memo(withGlobal<OwnProps>(
const hasCustomGreeting = Boolean(userFullInfo?.businessIntro); const hasCustomGreeting = Boolean(userFullInfo?.businessIntro);
const isAppConfigLoaded = global.isAppConfigLoaded; const isAppConfigLoaded = global.isAppConfigLoaded;
const canTranslate = selectCanTranslateChat(global, chatId) && !chatFullInfo?.isTranslationDisabled;
const shouldAutoTranslate = chat?.hasAutoTranslation;
const translationLanguage = selectTranslationLanguage(global);
return { return {
areAdsEnabled, areAdsEnabled,
isChatLoaded: true, isChatLoaded: true,
@ -842,6 +860,9 @@ export default memo(withGlobal<OwnProps>(
isAccountFrozen, isAccountFrozen,
hasCustomGreeting, hasCustomGreeting,
isAppConfigLoaded, isAppConfigLoaded,
canTranslate,
translationLanguage,
shouldAutoTranslate,
}; };
}, },
)(MessageList)); )(MessageList));

View File

@ -747,7 +747,7 @@ const Message: FC<OwnProps & StateProps> = ({
const detectedLanguage = useTextLanguage( const detectedLanguage = useTextLanguage(
text?.text, text?.text,
!(areTranslationsEnabled || shouldDetectChatLanguage), !(areTranslationsEnabled && shouldDetectChatLanguage),
getIsMessageListReady, getIsMessageListReady,
); );
useDetectChatLanguage(message, detectedLanguage, !shouldDetectChatLanguage, getIsMessageListReady); useDetectChatLanguage(message, detectedLanguage, !shouldDetectChatLanguage, getIsMessageListReady);

View File

@ -30,13 +30,25 @@ export default function useDetectChatLanguage(
if (isDisabled || (getIsReady && !getIsReady())) return; if (isDisabled || (getIsReady && !getIsReady())) return;
const isTranslatable = Boolean(message.content.text?.text.length); const isTranslatable = Boolean(message.content.text?.text.length);
processMessageMetadata(message.chatId, message.id, isTranslatable, detectedLanguage); processMessageMetadata({
chatId: message.chatId,
id: message.id,
isTranslatable,
detectedLanguage,
});
}, [message, detectedLanguage, isDisabled, getIsReady]); }, [message, detectedLanguage, isDisabled, getIsReady]);
} }
const throttledMakeChatDecision = throttle(makeChatDecision, THROTTLE_DELAY); const throttledMakeChatDecision = throttle(makeChatDecision, THROTTLE_DELAY);
function processMessageMetadata(chatId: string, id: number, isTranslatable: boolean, detectedLanguage?: string) { function processMessageMetadata({
chatId, id, isTranslatable, detectedLanguage,
}: {
chatId: string;
id: number;
isTranslatable: boolean;
detectedLanguage?: string;
}) {
const chatStats = CHAT_STATS.get(chatId) || new LimitedMap<number, MessageMetadata>(MESSAGES_LIMIT); const chatStats = CHAT_STATS.get(chatId) || new LimitedMap<number, MessageMetadata>(MESSAGES_LIMIT);
const previousMetadata = chatStats.get(id); const previousMetadata = chatStats.get(id);
@ -90,7 +102,7 @@ function makeChatDecision(chatId: string) {
} }
const translatableRatio = translatableCount / messagesChecked; const translatableRatio = translatableCount / messagesChecked;
const detectableRatio = detectableCount / messagesChecked; const detectableRatio = detectableCount / translatableCount;
if (translatableRatio < MIN_TRANSLATABLE_RATIO || detectableRatio < MIN_DETECTABLE_RATIO) { if (translatableRatio < MIN_TRANSLATABLE_RATIO || detectableRatio < MIN_DETECTABLE_RATIO) {
return; return;

View File

@ -1,7 +1,7 @@
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import { import {
memo, useCallback, useEffect, useMemo, useState, memo, useEffect, useMemo, useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
@ -11,12 +11,13 @@ import type {
import { ApiMediaFormat } from '../../../api/types'; import { ApiMediaFormat } from '../../../api/types';
import { ManagementProgress, ManagementScreens } from '../../../types'; import { ManagementProgress, ManagementScreens } from '../../../types';
import { getChatAvatarHash, getHasAdminRight, isChatPublic } from '../../../global/helpers'; import { getChatAvatarHash, getHasAdminRight, isChatChannel, isChatPublic } from '../../../global/helpers';
import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors'; import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors';
import { formatInteger } from '../../../util/textFormat'; import { formatInteger } from '../../../util/textFormat';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
@ -27,6 +28,7 @@ import FloatingActionButton from '../../ui/FloatingActionButton';
import InputText from '../../ui/InputText'; import InputText from '../../ui/InputText';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import Spinner from '../../ui/Spinner'; import Spinner from '../../ui/Spinner';
import Switcher from '../../ui/Switcher';
import TextArea from '../../ui/TextArea'; import TextArea from '../../ui/TextArea';
import './Management.scss'; import './Management.scss';
@ -46,6 +48,8 @@ type StateProps = {
canInvite?: boolean; canInvite?: boolean;
exportedInvites?: ApiExportedInvite[]; exportedInvites?: ApiExportedInvite[];
availableReactions?: ApiAvailableReaction[]; availableReactions?: ApiAvailableReaction[];
hasAutoTranslation?: boolean;
canToggleAutoTranslation?: boolean;
}; };
const CHANNEL_TITLE_EMPTY = 'Channel title can\'t be empty'; const CHANNEL_TITLE_EMPTY = 'Channel title can\'t be empty';
@ -63,6 +67,8 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
availableReactions, availableReactions,
onScreenSelect, onScreenSelect,
onClose, onClose,
hasAutoTranslation,
canToggleAutoTranslation,
}) => { }) => {
const { const {
updateChat, updateChat,
@ -72,6 +78,7 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
openChat, openChat,
loadExportedChatInvites, loadExportedChatInvites,
loadChatJoinRequests, loadChatJoinRequests,
toggleAutoTranslation,
} = getActions(); } = getActions();
const currentTitle = chat?.title || ''; const currentTitle = chat?.title || '';
@ -88,6 +95,12 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
const currentAvatarBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl); const currentAvatarBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl);
const lang = useOldLang(); const lang = useOldLang();
const hasAutoTranslationAvailable = chat && isChatChannel(chat);
const handleAutoTranslationChange = useLastCallback(() => {
toggleAutoTranslation({ chatId, isEnabled: !hasAutoTranslation });
});
useHistoryBack({ useHistoryBack({
isActive, isActive,
onBack: onClose, onBack: onClose,
@ -112,46 +125,46 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
}, [chatFullInfo?.adminMembersById]); }, [chatFullInfo?.adminMembersById]);
const removedUsersCount = chatFullInfo?.kickedMembers?.length || 0; const removedUsersCount = chatFullInfo?.kickedMembers?.length || 0;
const handleClickEditType = useCallback(() => { const handleClickEditType = useLastCallback(() => {
onScreenSelect(ManagementScreens.ChatPrivacyType); onScreenSelect(ManagementScreens.ChatPrivacyType);
}, [onScreenSelect]); });
const handleClickDiscussion = useCallback(() => { const handleClickDiscussion = useLastCallback(() => {
onScreenSelect(ManagementScreens.Discussion); onScreenSelect(ManagementScreens.Discussion);
}, [onScreenSelect]); });
const handleClickReactions = useCallback(() => { const handleClickReactions = useLastCallback(() => {
onScreenSelect(ManagementScreens.Reactions); onScreenSelect(ManagementScreens.Reactions);
}, [onScreenSelect]); });
const handleClickAdministrators = useCallback(() => { const handleClickAdministrators = useLastCallback(() => {
onScreenSelect(ManagementScreens.ChatAdministrators); onScreenSelect(ManagementScreens.ChatAdministrators);
}, [onScreenSelect]); });
const handleClickInvites = useCallback(() => { const handleClickInvites = useLastCallback(() => {
onScreenSelect(ManagementScreens.Invites); onScreenSelect(ManagementScreens.Invites);
}, [onScreenSelect]); });
const handleClickRequests = useCallback(() => { const handleClickRequests = useLastCallback(() => {
onScreenSelect(ManagementScreens.JoinRequests); onScreenSelect(ManagementScreens.JoinRequests);
}, [onScreenSelect]); });
const handleSetPhoto = useCallback((file: File) => { const handleSetPhoto = useLastCallback((file: File) => {
setPhoto(file); setPhoto(file);
setIsProfileFieldsTouched(true); setIsProfileFieldsTouched(true);
}, []); });
const handleTitleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const handleTitleChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value); setTitle(e.target.value);
setIsProfileFieldsTouched(true); setIsProfileFieldsTouched(true);
}, []); });
const handleAboutChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => { const handleAboutChange = useLastCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setAbout(e.target.value); setAbout(e.target.value);
setIsProfileFieldsTouched(true); setIsProfileFieldsTouched(true);
}, []); });
const handleUpdateChannel = useCallback(() => { const handleUpdateChannel = useLastCallback(() => {
const trimmedTitle = title.trim(); const trimmedTitle = title.trim();
const trimmedAbout = about.trim(); const trimmedAbout = about.trim();
@ -166,17 +179,17 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
about: trimmedAbout, about: trimmedAbout,
photo, photo,
}); });
}, [about, chatId, photo, title, updateChat]); });
const handleClickSubscribers = useCallback(() => { const handleClickSubscribers = useLastCallback(() => {
onScreenSelect(ManagementScreens.ChannelSubscribers); onScreenSelect(ManagementScreens.ChannelSubscribers);
}, [onScreenSelect]); });
const handleRemovedUsersClick = useCallback(() => { const handleRemovedUsersClick = useLastCallback(() => {
onScreenSelect(ManagementScreens.ChannelRemovedUsers); onScreenSelect(ManagementScreens.ChannelRemovedUsers);
}, [onScreenSelect]); });
const handleDeleteChannel = useCallback(() => { const handleDeleteChannel = useLastCallback(() => {
if (chat.isCreator) { if (chat.isCreator) {
deleteChannel({ chatId: chat.id }); deleteChannel({ chatId: chat.id });
} else { } else {
@ -186,7 +199,7 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
closeDeleteDialog(); closeDeleteDialog();
closeManagement(); closeManagement();
openChat({ id: undefined }); openChat({ id: undefined });
}, [chat.isCreator, chat.id, closeDeleteDialog, closeManagement, leaveChannel, deleteChannel, openChat]); });
const chatReactionsDescription = useMemo(() => { const chatReactionsDescription = useMemo(() => {
if (!chatFullInfo?.enabledReactions) { if (!chatFullInfo?.enabledReactions) {
@ -290,6 +303,22 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
{chatReactionsDescription} {chatReactionsDescription}
</span> </span>
</ListItem> </ListItem>
{hasAutoTranslationAvailable && (
<ListItem
icon="language"
narrow
ripple
disabled={!canToggleAutoTranslation}
onClick={handleAutoTranslationChange}
>
<span>{lang('AutomaticTranslation')}</span>
<Switcher
id="auto-translation"
label={lang('AutomaticTranslation')}
checked={hasAutoTranslation}
/>
</ListItem>
)}
</div> </div>
<div className="section"> <div className="section">
<ListItem <ListItem
@ -353,7 +382,11 @@ export default memo(withGlobal<OwnProps>(
const { management } = selectTabState(global); const { management } = selectTabState(global);
const { progress } = management; const { progress } = management;
const { invites } = management.byChatId[chatId] || {}; const { invites } = management.byChatId[chatId] || {};
const minLevelToToggleAutoTranslation = global.appConfig?.channelAutoTranslationLevelMin;
const hasAutoTranslation = chat?.hasAutoTranslation;
const chatBoostLevel = chat?.level;
const canToggleAutoTranslation = chatBoostLevel && minLevelToToggleAutoTranslation
? chatBoostLevel >= minLevelToToggleAutoTranslation : false;
return { return {
chat, chat,
chatFullInfo: selectChatFullInfo(global, chatId), chatFullInfo: selectChatFullInfo(global, chatId),
@ -362,6 +395,8 @@ export default memo(withGlobal<OwnProps>(
canInvite: getHasAdminRight(chat, 'inviteUsers'), canInvite: getHasAdminRight(chat, 'inviteUsers'),
exportedInvites: invites, exportedInvites: invites,
availableReactions: global.reactions.availableReactions, availableReactions: global.reactions.availableReactions,
hasAutoTranslation,
canToggleAutoTranslation,
}; };
}, },
(global, { chatId }) => { (global, { chatId }) => {

View File

@ -15,7 +15,7 @@ import renderText from '../../common/helpers/renderText';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
import useLastCallback from '../../../hooks/useLastCallback.ts'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview'; import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
@ -154,14 +154,14 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
if (isChatPublic(linkedGroup)) { if (isChatPublic(linkedGroup)) {
return renderText( return renderText(
`Do you want to make **${linkedGroup.title}** the discussion board for **${chat!.title}**?`, `Do you want to make **${linkedGroup.title}** the discussion board for **${chat?.title}**?`,
['br', 'simple_markdown'], ['br', 'simple_markdown'],
); );
} }
return renderText( return renderText(
// eslint-disable-next-line @stylistic/max-len // eslint-disable-next-line @stylistic/max-len
`Do you want to make **${linkedGroup.title}** the discussion board for **${chat!.title}**?\n\nAnyone from the channel will be able to see messages in this group.`, `Do you want to make **${linkedGroup.title}** the discussion board for **${chat?.title}**?\n\nAnyone from the channel will be able to see messages in this group.`,
['br', 'simple_markdown'], ['br', 'simple_markdown'],
); );
} }
@ -169,12 +169,14 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
function renderLinkedGroup() { function renderLinkedGroup() {
return ( return (
<div> <div>
<ListItem {linkedChat && (
className="chat-item-clickable" <ListItem
inactive className="chat-item-clickable"
> inactive
<GroupChatInfo chatId={linkedChat!.id} /> >
</ListItem> <GroupChatInfo chatId={linkedChat.id} />
</ListItem>
)}
<ListItem <ListItem
icon="delete" icon="delete"
ripple ripple
@ -188,7 +190,7 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
onClose={closeConfirmUnlinkGroupDialog} onClose={closeConfirmUnlinkGroupDialog}
header={renderUnlinkGroupHeader()} header={renderUnlinkGroupHeader()}
textParts={renderText( textParts={renderText(
lang(isChannel ? 'DiscussionUnlinkChannelAlert' : 'DiscussionUnlinkGroupAlert', linkedChat!.title), lang(isChannel ? 'DiscussionUnlinkChannelAlert' : 'DiscussionUnlinkGroupAlert', linkedChat?.title),
['br', 'simple_markdown'], ['br', 'simple_markdown'],
)} )}
confirmLabel={lang(isChannel ? 'DiscussionUnlinkGroup' : 'DiscussionUnlinkChannel')} confirmLabel={lang(isChannel ? 'DiscussionUnlinkGroup' : 'DiscussionUnlinkChannel')}

View File

@ -134,3 +134,7 @@
right: 0.5rem; right: 0.5rem;
transform: translate(0, -50%); transform: translate(0, -50%);
} }
.boostInfo {
margin: 0 -1rem;
}

View File

@ -226,8 +226,7 @@ const BoostStatistics = ({
return ( return (
<ListItem <ListItem
className="chat-item-clickable" className={buildClassName(styles.boostInfo, 'chat-item-clickable')}
onClick={() => handleBoosterClick(boost.userId)} onClick={() => handleBoosterClick(boost.userId)}
> >
<PrivateChatInfo <PrivateChatInfo

View File

@ -2984,6 +2984,19 @@ addActionHandler('updatePaidMessagesPrice', async (global, actions, payload): Pr
setGlobal(global); setGlobal(global);
}); });
addActionHandler('toggleAutoTranslation', async (global, actions, payload): Promise<void> => {
const { chatId, isEnabled } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const result = await callApi('toggleAutoTranslation', { chat, isEnabled });
if (!result) return;
global = getGlobal();
global = updateChat(global, chatId, { hasAutoTranslation: isEnabled || undefined });
setGlobal(global);
});
addActionHandler('resolveBusinessChatLink', async (global, actions, payload): Promise<void> => { addActionHandler('resolveBusinessChatLink', async (global, actions, payload): Promise<void> => {
const { slug, tabId = getCurrentTabId() } = payload; const { slug, tabId = getCurrentTabId() } = payload;
const result = await callApi('resolveBusinessChatLink', { slug }); const result = await callApi('resolveBusinessChatLink', { slug });

View File

@ -283,6 +283,9 @@ export function selectShouldDetectChatLanguage<T extends GlobalState>(
) { ) {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) return false; if (!chat) return false;
if (chat.hasAutoTranslation) return true;
const { canTranslateChats } = global.settings.byKey; const { canTranslateChats } = global.settings.byKey;
const isPremium = selectIsCurrentUserPremium(global); const isPremium = selectIsCurrentUserPremium(global);

View File

@ -1081,6 +1081,10 @@ export interface ActionPayloads {
chatId: string; chatId: string;
paidMessagesStars: number; paidMessagesStars: number;
} & WithTabId; } & WithTabId;
toggleAutoTranslation: {
chatId: string;
isEnabled: boolean;
} & WithTabId;
updateChat: { updateChat: {
chatId: string; chatId: string;

View File

@ -421,7 +421,7 @@ namespace Api {
export type TypeAccessPointRule = AccessPointRule; export type TypeAccessPointRule = AccessPointRule;
export type TypeTlsClientHello = TlsClientHello; export type TypeTlsClientHello = TlsClientHello;
export type TypeTlsBlock = TlsBlockString | TlsBlockRandom | TlsBlockZero | TlsBlockDomain | TlsBlockGrease | TlsBlockScope; export type TypeTlsBlock = TlsBlockString | TlsBlockRandom | TlsBlockZero | TlsBlockDomain | TlsBlockGrease | TlsBlockScope;
export namespace storage { export namespace storage {
export type TypeFileType = storage.FileUnknown | storage.FilePartial | storage.FileJpeg | storage.FileGif | storage.FilePng | storage.FilePdf | storage.FileMp3 | storage.FileMov | storage.FileMp4 | storage.FileWebp; export type TypeFileType = storage.FileUnknown | storage.FilePartial | storage.FileJpeg | storage.FileGif | storage.FilePng | storage.FilePdf | storage.FileMp3 | storage.FileMov | storage.FileMp4 | storage.FileWebp;
@ -17724,7 +17724,7 @@ namespace Api {
static fromReader(reader: Reader): TlsBlockScope; static fromReader(reader: Reader): TlsBlockScope;
} }
export namespace storage { export namespace storage {
export class FileUnknown extends VirtualClass<void> { export class FileUnknown extends VirtualClass<void> {
@ -22053,7 +22053,7 @@ namespace Api {
}>, Api.TypeDestroySessionRes> { }>, Api.TypeDestroySessionRes> {
sessionId: long; sessionId: long;
} }
export namespace auth { export namespace auth {
export class SendCode extends Request<Partial<{ export class SendCode extends Request<Partial<{

View File

@ -1727,6 +1727,7 @@ channels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool =
channels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel = messages.Chats; channels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel = messages.Chats;
channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
channels.updatePaidMessagesPrice#fc84653f channel:InputChannel send_paid_messages_stars:long = Updates; channels.updatePaidMessagesPrice#fc84653f channel:InputChannel send_paid_messages_stars:long = Updates;
channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Updates;
bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool; bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool;
bots.canSendMessage#1359f4e6 bot:InputUser = Bool; bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
bots.allowSendMessage#f132e3ef bot:InputUser = Updates; bots.allowSendMessage#f132e3ef bot:InputUser = Updates;
@ -1834,4 +1835,4 @@ premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:
premium.getMyBoosts#be77b4a = premium.MyBoosts; premium.getMyBoosts#be77b4a = premium.MyBoosts;
premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector<int> peer:InputPeer = premium.MyBoosts; premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector<int> peer:InputPeer = premium.MyBoosts;
premium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus; premium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus;
fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;`; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;`;

View File

@ -280,6 +280,7 @@
"channels.searchPosts", "channels.searchPosts",
"channels.reportSpam", "channels.reportSpam",
"channels.updatePaidMessagesPrice", "channels.updatePaidMessagesPrice",
"channels.toggleAutotranslation",
"bots.getBotRecommendations", "bots.getBotRecommendations",
"bots.canSendMessage", "bots.canSendMessage",
"bots.allowSendMessage", "bots.allowSendMessage",

View File

@ -1530,6 +1530,7 @@ export interface LangPair {
'ValueGiftSortByNumber': undefined; 'ValueGiftSortByNumber': undefined;
'ResellGiftsNoFound': undefined; 'ResellGiftsNoFound': undefined;
'ResellGiftsClearFilters': undefined; 'ResellGiftsClearFilters': undefined;
'AutomaticTranslation': undefined;
} }
export interface LangPairWithVariables<V = LangVariable> { export interface LangPairWithVariables<V = LangVariable> {
@ -1773,6 +1774,9 @@ export interface LangPairWithVariables<V = LangVariable> {
'VoipPeerIncompatible': { 'VoipPeerIncompatible': {
'user': V; 'user': V;
}; };
'NewDiscussionChatTitle': {
'name': V;
};
'LastSeenTodayAt': { 'LastSeenTodayAt': {
'time': V; 'time': V;
}; };
@ -2170,6 +2174,9 @@ export interface LangPairWithVariables<V = LangVariable> {
'from': V; 'from': V;
'title': V; 'title': V;
}; };
'ActionCreatedChatYou': {
'title': V;
};
'ActionPaymentDone': { 'ActionPaymentDone': {
'amount': V; 'amount': V;
'user': V; 'user': V;
@ -2488,9 +2495,6 @@ export interface LangPairWithVariables<V = LangVariable> {
'ComposerTitleForwardFrom': { 'ComposerTitleForwardFrom': {
'users': V; 'users': V;
}; };
'NewDiscussionChatTitle': {
'name': V;
}
} }
export interface LangPairPlural { export interface LangPairPlural {