Message List: Add ability to translate entire chats (#3464)
This commit is contained in:
parent
ef02a4a11d
commit
cdedb486f4
@ -16,7 +16,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
|
||||
fullUser: {
|
||||
about, commonChatsCount, pinnedMsgId, botInfo, blocked,
|
||||
profilePhoto, voiceMessagesForbidden, premiumGifts,
|
||||
fallbackPhoto, personalPhoto,
|
||||
fallbackPhoto, personalPhoto, translationsDisabled,
|
||||
},
|
||||
users,
|
||||
} = mtpUserFull;
|
||||
@ -29,6 +29,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
|
||||
pinnedMessageId: pinnedMsgId,
|
||||
isBlocked: Boolean(blocked),
|
||||
noVoiceMessages: voiceMessagesForbidden,
|
||||
isTranslationDisabled: translationsDisabled,
|
||||
...(profilePhoto instanceof GramJs.Photo && { profilePhoto: buildApiPhoto(profilePhoto) }),
|
||||
...(fallbackPhoto instanceof GramJs.Photo && { fallbackPhoto: buildApiPhoto(fallbackPhoto) }),
|
||||
...(personalPhoto instanceof GramJs.Photo && { personalPhoto: buildApiPhoto(personalPhoto) }),
|
||||
|
||||
@ -412,6 +412,7 @@ async function getFullChatInfo(chatId: string): Promise<FullChatData | undefined
|
||||
recentRequesters,
|
||||
requestsPending,
|
||||
chatPhoto,
|
||||
translationsDisabled,
|
||||
} = result.fullChat;
|
||||
|
||||
if (chatPhoto instanceof GramJs.Photo) {
|
||||
@ -437,6 +438,7 @@ async function getFullChatInfo(chatId: string): Promise<FullChatData | undefined
|
||||
enabledReactions: buildApiChatReactions(availableReactions),
|
||||
requestsPending,
|
||||
recentRequesterIds: recentRequesters?.map((userId) => buildApiPeerId(userId, 'user')),
|
||||
isTranslationDisabled: translationsDisabled,
|
||||
},
|
||||
users,
|
||||
userStatusesById,
|
||||
@ -490,6 +492,7 @@ async function getFullChannelInfo(
|
||||
stickerset,
|
||||
chatPhoto,
|
||||
participantsHidden,
|
||||
translationsDisabled,
|
||||
} = result.fullChat;
|
||||
|
||||
if (chatPhoto instanceof GramJs.Photo) {
|
||||
@ -563,6 +566,7 @@ async function getFullChannelInfo(
|
||||
statisticsDcId: statsDc,
|
||||
stickerSet: stickerset ? buildStickerSet(stickerset) : undefined,
|
||||
areParticipantsHidden: participantsHidden,
|
||||
isTranslationDisabled: translationsDisabled,
|
||||
},
|
||||
users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])],
|
||||
userStatusesById: statusesById,
|
||||
@ -1811,3 +1815,15 @@ export async function fetchChatlistInvites({
|
||||
chats: result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
export function togglePeerTranslations({
|
||||
chat, isEnabled,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
isEnabled: boolean;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.messages.TogglePeerTranslations({
|
||||
disabled: isEnabled ? undefined : true,
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
}));
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ export {
|
||||
getChatByPhoneNumber, toggleJoinToSend, toggleJoinRequest, fetchTopics, deleteTopic, togglePinnedTopic,
|
||||
editTopic, toggleForum, fetchTopicById, createTopic, toggleParticipantsHidden, checkChatlistInvite,
|
||||
joinChatlistInvite, createChalistInvite, editChatlistInvite, deleteChatlistInvite, fetchChatlistInvites,
|
||||
fetchLeaveChatlistSuggestions, leaveChatlist,
|
||||
fetchLeaveChatlistSuggestions, leaveChatlist, togglePeerTranslations,
|
||||
} from './chats';
|
||||
|
||||
export {
|
||||
|
||||
@ -76,6 +76,9 @@ export interface ApiChat {
|
||||
|
||||
unreadReactions?: number[];
|
||||
unreadMentions?: number[];
|
||||
|
||||
// Locally determined field
|
||||
detectedLanguage?: string;
|
||||
}
|
||||
|
||||
export interface ApiTypingStatus {
|
||||
@ -114,6 +117,7 @@ export interface ApiChatFullInfo {
|
||||
stickerSet?: ApiStickerSet;
|
||||
profilePhoto?: ApiPhoto;
|
||||
areParticipantsHidden?: boolean;
|
||||
isTranslationDisabled?: true;
|
||||
}
|
||||
|
||||
export interface ApiChatMember {
|
||||
|
||||
@ -42,6 +42,7 @@ export interface ApiUserFullInfo {
|
||||
personalPhoto?: ApiPhoto;
|
||||
noVoiceMessages?: boolean;
|
||||
premiumGifts?: ApiPremiumGiftOption[];
|
||||
isTranslationDisabled?: true;
|
||||
}
|
||||
|
||||
export type ApiFakeType = 'fake' | 'scam';
|
||||
|
||||
Binary file not shown.
Binary file not shown.
3
src/assets/premium/PremiumTranslate.svg
Normal file
3
src/assets/premium/PremiumTranslate.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.0988 19.1657L14.2578 17.3464L14.2843 17.3199C15.8244 15.6028 16.9219 13.6291 17.568 11.5403H19.2762C19.765 11.5403 20.1613 11.1441 20.1613 10.6552C20.1613 10.1664 19.765 9.77016 19.2762 9.77016H13.9657V8.88508C13.9657 8.39627 13.5694 8 13.0806 8C12.5918 8 12.1956 8.39627 12.1956 8.88508V9.77016H6.88065C6.3943 9.77016 6 10.1644 6 10.6508C6 11.1372 6.39428 11.5315 6.88065 11.5315H15.8863C15.2933 13.2397 14.3551 14.8594 13.0806 16.2755C12.4525 15.5798 11.9067 14.8325 11.4435 14.0494L11.283 13.7671C11.1272 13.4854 10.8306 13.3104 10.5086 13.3104H10.1511C9.8449 13.3104 9.59671 13.5586 9.59671 13.8648C9.59671 13.9526 9.6176 14.0392 9.65759 14.1173L9.73838 14.2719C10.3217 15.36 11.0462 16.393 11.9035 17.3464L8.0312 21.1654C7.71086 21.4814 7.6829 21.9818 7.94972 22.3299L8.02943 22.4205C8.37506 22.7661 8.93548 22.7661 9.28113 22.4205L13.0807 18.6209L15.1521 20.6923C15.3937 20.934 15.7856 20.934 16.0273 20.6923C16.0901 20.6295 16.1386 20.554 16.1696 20.4707L16.3061 20.1042C16.4278 19.7778 16.3466 19.4105 16.0989 19.1657L16.0988 19.1657ZM20.8755 15.0806H20.3321C19.9632 15.0806 19.6329 15.3095 19.5034 15.6549L16.0466 24.8729C15.9277 25.19 16.0884 25.5436 16.4056 25.6625C16.4744 25.6884 16.5474 25.7016 16.6209 25.7016H16.8917C17.2611 25.7016 17.5916 25.4722 17.7209 25.1261L18.4973 23.0463H22.7014L23.4857 25.1285C23.6156 25.4733 23.9455 25.7016 24.314 25.7016H24.5866C24.9254 25.7016 25.2 25.427 25.2 25.0882C25.2 25.0147 25.1868 24.9417 25.1609 24.8729L21.7042 15.6549C21.5747 15.3095 21.2444 15.0806 20.8755 15.0806H20.8755ZM20.6038 17.4438L22.0376 21.2762H19.17L20.6038 17.4438Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -29,7 +29,7 @@ export { default as MessageSelectToolbar } from '../components/middle/MessageSel
|
||||
export { default as SeenByModal } from '../components/common/SeenByModal';
|
||||
export { default as ReactorListModal } from '../components/middle/ReactorListModal';
|
||||
export { default as EmojiInteractionAnimation } from '../components/middle/EmojiInteractionAnimation';
|
||||
export { default as MessageLanguageModal } from '../components/middle/MessageLanguageModal';
|
||||
export { default as ChatLanguageModal } from '../components/middle/ChatLanguageModal';
|
||||
|
||||
export { default as LeftSearch } from '../components/left/search/LeftSearch';
|
||||
export { default as Settings } from '../components/left/settings/Settings';
|
||||
|
||||
@ -172,4 +172,12 @@
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.embed-loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { useRef } from '../../lib/teact/teact';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type {
|
||||
ApiUser, ApiMessage, ApiChat,
|
||||
} from '../../api/types';
|
||||
import type { ChatTranslatedMessages } from '../../global/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
|
||||
import {
|
||||
getMessageMediaHash,
|
||||
@ -12,21 +14,24 @@ import {
|
||||
getMessageRoundVideo,
|
||||
getUserColorKey,
|
||||
getMessageIsSpoiler,
|
||||
isMessageTranslatable,
|
||||
} from '../../global/helpers';
|
||||
import renderText from './helpers/renderText';
|
||||
import { getPictogramDimensions } from './helpers/mediaDimensions';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useThumbnail from '../../hooks/useThumbnail';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import { useFastClick } from '../../hooks/useFastClick';
|
||||
import useMessageTranslation from '../middle/message/hooks/useMessageTranslation';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
|
||||
import ActionMessage from '../middle/ActionMessage';
|
||||
import MessageSummary from './MessageSummary';
|
||||
import MediaSpoiler from './MediaSpoiler';
|
||||
import Skeleton from '../ui/Skeleton';
|
||||
|
||||
import './EmbeddedMessage.scss';
|
||||
|
||||
@ -39,6 +44,8 @@ type OwnProps = {
|
||||
noUserColors?: boolean;
|
||||
isProtected?: boolean;
|
||||
hasContextMenu?: boolean;
|
||||
chatTranslations?: ChatTranslatedMessages;
|
||||
requestedChatTranslationLanguage?: string;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onClick: NoneToVoidFunction;
|
||||
@ -55,6 +62,8 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
isProtected,
|
||||
noUserColors,
|
||||
hasContextMenu,
|
||||
chatTranslations,
|
||||
requestedChatTranslationLanguage,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onClick,
|
||||
@ -68,6 +77,16 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
const isRoundVideo = Boolean(message && getMessageRoundVideo(message));
|
||||
const isSpoiler = Boolean(message && getMessageIsSpoiler(message));
|
||||
|
||||
const shouldTranslate = message && isMessageTranslatable(message);
|
||||
const { isPending: isTranslationPending, translatedText } = useMessageTranslation(
|
||||
chatTranslations, message?.chatId, shouldTranslate ? message?.id : undefined, requestedChatTranslationLanguage,
|
||||
);
|
||||
|
||||
const {
|
||||
shouldRender: shouldRenderLoader,
|
||||
transitionClassNames,
|
||||
} = useShowTransition(isTranslationPending || (!message && !customText));
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const senderTitle = sender ? getSenderTitle(lang, sender) : message?.forwardInfo?.hiddenUserName;
|
||||
@ -85,6 +104,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
onClick={message && handleClick}
|
||||
onMouseDown={message && handleMouseDown}
|
||||
>
|
||||
{shouldRenderLoader && <Skeleton className={buildClassName('embed-loading', transitionClassNames)} />}
|
||||
{mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl, isRoundVideo, isProtected, isSpoiler)}
|
||||
<div className="message-text">
|
||||
<p dir="auto">
|
||||
@ -102,6 +122,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
lang={lang}
|
||||
message={message}
|
||||
noEmoji={Boolean(mediaThumbnail)}
|
||||
translatedText={translatedText}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
/>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { ApiFormattedText, ApiMessage } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { LangFn } from '../../hooks/useLang';
|
||||
|
||||
@ -20,6 +20,7 @@ import MessageText from './MessageText';
|
||||
interface OwnProps {
|
||||
lang: LangFn;
|
||||
message: ApiMessage;
|
||||
translatedText?: ApiFormattedText;
|
||||
noEmoji?: boolean;
|
||||
highlight?: string;
|
||||
truncateLength?: number;
|
||||
@ -33,6 +34,7 @@ interface OwnProps {
|
||||
function MessageSummary({
|
||||
lang,
|
||||
message,
|
||||
translatedText,
|
||||
noEmoji = false,
|
||||
highlight,
|
||||
truncateLength = TRUNCATED_SUMMARY_LENGTH,
|
||||
@ -47,7 +49,8 @@ function MessageSummary({
|
||||
const hasCustomEmoji = entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji);
|
||||
|
||||
if (!text || (!hasSpoilers && !hasCustomEmoji)) {
|
||||
const trimmedText = trimText(getMessageSummaryText(lang, message, noEmoji), truncateLength);
|
||||
const summaryText = translatedText?.text || getMessageSummaryText(lang, message, noEmoji);
|
||||
const trimmedText = trimText(summaryText, truncateLength);
|
||||
|
||||
return (
|
||||
<span>
|
||||
@ -64,6 +67,7 @@ function MessageSummary({
|
||||
return (
|
||||
<MessageText
|
||||
message={message}
|
||||
translatedText={translatedText}
|
||||
highlight={highlight}
|
||||
isSimple
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
.item {
|
||||
overflow: hidden;
|
||||
min-height: 25rem;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@ -14,4 +15,5 @@
|
||||
|
||||
.languages {
|
||||
overflow-y: auto;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useState,
|
||||
memo, useEffect, useMemo, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
@ -10,10 +10,12 @@ import type { ApiLanguage } from '../../../api/types';
|
||||
|
||||
import { setLanguage } from '../../../util/langProvider';
|
||||
import { IS_TRANSLATION_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
import { selectIsCurrentUserPremium } from '../../../global/selectors';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import RadioGroup from '../../ui/RadioGroup';
|
||||
import Loading from '../../ui/Loading';
|
||||
@ -26,13 +28,17 @@ type OwnProps = {
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
};
|
||||
|
||||
type StateProps = Pick<ISettings, 'languages' | 'language' | 'canTranslate' | 'doNotTranslate'>;
|
||||
type StateProps = {
|
||||
isCurrentUserPremium: boolean;
|
||||
} & Pick<ISettings, 'languages' | 'language' | 'canTranslate' | 'canTranslateChats' | 'doNotTranslate'>;
|
||||
|
||||
const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
isCurrentUserPremium,
|
||||
languages,
|
||||
language,
|
||||
canTranslate,
|
||||
canTranslateChats,
|
||||
doNotTranslate,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
@ -41,11 +47,14 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
loadLanguages,
|
||||
loadAttachBots,
|
||||
setSettingOption,
|
||||
openPremiumModal,
|
||||
} = getActions();
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<string>(language);
|
||||
const [isLoading, markIsLoading, unmarkIsLoading] = useFlag();
|
||||
|
||||
const canTranslateChatsEnabled = isCurrentUserPremium && canTranslateChats;
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
@ -54,7 +63,7 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [languages]);
|
||||
|
||||
const handleChange = useCallback((langCode: string) => {
|
||||
const handleChange = useLastCallback((langCode: string) => {
|
||||
setSelectedLanguage(langCode);
|
||||
markIsLoading();
|
||||
|
||||
@ -65,15 +74,27 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
|
||||
loadAttachBots(); // Should be refetched every language change
|
||||
});
|
||||
}, [markIsLoading, unmarkIsLoading, setSettingOption, loadAttachBots]);
|
||||
});
|
||||
|
||||
const options = useMemo(() => {
|
||||
return languages ? buildOptions(languages) : undefined;
|
||||
}, [languages]);
|
||||
|
||||
const handleShouldTranslateChange = useCallback((newValue: boolean) => {
|
||||
const handleShouldTranslateChange = useLastCallback((newValue: boolean) => {
|
||||
setSettingOption({ canTranslate: newValue });
|
||||
}, [setSettingOption]);
|
||||
});
|
||||
|
||||
const handleShouldTranslateChatsChange = useLastCallback((newValue: boolean) => {
|
||||
setSettingOption({ canTranslateChats: newValue });
|
||||
});
|
||||
|
||||
const handleShouldTranslateChatsClick = useLastCallback(() => {
|
||||
if (!isCurrentUserPremium) {
|
||||
openPremiumModal({
|
||||
initialSection: 'translations',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const doNotTranslateText = useMemo(() => {
|
||||
if (!IS_TRANSLATION_SUPPORTED || !doNotTranslate.length) {
|
||||
@ -88,9 +109,9 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
return lang('Languages', doNotTranslate.length);
|
||||
}, [doNotTranslate, lang, language]);
|
||||
|
||||
const handleDoNotSelectOpen = useCallback(() => {
|
||||
const handleDoNotSelectOpen = useLastCallback(() => {
|
||||
onScreenSelect(SettingsScreens.DoNotTranslate);
|
||||
}, [onScreenSelect]);
|
||||
});
|
||||
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
@ -102,12 +123,20 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
{IS_TRANSLATION_SUPPORTED && (
|
||||
<div className="settings-item">
|
||||
<Checkbox
|
||||
className="pb-2"
|
||||
label={lang('ShowTranslateButton')}
|
||||
checked={canTranslate}
|
||||
onCheck={handleShouldTranslateChange}
|
||||
/>
|
||||
{canTranslate && (
|
||||
<Checkbox
|
||||
className="pb-2"
|
||||
label={lang('ShowTranslateChatButton')}
|
||||
checked={canTranslateChatsEnabled}
|
||||
disabled={!isCurrentUserPremium}
|
||||
rightIcon={!isCurrentUserPremium ? 'lock' : undefined}
|
||||
onClickLabel={handleShouldTranslateChatsClick}
|
||||
onCheck={handleShouldTranslateChatsChange}
|
||||
/>
|
||||
{(canTranslate || canTranslateChatsEnabled) && (
|
||||
<ListItem
|
||||
onClick={handleDoNotSelectOpen}
|
||||
>
|
||||
@ -154,13 +183,17 @@ function buildOptions(languages: ApiLanguage[]) {
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const {
|
||||
language, languages, canTranslate, doNotTranslate,
|
||||
language, languages, canTranslate, canTranslateChats, doNotTranslate,
|
||||
} = global.settings.byKey;
|
||||
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
|
||||
return {
|
||||
isCurrentUserPremium,
|
||||
languages,
|
||||
language,
|
||||
canTranslate,
|
||||
canTranslateChats,
|
||||
doNotTranslate,
|
||||
};
|
||||
},
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { hexToRgb, lerpRgb } from '../../../util/switchTheme';
|
||||
|
||||
import ListItem from '../../ui/ListItem';
|
||||
|
||||
@ -13,23 +14,30 @@ type OwnProps = {
|
||||
text: string;
|
||||
onClick: VoidFunction;
|
||||
index: number;
|
||||
count: number;
|
||||
};
|
||||
|
||||
const COLORS = [
|
||||
'#F2862D', '#EB7B4D', '#E46D72', '#DD6091', '#CC5FBA', '#B464E7',
|
||||
'#9873FF', '#768DFF', '#55A5FC', '#52B0C9', '#4FBC93', '#4CC663',
|
||||
];
|
||||
].map(hexToRgb);
|
||||
|
||||
const PremiumFeatureItem: FC<OwnProps> = ({
|
||||
icon,
|
||||
title,
|
||||
text,
|
||||
index,
|
||||
count,
|
||||
onClick,
|
||||
}) => {
|
||||
const newIndex = (index / count) * COLORS.length;
|
||||
const colorA = COLORS[Math.floor(newIndex)];
|
||||
const colorB = COLORS[Math.ceil(newIndex)] ?? colorA;
|
||||
const { r, g, b } = lerpRgb(colorA, colorB, 1);
|
||||
|
||||
return (
|
||||
<ListItem buttonClassName={styles.root} onClick={onClick}>
|
||||
<img src={icon} className={styles.icon} alt="" style={`--item-color: ${COLORS[index]}`} />
|
||||
<img src={icon} className={styles.icon} alt="" style={`--item-color: rgb(${r},${g},${b})`} />
|
||||
<div className={styles.text}>
|
||||
<div className={styles.title}>{renderText(title, ['br'])}</div>
|
||||
<div className={styles.description}>{text}</div>
|
||||
|
||||
@ -35,6 +35,7 @@ export const PREMIUM_FEATURE_TITLES: Record<string, string> = {
|
||||
advanced_chat_management: 'PremiumPreviewAdvancedChatManagement',
|
||||
animated_userpics: 'PremiumPreviewAnimatedProfiles',
|
||||
emoji_status: 'PremiumPreviewEmojiStatus',
|
||||
translations: 'PremiumPreviewTranslations',
|
||||
};
|
||||
|
||||
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
||||
@ -50,6 +51,7 @@ export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
||||
advanced_chat_management: 'PremiumPreviewAdvancedChatManagementDescription',
|
||||
animated_userpics: 'PremiumPreviewAnimatedProfilesDescription',
|
||||
emoji_status: 'PremiumPreviewEmojiStatusDescription',
|
||||
translations: 'PremiumPreviewTranslationsDescription',
|
||||
};
|
||||
|
||||
export const PREMIUM_FEATURE_SECTIONS = [
|
||||
@ -65,15 +67,18 @@ export const PREMIUM_FEATURE_SECTIONS = [
|
||||
'profile_badge',
|
||||
'animated_userpics',
|
||||
'emoji_status',
|
||||
'translations',
|
||||
];
|
||||
|
||||
const PREMIUM_BOTTOM_VIDEOS: string[] = [
|
||||
'faster_download',
|
||||
'voice_to_text',
|
||||
'advanced_chat_management',
|
||||
'infinite_reactions',
|
||||
'profile_badge',
|
||||
'animated_userpics',
|
||||
'emoji_status',
|
||||
'translations',
|
||||
];
|
||||
|
||||
type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType, 'uploadMaxFileparts' | 'chatlistInvites' | 'chatlistJoined'>;
|
||||
|
||||
@ -42,6 +42,7 @@ import PremiumBadge from '../../../assets/premium/PremiumBadge.svg';
|
||||
import PremiumVideo from '../../../assets/premium/PremiumVideo.svg';
|
||||
import PremiumEmoji from '../../../assets/premium/PremiumEmoji.svg';
|
||||
import PremiumStatus from '../../../assets/premium/PremiumStatus.svg';
|
||||
import PremiumTranslate from '../../../assets/premium/PremiumTranslate.svg';
|
||||
|
||||
import styles from './PremiumMainModal.module.scss';
|
||||
|
||||
@ -60,6 +61,7 @@ const PREMIUM_FEATURE_COLOR_ICONS: Record<string, string> = {
|
||||
advanced_chat_management: PremiumChats,
|
||||
animated_userpics: PremiumVideo,
|
||||
emoji_status: PremiumStatus,
|
||||
translations: PremiumTranslate,
|
||||
};
|
||||
|
||||
export type OwnProps = {
|
||||
@ -275,6 +277,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
: lang(PREMIUM_FEATURE_DESCRIPTIONS[section])}
|
||||
icon={PREMIUM_FEATURE_COLOR_ICONS[section]}
|
||||
index={index}
|
||||
count={filteredSections.length}
|
||||
onClick={handleOpen(section)}
|
||||
/>
|
||||
);
|
||||
|
||||
16
src/components/middle/ChatLanguageModal.async.tsx
Normal file
16
src/components/middle/ChatLanguageModal.async.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React from '../../lib/teact/teact';
|
||||
import type { OwnProps } from './ChatLanguageModal';
|
||||
|
||||
import { Bundles } from '../../util/moduleLoader';
|
||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||
|
||||
const ChatLanguageModalAsync: FC<OwnProps> = (props) => {
|
||||
const { isOpen } = props;
|
||||
const ChatLanguageModal = useModuleLoader(Bundles.Extra, 'ChatLanguageModal', !isOpen);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return ChatLanguageModal ? <ChatLanguageModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default ChatLanguageModalAsync;
|
||||
@ -5,7 +5,12 @@ import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
|
||||
import { selectLanguageCode, selectRequestedTranslationLanguage, selectTabState } from '../../global/selectors';
|
||||
import {
|
||||
selectLanguageCode,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
selectRequestedMessageTranslationLanguage,
|
||||
selectTabState,
|
||||
} from '../../global/selectors';
|
||||
import { SUPPORTED_TRANSLATION_LANGUAGES } from '../../config';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
@ -17,7 +22,7 @@ import Modal from '../ui/Modal';
|
||||
import ListItem from '../ui/ListItem';
|
||||
import InputText from '../ui/InputText';
|
||||
|
||||
import styles from './MessageLanguageModal.module.scss';
|
||||
import styles from './ChatLanguageModal.module.scss';
|
||||
|
||||
type LanguageItem = {
|
||||
langCode: string;
|
||||
@ -36,23 +41,34 @@ type StateProps = {
|
||||
currentLanguageCode: string;
|
||||
};
|
||||
|
||||
const MessageLanguageModal: FC<OwnProps & StateProps> = ({
|
||||
const ChatLanguageModal: FC<OwnProps & StateProps> = ({
|
||||
isOpen,
|
||||
chatId,
|
||||
messageId,
|
||||
activeTranslationLanguage,
|
||||
currentLanguageCode,
|
||||
}) => {
|
||||
const { requestMessageTranslation, closeMessageLanguageModal } = getActions();
|
||||
const {
|
||||
requestMessageTranslation,
|
||||
closeChatLanguageModal,
|
||||
setSettingOption,
|
||||
requestChatTranslation,
|
||||
} = getActions();
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const lang = useLang();
|
||||
|
||||
const handleSelect = useLastCallback((toLanguageCode: string) => {
|
||||
if (!chatId || !messageId) return;
|
||||
const handleSelect = useLastCallback((langCode: string) => {
|
||||
if (!chatId) return;
|
||||
|
||||
requestMessageTranslation({ chatId, id: messageId, toLanguageCode });
|
||||
closeMessageLanguageModal();
|
||||
if (messageId) {
|
||||
requestMessageTranslation({ chatId, id: messageId, toLanguageCode: langCode });
|
||||
} else {
|
||||
setSettingOption({ translationLanguage: langCode });
|
||||
requestChatTranslation({ chatId, toLanguageCode: langCode });
|
||||
}
|
||||
|
||||
closeChatLanguageModal();
|
||||
});
|
||||
|
||||
const handleSearch = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@ -96,7 +112,7 @@ const MessageLanguageModal: FC<OwnProps & StateProps> = ({
|
||||
isOpen={isOpen}
|
||||
hasCloseButton
|
||||
title={lang('Language')}
|
||||
onClose={closeMessageLanguageModal}
|
||||
onClose={closeChatLanguageModal}
|
||||
>
|
||||
<InputText
|
||||
key="search"
|
||||
@ -109,7 +125,7 @@ const MessageLanguageModal: FC<OwnProps & StateProps> = ({
|
||||
{filteredDisplayedLanguages.map(({ langCode, originalName, translatedName }) => (
|
||||
<ListItem
|
||||
key={langCode}
|
||||
className={styles.listItem}
|
||||
className={buildClassName(styles.listItem, 'no-icon')}
|
||||
secondaryIcon={activeTranslationLanguage === langCode ? 'check' : undefined}
|
||||
disabled={activeTranslationLanguage === langCode}
|
||||
multiline
|
||||
@ -132,11 +148,14 @@ const MessageLanguageModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const { chatId, messageId } = selectTabState(global).messageLanguageModal || {};
|
||||
const { chatId, messageId } = selectTabState(global).chatLanguageModal || {};
|
||||
|
||||
const currentLanguageCode = selectLanguageCode(global);
|
||||
const activeTranslationLanguage = chatId && messageId
|
||||
? selectRequestedTranslationLanguage(global, chatId, messageId) : undefined;
|
||||
const activeTranslationLanguage = chatId
|
||||
? messageId
|
||||
? selectRequestedMessageTranslationLanguage(global, chatId, messageId)
|
||||
: selectRequestedChatTranslationLanguage(global, chatId)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
chatId,
|
||||
@ -145,4 +164,4 @@ export default memo(withGlobal<OwnProps>(
|
||||
currentLanguageCode,
|
||||
};
|
||||
},
|
||||
)(MessageLanguageModal));
|
||||
)(ChatLanguageModal));
|
||||
@ -1,8 +1,10 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useRef, useState } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useMemo, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { MessageListType } from '../../global/types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import type { IAnchorPosition } from '../../types';
|
||||
@ -15,6 +17,7 @@ import {
|
||||
import {
|
||||
selectBot,
|
||||
selectCanAnimateInterface,
|
||||
selectCanTranslateChat,
|
||||
selectChat,
|
||||
selectChatFullInfo,
|
||||
selectIsChatBotNotStarted,
|
||||
@ -22,6 +25,10 @@ import {
|
||||
selectIsInSelectMode,
|
||||
selectIsRightColumnShown,
|
||||
selectIsUserBlocked,
|
||||
selectLanguageCode,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
selectTranslationLanguage,
|
||||
selectUserFullInfo,
|
||||
} from '../../global/selectors';
|
||||
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
@ -30,6 +37,9 @@ import { useHotkeys } from '../../hooks/useHotkeys';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import HeaderMenuContainer from './HeaderMenuContainer.async';
|
||||
import DropdownMenu from '../ui/DropdownMenu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import MenuSeparator from '../ui/MenuSeparator';
|
||||
|
||||
interface OwnProps {
|
||||
chatId: string;
|
||||
@ -59,6 +69,12 @@ interface StateProps {
|
||||
shouldJoinToSend?: boolean;
|
||||
shouldSendJoinRequest?: boolean;
|
||||
noAnimation?: boolean;
|
||||
canTranslate?: boolean;
|
||||
isTranslating?: boolean;
|
||||
translationLanguage: string;
|
||||
language: string;
|
||||
detectedChatLanguage?: string;
|
||||
doNotTranslate: string[];
|
||||
}
|
||||
|
||||
// Chrome breaks layout when focusing input during transition
|
||||
@ -87,6 +103,12 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
shouldJoinToSend,
|
||||
shouldSendJoinRequest,
|
||||
noAnimation,
|
||||
canTranslate,
|
||||
isTranslating,
|
||||
translationLanguage,
|
||||
language,
|
||||
detectedChatLanguage,
|
||||
doNotTranslate,
|
||||
onTopicSearch,
|
||||
}) => {
|
||||
const {
|
||||
@ -98,6 +120,10 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
requestNextManagementScreen,
|
||||
showNotification,
|
||||
openChat,
|
||||
requestChatTranslation,
|
||||
togglePeerTranslations,
|
||||
openChatLanguageModal,
|
||||
setSettingOption,
|
||||
} = getActions();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
@ -136,6 +162,15 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
restartBot({ chatId });
|
||||
});
|
||||
|
||||
const handleTranslateClick = useLastCallback(() => {
|
||||
if (isTranslating) {
|
||||
requestChatTranslation({ chatId, toLanguageCode: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
requestChatTranslation({ chatId, toLanguageCode: translationLanguage });
|
||||
});
|
||||
|
||||
const handleJoinRequestsClick = useLastCallback(() => {
|
||||
requestNextManagementScreen({ screen: ManagementScreens.JoinRequests });
|
||||
});
|
||||
@ -179,12 +214,91 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
handleSearchClick();
|
||||
});
|
||||
|
||||
const getTextWithLanguage = useLastCallback((langKey: string, langCode: string) => {
|
||||
const simplified = langCode.split('-')[0];
|
||||
const translationKey = `TranslateLanguage${simplified.toUpperCase()}`;
|
||||
const name = lang(translationKey);
|
||||
if (name !== translationKey) {
|
||||
return lang(langKey, name);
|
||||
}
|
||||
|
||||
const translatedNames = new Intl.DisplayNames([language], { type: 'language' });
|
||||
const translatedName = translatedNames.of(langCode)!;
|
||||
return lang(`${langKey}Other`, translatedName);
|
||||
});
|
||||
|
||||
const buttonText = useMemo(() => {
|
||||
if (isTranslating) return lang('ShowOriginalButton');
|
||||
|
||||
return getTextWithLanguage('TranslateToButton', translationLanguage);
|
||||
}, [translationLanguage, getTextWithLanguage, isTranslating, lang]);
|
||||
|
||||
const doNotTranslateText = useMemo(() => {
|
||||
if (!detectedChatLanguage) return undefined;
|
||||
|
||||
return getTextWithLanguage('DoNotTranslateLanguage', detectedChatLanguage);
|
||||
}, [getTextWithLanguage, detectedChatLanguage]);
|
||||
|
||||
const handleHide = useLastCallback(() => {
|
||||
togglePeerTranslations({ chatId, isEnabled: false });
|
||||
requestChatTranslation({ chatId, toLanguageCode: undefined });
|
||||
});
|
||||
|
||||
const handleChangeLanguage = useLastCallback(() => {
|
||||
openChatLanguageModal({ chatId });
|
||||
});
|
||||
|
||||
const handleDoNotTranslate = useLastCallback(() => {
|
||||
if (!detectedChatLanguage) return;
|
||||
|
||||
setSettingOption({
|
||||
doNotTranslate: [...doNotTranslate, detectedChatLanguage],
|
||||
});
|
||||
requestChatTranslation({ chatId, toLanguageCode: undefined });
|
||||
|
||||
showNotification({ message: getTextWithLanguage('AddedToDoNotTranslate', detectedChatLanguage) });
|
||||
});
|
||||
|
||||
useHotkeys({
|
||||
'Mod+F': handleHotkeySearchClick,
|
||||
});
|
||||
|
||||
const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||
return ({ onTrigger, isOpen }) => (
|
||||
<Button
|
||||
round
|
||||
ripple={isRightColumnShown}
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
className={isOpen ? 'active' : ''}
|
||||
onClick={onTrigger}
|
||||
ariaLabel={lang('TranslateMessage')}
|
||||
>
|
||||
<i className="icon icon-language" aria-hidden />
|
||||
</Button>
|
||||
);
|
||||
}, [isRightColumnShown, lang]);
|
||||
|
||||
return (
|
||||
<div className="HeaderActions">
|
||||
{canTranslate && (
|
||||
<DropdownMenu
|
||||
className="stickers-more-menu with-menu-transitions"
|
||||
trigger={MoreMenuButton}
|
||||
positionX="right"
|
||||
>
|
||||
<MenuItem icon="language" onClick={handleTranslateClick}>
|
||||
{buttonText}
|
||||
</MenuItem>
|
||||
<MenuItem icon="replace" onClick={handleChangeLanguage}>
|
||||
{lang('Chat.Translate.Menu.To')}
|
||||
</MenuItem>
|
||||
<MenuSeparator />
|
||||
{detectedChatLanguage
|
||||
&& <MenuItem icon="hand-stop" onClick={handleDoNotTranslate}>{doNotTranslateText}</MenuItem>}
|
||||
<MenuItem icon="close-circle" onClick={handleHide}>{lang('Hide')}</MenuItem>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{!isMobile && (
|
||||
<>
|
||||
{canExpandActions && !shouldSendJoinRequest && (canSubscribe || shouldJoinToSend) && (
|
||||
@ -234,9 +348,9 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
onClick={handleSearchClick}
|
||||
ariaLabel="Search in this chat"
|
||||
ariaLabel={lang('Conversation.SearchPlaceholder')}
|
||||
>
|
||||
<i className="icon icon-search" />
|
||||
<i className="icon icon-search" aria-hidden />
|
||||
</Button>
|
||||
)}
|
||||
{canCall && (
|
||||
@ -248,7 +362,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
onClick={handleRequestCall}
|
||||
ariaLabel="Call"
|
||||
>
|
||||
<i className="icon icon-phone" />
|
||||
<i className="icon icon-phone" aria-hidden />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@ -263,7 +377,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
onClick={handleJoinRequestsClick}
|
||||
ariaLabel={isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}
|
||||
>
|
||||
<i className="icon icon-user" />
|
||||
<i className="icon icon-user" aria-hidden />
|
||||
<div className="badge">{pendingJoinRequests}</div>
|
||||
</Button>
|
||||
)}
|
||||
@ -278,7 +392,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
ariaLabel="More actions"
|
||||
onClick={handleHeaderMenuOpen}
|
||||
>
|
||||
<i className="icon icon-more" />
|
||||
<i className="icon icon-more" aria-hidden />
|
||||
</Button>
|
||||
{menuPosition && (
|
||||
<HeaderMenuContainer
|
||||
@ -318,15 +432,23 @@ export default memo(withGlobal<OwnProps>(
|
||||
}): StateProps => {
|
||||
const chat = selectChat(global, chatId);
|
||||
const isChannel = Boolean(chat && isChatChannel(chat));
|
||||
const language = selectLanguageCode(global);
|
||||
const translationLanguage = selectTranslationLanguage(global);
|
||||
const { doNotTranslate } = global.settings.byKey;
|
||||
|
||||
if (!chat || chat.isRestricted || selectIsInSelectMode(global)) {
|
||||
return {
|
||||
noMenu: true,
|
||||
language,
|
||||
translationLanguage,
|
||||
doNotTranslate,
|
||||
};
|
||||
}
|
||||
|
||||
const bot = selectBot(global, chatId);
|
||||
const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined;
|
||||
const userFullInfo = isUserId(chatId) ? selectUserFullInfo(global, chatId) : undefined;
|
||||
const fullInfo = chatFullInfo || userFullInfo;
|
||||
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
||||
const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID;
|
||||
const isDiscussionThread = messageListType === 'thread' && threadId !== MAIN_THREAD_ID;
|
||||
@ -350,6 +472,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest);
|
||||
const noAnimation = !selectCanAnimateInterface(global);
|
||||
|
||||
const isTranslating = Boolean(selectRequestedChatTranslationLanguage(global, chatId));
|
||||
const canTranslate = selectCanTranslateChat(global, chatId) && !fullInfo?.isTranslationDisabled;
|
||||
|
||||
return {
|
||||
noMenu: false,
|
||||
isChannel,
|
||||
@ -368,6 +493,12 @@ export default memo(withGlobal<OwnProps>(
|
||||
shouldJoinToSend,
|
||||
shouldSendJoinRequest,
|
||||
noAnimation,
|
||||
canTranslate,
|
||||
isTranslating,
|
||||
translationLanguage,
|
||||
language,
|
||||
doNotTranslate,
|
||||
detectedChatLanguage: chat.detectedLanguage,
|
||||
};
|
||||
},
|
||||
)(HeaderActions));
|
||||
|
||||
@ -21,7 +21,7 @@ import {
|
||||
selectTabState,
|
||||
selectUser,
|
||||
selectUserFullInfo,
|
||||
selectCanManage, selectIsRightColumnShown,
|
||||
selectCanManage, selectIsRightColumnShown, selectCanTranslateChat,
|
||||
} from '../../global/selectors';
|
||||
import {
|
||||
getCanAddContact,
|
||||
@ -86,6 +86,7 @@ export type OwnProps = {
|
||||
canEnterVoiceChat?: boolean;
|
||||
canCreateVoiceChat?: boolean;
|
||||
pendingJoinRequests?: number;
|
||||
canTranslate?: boolean;
|
||||
onSubscribeChannel: () => void;
|
||||
onSearchClick: () => void;
|
||||
onAsMessagesClick: () => void;
|
||||
@ -111,6 +112,7 @@ type StateProps = {
|
||||
isChatInfoShown?: boolean;
|
||||
isRightColumnShown?: boolean;
|
||||
canManage?: boolean;
|
||||
canTranslate?: boolean;
|
||||
};
|
||||
|
||||
const CLOSE_MENU_ANIMATION_DURATION = 200;
|
||||
@ -150,6 +152,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canEditTopic,
|
||||
canManage,
|
||||
isRightColumnShown,
|
||||
canTranslate,
|
||||
onJoinRequestsClick,
|
||||
onSubscribeChannel,
|
||||
onSearchClick,
|
||||
@ -174,6 +177,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
openEditTopicPanel,
|
||||
openChat,
|
||||
toggleManagement,
|
||||
togglePeerTranslations,
|
||||
} = getActions();
|
||||
|
||||
const { isMobile } = useAppLayout();
|
||||
@ -323,6 +327,11 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
closeMenu();
|
||||
});
|
||||
|
||||
const handleEnableTranslations = useLastCallback(() => {
|
||||
togglePeerTranslations({ chatId, isEnabled: true });
|
||||
closeMenu();
|
||||
});
|
||||
|
||||
const handleSelectMessages = useLastCallback(() => {
|
||||
enterMessageSelectMode();
|
||||
closeMenu();
|
||||
@ -538,6 +547,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
{lang('Statistics')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{canTranslate && (
|
||||
<MenuItem
|
||||
icon="language"
|
||||
onClick={handleEnableTranslations}
|
||||
>
|
||||
{lang('lng_context_translate')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{canReportChat && (
|
||||
<MenuItem
|
||||
icon="flag"
|
||||
@ -614,6 +631,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const chatBot = chatId !== REPLIES_USER_ID ? selectBot(global, chatId) : undefined;
|
||||
const userFullInfo = isPrivate ? selectUserFullInfo(global, chatId) : undefined;
|
||||
const chatFullInfo = !isPrivate ? selectChatFullInfo(global, chatId) : undefined;
|
||||
const fullInfo = userFullInfo || chatFullInfo;
|
||||
const canGiftPremium = Boolean(
|
||||
userFullInfo?.premiumGifts?.length
|
||||
&& !selectIsPremiumPurchaseBlocked(global),
|
||||
@ -625,6 +643,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
);
|
||||
const canEditTopic = topic && getCanManageTopic(chat, topic);
|
||||
const canManage = selectCanManage(global, chatId);
|
||||
// Context menu item should only be displayed if user hid translation panel
|
||||
const canTranslate = selectCanTranslateChat(global, chatId) && fullInfo?.isTranslationDisabled;
|
||||
|
||||
return {
|
||||
chat,
|
||||
@ -644,6 +664,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
canEditTopic,
|
||||
canManage,
|
||||
isRightColumnShown: selectIsRightColumnShown(global),
|
||||
canTranslate,
|
||||
};
|
||||
},
|
||||
)(HeaderMenuContainer));
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React from '../../lib/teact/teact';
|
||||
import type { OwnProps } from './MessageLanguageModal';
|
||||
|
||||
import { Bundles } from '../../util/moduleLoader';
|
||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||
|
||||
const MessageLanguageModalAsync: FC<OwnProps> = (props) => {
|
||||
const { isOpen } = props;
|
||||
const MessageLanguageModal = useModuleLoader(Bundles.Extra, 'MessageLanguageModal', !isOpen);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return MessageLanguageModal ? <MessageLanguageModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default MessageLanguageModalAsync;
|
||||
@ -87,7 +87,7 @@ import SeenByModal from '../common/SeenByModal.async';
|
||||
import EmojiInteractionAnimation from './EmojiInteractionAnimation.async';
|
||||
import ReactorListModal from './ReactorListModal.async';
|
||||
import GiftPremiumModal from '../main/premium/GiftPremiumModal.async';
|
||||
import MessageLanguageModal from './MessageLanguageModal.async';
|
||||
import ChatLanguageModal from './ChatLanguageModal.async';
|
||||
|
||||
import './MiddleColumn.scss';
|
||||
|
||||
@ -126,7 +126,7 @@ type StateProps = {
|
||||
isSeenByModalOpen: boolean;
|
||||
isReactorListModalOpen: boolean;
|
||||
isGiftPremiumModalOpen?: boolean;
|
||||
isMessageLanguageModalOpen?: boolean;
|
||||
isChatLanguageModalOpen?: boolean;
|
||||
withInterfaceAnimations?: boolean;
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
currentTransitionKey: number;
|
||||
@ -180,7 +180,7 @@ function MiddleColumn({
|
||||
isSeenByModalOpen,
|
||||
isReactorListModalOpen,
|
||||
isGiftPremiumModalOpen,
|
||||
isMessageLanguageModalOpen,
|
||||
isChatLanguageModalOpen,
|
||||
withInterfaceAnimations,
|
||||
shouldSkipHistoryAnimations,
|
||||
currentTransitionKey,
|
||||
@ -622,7 +622,7 @@ function MiddleColumn({
|
||||
/>
|
||||
<SeenByModal isOpen={isSeenByModalOpen} />
|
||||
<ReactorListModal isOpen={isReactorListModalOpen} />
|
||||
{IS_TRANSLATION_SUPPORTED && <MessageLanguageModal isOpen={isMessageLanguageModalOpen} />}
|
||||
{IS_TRANSLATION_SUPPORTED && <ChatLanguageModal isOpen={isChatLanguageModalOpen} />}
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
@ -668,7 +668,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const {
|
||||
messageLists, isLeftColumnShown, activeEmojiInteractions,
|
||||
seenByModal, giftPremiumModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations,
|
||||
messageLanguageModal,
|
||||
chatLanguageModal,
|
||||
} = selectTabState(global);
|
||||
const currentMessageList = selectCurrentMessageList(global);
|
||||
const { leftColumnWidth } = global;
|
||||
@ -686,7 +686,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isSeenByModalOpen: Boolean(seenByModal),
|
||||
isReactorListModalOpen: Boolean(reactorModal),
|
||||
isGiftPremiumModalOpen: giftPremiumModal?.isOpen,
|
||||
isMessageLanguageModalOpen: Boolean(messageLanguageModal),
|
||||
isChatLanguageModalOpen: Boolean(chatLanguageModal),
|
||||
withInterfaceAnimations: selectCanAnimateInterface(global),
|
||||
currentTransitionKey: Math.max(0, messageLists.length - 1),
|
||||
activeEmojiInteractions,
|
||||
|
||||
@ -22,10 +22,12 @@ import {
|
||||
selectIsMessageProtected,
|
||||
selectIsPremiumPurchaseBlocked,
|
||||
selectIsReactionPickerOpen,
|
||||
selectCanTranslateMessage,
|
||||
selectMessageCustomEmojiSets,
|
||||
selectMessageTranslations,
|
||||
selectRequestedTranslationLanguage,
|
||||
selectRequestedMessageTranslationLanguage,
|
||||
selectStickerSet,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
} from '../../../global/selectors';
|
||||
import {
|
||||
isActionMessage,
|
||||
@ -37,10 +39,8 @@ import {
|
||||
isMessageLocal,
|
||||
getMessageVideo,
|
||||
getChatMessageLink,
|
||||
isServiceNotificationMessage,
|
||||
} from '../../../global/helpers';
|
||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
||||
import { IS_TRANSLATION_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||
|
||||
@ -193,7 +193,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
toggleReaction,
|
||||
requestMessageTranslation,
|
||||
showOriginalMessage,
|
||||
openMessageLanguageModal,
|
||||
openChatLanguageModal,
|
||||
openReactionPicker,
|
||||
} = getActions();
|
||||
|
||||
@ -455,9 +455,9 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleSelectLanguage = useLastCallback(() => {
|
||||
openMessageLanguageModal({
|
||||
openChatLanguageModal({
|
||||
chatId: message.chatId,
|
||||
id: message.id,
|
||||
messageId: message.id,
|
||||
});
|
||||
closeMenu();
|
||||
});
|
||||
@ -610,7 +610,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
const isScheduled = messageListType === 'scheduled';
|
||||
const isChannel = chat && isChatChannel(chat);
|
||||
const isLocal = isMessageLocal(message);
|
||||
const isServiceNotification = isServiceNotificationMessage(message);
|
||||
const canShowSeenBy = Boolean(!isLocal
|
||||
&& chat
|
||||
&& seenByMaxChatMembers
|
||||
@ -634,17 +633,12 @@ export default memo(withGlobal<OwnProps>(
|
||||
const customEmojiSets = customEmojiSetsNotFiltered?.every<ApiStickerSet>(Boolean)
|
||||
? customEmojiSetsNotFiltered : undefined;
|
||||
|
||||
const translationRequestLanguage = selectRequestedTranslationLanguage(global, message.chatId, message.id);
|
||||
const translationRequestLanguage = selectRequestedMessageTranslationLanguage(global, message.chatId, message.id);
|
||||
const hasTranslation = translationRequestLanguage
|
||||
? Boolean(selectMessageTranslations(global, message.chatId, translationRequestLanguage)[message.id]?.text)
|
||||
: undefined;
|
||||
|
||||
const { canTranslate: isTranslationEnabled, doNotTranslate } = global.settings.byKey;
|
||||
|
||||
const canTranslateLanguage = !detectedLanguage || !doNotTranslate.includes(detectedLanguage);
|
||||
const canTranslate = IS_TRANSLATION_SUPPORTED && isTranslationEnabled && message.content.text
|
||||
&& canTranslateLanguage && !isLocal && !isServiceNotification && !isScheduled && !isAction && !hasTranslation
|
||||
&& !message.emojiOnlyCount;
|
||||
const canTranslate = !hasTranslation && selectCanTranslateMessage(global, message, detectedLanguage);
|
||||
const isChatTranslated = selectRequestedChatTranslationLanguage(global, message.chatId);
|
||||
|
||||
return {
|
||||
availableReactions: global.availableReactions,
|
||||
@ -683,8 +677,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
canScheduleUntilOnline: selectCanScheduleUntilOnline(global, message.chatId),
|
||||
threadId,
|
||||
canTranslate,
|
||||
canShowOriginal: hasTranslation,
|
||||
canSelectLanguage: hasTranslation,
|
||||
canShowOriginal: hasTranslation && !isChatTranslated,
|
||||
canSelectLanguage: hasTranslation && !isChatTranslated,
|
||||
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
||||
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
||||
};
|
||||
|
||||
@ -70,7 +70,7 @@ const Location: FC<OwnProps> = ({
|
||||
const { type, geo } = location;
|
||||
|
||||
const serverTime = getServerTime();
|
||||
const isExpired = isGeoLiveExpired(message, serverTime);
|
||||
const isExpired = isGeoLiveExpired(message);
|
||||
const secondsBeforeEnd = (type === 'geoLive' && !isExpired) ? message.date + location.period - serverTime
|
||||
: undefined;
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import { useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
||||
import type { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessage';
|
||||
|
||||
import { IS_ANDROID } from '../../../util/windowEnvironment';
|
||||
import { IS_ANDROID, IS_TRANSLATION_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
import { EMOJI_STATUS_LOOP_LIMIT, GENERAL_TOPIC_ID, IS_ELECTRON } from '../../../config';
|
||||
import {
|
||||
selectAllowedMessageActions,
|
||||
@ -62,8 +62,10 @@ import {
|
||||
selectOutgoingStatus,
|
||||
selectPerformanceSettingsValue,
|
||||
selectReplySender,
|
||||
selectRequestedTranslationLanguage,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
selectRequestedMessageTranslationLanguage,
|
||||
selectSender,
|
||||
selectShouldDetectChatLanguage,
|
||||
selectShouldLoopStickers,
|
||||
selectTabState,
|
||||
selectTheme,
|
||||
@ -90,6 +92,7 @@ import {
|
||||
isChatWithRepliesBot,
|
||||
isGeoLiveExpired,
|
||||
isMessageLocal,
|
||||
isMessageTranslatable,
|
||||
isOwnMessage,
|
||||
isReplyMessage,
|
||||
isUserId,
|
||||
@ -105,7 +108,6 @@ import { buildContentClassName } from './helpers/buildContentClassName';
|
||||
import { calculateMediaDimensions, getMinMediaWidth, MIN_MEDIA_WIDTH_WITH_TEXT } from './helpers/mediaDimensions';
|
||||
import { calculateAlbumLayout } from './helpers/calculateAlbumLayout';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
|
||||
import { isAnimatingScroll } from '../../../util/animateScroll';
|
||||
@ -127,6 +129,7 @@ import usePrevious from '../../../hooks/usePrevious';
|
||||
import useTextLanguage from '../../../hooks/useTextLanguage';
|
||||
import useAuthorWidth from '../hooks/useAuthorWidth';
|
||||
import { dispatchHeavyAnimationEvent } from '../../../hooks/useHeavyAnimationCheck';
|
||||
import useDetectChatLanguage from './hooks/useDetectChatLanguage';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Avatar from '../../common/Avatar';
|
||||
@ -251,7 +254,9 @@ type StateProps = {
|
||||
hasTopicChip?: boolean;
|
||||
chatTranslations?: ChatTranslatedMessages;
|
||||
areTranslationsEnabled?: boolean;
|
||||
shouldDetectChatLanguage?: boolean;
|
||||
requestedTranslationLanguage?: string;
|
||||
requestedChatTranslationLanguage?: string;
|
||||
withReactionEffects?: boolean;
|
||||
withStickerEffects?: boolean;
|
||||
isConnected: boolean;
|
||||
@ -362,7 +367,9 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
hasTopicChip,
|
||||
chatTranslations,
|
||||
areTranslationsEnabled,
|
||||
shouldDetectChatLanguage,
|
||||
requestedTranslationLanguage,
|
||||
requestedChatTranslationLanguage,
|
||||
withReactionEffects,
|
||||
withStickerEffects,
|
||||
isConnected,
|
||||
@ -552,6 +559,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
senderPeer,
|
||||
botSender,
|
||||
messageTopic,
|
||||
Boolean(requestedChatTranslationLanguage),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -597,13 +605,16 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location, action, game,
|
||||
} = getMessageContent(message);
|
||||
|
||||
useDetectChatLanguage(message, !shouldDetectChatLanguage);
|
||||
|
||||
const detectedLanguage = useTextLanguage(areTranslationsEnabled ? text?.text : undefined);
|
||||
|
||||
const shouldTranslate = isMessageTranslatable(message, !requestedChatTranslationLanguage);
|
||||
const { isPending: isTranslationPending, translatedText } = useMessageTranslation(
|
||||
chatTranslations, chatId, messageId, requestedTranslationLanguage,
|
||||
chatTranslations, chatId, shouldTranslate ? messageId : undefined, requestedTranslationLanguage,
|
||||
);
|
||||
// Used to display previous result while new one is loading
|
||||
const previousTranslatedText = usePrevious(translatedText, true);
|
||||
const previousTranslatedText = usePrevious(translatedText, Boolean(shouldTranslate));
|
||||
|
||||
const currentTranslatedText = translatedText || previousTranslatedText;
|
||||
|
||||
@ -628,7 +639,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
hasComments: repliesThreadInfo && repliesThreadInfo.messagesCount > 0,
|
||||
hasActionButton: canForward || canFocus,
|
||||
hasReactions,
|
||||
isGeoLiveActive: location?.type === 'geoLive' && !isGeoLiveExpired(message, getServerTime()),
|
||||
isGeoLiveActive: location?.type === 'geoLive' && !isGeoLiveExpired(message),
|
||||
withVoiceTranscription,
|
||||
});
|
||||
|
||||
@ -925,6 +936,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
noUserColors={isOwn || isChannel}
|
||||
isProtected={isProtected}
|
||||
sender={replyMessageSender}
|
||||
chatTranslations={chatTranslations}
|
||||
requestedChatTranslationLanguage={requestedChatTranslationLanguage}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onClick={handleReplyClick}
|
||||
@ -1441,7 +1454,12 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const isLocation = Boolean(getMessageLocation(message));
|
||||
const chatTranslations = selectChatTranslations(global, chatId);
|
||||
const requestedTranslationLanguage = selectRequestedTranslationLanguage(global, chatId, message.id);
|
||||
|
||||
const requestedTranslationLanguage = selectRequestedMessageTranslationLanguage(global, chatId, message.id);
|
||||
const requestedChatTranslationLanguage = selectRequestedChatTranslationLanguage(global, chatId);
|
||||
|
||||
const areTranslationsEnabled = IS_TRANSLATION_SUPPORTED && global.settings.byKey.canTranslate
|
||||
&& !requestedChatTranslationLanguage; // Stop separate language detection if chat translation is requested
|
||||
|
||||
const isConnected = global.connectionState === 'connectionStateReady';
|
||||
|
||||
@ -1500,8 +1518,10 @@ export default memo(withGlobal<OwnProps>(
|
||||
genericEffects: global.genericEmojiEffects,
|
||||
hasTopicChip,
|
||||
chatTranslations,
|
||||
areTranslationsEnabled: global.settings.byKey.canTranslate,
|
||||
areTranslationsEnabled,
|
||||
shouldDetectChatLanguage: selectShouldDetectChatLanguage(global, chatId),
|
||||
requestedTranslationLanguage,
|
||||
requestedChatTranslationLanguage,
|
||||
hasLinkedChat: Boolean(chatFullInfo?.linkedChatId),
|
||||
withReactionEffects: selectPerformanceSettingsValue(global, 'reactionEffects'),
|
||||
withStickerEffects: selectPerformanceSettingsValue(global, 'stickerEffects'),
|
||||
|
||||
104
src/components/middle/message/hooks/useDetectChatLanguage.ts
Normal file
104
src/components/middle/message/hooks/useDetectChatLanguage.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import type { ApiMessage } from '../../../../api/types';
|
||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../../config';
|
||||
import { getActions } from '../../../../global';
|
||||
import useTextLanguage from '../../../../hooks/useTextLanguage';
|
||||
import LimitedMap from '../../../../util/primitives/LimitedMap';
|
||||
import { throttle } from '../../../../util/schedulers';
|
||||
|
||||
// https://github.com/DrKLO/Telegram/blob/dfd74f809e97d1ecad9672fc7388cb0223a95dfc/TMessagesProj/src/main/java/org/telegram/messenger/TranslateController.java#L35
|
||||
const MIN_MESSAGES_CHECKED = 8;
|
||||
const MIN_TRANSLATABLE_RATIO = 0.3;
|
||||
const MIN_DETECTABLE_RATIO = 0.6;
|
||||
|
||||
const THROTTLE_DELAY = 1000;
|
||||
const MESSAGES_LIMIT = 150;
|
||||
|
||||
type MessageMetadata = {
|
||||
id: number;
|
||||
isTranslatable: boolean;
|
||||
detectedLanguage: string | undefined;
|
||||
};
|
||||
|
||||
const CHAT_STATS = new Map<string, LimitedMap<number, MessageMetadata>>();
|
||||
|
||||
export default function useDetectChatLanguage(message: ApiMessage, isDisabled?: boolean) {
|
||||
const canProcess = !isDisabled && message.chatId !== SERVICE_NOTIFICATIONS_USER_ID;
|
||||
|
||||
const isTranslatable = Boolean(message.content.text?.text.length);
|
||||
const detectedLanguage = useTextLanguage(message.content.text?.text, !canProcess);
|
||||
|
||||
if (!canProcess) return;
|
||||
|
||||
processMessageMetadata(message.chatId, message.id, isTranslatable, detectedLanguage);
|
||||
}
|
||||
|
||||
const throttledMakeChatDecision = throttle(makeChatDecision, THROTTLE_DELAY);
|
||||
|
||||
function processMessageMetadata(chatId: string, id: number, isTranslatable: boolean, detectedLanguage?: string) {
|
||||
const chatStats = CHAT_STATS.get(chatId) || new LimitedMap<number, MessageMetadata>(MESSAGES_LIMIT);
|
||||
|
||||
const previousMetadata = chatStats.get(id);
|
||||
if (previousMetadata && previousMetadata.detectedLanguage === detectedLanguage
|
||||
&& previousMetadata.isTranslatable === isTranslatable
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
chatStats.set(id, {
|
||||
id,
|
||||
isTranslatable,
|
||||
detectedLanguage,
|
||||
});
|
||||
|
||||
CHAT_STATS.set(chatId, chatStats);
|
||||
|
||||
throttledMakeChatDecision(chatId);
|
||||
}
|
||||
|
||||
function makeChatDecision(chatId: string) {
|
||||
const { updateChatDetectedLanguage } = getActions();
|
||||
const chatStats = CHAT_STATS.get(chatId);
|
||||
if (!chatStats) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messagesChecked = chatStats.size;
|
||||
if (messagesChecked < MIN_MESSAGES_CHECKED) {
|
||||
return;
|
||||
}
|
||||
|
||||
let translatableCount = 0;
|
||||
let detectableCount = 0;
|
||||
const languageOccurrences = new Map<string, number>();
|
||||
|
||||
for (const metadata of chatStats.values()) {
|
||||
if (metadata.isTranslatable) {
|
||||
translatableCount++;
|
||||
}
|
||||
|
||||
if (metadata.detectedLanguage) {
|
||||
detectableCount++;
|
||||
}
|
||||
|
||||
const language = metadata.detectedLanguage;
|
||||
if (language) {
|
||||
const occurrences = languageOccurrences.get(language) || 0;
|
||||
languageOccurrences.set(language, occurrences + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const translatableRatio = translatableCount / messagesChecked;
|
||||
const detectableRatio = detectableCount / messagesChecked;
|
||||
|
||||
if (translatableRatio < MIN_TRANSLATABLE_RATIO || detectableRatio < MIN_DETECTABLE_RATIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mostFrequentLanguage = Array.from(languageOccurrences.entries())
|
||||
.sort(([, a], [, b]) => b - a)[0][0];
|
||||
|
||||
updateChatDetectedLanguage({
|
||||
chatId,
|
||||
detectedLanguage: mostFrequentLanguage,
|
||||
});
|
||||
}
|
||||
@ -26,11 +26,12 @@ export default function useInnerHandlers(
|
||||
senderPeer?: ApiUser | ApiChat,
|
||||
botSender?: ApiUser,
|
||||
messageTopic?: ApiTopic,
|
||||
isTranslatingChat?: boolean,
|
||||
) {
|
||||
const {
|
||||
openChat, showNotification, focusMessage, openMediaViewer, openAudioPlayer,
|
||||
markMessagesRead, cancelSendingMessage, sendPollVote, openForwardMenu, focusMessageInComments,
|
||||
openMessageLanguageModal,
|
||||
openChatLanguageModal,
|
||||
} = getActions();
|
||||
|
||||
const {
|
||||
@ -160,7 +161,7 @@ export default function useInnerHandlers(
|
||||
const handleTranslationClick = useLastCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
openMessageLanguageModal({ chatId, id: messageId });
|
||||
openChatLanguageModal({ chatId, messageId: !isTranslatingChat ? messageId : undefined });
|
||||
});
|
||||
|
||||
const handleOpenThread = useLastCallback(() => {
|
||||
|
||||
@ -1,27 +1,116 @@
|
||||
import { useEffect } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
import type { ChatTranslatedMessages } from '../../../../global/types';
|
||||
import { throttle } from '../../../../util/schedulers';
|
||||
|
||||
const MESSAGE_LIMIT_PER_REQUEST = 20;
|
||||
const THROTTLE_DELAY = 500;
|
||||
const PENDING_TRANSLATIONS = new Map<string, Map<string, number[]>>();
|
||||
|
||||
export default function useMessageTranslation(
|
||||
chatTranslations: ChatTranslatedMessages | undefined,
|
||||
chatId: string,
|
||||
messageId: number,
|
||||
chatId?: string,
|
||||
messageId?: number,
|
||||
requestedLanguageCode?: string,
|
||||
) {
|
||||
const { translateMessages } = getActions();
|
||||
const messageTranslation = requestedLanguageCode
|
||||
const messageTranslation = requestedLanguageCode && messageId
|
||||
? chatTranslations?.byLangCode[requestedLanguageCode]?.[messageId] : undefined;
|
||||
|
||||
const { isPending, text } = messageTranslation || {};
|
||||
|
||||
useEffect(() => {
|
||||
if (!text && !isPending && requestedLanguageCode) {
|
||||
translateMessages({ chatId, messageIds: [messageId], toLanguageCode: requestedLanguageCode });
|
||||
if (!chatId || !messageId) return;
|
||||
|
||||
if (!text && isPending === undefined && requestedLanguageCode) {
|
||||
addPendingTranslation(chatId, messageId, requestedLanguageCode);
|
||||
}
|
||||
}, [chatId, text, isPending, messageId, requestedLanguageCode, translateMessages]);
|
||||
}, [chatId, text, isPending, messageId, requestedLanguageCode]);
|
||||
|
||||
if (!chatId || !messageId) {
|
||||
return {
|
||||
isPending: false,
|
||||
translatedText: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isPending,
|
||||
translatedText: text,
|
||||
};
|
||||
}
|
||||
|
||||
const throttledProcessPending = throttle(processPending, THROTTLE_DELAY);
|
||||
|
||||
function processPending() {
|
||||
const { translateMessages } = getActions();
|
||||
let hasUnprocessed = false;
|
||||
PENDING_TRANSLATIONS.forEach((chats, toLanguageCode) => {
|
||||
chats.forEach((messageIds, chatId) => {
|
||||
const messageIdsToTranslate = messageIds.slice(0, MESSAGE_LIMIT_PER_REQUEST);
|
||||
|
||||
if (messageIdsToTranslate.length < messageIds.length) {
|
||||
hasUnprocessed = true;
|
||||
}
|
||||
|
||||
translateMessages({ chatId, messageIds: messageIdsToTranslate, toLanguageCode });
|
||||
|
||||
removePendingTranslations(chatId, messageIdsToTranslate, toLanguageCode);
|
||||
});
|
||||
});
|
||||
|
||||
if (hasUnprocessed) {
|
||||
throttledProcessPending();
|
||||
}
|
||||
}
|
||||
|
||||
function addPendingTranslation(
|
||||
chatId: string,
|
||||
messageId: number,
|
||||
toLanguageCode: string,
|
||||
) {
|
||||
const languageTranslations = PENDING_TRANSLATIONS.get(toLanguageCode) || new Map<string, number[]>();
|
||||
const messageIds = languageTranslations.get(chatId) || [];
|
||||
|
||||
if (messageIds.includes(messageId)) {
|
||||
throttledProcessPending();
|
||||
return;
|
||||
}
|
||||
|
||||
messageIds.push(messageId);
|
||||
languageTranslations.set(chatId, messageIds);
|
||||
PENDING_TRANSLATIONS.set(toLanguageCode, languageTranslations);
|
||||
|
||||
getActions().markMessagesTranslationPending({ chatId, messageIds, toLanguageCode });
|
||||
|
||||
throttledProcessPending();
|
||||
}
|
||||
|
||||
function removePendingTranslations(
|
||||
chatId: string,
|
||||
messageIds: number[],
|
||||
toLanguageCode: string,
|
||||
) {
|
||||
const languageTranslations = PENDING_TRANSLATIONS.get(toLanguageCode);
|
||||
if (!languageTranslations?.size) {
|
||||
PENDING_TRANSLATIONS.delete(toLanguageCode);
|
||||
return;
|
||||
}
|
||||
|
||||
const oldMessageIds = languageTranslations.get(chatId);
|
||||
if (!oldMessageIds?.length) {
|
||||
languageTranslations.delete(chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
const newMessageIds = oldMessageIds.filter((id) => !messageIds.includes(id));
|
||||
|
||||
if (!newMessageIds?.length) {
|
||||
languageTranslations.delete(chatId);
|
||||
if (!languageTranslations.size) {
|
||||
PENDING_TRANSLATIONS.delete(toLanguageCode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
languageTranslations.set(chatId, newMessageIds);
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
cursor: var(--custom-cursor, default);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
@ -54,6 +54,10 @@ const Checkbox: FC<OwnProps> = ({
|
||||
const labelRef = useRef<HTMLLabelElement>(null);
|
||||
|
||||
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
onChange(event);
|
||||
}
|
||||
@ -61,7 +65,7 @@ const Checkbox: FC<OwnProps> = ({
|
||||
if (onCheck) {
|
||||
onCheck(event.currentTarget.checked);
|
||||
}
|
||||
}, [onChange, onCheck]);
|
||||
}, [disabled, onChange, onCheck]);
|
||||
|
||||
function handleClick(event: React.MouseEvent) {
|
||||
if (event.target !== labelRef.current) {
|
||||
|
||||
@ -56,6 +56,7 @@ import {
|
||||
updateListedTopicIds,
|
||||
updateChatFullInfo,
|
||||
replaceChatFullInfo,
|
||||
updateUserFullInfo,
|
||||
} from '../../reducers';
|
||||
import {
|
||||
selectChat, selectUser, selectChatListType, selectIsChatPinned,
|
||||
@ -73,6 +74,7 @@ import {
|
||||
isChatChannel,
|
||||
isChatSuperGroup,
|
||||
isUserBot,
|
||||
isUserId,
|
||||
} from '../../helpers';
|
||||
import { formatShareText, parseChooseParameter, processDeepLink } from '../../../util/deeplink';
|
||||
import { updateGroupCall } from '../../reducers/calls';
|
||||
@ -2158,6 +2160,39 @@ addActionHandler('openDeleteChatFolderModal', async (global, actions, payload):
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('updateChatDetectedLanguage', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId, detectedLanguage } = payload;
|
||||
|
||||
global = getGlobal();
|
||||
global = updateChat(global, chatId, {
|
||||
detectedLanguage,
|
||||
});
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('togglePeerTranslations', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, isEnabled } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
const result = await callApi('togglePeerTranslations', { chat, isEnabled });
|
||||
|
||||
if (result === undefined) return;
|
||||
|
||||
global = getGlobal();
|
||||
if (isUserId(chatId)) {
|
||||
global = updateUserFullInfo(global, chatId, {
|
||||
isTranslationDisabled: isEnabled ? undefined : true,
|
||||
});
|
||||
} else {
|
||||
global = updateChatFullInfo(global, chatId, {
|
||||
isTranslationDisabled: isEnabled ? undefined : true,
|
||||
});
|
||||
}
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
async function loadChats(
|
||||
listType: 'active' | 'archived',
|
||||
offsetId?: string,
|
||||
|
||||
@ -44,6 +44,7 @@ import {
|
||||
removeOutlyingList,
|
||||
removeRequestedMessageTranslation,
|
||||
replaceScheduledMessages,
|
||||
replaceSettings,
|
||||
replaceThreadParam,
|
||||
safeReplacePinnedIds,
|
||||
safeReplaceViewportIds,
|
||||
@ -63,6 +64,7 @@ import {
|
||||
import {
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectTranslationLanguage,
|
||||
selectCurrentChat,
|
||||
selectCurrentMessageList,
|
||||
selectDraft,
|
||||
@ -1493,10 +1495,13 @@ addActionHandler('forwardToSavedMessages', (global, actions, payload): ActionRet
|
||||
|
||||
addActionHandler('requestMessageTranslation', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, id, toLanguageCode = selectLanguageCode(global), tabId = getCurrentTabId(),
|
||||
chatId, id, toLanguageCode = selectTranslationLanguage(global), tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
global = updateRequestedMessageTranslation(global, chatId, id, toLanguageCode, tabId);
|
||||
global = replaceSettings(global, {
|
||||
translationLanguage: toLanguageCode,
|
||||
});
|
||||
|
||||
return global;
|
||||
});
|
||||
@ -1511,6 +1516,20 @@ addActionHandler('showOriginalMessage', (global, actions, payload): ActionReturn
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('markMessagesTranslationPending', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, messageIds, toLanguageCode = selectLanguageCode(global),
|
||||
} = payload;
|
||||
|
||||
messageIds.forEach((id) => {
|
||||
global = updateMessageTranslation(global, chatId, id, toLanguageCode, {
|
||||
isPending: true,
|
||||
});
|
||||
});
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('translateMessages', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, messageIds, toLanguageCode = selectLanguageCode(global),
|
||||
@ -1519,11 +1538,7 @@ addActionHandler('translateMessages', (global, actions, payload): ActionReturnTy
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return undefined;
|
||||
|
||||
messageIds.forEach((id) => {
|
||||
global = updateMessageTranslation(global, chatId, id, toLanguageCode, {
|
||||
isPending: true,
|
||||
});
|
||||
});
|
||||
actions.markMessagesTranslationPending({ chatId, messageIds, toLanguageCode });
|
||||
|
||||
callApi('translateText', {
|
||||
chat,
|
||||
|
||||
@ -4,7 +4,7 @@ import { IS_ELECTRON } from '../../../config';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
import {
|
||||
exitMessageSelectMode, replaceTabThreadParam, updateCurrentMessageList,
|
||||
exitMessageSelectMode, replaceTabThreadParam, updateCurrentMessageList, updateRequestedChatTranslation,
|
||||
} from '../../reducers';
|
||||
import {
|
||||
selectChat, selectCurrentMessageList, selectTabState,
|
||||
@ -175,3 +175,8 @@ addActionHandler('closeChatlistModal', (global, actions, payload): ActionReturnT
|
||||
chatlistModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('requestChatTranslation', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId, toLanguageCode, tabId = getCurrentTabId() } = payload;
|
||||
return updateRequestedChatTranslation(global, chatId, toLanguageCode, tabId);
|
||||
});
|
||||
|
||||
@ -42,8 +42,9 @@ import {
|
||||
selectSender,
|
||||
selectChatScheduledMessages,
|
||||
selectTabState,
|
||||
selectRequestedTranslationLanguage,
|
||||
selectRequestedMessageTranslationLanguage,
|
||||
selectPinnedIds,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
} from '../../selectors';
|
||||
import { compact, findLast } from '../../../util/iteratees';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
@ -771,21 +772,23 @@ addActionHandler('closeSeenByModal', (global, actions, payload): ActionReturnTyp
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('openMessageLanguageModal', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId, id, tabId = getCurrentTabId() } = payload;
|
||||
addActionHandler('openChatLanguageModal', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const activeLanguage = selectRequestedTranslationLanguage(global, chatId, id, tabId);
|
||||
const activeLanguage = messageId
|
||||
? selectRequestedMessageTranslationLanguage(global, chatId, messageId, tabId)
|
||||
: selectRequestedChatTranslationLanguage(global, chatId, tabId);
|
||||
|
||||
return updateTabState(global, {
|
||||
messageLanguageModal: { chatId, messageId: id, activeLanguage },
|
||||
chatLanguageModal: { chatId, messageId, activeLanguage },
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeMessageLanguageModal', (global, actions, payload): ActionReturnType => {
|
||||
addActionHandler('closeChatLanguageModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
messageLanguageModal: undefined,
|
||||
chatLanguageModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import { IS_OPUS_SUPPORTED, isWebpSupported } from '../../util/windowEnvironment
|
||||
import { getChatTitle, isUserId } from './chats';
|
||||
import { getGlobal } from '../index';
|
||||
import { areSortedArraysIntersecting, unique } from '../../util/iteratees';
|
||||
import { getServerTime } from '../../util/serverTime';
|
||||
|
||||
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
|
||||
|
||||
@ -246,10 +247,21 @@ export function getMessageContentFilename(message: ApiMessage) {
|
||||
return baseFilename;
|
||||
}
|
||||
|
||||
export function isGeoLiveExpired(message: ApiMessage, timestamp = Date.now() / 1000) {
|
||||
export function isGeoLiveExpired(message: ApiMessage) {
|
||||
const { location } = message.content;
|
||||
if (location?.type !== 'geoLive') return false;
|
||||
return (timestamp - (message.date || 0) >= location.period);
|
||||
return getServerTime() - (message.date || 0) >= location.period;
|
||||
}
|
||||
|
||||
export function isMessageTranslatable(message: ApiMessage, allowOutgoing?: boolean) {
|
||||
const { text, game } = message.content;
|
||||
|
||||
const isLocal = isMessageLocal(message);
|
||||
const isServiceNotification = isServiceNotificationMessage(message);
|
||||
const isAction = isActionMessage(message);
|
||||
|
||||
return Boolean(text?.text.length && !message.emojiOnlyCount && !game && (allowOutgoing || !message.isOutgoing)
|
||||
&& !isLocal && !isServiceNotification && !isAction && !message.isScheduled);
|
||||
}
|
||||
|
||||
export function getMessageSingleInlineButton(message: ApiMessage) {
|
||||
|
||||
@ -221,6 +221,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
isConnectionStatusMinimized: true,
|
||||
shouldArchiveAndMuteNewNonContact: false,
|
||||
canTranslate: false,
|
||||
canTranslateChats: true,
|
||||
doNotTranslate: [],
|
||||
canDisplayChatInTitle: true,
|
||||
shouldAllowHttpTransport: true,
|
||||
|
||||
@ -79,6 +79,40 @@ export function updateMessageTranslations<T extends GlobalState>(
|
||||
return global;
|
||||
}
|
||||
|
||||
export function updateRequestedChatTranslation<T extends GlobalState>(
|
||||
global: T, chatId: string, toLanguageCode?: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const tabState = selectTabState(global, tabId);
|
||||
global = updateTabState(global, {
|
||||
requestedTranslations: {
|
||||
...tabState.requestedTranslations,
|
||||
byChatId: {
|
||||
...tabState.requestedTranslations.byChatId,
|
||||
[chatId]: {
|
||||
toLanguage: toLanguageCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function removeRequestedChatTranslation<T extends GlobalState>(
|
||||
global: T, chatId: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const tabState = selectTabState(global, tabId);
|
||||
|
||||
global = updateTabState(global, {
|
||||
requestedTranslations: {
|
||||
...tabState.requestedTranslations,
|
||||
byChatId: omit(tabState.requestedTranslations.byChatId, [chatId]),
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function updateRequestedMessageTranslation<T extends GlobalState>(
|
||||
global: T, chatId: string, messageId: number, toLanguageCode: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
|
||||
@ -12,12 +12,15 @@ import {
|
||||
getHasAdminRight,
|
||||
isChatSuperGroup,
|
||||
} from '../helpers';
|
||||
import { selectBot, selectUser } from './users';
|
||||
import {
|
||||
selectBot, selectIsCurrentUserPremium, selectUser, selectUserFullInfo,
|
||||
} from './users';
|
||||
import {
|
||||
ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE, SERVICE_NOTIFICATIONS_USER_ID,
|
||||
} from '../../config';
|
||||
import { selectTabState } from './tabs';
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { IS_TRANSLATION_SUPPORTED } from '../../util/windowEnvironment';
|
||||
|
||||
export function selectChat<T extends GlobalState>(global: T, chatId: string): ApiChat | undefined {
|
||||
return global.chats.byId[chatId];
|
||||
@ -61,7 +64,7 @@ export function selectChatOnlineCount<T extends GlobalState>(global: T, chat: Ap
|
||||
|
||||
return fullInfo.members.reduce((onlineCount, { userId }) => {
|
||||
if (
|
||||
userId !== global.currentUserId
|
||||
!selectIsChatWithSelf(global, userId)
|
||||
&& global.users.byId[userId]
|
||||
&& isUserOnline(global.users.byId[userId], global.users.statusesById[userId])
|
||||
) {
|
||||
@ -263,3 +266,43 @@ export function selectCanShareFolder<T extends GlobalState>(global: T, folderId:
|
||||
return selectCanInviteToChat(global, chatId);
|
||||
});
|
||||
}
|
||||
|
||||
export function selectShouldDetectChatLanguage<T extends GlobalState>(
|
||||
global: T, chatId: string,
|
||||
) {
|
||||
const chat = selectChat(global, chatId);
|
||||
const fullInfo = isUserId(chatId) ? selectUserFullInfo(global, chatId) : selectChatFullInfo(global, chatId);
|
||||
if (!chat || !fullInfo) return false;
|
||||
const { canTranslateChats } = global.settings.byKey;
|
||||
|
||||
const isPremium = selectIsCurrentUserPremium(global);
|
||||
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
||||
|
||||
return IS_TRANSLATION_SUPPORTED && canTranslateChats && isPremium && !isSavedMessages;
|
||||
}
|
||||
|
||||
export function selectCanTranslateChat<T extends GlobalState>(
|
||||
global: T, chatId: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return false;
|
||||
|
||||
const requestedTranslation = selectRequestedChatTranslationLanguage(global, chatId, tabId);
|
||||
if (requestedTranslation) return true; // Prevent translation dropping on reevaluation
|
||||
|
||||
const isLanguageDetectable = selectShouldDetectChatLanguage(global, chatId);
|
||||
const detectedLanguage = chat.detectedLanguage;
|
||||
|
||||
const { doNotTranslate } = global.settings.byKey;
|
||||
|
||||
return Boolean(isLanguageDetectable && detectedLanguage && !doNotTranslate.includes(detectedLanguage));
|
||||
}
|
||||
|
||||
export function selectRequestedChatTranslationLanguage<T extends GlobalState>(
|
||||
global: T, chatId: string,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const { requestedTranslations } = selectTabState(global, tabId);
|
||||
|
||||
return requestedTranslations.byChatId[chatId]?.toLanguage;
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
GENERAL_TOPIC_ID, REPLIES_USER_ID, SERVICE_NOTIFICATIONS_USER_ID,
|
||||
} from '../../config';
|
||||
import {
|
||||
selectChat, selectChatFullInfo, selectIsChatWithSelf,
|
||||
selectChat, selectChatFullInfo, selectIsChatWithSelf, selectRequestedChatTranslationLanguage,
|
||||
} from './chats';
|
||||
import {
|
||||
selectBot,
|
||||
@ -50,7 +50,7 @@ import {
|
||||
isUserRightBanned,
|
||||
canSendReaction,
|
||||
getAllowedAttachmentOptions,
|
||||
isLocalMessageId, isMessageFailed,
|
||||
isLocalMessageId, isMessageFailed, isMessageTranslatable,
|
||||
} from '../helpers';
|
||||
import { findLast } from '../../util/iteratees';
|
||||
import { selectIsStickerFavorite } from './symbols';
|
||||
@ -58,6 +58,7 @@ import { getServerTime } from '../../util/serverTime';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import { selectTabState } from './tabs';
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { IS_TRANSLATION_SUPPORTED } from '../../util/windowEnvironment';
|
||||
|
||||
const MESSAGE_EDIT_ALLOWED_TIME = 172800; // 48 hours
|
||||
|
||||
@ -1271,10 +1272,11 @@ export function selectMessageTranslations<T extends GlobalState>(
|
||||
return selectChatTranslations(global, chatId)?.byLangCode[toLanguageCode] || {};
|
||||
}
|
||||
|
||||
export function selectRequestedTranslationLanguage<T extends GlobalState>(
|
||||
global: T, chatId: string, messageId: number, tabId = getCurrentTabId(),
|
||||
export function selectRequestedMessageTranslationLanguage<T extends GlobalState>(
|
||||
global: T, chatId: string, messageId: number, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): string | undefined {
|
||||
return selectTabState(global, tabId).requestedTranslations.byChatId[chatId]?.manualMessages?.[messageId];
|
||||
const requestedInChat = selectTabState(global, tabId).requestedTranslations.byChatId[chatId];
|
||||
return requestedInChat?.toLanguage || requestedInChat?.manualMessages?.[messageId];
|
||||
}
|
||||
|
||||
export function selectForwardsCanBeSentToChat<T extends GlobalState>(
|
||||
@ -1315,3 +1317,19 @@ export function selectForwardsCanBeSentToChat<T extends GlobalState>(
|
||||
|| (isPlainText && !canSendPlainText);
|
||||
});
|
||||
}
|
||||
|
||||
export function selectCanTranslateMessage<T extends GlobalState>(
|
||||
global: T, message: ApiMessage, detectedLanguage?: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const { canTranslate: isTranslationEnabled, doNotTranslate } = global.settings.byKey;
|
||||
|
||||
const canTranslateLanguage = !detectedLanguage || !doNotTranslate.includes(detectedLanguage);
|
||||
|
||||
const isTranslatable = isMessageTranslatable(message);
|
||||
|
||||
// Separate translations are disabled when chat translation enabled
|
||||
const chatRequestedLanguage = selectRequestedChatTranslationLanguage(global, message.chatId, tabId);
|
||||
|
||||
return IS_TRANSLATION_SUPPORTED && isTranslationEnabled && canTranslateLanguage && isTranslatable
|
||||
&& !chatRequestedLanguage;
|
||||
}
|
||||
|
||||
@ -15,3 +15,7 @@ export function selectLanguageCode<T extends GlobalState>(global: T) {
|
||||
export function selectCanSetPasscode<T extends GlobalState>(global: T) {
|
||||
return global.authRememberMe && global.isCacheApiSupported;
|
||||
}
|
||||
|
||||
export function selectTranslationLanguage<T extends GlobalState>(global: T) {
|
||||
return global.settings.byKey.translationLanguage || selectLanguageCode(global);
|
||||
}
|
||||
|
||||
@ -564,9 +564,9 @@ export type TabState = {
|
||||
requestedTranslations: {
|
||||
byChatId: Record<string, ChatRequestedTranslations>;
|
||||
};
|
||||
messageLanguageModal?: {
|
||||
chatLanguageModal?: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
messageId?: number;
|
||||
activeLanguage?: string;
|
||||
};
|
||||
|
||||
@ -1445,11 +1445,11 @@ export interface ActionPayloads {
|
||||
disableContextMenuHint: undefined;
|
||||
focusNextReply: WithTabId | undefined;
|
||||
|
||||
openMessageLanguageModal: {
|
||||
openChatLanguageModal: {
|
||||
chatId: string;
|
||||
id: number;
|
||||
messageId?: number;
|
||||
} & WithTabId;
|
||||
closeMessageLanguageModal: WithTabId | undefined;
|
||||
closeChatLanguageModal: WithTabId | undefined;
|
||||
|
||||
// poll result
|
||||
openPollResults: {
|
||||
@ -1640,6 +1640,10 @@ export interface ActionPayloads {
|
||||
about: string;
|
||||
photo?: File;
|
||||
} & WithTabId;
|
||||
updateChatDetectedLanguage: {
|
||||
chatId: string;
|
||||
detectedLanguage?: string;
|
||||
};
|
||||
toggleSignatures: {
|
||||
chatId: string;
|
||||
isEnabled: boolean;
|
||||
@ -1734,6 +1738,16 @@ export interface ActionPayloads {
|
||||
url: string;
|
||||
} & WithTabId;
|
||||
|
||||
requestChatTranslation: {
|
||||
chatId: string;
|
||||
toLanguageCode?: string;
|
||||
} & WithTabId;
|
||||
|
||||
togglePeerTranslations: {
|
||||
chatId: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
|
||||
// Messages
|
||||
setEditingDraft: {
|
||||
text?: ApiFormattedText;
|
||||
@ -1799,6 +1813,11 @@ export interface ActionPayloads {
|
||||
id: number;
|
||||
} & WithTabId;
|
||||
|
||||
markMessagesTranslationPending: {
|
||||
chatId: string;
|
||||
messageIds: number[];
|
||||
toLanguageCode?: string;
|
||||
};
|
||||
translateMessages: {
|
||||
chatId: string;
|
||||
messageIds: number[];
|
||||
|
||||
@ -4,14 +4,16 @@ import { detectLanguage } from '../util/languageDetection';
|
||||
|
||||
import useSyncEffect from './useSyncEffect';
|
||||
|
||||
export default function useTextLanguage(text?: string) {
|
||||
const [language, setLanguage] = useState<string>();
|
||||
export default function useTextLanguage(text?: string, isDisabled?: boolean) {
|
||||
const [language, setLanguage] = useState<string | undefined>();
|
||||
|
||||
useSyncEffect(() => {
|
||||
if (text) {
|
||||
if (text && !isDisabled) {
|
||||
detectLanguage(text).then(setLanguage);
|
||||
} else {
|
||||
setLanguage(undefined);
|
||||
}
|
||||
}, [text]);
|
||||
}, [isDisabled, text]);
|
||||
|
||||
return language;
|
||||
}
|
||||
|
||||
@ -1305,6 +1305,7 @@ messages.getTopReactions#bb8125ba limit:int hash:long = messages.Reactions;
|
||||
messages.getRecentReactions#39461db2 limit:int hash:long = messages.Reactions;
|
||||
messages.clearRecentReactions#9dfeefb4 = Bool;
|
||||
messages.getExtendedMedia#84f80814 peer:InputPeer id:Vector<int> = Updates;
|
||||
messages.togglePeerTranslations#e47cb579 flags:# disabled:flags.0?true peer:InputPeer = Bool;
|
||||
messages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp;
|
||||
messages.requestAppWebView#8c5a3b3c flags:# write_allowed:flags.0?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = AppWebViewResult;
|
||||
updates.getState#edd4882a = updates.State;
|
||||
|
||||
@ -166,6 +166,7 @@
|
||||
"messages.getExtendedMedia",
|
||||
"messages.getBotApp",
|
||||
"messages.requestAppWebView",
|
||||
"messages.togglePeerTranslations",
|
||||
"updates.getState",
|
||||
"updates.getDifference",
|
||||
"updates.getChannelDifference",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -49,6 +49,15 @@
|
||||
.icon-volume-3:before {
|
||||
content: "\e991";
|
||||
}
|
||||
.icon-hand-stop:before {
|
||||
content: "\e9c3";
|
||||
}
|
||||
.icon-more-circle:before {
|
||||
content: "\e9c4";
|
||||
}
|
||||
.icon-close-circle:before {
|
||||
content: "\e9c5";
|
||||
}
|
||||
.icon-settings-filled:before {
|
||||
content: "\e9c1";
|
||||
}
|
||||
|
||||
@ -97,6 +97,8 @@ export interface ISettings extends NotifySettings, Record<string, any> {
|
||||
isConnectionStatusMinimized: boolean;
|
||||
shouldArchiveAndMuteNewNonContact?: boolean;
|
||||
canTranslate: boolean;
|
||||
canTranslateChats: boolean;
|
||||
translationLanguage?: string;
|
||||
doNotTranslate: string[];
|
||||
canDisplayChatInTitle: boolean;
|
||||
shouldShowLoginCodeInChatList?: boolean;
|
||||
|
||||
@ -64,7 +64,9 @@ export async function loadModule<B extends Bundles>(bundleName: B) {
|
||||
await loadBundle(bundleName);
|
||||
}
|
||||
|
||||
export function getModuleFromMemory<B extends Bundles, M extends BundleModules<B>>(bundleName: B, moduleName: M) {
|
||||
export function getModuleFromMemory<B extends Bundles, M extends BundleModules<B>>(
|
||||
bundleName: B, moduleName: M,
|
||||
): ImportedBundles[B][M] | undefined {
|
||||
const bundle = MEMORY_CACHE[bundleName] as ImportedBundles[B];
|
||||
|
||||
if (!bundle) {
|
||||
|
||||
74
src/util/primitives/LimitedMap.ts
Normal file
74
src/util/primitives/LimitedMap.ts
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* A Map that has a limited size. When the limit is reached, the oldest entry is removed.
|
||||
* Ignores last access time, only cares about insertion order.
|
||||
*/
|
||||
export default class LimitedMap<K, V> {
|
||||
private map: Map<K, V>;
|
||||
|
||||
private insertionQueue: Set<K>;
|
||||
|
||||
constructor(private limit: number) {
|
||||
this.map = new Map();
|
||||
this.insertionQueue = new Set<K>();
|
||||
}
|
||||
|
||||
public get(key: K): V | undefined {
|
||||
return this.map.get(key);
|
||||
}
|
||||
|
||||
public set(key: K, value: V): this {
|
||||
if (this.map.size === this.limit) {
|
||||
const keyToRemove = Array.from(this.insertionQueue).shift();
|
||||
if (keyToRemove) {
|
||||
this.map.delete(keyToRemove);
|
||||
this.insertionQueue.delete(keyToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
this.map.set(key, value);
|
||||
this.insertionQueue.add(key);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public delete(key: K): boolean {
|
||||
const result = this.map.delete(key);
|
||||
if (result) {
|
||||
this.insertionQueue.delete(key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.map.clear();
|
||||
this.insertionQueue.clear();
|
||||
}
|
||||
|
||||
public forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
|
||||
this.map.forEach(callbackfn, thisArg);
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.map.size;
|
||||
}
|
||||
|
||||
public get [Symbol.toStringTag](): string {
|
||||
return this.map[Symbol.toStringTag];
|
||||
}
|
||||
|
||||
public [Symbol.iterator](): IterableIterator<[K, V]> {
|
||||
return this.map[Symbol.iterator]();
|
||||
}
|
||||
|
||||
public entries(): IterableIterator<[K, V]> {
|
||||
return this.map.entries();
|
||||
}
|
||||
|
||||
public keys(): IterableIterator<K> {
|
||||
return this.map.keys();
|
||||
}
|
||||
|
||||
public values(): IterableIterator<V> {
|
||||
return this.map.values();
|
||||
}
|
||||
}
|
||||
@ -108,14 +108,24 @@ export function hexToRgb(hex: string): RGBAColor {
|
||||
};
|
||||
}
|
||||
|
||||
export function lerpRgb(start: RGBAColor, end: RGBAColor, interpolationRatio: number): RGBAColor {
|
||||
const r = Math.round(lerp(start.r, end.r, interpolationRatio));
|
||||
const g = Math.round(lerp(start.g, end.g, interpolationRatio));
|
||||
const b = Math.round(lerp(start.b, end.b, interpolationRatio));
|
||||
const a = start.a !== undefined
|
||||
? Math.round(lerp(start.a!, end.a!, interpolationRatio))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
r, g, b, a,
|
||||
};
|
||||
}
|
||||
|
||||
function applyColorAnimationStep(startIndex: number, endIndex: number, interpolationRatio: number = 1) {
|
||||
colors.forEach(({ property, colors: propertyColors }) => {
|
||||
const r = Math.round(lerp(propertyColors[startIndex].r, propertyColors[endIndex].r, interpolationRatio));
|
||||
const g = Math.round(lerp(propertyColors[startIndex].g, propertyColors[endIndex].g, interpolationRatio));
|
||||
const b = Math.round(lerp(propertyColors[startIndex].b, propertyColors[endIndex].b, interpolationRatio));
|
||||
const a = propertyColors[startIndex].a !== undefined
|
||||
? Math.round(lerp(propertyColors[startIndex].a!, propertyColors[endIndex].a!, interpolationRatio))
|
||||
: undefined;
|
||||
const {
|
||||
r, g, b, a,
|
||||
} = lerpRgb(propertyColors[startIndex], propertyColors[endIndex], interpolationRatio);
|
||||
|
||||
const roundedA = a !== undefined ? Math.round((a / 255) * 10 ** DECIMAL_PLACES) / 10 ** DECIMAL_PLACES : undefined;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user