Composer: Add silent post button and hide them on text (#5749)

This commit is contained in:
zubiden 2025-03-21 14:02:27 +04:00 committed by Alexander Zinchuk
parent 440001b938
commit 7522a16188
10 changed files with 233 additions and 91 deletions

View File

@ -735,27 +735,26 @@ async function getFullChannelInfo(
};
}
export function updateChatMutedState({
chat, isMuted, mutedUntil = 0,
export function updateChatNotifySettings({
chat, settings,
}: {
chat: ApiChat; isMuted?: boolean; mutedUntil?: number;
chat: ApiChat; settings: Partial<ApiPeerNotifySettings>;
}) {
if (isMuted && !mutedUntil) {
mutedUntil = MAX_INT_32;
}
invokeRequest(new GramJs.account.UpdateNotifySettings({
peer: new GramJs.InputNotifyPeer({
peer: buildInputPeer(chat.id, chat.accessHash),
}),
settings: new GramJs.InputPeerNotifySettings({ muteUntil: mutedUntil }),
settings: new GramJs.InputPeerNotifySettings({
muteUntil: settings.mutedUntil,
showPreviews: settings.shouldShowPreviews,
silent: settings.isSilentPosting,
}),
}));
sendApiUpdate({
'@type': 'updateChatNotifySettings',
chatId: chat.id,
settings: {
mutedUntil,
},
settings,
});
void requestChatUpdate({

View File

@ -1271,6 +1271,18 @@
"AriaOpenBotMenu" = "Open bot menu";
"AriaOpenSymbolMenu" = "Choose emoji, sticker or GIF";
"AriaComposerOpenScheduled" = "Open scheduled messages";
"AriaComposerBotKeyboard" = "Show bot keyboard";
"AriaComposerSilentPostingEnable" = "Enable silent notifications.";
"AriaComposerSilentPostingDisable" = "Disable silent notifications.";
"ComposerSilentPostingEnabledTootlip" = "Subscribers will receive a silent notification.";
"ComposerSilentPostingDisabledTootlip" = "Subscribers will be notified when you post.";
"ComposerPlaceholder" = "Message";
"ComposerPlaceholderBroadcast" = "Broadcast";
"ComposerPlaceholderBroadcastSilent" = "Silent Broadcast";
"ComposerPlaceholderTopic" = "Message in {topic}";
"ComposerPlaceholderTopicGeneral" = "Message in General";
"ComposerStoryPlaceholderLocked" = "Replies restricted";
"ComposerPlaceholderNoText" = "Text not allowed";
"AriaComposerCancelVoice" = "Cancel voice recording";
"PreviewForwardedMessage_one" = "{count} forwarded message";
"PreviewForwardedMessage_other" = "{count} forwarded messages";

View File

@ -292,6 +292,24 @@
color: #fff;
}
}
.composer-action-buttons-container {
width: auto;
position: relative;
+ .AttachMenu {
margin-left: var(--action-button-compact-fix);
}
}
.composer-action-buttons {
display: flex;
top: 0;
right: 0;
left: auto;
width: auto;
height: auto;
}
}
.mobile-symbol-menu-button {
@ -384,25 +402,32 @@
}
.message-input-wrapper {
--action-button-size: var(--base-height, 3.5rem);
--action-button-compact-fix: -1rem;
display: flex;
@media (max-width: 600px) {
--action-button-size: 2.875rem;
--action-button-compact-fix: -0.6875rem;
}
.input-scroller {
margin-right: 0.5rem;
padding-right: 0.25rem;
--_scroller-right-gap: calc((var(--action-button-size) + var(--action-button-compact-fix) - 0.125rem));
margin-right: calc(-1 * var(--_scroller-right-gap));
padding-right: var(--_scroller-right-gap);
}
> .Spinner {
align-self: center;
--spinner-size: 1.5rem;
margin-right: -0.5rem;
margin-right: 0.5rem;
}
> .AttachMenu > .Button,
> .Button {
.composer-action-button {
flex-shrink: 0;
background: none !important;
width: var(--base-height, 3.5rem);
height: var(--base-height, 3.5rem);
width: var(--action-button-size);
height: var(--action-button-size);
margin: 0;
padding: 0;
align-self: flex-end;
@ -411,17 +436,8 @@
color: var(--color-composer-button);
}
+ .Button, + .AttachMenu {
margin-left: -1rem;
}
@media (max-width: 600px) {
width: 2.875rem;
height: 2.875rem;
+ .Button, + .AttachMenu {
margin-left: -0.6875rem;
}
+ .composer-action-button {
margin-left: var(--action-button-compact-fix);
}
&.bot-menu {

View File

@ -66,6 +66,7 @@ import {
isSystemBot,
isUserId,
} from '../../global/helpers';
import { getChatNotifySettings } from '../../global/helpers/notifications';
import {
selectBot,
selectCanPlayAnimatedEmojis,
@ -86,6 +87,8 @@ import {
selectIsReactionPickerOpen,
selectIsRightColumnShown,
selectNewestMessageWithBotKeyboardButtons,
selectNotifyDefaults,
selectNotifyException,
selectNoWebPage,
selectPeerStory,
selectPerformanceSettingsValue,
@ -126,6 +129,7 @@ import useDerivedState from '../../hooks/useDerivedState';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
import useFlag from '../../hooks/useFlag';
import useGetSelectionRange from '../../hooks/useGetSelectionRange';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
@ -170,6 +174,7 @@ import ReactionSelector from '../middle/message/reactions/ReactionSelector';
import Button from '../ui/Button';
import ResponsiveHoverButton from '../ui/ResponsiveHoverButton';
import Spinner from '../ui/Spinner';
import Transition from '../ui/Transition';
import Avatar from './Avatar';
import Icon from './icons/Icon';
import ReactionAnimatedEmoji from './reactions/ReactionAnimatedEmoji';
@ -272,6 +277,7 @@ type StateProps =
canPlayEffect?: boolean;
shouldPlayEffect?: boolean;
maxMessageLength: number;
isSilentPosting?: boolean;
};
enum MainButtonState {
@ -304,9 +310,6 @@ const Composer: FC<OwnProps & StateProps> = ({
canScheduleUntilOnline,
isReady,
isMobile,
onDropHide,
onFocus,
onBlur,
editingMessage,
chatId,
threadId,
@ -376,7 +379,6 @@ const Composer: FC<OwnProps & StateProps> = ({
quickReplyMessages,
quickReplies,
canSendQuickReplies,
onForward,
webPagePreview,
noWebPage,
isContactRequirePremium,
@ -386,6 +388,11 @@ const Composer: FC<OwnProps & StateProps> = ({
canPlayEffect,
shouldPlayEffect,
maxMessageLength,
isSilentPosting,
onDropHide,
onFocus,
onBlur,
onForward,
}) => {
const {
sendMessage,
@ -412,9 +419,11 @@ const Composer: FC<OwnProps & StateProps> = ({
saveEffectInDraft,
setReactionEffect,
hideEffectInComposer,
updateChatSilentPosting,
} = getActions();
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const inputRef = useRef<HTMLDivElement>(null);
@ -785,21 +794,21 @@ const Composer: FC<OwnProps & StateProps> = ({
const notificationNumber = customEmojiNotificationNumber.current;
if (!notificationNumber) {
showNotification({
message: lang('UnlockPremiumEmojiHint'),
message: oldLang('UnlockPremiumEmojiHint'),
action: {
action: 'openPremiumModal',
payload: { initialSection: 'animated_emoji' },
},
actionText: lang('PremiumMore'),
actionText: oldLang('PremiumMore'),
});
} else {
showNotification({
message: lang('UnlockPremiumEmojiHint2'),
message: oldLang('UnlockPremiumEmojiHint2'),
action: {
action: 'openChat',
payload: { id: currentUserId, shouldReplaceHistory: true },
},
actionText: lang('Open'),
actionText: oldLang('Open'),
});
}
customEmojiNotificationNumber.current = Number(!notificationNumber);
@ -910,7 +919,7 @@ const Composer: FC<OwnProps & StateProps> = ({
: slowMode.seconds - secondsSinceLastMessage!;
showDialog({
data: {
message: lang('SlowModeHint', formatMediaDuration(secondsRemaining)),
message: oldLang('SlowModeHint', formatMediaDuration(secondsRemaining)),
isSlowMode: true,
hasErrorKey: false,
},
@ -942,6 +951,7 @@ const Composer: FC<OwnProps & StateProps> = ({
if (!currentMessageList && !storyId) {
return;
}
isSilent = isSilent || isSilentPosting;
const { text, entities } = parseHtmlAsFormattedText(getHtml());
if (!text && !attachmentsToSend.length) {
@ -1018,6 +1028,8 @@ const Composer: FC<OwnProps & StateProps> = ({
return;
}
isSilent = isSilent || isSilentPosting;
let currentAttachments = attachments;
if (activeVoiceRecording) {
@ -1129,7 +1141,7 @@ const Composer: FC<OwnProps & StateProps> = ({
threadId,
queryId,
scheduledAt,
isSilent,
isSilent: isSilent || isSilentPosting,
});
return;
}
@ -1197,6 +1209,8 @@ const Composer: FC<OwnProps & StateProps> = ({
return;
}
isSilent = isSilent || isSilentPosting;
if (isInScheduledList || isScheduleRequested) {
forceShowSymbolMenu();
requestCalendar((scheduledAt) => {
@ -1225,6 +1239,8 @@ const Composer: FC<OwnProps & StateProps> = ({
return;
}
isSilent = isSilent || isSilentPosting;
sticker = {
...sticker,
isPreloadedGlobally: true,
@ -1261,6 +1277,8 @@ const Composer: FC<OwnProps & StateProps> = ({
return;
}
isSilent = isSilent || isSilentPosting;
if (isInScheduledList || isScheduleRequested) {
requestCalendar((scheduledAt) => {
handleMessageSchedule({
@ -1308,7 +1326,7 @@ const Composer: FC<OwnProps & StateProps> = ({
});
closePollModal();
} else {
sendMessage({ messageList: currentMessageList, poll });
sendMessage({ messageList: currentMessageList, poll, isSilent: isSilentPosting });
closePollModal();
}
});
@ -1378,6 +1396,17 @@ const Composer: FC<OwnProps & StateProps> = ({
});
});
const handleToggleSilentPosting = useLastCallback(() => {
const newValue = !isSilentPosting;
updateChatSilentPosting({ chatId, isEnabled: newValue });
showNotification({
localId: 'silentPosting',
icon: newValue ? 'mute' : 'unmute',
message: lang(`ComposerSilentPosting${newValue ? 'Enabled' : 'Disabled'}Tootlip`),
});
});
useEffect(() => {
if (isRightColumnShown && isMobile) {
closeSymbolMenu();
@ -1396,10 +1425,11 @@ const Composer: FC<OwnProps & StateProps> = ({
}
}, [isSelectModeActive, enableHover, disableHover, isReady]);
const withBotMenuButton = isChatWithBot && botMenuButton?.type === 'webApp' && !editingMessage;
const isBotMenuButtonOpen = useDerivedState(() => {
return withBotMenuButton && !getHtml() && !activeVoiceRecording;
}, [withBotMenuButton, getHtml, activeVoiceRecording]);
const hasText = useDerivedState(() => Boolean(getHtml()), [getHtml]);
const withBotMenuButton = isChatWithBot && botMenuButton?.type === 'webApp' && !editingMessage
&& messageListType === 'thread';
const isBotMenuButtonOpen = withBotMenuButton && !hasText && !activeVoiceRecording;
const [timedPlaceholderLangKey, timedPlaceholderDate] = useMemo(() => {
if (slowMode?.nextSendDate) {
@ -1419,11 +1449,33 @@ const Composer: FC<OwnProps & StateProps> = ({
|| isCustomSendMenuOpen || Boolean(activeVoiceRecording) || attachments.length > 0 || isInputHasFocus;
const isReactionSelectorOpen = isComposerHasFocus && !isReactionPickerOpen && isInStoryViewer && !isAttachMenuOpen
&& !isSymbolMenuOpen;
const placeholderForForumAsMessages = chat?.isForum && chat?.isForumAsMessages && threadId === MAIN_THREAD_ID
? (replyToTopic
? lang('Chat.InputPlaceholderReplyInTopic', replyToTopic.title)
: lang('Message.Placeholder.MessageInGeneral'))
: undefined;
const placeholder = useMemo(() => {
if (activeVoiceRecording && windowWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER) {
return '';
}
if (!isComposerBlocked) {
if (botKeyboardPlaceholder) return botKeyboardPlaceholder;
if (inputPlaceholder) return inputPlaceholder;
if (chat?.isForum && chat?.isForumAsMessages && threadId === MAIN_THREAD_ID) {
return replyToTopic
? lang('ComposerPlaceholderTopic', { topic: replyToTopic.title })
: lang('ComposerPlaceholderTopicGeneral');
}
if (isChannel) {
return lang(isSilentPosting ? 'ComposerPlaceholderBroadcastSilent' : 'ComposerPlaceholderBroadcast');
}
return lang('ComposerPlaceholder');
}
if (isInStoryViewer) return lang('ComposerStoryPlaceholderLocked');
return lang('ComposerPlaceholderNoText');
}, [
activeVoiceRecording, botKeyboardPlaceholder, chat, inputPlaceholder, isChannel, isComposerBlocked,
isInStoryViewer, isSilentPosting, lang, replyToTopic, threadId, windowWidth,
]);
useEffect(() => {
if (isComposerHasFocus) {
@ -1452,7 +1504,7 @@ const Composer: FC<OwnProps & StateProps> = ({
if (areVoiceMessagesNotAllowed) {
if (!canSendVoiceByPrivacy) {
showNotification({
message: lang('VoiceMessagesRestrictedByPrivacy', chat?.title),
message: oldLang('VoiceMessagesRestrictedByPrivacy', chat?.title),
});
} else if (!canSendVoices) {
showAllowedMessageTypesNotification({ chatId });
@ -1788,9 +1840,10 @@ const Composer: FC<OwnProps & StateProps> = ({
round
color="translucent"
onClick={isSendAsMenuOpen ? closeSendAsMenu : handleSendAsMenuOpen}
ariaLabel={lang('SendMessageAsTitle')}
ariaLabel={oldLang('SendMessageAsTitle')}
className={buildClassName(
'send-as-button',
'composer-action-button',
shouldAnimateSendAsButtonRef.current && 'appear-animation',
)}
>
@ -1840,13 +1893,7 @@ const Composer: FC<OwnProps & StateProps> = ({
isReady={isReady}
isActive={!hasAttachments}
getHtml={getHtml}
placeholder={
activeVoiceRecording && windowWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER
? ''
: (!isComposerBlocked
? (botKeyboardPlaceholder || inputPlaceholder || lang(placeholderForForumAsMessages || 'Message'))
: isInStoryViewer ? lang('StoryRepliesLocked') : lang('Chat.PlaceholderTextNotAllowed'))
}
placeholder={placeholder}
timedPlaceholderDate={timedPlaceholderDate}
timedPlaceholderLangKey={timedPlaceholderLangKey}
forcedPlaceholder={inlineBotHelp}
@ -1866,29 +1913,55 @@ const Composer: FC<OwnProps & StateProps> = ({
{isInlineBotLoading && Boolean(inlineBotId) && (
<Spinner color="gray" />
)}
{withScheduledButton && (
<Button
round
faded
className="scheduled-button"
color="translucent"
onClick={handleAllScheduledClick}
ariaLabel="Open scheduled messages"
>
<Icon name="schedule" />
</Button>
)}
{Boolean(botKeyboardMessageId) && !activeVoiceRecording && !editingMessage && (
<ResponsiveHoverButton
className={isBotKeyboardOpen ? 'activated' : ''}
round
color="translucent"
onActivate={openBotKeyboard}
ariaLabel="Open bot command keyboard"
>
<Icon name="bot-command" />
</ResponsiveHoverButton>
)}
<Transition
className="composer-action-buttons-container"
slideClassName="composer-action-buttons"
activeKey={Number(hasText)}
direction="inverse"
name="slideFadeAndroid"
>
{!hasText && (
<>
{isChannel && (
<Button
round
faded
className="composer-action-button"
color="translucent"
onClick={handleToggleSilentPosting}
ariaLabel={lang(
isSilentPosting ? 'AriaComposerSilentPostingDisable' : 'AriaComposerSilentPostingEnable',
)}
>
<Icon name={isSilentPosting ? 'mute' : 'unmute'} />
</Button>
)}
{withScheduledButton && (
<Button
round
faded
className="composer-action-button scheduled-button"
color="translucent"
onClick={handleAllScheduledClick}
ariaLabel={lang('AriaComposerOpenScheduled')}
>
<Icon name="schedule" />
</Button>
)}
{Boolean(botKeyboardMessageId) && !activeVoiceRecording && !editingMessage && (
<ResponsiveHoverButton
className={buildClassName('composer-action-button', isBotKeyboardOpen && 'activated')}
round
color="translucent"
onActivate={openBotKeyboard}
ariaLabel={lang('AriaComposerBotKeyboard')}
>
<Icon name="bot-command" />
</ResponsiveHoverButton>
)}
</>
)}
</Transition>
</>
)}
{activeVoiceRecording && Boolean(currentRecordTime) && (
@ -1968,7 +2041,7 @@ const Composer: FC<OwnProps & StateProps> = ({
className={buildClassName('view-once', isViewOnceEnabled && 'active')}
round
color="secondary"
ariaLabel={lang('Chat.PlayOnceVoiceMessageTooltip')}
ariaLabel={oldLang('Chat.PlayOnceVoiceMessageTooltip')}
onClick={toogleViewOnceEnabled}
>
<Icon name="view-once" />
@ -1994,7 +2067,7 @@ const Composer: FC<OwnProps & StateProps> = ({
onClick={handleLikeStory}
onContextMenu={handleStoryPickerContextMenu}
onMouseDown={handleBeforeStoryPickerContextMenu}
ariaLabel={lang('AccDescrLike')}
ariaLabel={oldLang('AccDescrLike')}
ref={storyReactionRef}
>
{sentStoryReaction && (
@ -2023,7 +2096,7 @@ const Composer: FC<OwnProps & StateProps> = ({
disabled={areVoiceMessagesNotAllowed}
allowDisabledClick
noFastClick
ariaLabel={lang(sendButtonAriaLabel)}
ariaLabel={oldLang(sendButtonAriaLabel)}
onClick={mainButtonHandler}
onContextMenu={
mainButtonState === MainButtonState.Send && canShowCustomSendMenu ? handleContextMenu : undefined
@ -2140,6 +2213,11 @@ export default memo(withGlobal<OwnProps>(
const canSendQuickReplies = isChatWithUser && !isChatWithBot && !isInScheduledList && !isChatWithSelf;
const noWebPage = selectNoWebPage(global, chatId, threadId);
const isSilentPosting = chat && getChatNotifySettings(
chat,
selectNotifyDefaults(global),
selectNotifyException(global, chatId),
)?.isSilentPosting;
const areEffectsSupported = isChatWithUser && !isChatWithBot
&& !isInScheduledList && !isChatWithSelf && type !== 'story' && chatId !== SERVICE_NOTIFICATIONS_USER_ID;
@ -2227,6 +2305,7 @@ export default memo(withGlobal<OwnProps>(
canPlayEffect,
shouldPlayEffect,
maxMessageLength,
isSilentPosting,
};
},
)(Composer));

View File

@ -20,6 +20,7 @@ import {
getMessageWebPagePhoto,
getMessageWebPageVideo,
} from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import { getDebugLogs } from '../../../util/debugConsole';
import { validateFiles } from '../../../util/files';
import { openSystemFilesDialog } from '../../../util/systemFilesDialog';
@ -175,7 +176,7 @@ const AttachMenu: FC<OwnProps> = ({
editingMessage && canEditMedia ? (
<ResponsiveHoverButton
id="replace-menu-button"
className={isAttachMenuOpen ? 'AttachMenu--button activated' : 'AttachMenu--button'}
className={buildClassName('AttachMenu--button composer-action-button', isAttachMenuOpen && 'activated')}
round
color="translucent"
onActivate={handleToggleAttachMenu}
@ -189,7 +190,7 @@ const AttachMenu: FC<OwnProps> = ({
<ResponsiveHoverButton
id="attach-menu-button"
disabled={Boolean(editingMessage)}
className={isAttachMenuOpen ? 'AttachMenu--button activated' : 'AttachMenu--button'}
className={buildClassName('AttachMenu--button composer-action-button', isAttachMenuOpen && 'activated')}
round
color="translucent"
onActivate={handleToggleAttachMenu}

View File

@ -46,7 +46,7 @@ const BotMenuButton: FC<OwnProps> = ({
return (
<Button
className={buildClassName('bot-menu', isOpen && 'open')}
className={buildClassName('composer-action-button bot-menu', isOpen && 'open')}
round
color="translucent"
disabled={isDisabled}

View File

@ -92,7 +92,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
const [contextMenuAnchor, setContextMenuAnchor] = useState<IAnchorPosition | undefined>(undefined);
const symbolMenuButtonClassName = buildClassName(
'mobile-symbol-menu-button',
'composer-action-button mobile-symbol-menu-button',
!isReady && 'not-ready',
isSymbolMenuLoaded
? (isSymbolMenuOpen && 'menu-opened')
@ -157,7 +157,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
</Button>
) : (
<ResponsiveHoverButton
className={buildClassName('symbol-menu-button', isSymbolMenuOpen && 'activated')}
className={buildClassName('composer-action-button symbol-menu-button', isSymbolMenuOpen && 'activated')}
round
color="translucent"
onActivate={handleActivateSymbolMenu}

View File

@ -23,6 +23,7 @@ import {
CHAT_LIST_LOAD_SLICE,
DEBUG,
GLOBAL_SUGGESTED_CHANNELS_ID,
MAX_INT_32,
RE_TG_LINK,
SAVED_FOLDER_ID,
SERVICE_NOTIFICATIONS_USER_ID,
@ -625,13 +626,29 @@ addActionHandler('requestSavedDialogUpdate', async (global, actions, payload): P
});
addActionHandler('updateChatMutedState', (global, actions, payload): ActionReturnType => {
const { chatId, isMuted, mutedUntil } = payload;
const { chatId, isMuted } = payload;
let { mutedUntil } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
if (isMuted && !mutedUntil) {
mutedUntil = MAX_INT_32;
}
void callApi('updateChatNotifySettings', { chat, settings: { mutedUntil } });
});
addActionHandler('updateChatSilentPosting', (global, actions, payload): ActionReturnType => {
const { chatId, isEnabled } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
void callApi('updateChatMutedState', { chat, isMuted, mutedUntil });
void callApi('updateChatNotifySettings', { chat, settings: { isSilentPosting: isEnabled } });
});
addActionHandler('updateTopicMutedState', (global, actions, payload): ActionReturnType => {

View File

@ -1051,6 +1051,10 @@ export interface ActionPayloads {
isMuted?: boolean;
mutedUntil?: number;
};
updateChatSilentPosting: {
chatId: string;
isEnabled: boolean;
};
updateChat: {
chatId: string;

View File

@ -1087,6 +1087,17 @@ export interface LangPair {
'AriaOpenBotMenu': undefined;
'AriaOpenSymbolMenu': undefined;
'AriaComposerOpenScheduled': undefined;
'AriaComposerBotKeyboard': undefined;
'AriaComposerSilentPostingEnable': undefined;
'AriaComposerSilentPostingDisable': undefined;
'ComposerSilentPostingEnabledTootlip': undefined;
'ComposerSilentPostingDisabledTootlip': undefined;
'ComposerPlaceholder': undefined;
'ComposerPlaceholderBroadcast': undefined;
'ComposerPlaceholderBroadcastSilent': undefined;
'ComposerPlaceholderTopicGeneral': undefined;
'ComposerStoryPlaceholderLocked': undefined;
'ComposerPlaceholderNoText': undefined;
'AriaComposerCancelVoice': undefined;
'PreviewEditMessage': undefined;
'FileDropZoneTitle': undefined;
@ -1706,6 +1717,9 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'MediaViewDownloading': {
'count': V;
};
'ComposerPlaceholderTopic': {
'topic': V;
};
'ChannelManagementLinkDiscussion': {
'group': V;
'channel': V;