diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 0e5a0a8e0..5a499e824 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -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; }) { - 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({ diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 2c4652302..3b66b28bc 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -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"; diff --git a/src/components/common/Composer.scss b/src/components/common/Composer.scss index 261b44e7b..e31af8474 100644 --- a/src/components/common/Composer.scss +++ b/src/components/common/Composer.scss @@ -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 { diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 8bc9101e7..5ba2cf48e 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -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 = ({ canScheduleUntilOnline, isReady, isMobile, - onDropHide, - onFocus, - onBlur, editingMessage, chatId, threadId, @@ -376,7 +379,6 @@ const Composer: FC = ({ quickReplyMessages, quickReplies, canSendQuickReplies, - onForward, webPagePreview, noWebPage, isContactRequirePremium, @@ -386,6 +388,11 @@ const Composer: FC = ({ canPlayEffect, shouldPlayEffect, maxMessageLength, + isSilentPosting, + onDropHide, + onFocus, + onBlur, + onForward, }) => { const { sendMessage, @@ -412,9 +419,11 @@ const Composer: FC = ({ 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(null); @@ -785,21 +794,21 @@ const Composer: FC = ({ 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 = ({ : 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 = ({ if (!currentMessageList && !storyId) { return; } + isSilent = isSilent || isSilentPosting; const { text, entities } = parseHtmlAsFormattedText(getHtml()); if (!text && !attachmentsToSend.length) { @@ -1018,6 +1028,8 @@ const Composer: FC = ({ return; } + isSilent = isSilent || isSilentPosting; + let currentAttachments = attachments; if (activeVoiceRecording) { @@ -1129,7 +1141,7 @@ const Composer: FC = ({ threadId, queryId, scheduledAt, - isSilent, + isSilent: isSilent || isSilentPosting, }); return; } @@ -1197,6 +1209,8 @@ const Composer: FC = ({ return; } + isSilent = isSilent || isSilentPosting; + if (isInScheduledList || isScheduleRequested) { forceShowSymbolMenu(); requestCalendar((scheduledAt) => { @@ -1225,6 +1239,8 @@ const Composer: FC = ({ return; } + isSilent = isSilent || isSilentPosting; + sticker = { ...sticker, isPreloadedGlobally: true, @@ -1261,6 +1277,8 @@ const Composer: FC = ({ return; } + isSilent = isSilent || isSilentPosting; + if (isInScheduledList || isScheduleRequested) { requestCalendar((scheduledAt) => { handleMessageSchedule({ @@ -1308,7 +1326,7 @@ const Composer: FC = ({ }); closePollModal(); } else { - sendMessage({ messageList: currentMessageList, poll }); + sendMessage({ messageList: currentMessageList, poll, isSilent: isSilentPosting }); closePollModal(); } }); @@ -1378,6 +1396,17 @@ const Composer: FC = ({ }); }); + 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 = ({ } }, [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 = ({ || 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 = ({ 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 = ({ 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 = ({ 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 = ({ {isInlineBotLoading && Boolean(inlineBotId) && ( )} - {withScheduledButton && ( - - )} - {Boolean(botKeyboardMessageId) && !activeVoiceRecording && !editingMessage && ( - - - - )} + + {!hasText && ( + <> + {isChannel && ( + + )} + {withScheduledButton && ( + + )} + {Boolean(botKeyboardMessageId) && !activeVoiceRecording && !editingMessage && ( + + + + )} + + )} + )} {activeVoiceRecording && Boolean(currentRecordTime) && ( @@ -1968,7 +2041,7 @@ const Composer: FC = ({ className={buildClassName('view-once', isViewOnceEnabled && 'active')} round color="secondary" - ariaLabel={lang('Chat.PlayOnceVoiceMessageTooltip')} + ariaLabel={oldLang('Chat.PlayOnceVoiceMessageTooltip')} onClick={toogleViewOnceEnabled} > @@ -1994,7 +2067,7 @@ const Composer: FC = ({ onClick={handleLikeStory} onContextMenu={handleStoryPickerContextMenu} onMouseDown={handleBeforeStoryPickerContextMenu} - ariaLabel={lang('AccDescrLike')} + ariaLabel={oldLang('AccDescrLike')} ref={storyReactionRef} > {sentStoryReaction && ( @@ -2023,7 +2096,7 @@ const Composer: FC = ({ 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( 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( canPlayEffect, shouldPlayEffect, maxMessageLength, + isSilentPosting, }; }, )(Composer)); diff --git a/src/components/middle/composer/AttachMenu.tsx b/src/components/middle/composer/AttachMenu.tsx index a795d5024..f79b3fcb5 100644 --- a/src/components/middle/composer/AttachMenu.tsx +++ b/src/components/middle/composer/AttachMenu.tsx @@ -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 = ({ editingMessage && canEditMedia ? ( = ({ = ({ return ( ) : ( { - 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 => { diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 36eceb7ef..9a6041d3b 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -1051,6 +1051,10 @@ export interface ActionPayloads { isMuted?: boolean; mutedUntil?: number; }; + updateChatSilentPosting: { + chatId: string; + isEnabled: boolean; + }; updateChat: { chatId: string; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 00cd74f8e..e2b2ad538 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -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 { 'MediaViewDownloading': { 'count': V; }; + 'ComposerPlaceholderTopic': { + 'topic': V; + }; 'ChannelManagementLinkDiscussion': { 'group': V; 'channel': V;