Introduce Granular Media Permissions (#2576)

This commit is contained in:
Alexander Zinchuk 2023-02-28 18:43:18 +01:00
parent 06a7bf1162
commit 3cc27156cb
32 changed files with 1035 additions and 307 deletions

View File

@ -1238,6 +1238,14 @@ export function deleteChatMember(chat: ApiChat, user: ApiUser) {
changeInfo: true,
inviteUsers: true,
pinMessages: true,
manageTopics: true,
sendPhotos: true,
sendVideos: true,
sendRoundvideos: true,
sendAudios: true,
sendVoices: true,
sendDocs: true,
sendPlain: true,
},
untilDate: MAX_INT_32,
});

View File

@ -263,7 +263,7 @@ export function sendMessage(
// This is expected to arrive after `updateMessageSendSucceeded` which replaces the local ID,
// so in most cases this will be simply ignored
setTimeout(() => {
const timeout = setTimeout(() => {
onUpdate({
'@type': localMessage.isScheduled ? 'updateScheduledMessage' : 'updateMessage',
id: localMessage.id,
@ -326,21 +326,31 @@ export function sendMessage(
const RequestClass = media ? GramJs.messages.SendMedia : GramJs.messages.SendMessage;
await invokeRequest(new RequestClass({
clearDraft: true,
message: text || '',
entities: entities ? entities.map(buildMtpMessageEntity) : undefined,
peer: buildInputPeer(chat.id, chat.accessHash),
randomId,
...(isSilent && { silent: isSilent }),
...(scheduledAt && { scheduleDate: scheduledAt }),
...(replyingTo && { replyToMsgId: replyingTo }),
...(replyingToTopId && { topMsgId: replyingToTopId }),
...(media && { media }),
...(noWebPage && { noWebpage: noWebPage }),
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
...(shouldUpdateStickerSetsOrder && { updateStickersetsOrder: shouldUpdateStickerSetsOrder }),
}), true);
try {
await invokeRequest(new RequestClass({
clearDraft: true,
message: text || '',
entities: entities ? entities.map(buildMtpMessageEntity) : undefined,
peer: buildInputPeer(chat.id, chat.accessHash),
randomId,
...(isSilent && { silent: isSilent }),
...(scheduledAt && { scheduleDate: scheduledAt }),
...(replyingTo && { replyToMsgId: replyingTo }),
...(replyingToTopId && { topMsgId: replyingToTopId }),
...(media && { media }),
...(noWebPage && { noWebpage: noWebPage }),
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
...(shouldUpdateStickerSetsOrder && { updateStickersetsOrder: shouldUpdateStickerSetsOrder }),
}), true, true);
} catch (error: any) {
onUpdate({
'@type': 'updateMessageSendFailed',
chatId: chat.id,
localId: localMessage.id,
error: error.message,
});
clearTimeout(timeout);
}
})();
return queue;

View File

@ -157,8 +157,15 @@ export interface ApiChatBannedRights {
changeInfo?: true;
inviteUsers?: true;
pinMessages?: true;
untilDate?: number;
manageTopics?: true;
sendPhotos?: true;
sendVideos?: true;
sendRoundvideos?: true;
sendAudios?: true;
sendVoices?: true;
sendDocs?: true;
sendPlain?: true;
untilDate?: number;
}
export interface ApiRestrictionReason {

View File

@ -243,9 +243,7 @@ export type ApiUpdateMessageSendFailed = {
'@type': 'updateMessageSendFailed';
chatId: string;
localId: number;
sendingState: {
'@type': 'messageSendingStateFailed';
};
error: string;
};
export type ApiUpdateCommonBoxMessages = {

View File

@ -18,4 +18,20 @@
width: 100%;
height: 100%;
}
.MessageOutgoingStatus--failed::before {
content: "";
display: block;
position: absolute;
inset: 0.25rem;
border-radius: 50%;
background: white;
}
.icon-message-failed {
background: none;
color: var(--color-error);
z-index: 1;
position: relative;
}
}

View File

@ -19,7 +19,11 @@ const MessageOutgoingStatus: FC<OwnProps> = ({ status }) => {
return (
<div className="MessageOutgoingStatus">
<Transition name="reveal" activeKey={Keys[status]}>
<i className={`icon-message-${status}`} />
{status === 'failed' ? (
<div className="MessageOutgoingStatus--failed">
<i className="icon-message-failed" />
</div>
) : <i className={`icon-message-${status}`} />}
</Transition>
</div>
);

View File

@ -7,7 +7,11 @@ import type { GlobalState } from '../../../global/types';
import type { ApiAttachMenuPeerType } from '../../../api/types';
import type { ISettings } from '../../../types';
import { CONTENT_TYPES_WITH_PREVIEW } from '../../../config';
import {
CONTENT_TYPES_WITH_PREVIEW, SUPPORTED_AUDIO_CONTENT_TYPES,
SUPPORTED_IMAGE_CONTENT_TYPES,
SUPPORTED_VIDEO_CONTENT_TYPES,
} from '../../../config';
import { IS_TOUCH_ENV } from '../../../util/environment';
import { openSystemFilesDialog } from '../../../util/systemFilesDialog';
import { validateFiles } from '../../../util/files';
@ -29,6 +33,10 @@ export type OwnProps = {
isButtonVisible: boolean;
canAttachMedia: boolean;
canAttachPolls: boolean;
canSendPhotos: boolean;
canSendVideos: boolean;
canSendDocuments: boolean;
canSendAudios: boolean;
isScheduled?: boolean;
attachBots: GlobalState['attachMenu']['bots'];
peerType?: ApiAttachMenuPeerType;
@ -43,6 +51,10 @@ const AttachMenu: FC<OwnProps> = ({
isButtonVisible,
canAttachMedia,
canAttachPolls,
canSendPhotos,
canSendVideos,
canSendDocuments,
canSendAudios,
attachBots,
peerType,
isScheduled,
@ -53,6 +65,9 @@ const AttachMenu: FC<OwnProps> = ({
const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag();
const [handleMouseEnter, handleMouseLeave, markMouseInside] = useMouseInside(isAttachMenuOpen, closeAttachMenu);
const canSendVideoAndPhoto = canSendPhotos && canSendVideos;
const canSendVideoOrPhoto = canSendPhotos || canSendVideos;
const [isAttachmentBotMenuOpen, markAttachmentBotMenuOpen, unmarkAttachmentBotMenuOpen] = useFlag();
useEffect(() => {
if (isAttachMenuOpen) {
@ -79,14 +94,19 @@ const AttachMenu: FC<OwnProps> = ({
const handleQuickSelect = useCallback(() => {
openSystemFilesDialog(
Array.from(CONTENT_TYPES_WITH_PREVIEW).join(','),
Array.from(canSendVideoAndPhoto ? CONTENT_TYPES_WITH_PREVIEW : (
canSendPhotos ? SUPPORTED_IMAGE_CONTENT_TYPES : SUPPORTED_VIDEO_CONTENT_TYPES
)).join(','),
(e) => handleFileSelect(e, true),
);
}, [handleFileSelect]);
}, [canSendPhotos, canSendVideoAndPhoto, handleFileSelect]);
const handleDocumentSelect = useCallback(() => {
openSystemFilesDialog('*', (e) => handleFileSelect(e, false));
}, [handleFileSelect]);
openSystemFilesDialog(!canSendDocuments && canSendAudios
? Array.from(SUPPORTED_AUDIO_CONTENT_TYPES).join(',') : (
'*'
), (e) => handleFileSelect(e, false));
}, [canSendAudios, canSendDocuments, handleFileSelect]);
const bots = useMemo(() => {
return Object.values(attachBots).filter((bot) => {
@ -141,8 +161,18 @@ const AttachMenu: FC<OwnProps> = ({
)}
{canAttachMedia && (
<>
<MenuItem icon="photo" onClick={handleQuickSelect}>{lang('AttachmentMenu.PhotoOrVideo')}</MenuItem>
<MenuItem icon="document" onClick={handleDocumentSelect}>{lang('AttachDocument')}</MenuItem>
{canSendVideoOrPhoto && (
<MenuItem icon="photo" onClick={handleQuickSelect}>
{lang(canSendVideoAndPhoto ? 'AttachmentMenu.PhotoOrVideo'
: (canSendPhotos ? 'InputAttach.Popover.Photo' : 'InputAttach.Popover.Video'))}
</MenuItem>
)}
{(canSendDocuments || canSendAudios)
&& (
<MenuItem icon="document" onClick={handleDocumentSelect}>
{lang(!canSendDocuments && canSendAudios ? 'InputAttach.Popover.Music' : 'AttachDocument')}
</MenuItem>
)}
</>
)}
{canAttachPolls && (

View File

@ -61,6 +61,8 @@ export type OwnProps = {
isReady?: boolean;
shouldSchedule?: boolean;
shouldSuggestCompression?: boolean;
shouldForceCompression?: boolean;
shouldForceAsFile?: boolean;
isForCurrentMessageList?: boolean;
onCaptionUpdate: (html: string) => void;
onSend: (sendCompressed: boolean, sendGrouped: boolean) => void;
@ -109,6 +111,8 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
customEmojiForEmoji,
attachmentSettings,
shouldSuggestCompression,
shouldForceCompression,
shouldForceAsFile,
isForCurrentMessageList,
onAttachmentsUpdate,
onCaptionUpdate,
@ -140,6 +144,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
const [shouldSendCompressed, setShouldSendCompressed] = useState(
shouldSuggestCompression ?? attachmentSettings.shouldCompress,
);
const isSendingCompressed = Boolean((shouldSendCompressed || shouldForceCompression) && !shouldForceAsFile);
const [shouldSendGrouped, setShouldSendGrouped] = useState(attachmentSettings.shouldSendGrouped);
const {
@ -241,15 +246,15 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
if (isOpen) {
const send = (shouldSchedule || shouldSendScheduled) ? onSendScheduled
: isSilent ? onSendSilent : onSend;
send(shouldSendCompressed, shouldSendGrouped);
send(isSendingCompressed, shouldSendGrouped);
updateAttachmentSettings({
shouldCompress: shouldSendCompressed,
shouldCompress: isSendingCompressed,
shouldSendGrouped,
});
}
}, [
isOpen, shouldSchedule, onSendScheduled, onSend, updateAttachmentSettings, shouldSendCompressed, shouldSendGrouped,
onSendSilent,
isOpen, shouldSchedule, onSendScheduled, onSendSilent, onSend, isSendingCompressed, shouldSendGrouped,
updateAttachmentSettings,
]);
const handleSendSilent = useCallback(() => {
@ -366,7 +371,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
return leftCharsBeforeLimit <= MAX_LEFT_CHARS_TO_SHOW ? leftCharsBeforeLimit : undefined;
}, [captionLimit, getHtml, renderingIsOpen]);
const isQuickGallery = shouldSendCompressed && hasOnlyMedia;
const isQuickGallery = isSendingCompressed && hasOnlyMedia;
const [areAllPhotos, areAllVideos, areAllAudios] = useMemo(() => {
if (!isQuickGallery || !renderingAttachments) return [false, false, false];
@ -413,7 +418,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
{hasMedia && (
<>
{
shouldSendCompressed ? (
!shouldForceAsFile && !shouldForceCompression && (isSendingCompressed ? (
// eslint-disable-next-line react/jsx-no-bind
<MenuItem icon="document" onClick={() => setShouldSendCompressed(false)}>
{lang(isMultiple ? 'Attachment.SendAsFiles' : 'Attachment.SendAsFile')}
@ -423,9 +428,9 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
<MenuItem icon="photo" onClick={() => setShouldSendCompressed(true)}>
{isMultiple ? 'Send All as Media' : 'Send as Media'}
</MenuItem>
)
))
}
{shouldSendCompressed && (
{isSendingCompressed && (
hasSpoiler ? (
<MenuItem icon="spoiler-disable" onClick={handleDisableSpoilers}>
{lang('Attachment.DisableSpoiler')}
@ -496,7 +501,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
{renderingAttachments.map((attachment, i) => (
<AttachmentModalItem
attachment={attachment}
shouldDisplayCompressed={shouldSendCompressed}
shouldDisplayCompressed={isSendingCompressed}
shouldDisplayGrouped={shouldSendGrouped}
isSingle={renderingAttachments.length === 1}
index={i}
@ -548,6 +553,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
onRemoveSymbol={onRemoveSymbol}
onEmojiSelect={onEmojiSelect}
isAttachmentModal
canSendPlainText
className="attachment-modal-symbol-menu with-menu-transitions"
/>
<MessageInput

View File

@ -508,6 +508,15 @@
text-overflow: ellipsis;
max-width: 100%;
&.with-icon {
display: inline-flex;
align-items: center;
}
.placeholder-icon {
margin-inline-end: 0.25rem;
}
@media (min-width: 600px) {
left: 0.75rem;
}

View File

@ -294,6 +294,7 @@ const Composer: FC<OwnProps & StateProps> = ({
callAttachBot,
addRecentCustomEmoji,
showNotification,
showAllowedMessageTypesNotification,
} = getActions();
const lang = useLang();
@ -355,8 +356,15 @@ const Composer: FC<OwnProps & StateProps> = ({
const [attachments, setAttachments] = useState<ApiAttachment[]>([]);
const hasAttachments = Boolean(attachments.length);
const {
canSendStickers, canSendGifs, canAttachMedia, canAttachPolls, canAttachEmbedLinks,
canSendVoices, canSendPlainText, canSendAudios, canSendVideos, canSendPhotos, canSendDocuments,
} = useMemo(() => getAllowedAttachmentOptions(chat, isChatWithBot), [chat, isChatWithBot]);
const {
shouldSuggestCompression,
shouldForceCompression,
shouldForceAsFile,
handleAppendFiles,
handleFileSelect,
onCaptionUpdate,
@ -367,6 +375,11 @@ const Composer: FC<OwnProps & StateProps> = ({
setHtml,
setAttachments,
fileSizeLimit,
chatId,
canSendAudios,
canSendVideos,
canSendPhotos,
canSendDocuments,
});
const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag();
@ -403,10 +416,6 @@ const Composer: FC<OwnProps & StateProps> = ({
}
}, [getHtml, isEditingRef, sendMessageAction]);
const {
canSendStickers, canSendGifs, canAttachMedia, canAttachPolls, canAttachEmbedLinks,
} = useMemo(() => getAllowedAttachmentOptions(chat, isChatWithBot), [chat, isChatWithBot]);
const isAdmin = chat && isChatAdmin(chat);
const slowMode = getChatSlowModeOptions(chat);
@ -492,6 +501,7 @@ const Composer: FC<OwnProps & StateProps> = ({
);
const insertHtmlAndUpdateCursor = useCallback((newHtml: string, inputId: string = EDITABLE_INPUT_ID) => {
if (inputId === EDITABLE_INPUT_ID && !canSendPlainText) return;
const selection = window.getSelection()!;
let messageInput: HTMLDivElement;
if (inputId === EDITABLE_INPUT_ID) {
@ -515,7 +525,7 @@ const Composer: FC<OwnProps & StateProps> = ({
requestAnimationFrame(() => {
focusEditableElement(messageInput);
});
}, [getHtml, setHtml]);
}, [canSendPlainText, getHtml, setHtml]);
const insertFormattedTextAndUpdateCursor = useCallback((
text: ApiFormattedText, inputId: string = EDITABLE_INPUT_ID,
@ -1060,6 +1070,12 @@ const Composer: FC<OwnProps & StateProps> = ({
insertHtmlAndUpdateCursor(newHtml, inputId);
}, [insertHtmlAndUpdateCursor]);
useEffect(() => {
if (canSendPlainText) return;
setHtml('');
}, [canSendPlainText, setHtml, attachments]);
const insertTextAndUpdateCursorAttachmentModal = useCallback((text: string) => {
insertTextAndUpdateCursor(text, EDITABLE_INPUT_MODAL_ID);
}, [insertTextAndUpdateCursor]);
@ -1107,7 +1123,7 @@ const Composer: FC<OwnProps & StateProps> = ({
}, [isSelectModeActive, enableHover, disableHover, isReady]);
const areVoiceMessagesNotAllowed = mainButtonState === MainButtonState.Record
&& (!canAttachMedia || !canSendVoiceByPrivacy);
&& (!canAttachMedia || !canSendVoiceByPrivacy || !canSendVoices);
const mainButtonHandler = useCallback(() => {
switch (mainButtonState) {
@ -1120,6 +1136,8 @@ const Composer: FC<OwnProps & StateProps> = ({
showNotification({
message: lang('VoiceMessagesRestrictedByPrivacy', chat?.title),
});
} else if (!canSendVoices) {
showAllowedMessageTypesNotification({ chatId });
}
} else {
startRecordingVoice();
@ -1143,7 +1161,7 @@ const Composer: FC<OwnProps & StateProps> = ({
}, [
mainButtonState, handleSend, handleEditComplete, activeVoiceRecording, requestCalendar, areVoiceMessagesNotAllowed,
canSendVoiceByPrivacy, showNotification, lang, chat?.title, startRecordingVoice, pauseRecordingVoice,
handleMessageSchedule,
handleMessageSchedule, chatId, showAllowedMessageTypesNotification, canSendVoices,
]);
const prevEditedMessage = usePrevious(editingMessage, true);
@ -1224,6 +1242,8 @@ const Composer: FC<OwnProps & StateProps> = ({
getHtml={getHtml}
isReady={isReady}
shouldSuggestCompression={shouldSuggestCompression}
shouldForceCompression={shouldForceCompression}
shouldForceAsFile={shouldForceAsFile}
isForCurrentMessageList={isForCurrentMessageList}
onCaptionUpdate={onCaptionUpdate}
onSendSilent={handleSendSilentAttachments}
@ -1335,37 +1355,43 @@ const Composer: FC<OwnProps & StateProps> = ({
/>
</Button>
)}
<SymbolMenuButton
chatId={chatId}
threadId={threadId}
isMobile={isMobile}
isReady={isReady}
isSymbolMenuOpen={isSymbolMenuOpen}
openSymbolMenu={openSymbolMenu}
closeSymbolMenu={closeSymbolMenu}
canSendStickers={canSendStickers}
canSendGifs={canSendGifs}
onGifSelect={handleGifSelect}
onStickerSelect={handleStickerSelect}
onCustomEmojiSelect={handleCustomEmojiSelect}
onRemoveSymbol={removeSymbol}
onEmojiSelect={insertTextAndUpdateCursor}
closeBotCommandMenu={closeBotCommandMenu}
closeSendAsMenu={closeSendAsMenu}
isSymbolMenuForced={isSymbolMenuForced}
/>
{(canSendPlainText || canSendGifs || canSendStickers) && (
<SymbolMenuButton
chatId={chatId}
threadId={threadId}
isMobile={isMobile}
isReady={isReady}
isSymbolMenuOpen={isSymbolMenuOpen}
openSymbolMenu={openSymbolMenu}
closeSymbolMenu={closeSymbolMenu}
canSendStickers={canSendStickers}
canSendGifs={canSendGifs}
onGifSelect={handleGifSelect}
onStickerSelect={handleStickerSelect}
onCustomEmojiSelect={handleCustomEmojiSelect}
onRemoveSymbol={removeSymbol}
onEmojiSelect={insertTextAndUpdateCursor}
closeBotCommandMenu={closeBotCommandMenu}
closeSendAsMenu={closeSendAsMenu}
isSymbolMenuForced={isSymbolMenuForced}
canSendPlainText={canSendPlainText}
/>
)}
<MessageInput
ref={inputRef}
id="message-input-text"
editableInputId={EDITABLE_INPUT_ID}
chatId={chatId}
canSendPlainText={canSendPlainText}
threadId={threadId}
isActive={!hasAttachments}
getHtml={getHtml}
placeholder={
activeVoiceRecording && windowWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER
? ''
: botKeyboardPlaceholder || lang('Message')
: (canSendPlainText
? (botKeyboardPlaceholder || lang('Message'))
: lang('Chat.PlaceholderTextNotAllowed'))
}
forcedPlaceholder={inlineBotHelp}
canAutoFocus={isReady && isForCurrentMessageList && !hasAttachments}
@ -1413,6 +1439,10 @@ const Composer: FC<OwnProps & StateProps> = ({
isButtonVisible={!activeVoiceRecording && !editingMessage}
canAttachMedia={canAttachMedia}
canAttachPolls={canAttachPolls}
canSendPhotos={canSendPhotos}
canSendVideos={canSendVideos}
canSendDocuments={canSendDocuments}
canSendAudios={canSendAudios}
onFileSelect={handleFileSelect}
onPollCreate={openPollModal}
isScheduled={shouldSchedule}

View File

@ -22,8 +22,8 @@ import './GifPicker.scss';
type OwnProps = {
className: string;
loadAndPlay: boolean;
canSendGifs: boolean;
onGifSelect: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
canSendGifs?: boolean;
onGifSelect?: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
};
type StateProps = {

View File

@ -54,6 +54,7 @@ type OwnProps = {
canAutoFocus: boolean;
shouldSuppressFocus?: boolean;
shouldSuppressTextFormatter?: boolean;
canSendPlainText?: boolean;
onUpdate: (html: string) => void;
onSuppressedFocus?: () => void;
onSend: () => void;
@ -102,6 +103,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
getHtml,
placeholder,
forcedPlaceholder,
canSendPlainText,
canAutoFocus,
noFocusInterception,
shouldSuppressFocus,
@ -117,6 +119,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
const {
editLastMessage,
replyToNextMessage,
showAllowedMessageTypesNotification,
} = getActions();
// eslint-disable-next-line no-null/no-null
@ -405,6 +408,11 @@ const MessageInput: FC<OwnProps & StateProps> = ({
}
}
function handleClick() {
if (isAttachmentModalInput || canSendPlainText) return;
showAllowedMessageTypesNotification({ chatId });
}
useEffect(() => {
if (IS_TOUCH_ENV) {
return;
@ -506,13 +514,17 @@ const MessageInput: FC<OwnProps & StateProps> = ({
return (
<div id={id} onClick={shouldSuppressFocus ? onSuppressedFocus : undefined} dir={lang.isRtl ? 'rtl' : undefined}>
<div className={buildClassName('custom-scroll', SCROLLER_CLASS)} onScroll={onScroll}>
<div
className={buildClassName('custom-scroll', SCROLLER_CLASS)}
onScroll={onScroll}
onClick={!isAttachmentModalInput && !canSendPlainText ? handleClick : undefined}
>
<div className="input-scroller-content">
<div
ref={inputRef}
id={editableInputId || EDITABLE_INPUT_ID}
className={className}
contentEditable
contentEditable={isAttachmentModalInput || canSendPlainText}
role="textbox"
dir="auto"
tabIndex={0}
@ -524,7 +536,18 @@ const MessageInput: FC<OwnProps & StateProps> = ({
onTouchCancel={IS_ANDROID ? processSelectionWithTimeout : undefined}
aria-label={placeholder}
/>
{!forcedPlaceholder && <span className="placeholder-text" dir="auto">{placeholder}</span>}
{!forcedPlaceholder && (
<span
className={buildClassName(
'placeholder-text',
!isAttachmentModalInput && !canSendPlainText && 'with-icon',
)}
dir="auto"
>
{!isAttachmentModalInput && !canSendPlainText && <i className="icon-lock-badge placeholder-icon" />}
{placeholder}
</span>
)}
<canvas ref={sharedCanvasRef} className="shared-canvas" />
<canvas ref={sharedCanvasHqRef} className="shared-canvas" />
<div ref={absoluteContainerRef} className="absolute-video-container" />

View File

@ -44,7 +44,7 @@ type OwnProps = {
threadId?: number;
className: string;
loadAndPlay: boolean;
canSendStickers: boolean;
canSendStickers?: boolean;
onStickerSelect: (
sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean, shouldUpdateStickerSetsOrder?: boolean,
) => void;

View File

@ -30,6 +30,7 @@ import Portal from '../../ui/Portal';
import './SymbolMenu.scss';
const ANIMATION_DURATION = 350;
const STICKERS_TAB_INDEX = 2;
export type OwnProps = {
chatId: string;
@ -55,6 +56,7 @@ export type OwnProps = {
addRecentCustomEmoji: GlobalActions['addRecentCustomEmoji'];
className?: string;
isAttachmentModal?: boolean;
canSendPlainText?: boolean;
positionX?: 'left' | 'right';
positionY?: 'top' | 'bottom';
transformOriginX?: number;
@ -83,6 +85,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
onClose,
onEmojiSelect,
isAttachmentModal,
canSendPlainText,
onCustomEmojiSelect,
onStickerSelect,
className,
@ -114,6 +117,12 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
onLoad();
}, [onLoad]);
// If we can't send plain text, we should always show the stickers tab
useEffect(() => {
if (canSendPlainText) return;
setActiveTab(STICKERS_TAB_INDEX);
}, [canSendPlainText]);
useEffect(() => {
if (!lastSyncTime) return;
if (isCurrentUserPremium) {
@ -218,8 +227,6 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
/>
);
case SymbolMenuTabs.Stickers:
if (!canSendStickers) return undefined;
return (
<StickerPicker
className="picker-tab"
@ -231,8 +238,6 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
/>
);
case SymbolMenuTabs.GIFs:
if (!canSendGifs || !onGifSelect) return undefined;
return (
<GifPicker
className="picker-tab"
@ -278,6 +283,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
onRemoveSymbol={onRemoveSymbol}
onSearchOpen={handleSearch}
isAttachmentModal={isAttachmentModal}
canSendPlainText={canSendPlainText}
/>
</>
);

View File

@ -44,6 +44,7 @@ type OwnProps = {
closeSendAsMenu?: VoidFunction;
isSymbolMenuForced?: boolean;
isAttachmentModal?: boolean;
canSendPlainText?: boolean;
className?: string;
};
@ -61,6 +62,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
onStickerSelect,
onGifSelect,
isAttachmentModal,
canSendPlainText,
onRemoveSymbol,
onEmojiSelect,
closeBotCommandMenu,
@ -196,6 +198,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
addRecentEmoji={addRecentEmoji}
addRecentCustomEmoji={addRecentCustomEmoji}
isAttachmentModal={isAttachmentModal}
canSendPlainText={canSendPlainText}
className={className}
positionX={isAttachmentModal ? positionX : undefined}
positionY={isAttachmentModal ? positionY : undefined}

View File

@ -11,6 +11,7 @@ type OwnProps = {
onRemoveSymbol: () => void;
onSearchOpen: (type: 'stickers' | 'gifs') => void;
isAttachmentModal?: boolean;
canSendPlainText?: boolean;
};
export enum SymbolMenuTabs {
@ -36,6 +37,7 @@ const SYMBOL_MENU_TAB_ICONS = {
const SymbolMenuFooter: FC<OwnProps> = ({
activeTab, onSwitchTab, onRemoveSymbol, onSearchOpen, isAttachmentModal,
canSendPlainText,
}) => {
const lang = useLang();
@ -78,8 +80,8 @@ const SymbolMenuFooter: FC<OwnProps> = ({
</Button>
)}
{renderTabButton(SymbolMenuTabs.Emoji)}
{renderTabButton(SymbolMenuTabs.CustomEmoji)}
{canSendPlainText && renderTabButton(SymbolMenuTabs.Emoji)}
{canSendPlainText && renderTabButton(SymbolMenuTabs.CustomEmoji)}
{!isAttachmentModal && renderTabButton(SymbolMenuTabs.Stickers)}
{!isAttachmentModal && renderTabButton(SymbolMenuTabs.GIFs)}

View File

@ -5,19 +5,36 @@ import type { ApiAttachment } from '../../../../api/types';
import buildAttachment from '../helpers/buildAttachment';
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
import {
SUPPORTED_AUDIO_CONTENT_TYPES,
SUPPORTED_IMAGE_CONTENT_TYPES,
SUPPORTED_VIDEO_CONTENT_TYPES,
} from '../../../../config';
export default function useAttachmentModal({
attachments,
fileSizeLimit,
setHtml,
setAttachments,
chatId,
canSendAudios,
canSendVideos,
canSendPhotos,
canSendDocuments,
}: {
attachments: ApiAttachment[];
fileSizeLimit: number;
setHtml: (html: string) => void;
setAttachments: (attachments: ApiAttachment[]) => void;
chatId: string;
canSendAudios?: boolean;
canSendVideos?: boolean;
canSendPhotos?: boolean;
canSendDocuments?: boolean;
}) {
const { openLimitReachedModal } = getActions();
const { openLimitReachedModal, showAllowedMessageTypesNotification } = getActions();
const [shouldForceAsFile, setShouldForceAsFile] = useState<boolean>(false);
const [shouldForceCompression, setShouldForceCompression] = useState<boolean>(false);
const [shouldSuggestCompression, setShouldSuggestCompression] = useState<boolean | undefined>(undefined);
const handleClearAttachments = useCallback(() => {
@ -28,18 +45,40 @@ export default function useAttachmentModal({
(newValue: ApiAttachment[] | ((current: ApiAttachment[]) => ApiAttachment[])) => {
const newAttachments = typeof newValue === 'function' ? newValue(attachments) : newValue;
if (!newAttachments.length) {
setAttachments(MEMO_EMPTY_ARRAY);
handleClearAttachments();
return;
}
if (newAttachments.some(({ size }) => size > fileSizeLimit)) {
if (newAttachments.some((attachment) => {
const type = getAttachmentType(attachment);
return (type === 'audio' && !canSendAudios && !canSendDocuments)
|| (type === 'video' && !canSendVideos && !canSendDocuments)
|| (type === 'image' && !canSendPhotos && !canSendDocuments)
|| (type === 'file' && !canSendDocuments);
})) {
showAllowedMessageTypesNotification({ chatId });
} else if (newAttachments.some(({ size }) => size > fileSizeLimit)) {
openLimitReachedModal({
limit: 'uploadMaxFileparts',
});
} else {
setAttachments(newAttachments);
const shouldForce = newAttachments.some((attachment) => {
const type = getAttachmentType(attachment);
return (type === 'audio' && !canSendAudios)
|| (type === 'video' && !canSendVideos)
|| (type === 'image' && !canSendPhotos);
});
setShouldForceAsFile(Boolean(shouldForce && canSendDocuments));
setShouldForceCompression(!canSendDocuments);
}
}, [attachments, fileSizeLimit, openLimitReachedModal, setAttachments],
}, [
attachments, canSendAudios, canSendDocuments, canSendPhotos, canSendVideos, chatId, fileSizeLimit,
handleClearAttachments, openLimitReachedModal, setAttachments, showAllowedMessageTypesNotification,
],
);
const handleAppendFiles = useCallback(async (files: File[], isSpoiler?: boolean) => {
@ -63,5 +102,23 @@ export default function useAttachmentModal({
onCaptionUpdate: setHtml,
handleClearAttachments,
handleSetAttachments,
shouldForceCompression,
shouldForceAsFile,
};
}
function getAttachmentType(attachment: ApiAttachment) {
if (SUPPORTED_IMAGE_CONTENT_TYPES.has(attachment.mimeType)) {
return 'image';
}
if (SUPPORTED_VIDEO_CONTENT_TYPES.has(attachment.mimeType)) {
return 'video';
}
if (SUPPORTED_AUDIO_CONTENT_TYPES.has(attachment.mimeType)) {
return 'audio';
}
return 'file';
}

View File

@ -0,0 +1,119 @@
import type React from '../../../lib/teact/teact';
import { useCallback, useEffect, useState } from '../../../lib/teact/teact';
import type { ApiChatBannedRights } from '../../../api/types';
const FLOATING_BUTTON_ANIMATION_TIMEOUT_MS = 250;
const MEDIA_PERMISSIONS: Array<keyof ApiChatBannedRights> = [
'embedLinks',
'sendPolls',
'sendPhotos',
'sendVideos',
'sendRoundvideos',
'sendVoices',
'sendAudios',
'sendDocs',
'sendStickers',
'sendGifs',
];
const MESSAGE_PERMISSIONS: typeof MEDIA_PERMISSIONS = [...MEDIA_PERMISSIONS, 'sendPlain'];
export default function useManagePermissions(defaultPermissions: ApiChatBannedRights | undefined) {
const [permissions, setPermissions] = useState<ApiChatBannedRights>({});
const [havePermissionChanged, setHavePermissionChanged] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setPermissions(defaultPermissions || {});
setHavePermissionChanged(false);
setTimeout(() => {
setIsLoading(false);
}, FLOATING_BUTTON_ANIMATION_TIMEOUT_MS);
}, [defaultPermissions]);
const handlePermissionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { name: targetName } = e.target;
const name = targetName as Exclude<keyof ApiChatBannedRights, 'untilDate'>;
function getUpdatedPermissionValue(value: true | undefined) {
return value ? undefined : true;
}
const oldPermissions = permissions;
let newPermissions: ApiChatBannedRights = {
...oldPermissions,
[name]: getUpdatedPermissionValue(oldPermissions[name]),
...(name === 'sendStickers' && {
sendGifs: getUpdatedPermissionValue(oldPermissions[name]),
}),
};
const checkMedia = () => {
const mediaPermissions = MEDIA_PERMISSIONS.map((key) => newPermissions[key]);
if (mediaPermissions.some((v) => !v)) {
newPermissions = {
...newPermissions,
sendMedia: undefined,
};
} else if (mediaPermissions.every(Boolean)) {
newPermissions = {
...newPermissions,
sendMedia: true,
};
}
};
if (name !== 'sendMedia') {
checkMedia();
} else {
newPermissions = {
...newPermissions,
...(MEDIA_PERMISSIONS.reduce((acc, key) => (
Object.assign(acc, { [key]: newPermissions.sendMedia })
), {})),
};
}
// Embed links can't be enabled if plain text is banned
if (name !== 'embedLinks' && !newPermissions.embedLinks && newPermissions.sendPlain) {
newPermissions = {
...newPermissions,
embedLinks: true,
};
}
if (name !== 'sendPlain' && !newPermissions.embedLinks && newPermissions.sendPlain) {
newPermissions = {
...newPermissions,
sendPlain: undefined,
};
}
if (name !== 'sendMedia') {
checkMedia();
}
const sendMessages = MESSAGE_PERMISSIONS.every((key) => newPermissions[key]);
newPermissions = {
...newPermissions,
sendMessages: sendMessages ? true : undefined,
};
setPermissions(newPermissions);
setHavePermissionChanged(!defaultPermissions || Object.keys(newPermissions).some((k) => {
const key = k as Exclude<keyof ApiChatBannedRights, 'untilDate'>;
return Boolean(defaultPermissions[key]) !== Boolean(newPermissions[key]);
}));
}, [defaultPermissions, permissions]);
return {
permissions,
isLoading,
havePermissionChanged,
handlePermissionChange,
setIsLoading,
};
}

View File

@ -100,6 +100,7 @@ const ManageChatRemovedUsers: FC<OwnProps & StateProps> = ({
<PrivateChatInfo
userId={member.userId}
status={getRemovedBy(member)}
forceShowSelf
/>
</ListItem>
))}

View File

@ -63,9 +63,24 @@ type StateProps = {
const GROUP_TITLE_EMPTY = 'Group title can\'t be empty';
const GROUP_MAX_DESCRIPTION = 255;
const ALL_PERMISSIONS: Array<keyof ApiChatBannedRights> = [
'sendMessages',
'embedLinks',
'sendPolls',
'changeInfo',
'inviteUsers',
'pinMessages',
'manageTopics',
'sendPhotos',
'sendVideos',
'sendRoundvideos',
'sendVoices',
'sendAudios',
'sendDocs',
];
// Some checkboxes control multiple rights, and some rights are not controlled from Permissions screen,
// so we need to define the amount manually
const TOTAL_PERMISSIONS_COUNT = 9;
const TOTAL_PERMISSIONS_COUNT = ALL_PERMISSIONS.length + 1;
const ManageGroup: FC<OwnProps & StateProps> = ({
chatId,
@ -245,16 +260,7 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
return 0;
}
let totalCount = [
'sendMessages',
'sendMedia',
'embedLinks',
'sendPolls',
'changeInfo',
'inviteUsers',
'pinMessages',
'manageTopics',
].filter(
let totalCount = ALL_PERMISSIONS.filter(
(key) => !chat.defaultBannedRights![key as keyof ApiChatBannedRights],
).length;
@ -347,7 +353,7 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
>
<span className="title">{lang('ChannelPermissions')}</span>
<span className="subtitle" dir="auto">
{enabledPermissionsCount}/{TOTAL_PERMISSIONS_COUNT}
{enabledPermissionsCount}/{TOTAL_PERMISSIONS_COUNT - (chat.isForum ? 0 : 1)}
</span>
</ListItem>
<ListItem

View File

@ -1,15 +1,18 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useCallback, useEffect, useMemo, useState,
memo, useCallback, useMemo, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import { ManagementScreens } from '../../../types';
import type { ApiChat, ApiChatBannedRights, ApiChatMember } from '../../../api/types';
import stopEvent from '../../../util/stopEvent';
import buildClassName from '../../../util/buildClassName';
import useLang from '../../../hooks/useLang';
import { selectChat } from '../../../global/selectors';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useManagePermissions from '../hooks/useManagePermissions';
import ListItem from '../../ui/ListItem';
import Checkbox from '../../ui/Checkbox';
@ -30,9 +33,11 @@ type StateProps = {
currentUserId?: string;
};
const FLOATING_BUTTON_ANIMATION_TIMEOUT_MS = 250;
const ITEM_HEIGHT = 24 + 32;
const BEFORE_ITEMS_COUNT = 2;
const ITEMS_COUNT = 9;
function getLangKeyForBannedRightKey(key: string) {
function getLangKeyForBannedRightKey(key: keyof ApiChatBannedRights) {
switch (key) {
case 'sendMessages':
return 'UserRestrictionsNoSend';
@ -52,6 +57,20 @@ function getLangKeyForBannedRightKey(key: string) {
return 'UserRestrictionsPinMessages';
case 'manageTopics':
return 'GroupPermission.NoManageTopics';
case 'sendPlain':
return 'UserRestrictionsNoSendText';
case 'sendDocs':
return 'UserRestrictionsNoSendDocs';
case 'sendRoundvideos':
return 'UserRestrictionsNoSendRound';
case 'sendVoices':
return 'UserRestrictionsNoSendVoice';
case 'sendAudios':
return 'UserRestrictionsNoSendMusic';
case 'sendVideos':
return 'UserRestrictionsNoSendVideos';
case 'sendPhotos':
return 'UserRestrictionsNoSendPhotos';
default:
return undefined;
}
@ -67,11 +86,10 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
}) => {
const { updateChatDefaultBannedRights } = getActions();
const [permissions, setPermissions] = useState<ApiChatBannedRights>({});
const [havePermissionChanged, setHavePermissionChanged] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const {
permissions, havePermissionChanged, isLoading, handlePermissionChange, setIsLoading,
} = useManagePermissions(chat?.defaultBannedRights);
const lang = useLang();
const { isForum } = chat || {};
useHistoryBack({
@ -92,30 +110,11 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
onScreenSelect(ManagementScreens.GroupUserPermissions);
}, [currentUserId, onChatMemberSelect, onScreenSelect]);
useEffect(() => {
setPermissions((chat?.defaultBannedRights) || {});
setHavePermissionChanged(false);
setTimeout(() => {
setIsLoading(false);
}, FLOATING_BUTTON_ANIMATION_TIMEOUT_MS);
}, [chat]);
const handlePermissionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { name } = e.target;
function getUpdatedPermissionValue(value: true | undefined) {
return value ? undefined : true;
}
setPermissions((p) => ({
...p,
[name]: getUpdatedPermissionValue(p[name as Exclude<keyof ApiChatBannedRights, 'untilDate'>]),
...(name === 'sendStickers' && {
sendGifs: getUpdatedPermissionValue(p[name]),
}),
}));
setHavePermissionChanged(true);
}, []);
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
const handleOpenMediaDropdown = useCallback((e: React.MouseEvent) => {
stopEvent(e);
setIsMediaDropdownOpen(!isMediaDropdownOpen);
}, [isMediaDropdownOpen]);
const handleSavePermissions = useCallback(() => {
if (!chat) {
@ -124,7 +123,7 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
setIsLoading(true);
updateChatDefaultBannedRights({ chatId: chat.id, bannedRights: permissions });
}, [chat, permissions, updateChatDefaultBannedRights]);
}, [chat, permissions, setIsLoading, updateChatDefaultBannedRights]);
const removedUsersCount = useMemo(() => {
if (!chat || !chat.fullInfo || !chat.fullInfo.kickedMembers) {
@ -150,10 +149,11 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
const { defaultBannedRights } = chat;
return Object.keys(bannedRights).reduce((result, key) => {
return Object.keys(bannedRights).reduce((result, k) => {
const key = k as keyof ApiChatBannedRights;
if (
!bannedRights[key as keyof ApiChatBannedRights]
|| (defaultBannedRights?.[key as keyof ApiChatBannedRights])
!bannedRights[key]
|| (defaultBannedRights?.[key])
|| key === 'sendInline' || key === 'viewMessages' || key === 'sendGames'
) {
return result;
@ -172,15 +172,19 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
}, [chat, lang]);
return (
<div className="Management">
<div
className="Management with-shifted-dropdown"
style={`--shift-height: ${ITEMS_COUNT * ITEM_HEIGHT}px;`
+ `--before-shift-height: ${BEFORE_ITEMS_COUNT * ITEM_HEIGHT}px;`}
>
<div className="custom-scroll">
<div className="section">
<div className="section without-bottom-shadow">
<h3 className="section-heading" dir="auto">{lang('ChannelPermissionsHeader')}</h3>
<div className="ListItem no-selection">
<Checkbox
name="sendMessages"
checked={!permissions.sendMessages}
name="sendPlain"
checked={!permissions.sendPlain}
label={lang('UserRestrictionsSend')}
blocking
onChange={handlePermissionChange}
@ -192,77 +196,158 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
checked={!permissions.sendMedia}
label={lang('UserRestrictionsSendMedia')}
blocking
rightIcon={isMediaDropdownOpen ? 'up' : 'down'}
onChange={handlePermissionChange}
onClickLabel={handleOpenMediaDropdown}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendStickers"
checked={!permissions.sendStickers && !permissions.sendGifs}
label={lang('UserRestrictionsSendStickers')}
blocking
onChange={handlePermissionChange}
/>
<div className="DropdownListTrap">
<div
className={buildClassName(
'DropdownList',
isMediaDropdownOpen && 'DropdownList--open',
)}
>
<div className="ListItem no-selection">
<Checkbox
name="sendPhotos"
checked={!permissions.sendPhotos}
label={lang('UserRestrictionsSendPhotos')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendVideos"
checked={!permissions.sendVideos}
label={lang('UserRestrictionsSendVideos')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendStickers"
checked={!permissions.sendStickers && !permissions.sendGifs}
label={lang('UserRestrictionsSendStickers')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendAudios"
checked={!permissions.sendAudios}
label={lang('UserRestrictionsSendMusic')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendDocs"
checked={!permissions.sendDocs}
label={lang('UserRestrictionsSendFiles')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendVoices"
checked={!permissions.sendVoices}
label={lang('UserRestrictionsSendVoices')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendRoundvideos"
checked={!permissions.sendRoundvideos}
label={lang('UserRestrictionsSendRound')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="embedLinks"
checked={!permissions.embedLinks}
label={lang('UserRestrictionsEmbedLinks')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendPolls"
checked={!permissions.sendPolls}
label={lang('UserRestrictionsSendPolls')}
blocking
onChange={handlePermissionChange}
/>
</div>
</div>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendPolls"
checked={!permissions.sendPolls}
label={lang('UserRestrictionsSendPolls')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="embedLinks"
checked={!permissions.embedLinks}
label={lang('UserRestrictionsEmbedLinks')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="inviteUsers"
checked={!permissions.inviteUsers}
label={lang('UserRestrictionsInviteUsers')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="pinMessages"
checked={!permissions.pinMessages}
label={lang('UserRestrictionsPinMessages')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="changeInfo"
checked={!permissions.changeInfo}
label={lang('UserRestrictionsChangeInfo')}
blocking
onChange={handlePermissionChange}
/>
</div>
{isForum && (
<div className={buildClassName('part', isMediaDropdownOpen && 'shifted')}>
<div className="ListItem no-selection">
<Checkbox
name="manageTopics"
checked={!permissions.manageTopics}
label={lang('CreateTopicsPermission')}
name="inviteUsers"
checked={!permissions.inviteUsers}
label={lang('UserRestrictionsInviteUsers')}
blocking
onChange={handlePermissionChange}
/>
</div>
)}
<div className="ListItem no-selection">
<Checkbox
name="pinMessages"
checked={!permissions.pinMessages}
label={lang('UserRestrictionsPinMessages')}
blocking
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="changeInfo"
checked={!permissions.changeInfo}
label={lang('UserRestrictionsChangeInfo')}
blocking
onChange={handlePermissionChange}
/>
</div>
{isForum && (
<div className="ListItem no-selection">
<Checkbox
name="manageTopics"
checked={!permissions.manageTopics}
label={lang('CreateTopicsPermission')}
blocking
onChange={handlePermissionChange}
/>
</div>
)}
</div>
</div>
<div className="section">
<div
className={buildClassName(
'section',
isMediaDropdownOpen && 'shifted',
)}
>
<ListItem
icon="delete-user"
multiline
@ -274,7 +359,12 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
</ListItem>
</div>
<div className="section">
<div
className={buildClassName(
'section',
isMediaDropdownOpen && 'shifted',
)}
>
<h3 className="section-heading" dir="auto">{lang('PrivacyExceptions')}</h3>
<ListItem
@ -294,6 +384,7 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
<PrivateChatInfo
userId={member.userId}
status={getMemberExceptions(member)}
forceShowSelf
/>
</ListItem>
))}

View File

@ -8,6 +8,9 @@ import type { ApiChat, ApiChatBannedRights } from '../../../api/types';
import { ManagementScreens } from '../../../types';
import { selectChat } from '../../../global/selectors';
import stopEvent from '../../../util/stopEvent';
import buildClassName from '../../../util/buildClassName';
import useManagePermissions from '../hooks/useManagePermissions';
import useLang from '../../../hooks/useLang';
import useFlag from '../../../hooks/useFlag';
import useHistoryBack from '../../../hooks/useHistoryBack';
@ -33,6 +36,12 @@ type StateProps = {
isFormFullyDisabled?: boolean;
};
const ITEM_HEIGHT = 24 + 32;
const SHIFT_HEIGHT_MINUS = 1;
const BEFORE_ITEMS_COUNT = 2;
const BEFORE_USER_INFO_HEIGHT = 96;
const ITEMS_COUNT = 9;
const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
chat,
selectedChatMemberId,
@ -43,9 +52,17 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
}) => {
const { updateChatMemberBannedRights } = getActions();
const [permissions, setPermissions] = useState<ApiChatBannedRights>({});
const [havePermissionChanged, setHavePermissionChanged] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const selectedChatMember = useMemo(() => {
if (!chat || !chat.fullInfo || !chat.fullInfo.members) {
return undefined;
}
return chat.fullInfo.members.find(({ userId }) => userId === selectedChatMemberId);
}, [chat, selectedChatMemberId]);
const {
permissions, havePermissionChanged, isLoading, handlePermissionChange, setIsLoading,
} = useManagePermissions(selectedChatMember?.bannedRights || chat?.defaultBannedRights);
const [isBanConfirmationDialogOpen, openBanConfirmationDialog, closeBanConfirmationDialog] = useFlag();
const lang = useLang();
@ -56,43 +73,12 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
onBack: onClose,
});
const selectedChatMember = useMemo(() => {
if (!chat || !chat.fullInfo || !chat.fullInfo.members) {
return undefined;
}
return chat.fullInfo.members.find(({ userId }) => userId === selectedChatMemberId);
}, [chat, selectedChatMemberId]);
useEffect(() => {
if (chat?.fullInfo && selectedChatMemberId && !selectedChatMember) {
onScreenSelect(ManagementScreens.GroupPermissions);
}
}, [chat, onScreenSelect, selectedChatMember, selectedChatMemberId]);
useEffect(() => {
setPermissions((selectedChatMember?.bannedRights) || (chat?.defaultBannedRights) || {});
setHavePermissionChanged(false);
setIsLoading(false);
}, [chat, selectedChatMember]);
const handlePermissionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { name } = e.target;
function getUpdatedPermissionValue(value: true | undefined) {
return value ? undefined : true;
}
setPermissions((p) => ({
...p,
[name]: getUpdatedPermissionValue(p[name as Exclude<keyof ApiChatBannedRights, 'untilDate'>]),
...(name === 'sendStickers' && {
sendGifs: getUpdatedPermissionValue(p[name]),
}),
}));
setHavePermissionChanged(true);
}, []);
const handleSavePermissions = useCallback(() => {
if (!chat || !selectedChatMemberId) {
return;
@ -104,7 +90,7 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
userId: selectedChatMemberId,
bannedRights: permissions,
});
}, [chat, selectedChatMemberId, permissions, updateChatMemberBannedRights]);
}, [chat, selectedChatMemberId, setIsLoading, updateChatMemberBannedRights, permissions]);
const handleBanFromGroup = useCallback(() => {
if (!chat || !selectedChatMemberId) {
@ -132,116 +118,216 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
return chat.defaultBannedRights[key];
}, [chat, isFormFullyDisabled]);
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
const handleOpenMediaDropdown = useCallback((e: React.MouseEvent) => {
stopEvent(e);
setIsMediaDropdownOpen(!isMediaDropdownOpen);
}, [isMediaDropdownOpen]);
if (!selectedChatMember) {
return undefined;
}
return (
<div className="Management">
<div
className="Management with-shifted-dropdown"
style={`--shift-height: ${ITEMS_COUNT * ITEM_HEIGHT - SHIFT_HEIGHT_MINUS}px;`
+ `--before-shift-height: ${BEFORE_ITEMS_COUNT * ITEM_HEIGHT + BEFORE_USER_INFO_HEIGHT}px;`}
>
<div className="custom-scroll">
<div className="section">
<div className="section without-bottom-shadow">
<ListItem inactive className="chat-item-clickable">
<PrivateChatInfo userId={selectedChatMember.userId} />
<PrivateChatInfo userId={selectedChatMember.userId} forceShowSelf />
</ListItem>
<h3 className="section-heading mt-4" dir="auto">{lang('UserRestrictionsCanDo')}</h3>
<div className="ListItem no-selection">
<Checkbox
name="sendMessages"
checked={!permissions.sendMessages}
name="sendPlain"
checked={!permissions.sendPlain}
label={lang('UserRestrictionsSend')}
blocking
disabled={getControlIsDisabled('sendMessages')}
disabled={getControlIsDisabled('sendPlain')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendMedia"
checked={!permissions.sendMedia}
label={lang('UserRestrictionsSendMedia')}
blocking
rightIcon={isMediaDropdownOpen ? 'up' : 'down'}
disabled={getControlIsDisabled('sendMedia')}
onChange={handlePermissionChange}
onClickLabel={handleOpenMediaDropdown}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendStickers"
checked={!permissions.sendStickers && !permissions.sendGifs}
label={lang('UserRestrictionsSendStickers')}
blocking
disabled={getControlIsDisabled('sendStickers')}
onChange={handlePermissionChange}
/>
<div className="DropdownListTrap">
<div
className={buildClassName(
'DropdownList',
isMediaDropdownOpen && 'DropdownList--open',
)}
>
<div className="ListItem no-selection">
<Checkbox
name="sendPhotos"
checked={!permissions.sendPhotos}
label={lang('UserRestrictionsSendPhotos')}
blocking
disabled={getControlIsDisabled('sendPhotos')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendVideos"
checked={!permissions.sendVideos}
label={lang('UserRestrictionsSendVideos')}
blocking
disabled={getControlIsDisabled('sendVideos')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendStickers"
checked={!permissions.sendStickers && !permissions.sendGifs}
label={lang('UserRestrictionsSendStickers')}
blocking
disabled={getControlIsDisabled('sendStickers')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendAudios"
checked={!permissions.sendAudios}
label={lang('UserRestrictionsSendMusic')}
blocking
disabled={getControlIsDisabled('sendAudios')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendDocs"
checked={!permissions.sendDocs}
label={lang('UserRestrictionsSendFiles')}
blocking
disabled={getControlIsDisabled('sendDocs')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendVoices"
checked={!permissions.sendVoices}
label={lang('UserRestrictionsSendVoices')}
blocking
disabled={getControlIsDisabled('sendVoices')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendRoundvideos"
checked={!permissions.sendRoundvideos}
label={lang('UserRestrictionsSendRound')}
blocking
disabled={getControlIsDisabled('sendRoundvideos')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="embedLinks"
checked={!permissions.embedLinks}
label={lang('UserRestrictionsEmbedLinks')}
blocking
disabled={getControlIsDisabled('embedLinks')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendPolls"
checked={!permissions.sendPolls}
label={lang('UserRestrictionsSendPolls')}
blocking
disabled={getControlIsDisabled('sendPolls')}
onChange={handlePermissionChange}
/>
</div>
</div>
</div>
<div className="ListItem no-selection">
<Checkbox
name="sendPolls"
checked={!permissions.sendPolls}
label={lang('UserRestrictionsSendPolls')}
blocking
disabled={getControlIsDisabled('sendPolls')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="embedLinks"
checked={!permissions.embedLinks}
label={lang('UserRestrictionsEmbedLinks')}
blocking
disabled={getControlIsDisabled('embedLinks')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="inviteUsers"
checked={!permissions.inviteUsers}
label={lang('UserRestrictionsInviteUsers')}
blocking
disabled={getControlIsDisabled('inviteUsers')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="pinMessages"
checked={!permissions.pinMessages}
label={lang('UserRestrictionsPinMessages')}
blocking
disabled={getControlIsDisabled('pinMessages')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="changeInfo"
checked={!permissions.changeInfo}
label={lang('UserRestrictionsChangeInfo')}
blocking
disabled={getControlIsDisabled('changeInfo')}
onChange={handlePermissionChange}
/>
</div>
{isForum && (
<div className={buildClassName('part', isMediaDropdownOpen && 'shifted')}>
<div className="ListItem no-selection">
<Checkbox
name="manageTopics"
checked={!permissions.manageTopics}
label={lang('CreateTopicsPermission')}
name="inviteUsers"
checked={!permissions.inviteUsers}
label={lang('UserRestrictionsInviteUsers')}
blocking
disabled={getControlIsDisabled('manageTopics')}
disabled={getControlIsDisabled('inviteUsers')}
onChange={handlePermissionChange}
/>
</div>
)}
<div className="ListItem no-selection">
<Checkbox
name="pinMessages"
checked={!permissions.pinMessages}
label={lang('UserRestrictionsPinMessages')}
blocking
disabled={getControlIsDisabled('pinMessages')}
onChange={handlePermissionChange}
/>
</div>
<div className="ListItem no-selection">
<Checkbox
name="changeInfo"
checked={!permissions.changeInfo}
label={lang('UserRestrictionsChangeInfo')}
blocking
disabled={getControlIsDisabled('changeInfo')}
onChange={handlePermissionChange}
/>
</div>
{isForum && (
<div className="ListItem no-selection">
<Checkbox
name="manageTopics"
checked={!permissions.manageTopics}
label={lang('CreateTopicsPermission')}
blocking
disabled={getControlIsDisabled('manageTopics')}
onChange={handlePermissionChange}
/>
</div>
)}
</div>
</div>
{!isFormFullyDisabled && (
<div className="section">
<div
className={buildClassName(
'section',
isMediaDropdownOpen && 'shifted',
)}
>
<ListItem icon="delete-user" ripple destructive onClick={openBanConfirmationDialog}>
{lang('UserRestrictionsBlock')}
</ListItem>

View File

@ -305,3 +305,61 @@
margin-top: 0.5rem;
}
}
.DropdownList {
transition: 0.25s ease-in-out transform;
transform: translateY(calc(-100%));
position: absolute;
width: 100%;
left: 0;
padding: 0 1.5rem 0 2.5rem;
background: var(--color-background);
&--open {
transform: translateY(-2rem);
}
}
.DropdownListTrap {
height: 0;
width: 100%;
&::before {
position: absolute;
top: 0;
left: 0;
right: 0;
height: calc(var(--before-shift-height) + 2.5rem);
background: var(--color-background);
content: "";
z-index: 1;
}
}
.with-shifted-dropdown {
.ListItem, .section-heading {
position: relative;
z-index: 2;
}
.without-bottom-shadow {
box-shadow: none;
padding-bottom: 0;
}
.part {
background-color: var(--color-background);
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
margin: 0 -1.5rem;
padding: 0 1.5rem 1rem;
}
.section, .part {
position: relative;
transition: 0.25s ease-in-out transform;
&.shifted {
transform: translateY(var(--shift-height));
}
}
}

View File

@ -70,6 +70,7 @@
.Checkbox-main {
&::before,
&::after {
pointer-events: none;
content: "";
display: block;
position: absolute;
@ -96,12 +97,19 @@
}
.label {
display: block;
display: flex;
align-items: center;
text-align: initial;
flex-wrap: wrap;
column-gap: 0.25rem;
}
.right-icon {
margin-left: auto;
color: var(--color-text-secondary);
font-size: 1.25rem;
}
.subLabel {
display: block;
font-size: 0.875rem;

View File

@ -1,6 +1,6 @@
import type { ChangeEvent } from 'react';
import type { FC, TeactNode } from '../../lib/teact/teact';
import React, { memo, useCallback } from '../../lib/teact/teact';
import React, { memo, useCallback, useRef } from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName';
import useLang from '../../hooks/useLang';
@ -17,6 +17,7 @@ type OwnProps = {
label: TeactNode;
subLabel?: string;
checked: boolean;
rightIcon?: string;
disabled?: boolean;
tabIndex?: number;
round?: boolean;
@ -26,6 +27,7 @@ type OwnProps = {
className?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
onCheck?: (isChecked: boolean) => void;
onClickLabel?: (e: React.MouseEvent) => void;
};
const Checkbox: FC<OwnProps> = ({
@ -41,10 +43,16 @@ const Checkbox: FC<OwnProps> = ({
blocking,
isLoading,
className,
rightIcon,
onChange,
onCheck,
onClickLabel,
}) => {
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const labelRef = useRef<HTMLLabelElement>(null);
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
if (onChange) {
onChange(event);
@ -55,6 +63,16 @@ const Checkbox: FC<OwnProps> = ({
}
}, [onChange, onCheck]);
function handleClick(event: React.MouseEvent) {
if (event.target !== labelRef.current) {
onClickLabel?.(event);
}
}
function handleInputClick(event: React.MouseEvent) {
event.stopPropagation();
}
const labelClassName = buildClassName(
'Checkbox',
disabled && 'disabled',
@ -65,7 +83,13 @@ const Checkbox: FC<OwnProps> = ({
);
return (
<label className={labelClassName} dir={lang.isRtl ? 'rtl' : undefined}>
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<label
className={labelClassName}
dir={lang.isRtl ? 'rtl' : undefined}
onClick={onClickLabel ? handleClick : undefined}
ref={labelRef}
>
<input
type="checkbox"
id={id}
@ -75,9 +99,13 @@ const Checkbox: FC<OwnProps> = ({
disabled={disabled}
tabIndex={tabIndex}
onChange={handleChange}
onClick={onClickLabel ? handleInputClick : undefined}
/>
<div className="Checkbox-main">
<span className="label" dir="auto">{typeof label === 'string' ? renderText(label) : label}</span>
<span className="label" dir="auto">
{typeof label === 'string' ? renderText(label) : label}
{rightIcon && <i className={`icon-${rightIcon} right-icon`} />}
</span>
{subLabel && <span className="subLabel" dir="auto">{renderText(subLabel)}</span>}
</div>
{isLoading && <Spinner />}

View File

@ -4,7 +4,7 @@
bottom: 1rem;
transform: translateY(calc(5rem - var(--call-header-height, 0rem)));
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 1;
z-index: 2;
body.animation-level-0 & {
transition: none !important;

View File

@ -80,7 +80,7 @@ import {
selectIsCurrentUserPremium,
selectForwardsContainVoiceMessages,
selectTabState,
selectThreadIdFromMessage,
selectThreadIdFromMessage, selectForwardsCanBeSentToChat,
} from '../../selectors';
import {
debounce, onTickEnd, rafPromise,
@ -1377,6 +1377,11 @@ addActionHandler('setForwardChatOrTopic', async (global, actions, payload): Prom
}
}
if (!selectForwardsCanBeSentToChat(global, chatId, tabId)) {
actions.showAllowedMessageTypesNotification({ chatId, tabId });
return;
}
global = updateTabState(global, {
forwardMessages: {
...selectTabState(global, tabId).forwardMessages,

View File

@ -627,6 +627,19 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
setGlobal(global);
break;
}
case 'updateMessageSendFailed': {
const { chatId, localId, error } = update;
if (error.match(/CHAT_SEND_.+?FORBIDDEN/)) {
Object.values(global.byTabId).forEach(({ id: tabId }) => {
actions.showAllowedMessageTypesNotification({ chatId, tabId });
});
}
global = updateChatMessage(global, chatId, localId, { sendingState: 'messageSendingStateFailed' });
setGlobal(global);
}
}
});

View File

@ -15,13 +15,15 @@ import {
selectChatMessage, selectCurrentChat, selectCurrentMessageList, selectTabState, selectIsTrustedBot, selectChat,
} from '../../selectors';
import generateIdFor from '../../../util/generateIdFor';
import { unique } from '../../../util/iteratees';
import { compact, unique } from '../../../util/iteratees';
import { getAllMultitabTokens, getCurrentTabId, reestablishMasterToSelf } from '../../../util/establishMultitabRole';
import { getAllNotificationsCount } from '../../../util/folderManager';
import updateIcon from '../../../util/updateIcon';
import setPageTitle from '../../../util/updatePageTitle';
import { updateTabState } from '../../reducers/tabs';
import { getIsMobile, getIsTablet } from '../../../hooks/useAppLayout';
import * as langProvider from '../../../util/langProvider';
import { getAllowedAttachmentOptions } from '../../helpers';
export const APP_VERSION_URL = 'version.txt';
const MAX_STORED_EMOJIS = 8 * 4; // Represents four rows of recent emojis
@ -264,7 +266,7 @@ addActionHandler('reorderStickerSets', (global, actions, payload): ActionReturnT
});
addActionHandler('showNotification', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId(), ...notification } = payload!;
const { tabId = getCurrentTabId(), ...notification } = payload;
notification.localId = generateIdFor({});
const newNotifications = [...selectTabState(global, tabId).notifications];
@ -280,6 +282,44 @@ addActionHandler('showNotification', (global, actions, payload): ActionReturnTyp
}, tabId);
});
addActionHandler('showAllowedMessageTypesNotification', (global, actions, payload): ActionReturnType => {
const { chatId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const {
canSendPlainText, canSendPhotos, canSendVideos, canSendDocuments, canSendAudios,
canSendStickers, canSendRoundVideos, canSendVoices,
} = getAllowedAttachmentOptions(chat);
const allowedContent = compact([
canSendPlainText ? 'Chat.SendAllowedContentTypeText' : undefined,
canSendPhotos ? 'Chat.SendAllowedContentTypePhoto' : undefined,
canSendVideos ? 'Chat.SendAllowedContentTypeVideo' : undefined,
canSendVoices ? 'Chat.SendAllowedContentTypeVoiceMessage' : undefined,
canSendRoundVideos ? 'Chat.SendAllowedContentTypeVideoMessage' : undefined,
canSendDocuments ? 'Chat.SendAllowedContentTypeFile' : undefined,
canSendAudios ? 'Chat.SendAllowedContentTypeMusic' : undefined,
canSendStickers ? 'Chat.SendAllowedContentTypeSticker' : undefined,
]).map((l) => langProvider.translate(l));
if (!allowedContent.length) {
actions.showNotification({
message: langProvider.translate('Chat.SendNotAllowedText'),
tabId,
});
return;
}
const lastDelimiter = langProvider.translate('AutoDownloadSettings.LastDelimeter');
const allowedContentString = allowedContent.join(', ').replace(/,([^,]*)$/, `${lastDelimiter}$1`);
actions.showNotification({
message: langProvider.translate('Chat.SendAllowedContentText', allowedContentString),
tabId,
});
});
addActionHandler('dismissNotification', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload;
const newNotifications = selectTabState(global, tabId)
@ -636,6 +676,7 @@ addActionHandler('updatePageTitle', (global, actions, payload): ActionReturnType
addCallback((global: GlobalState) => {
if (global.notificationIndex === undefined || global.allNotificationsCount === undefined) return;
// eslint-disable-next-line eslint-multitab-tt/no-getactions-in-actions
const { updatePageTitle } = getActions();
const index = global.notificationIndex;

View File

@ -217,6 +217,13 @@ export interface IAllowedAttachmentOptions {
canSendStickers: boolean;
canSendGifs: boolean;
canAttachEmbedLinks: boolean;
canSendPhotos: boolean;
canSendVideos: boolean;
canSendRoundVideos: boolean;
canSendAudios: boolean;
canSendVoices: boolean;
canSendPlainText: boolean;
canSendDocuments: boolean;
}
export function getAllowedAttachmentOptions(chat?: ApiChat, isChatWithBot = false): IAllowedAttachmentOptions {
@ -227,6 +234,13 @@ export function getAllowedAttachmentOptions(chat?: ApiChat, isChatWithBot = fals
canSendStickers: false,
canSendGifs: false,
canAttachEmbedLinks: false,
canSendPhotos: false,
canSendVideos: false,
canSendRoundVideos: false,
canSendAudios: false,
canSendVoices: false,
canSendPlainText: false,
canSendDocuments: false,
};
}
@ -238,6 +252,13 @@ export function getAllowedAttachmentOptions(chat?: ApiChat, isChatWithBot = fals
canSendStickers: isAdmin || !isUserRightBanned(chat, 'sendStickers'),
canSendGifs: isAdmin || !isUserRightBanned(chat, 'sendGifs'),
canAttachEmbedLinks: isAdmin || !isUserRightBanned(chat, 'embedLinks'),
canSendPhotos: isAdmin || !isUserRightBanned(chat, 'sendPhotos'),
canSendVideos: isAdmin || !isUserRightBanned(chat, 'sendVideos'),
canSendRoundVideos: isAdmin || !isUserRightBanned(chat, 'sendRoundvideos'),
canSendAudios: isAdmin || !isUserRightBanned(chat, 'sendAudios'),
canSendVoices: isAdmin || !isUserRightBanned(chat, 'sendVoices'),
canSendPlainText: isAdmin || !isUserRightBanned(chat, 'sendPlain'),
canSendDocuments: isAdmin || !isUserRightBanned(chat, 'sendDocs'),
};
}

View File

@ -44,7 +44,7 @@ import {
isServiceNotificationMessage,
isUserId,
isUserRightBanned,
canSendReaction,
canSendReaction, getAllowedAttachmentOptions,
} from '../helpers';
import { findLast } from '../../util/iteratees';
import { selectIsStickerFavorite } from './symbols';
@ -1279,3 +1279,42 @@ export function selectForwardsContainVoiceMessages<T extends GlobalState>(
return Boolean(message.content.voice) || message.content.video?.isRound;
});
}
export function selectForwardsCanBeSentToChat<T extends GlobalState>(
global: T,
toChatId: string,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const { messageIds, fromChatId } = selectTabState(global, tabId).forwardMessages;
const chat = selectChat(global, toChatId);
if (!messageIds || !chat) return false;
const chatMessages = selectChatMessages(global, fromChatId!);
const {
canSendVoices, canSendRoundVideos, canSendStickers, canSendDocuments, canSendAudios, canSendVideos,
canSendPhotos, canSendGifs, canSendPlainText,
} = getAllowedAttachmentOptions(chat);
return !messageIds.some((messageId) => {
const message = chatMessages[messageId];
const isVoice = message.content.voice;
const isRoundVideo = message.content.video?.isRound;
const isPhoto = message.content.photo;
const isGif = message.content.video?.isGif;
const isVideo = message.content.video && !isRoundVideo && !isGif;
const isAudio = message.content.audio;
const isDocument = message.content.document;
const isSticker = message.content.sticker;
const isPlainText = message.content.text
&& !isVoice && !isRoundVideo && !isSticker && !isDocument && !isAudio && !isVideo && !isPhoto && !isGif;
return (isVoice && !canSendVoices)
|| (isRoundVideo && !canSendRoundVideos)
|| (isSticker && !canSendStickers)
|| (isDocument && !canSendDocuments)
|| (isAudio && !canSendAudios)
|| (isVideo && !canSendVideos)
|| (isPhoto && !canSendPhotos)
|| (isGif && !canSendGifs)
|| (isPlainText && !canSendPlainText);
});
}

View File

@ -2104,6 +2104,9 @@ export interface ActionPayloads {
actionText?: string;
action?: CallbackAction;
} & WithTabId;
showAllowedMessageTypesNotification: {
chatId: string;
} & WithTabId;
dismissNotification: { localId: string } & WithTabId;
updatePageTitle: {