Support protected ("no forwards") chats and messages (#1602)

This commit is contained in:
Alexander Zinchuk 2021-12-31 18:17:49 +01:00
parent 72ae33139a
commit d1d463c7d2
36 changed files with 274 additions and 64 deletions

View File

@ -49,6 +49,9 @@ function buildApiChatFieldsFromPeerEntity(
...(peerEntity.participantsCount && { membersCount: peerEntity.participantsCount }), ...(peerEntity.participantsCount && { membersCount: peerEntity.participantsCount }),
joinDate: peerEntity.date, joinDate: peerEntity.date,
}), }),
...((peerEntity instanceof GramJs.Chat || peerEntity instanceof GramJs.Channel) && {
isProtected: Boolean('noforwards' in peerEntity && peerEntity.noforwards),
}),
...(isSupport && { isSupport: true }), ...(isSupport && { isSupport: true }),
...buildApiChatPermissions(peerEntity), ...buildApiChatPermissions(peerEntity),
...(('creator' in peerEntity) && { isCreator: peerEntity.creator }), ...(('creator' in peerEntity) && { isCreator: peerEntity.creator }),

View File

@ -140,7 +140,7 @@ type UniversalMessage = (
& Pick<Partial<GramJs.Message & GramJs.MessageService>, ( & Pick<Partial<GramJs.Message & GramJs.MessageService>, (
'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' | 'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' |
'media' | 'action' | 'views' | 'editDate' | 'editHide' | 'mediaUnread' | 'groupedId' | 'mentioned' | 'viaBotId' | 'media' | 'action' | 'views' | 'editDate' | 'editHide' | 'mediaUnread' | 'groupedId' | 'mentioned' | 'viaBotId' |
'replies' | 'fromScheduled' | 'postAuthor' 'replies' | 'fromScheduled' | 'postAuthor' | 'noforwards'
)> )>
); );
@ -209,6 +209,7 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
...(mtpMessage.viaBotId && { viaBotId: buildApiPeerId(mtpMessage.viaBotId, 'user') }), ...(mtpMessage.viaBotId && { viaBotId: buildApiPeerId(mtpMessage.viaBotId, 'user') }),
...(replies?.comments && { threadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }), ...(replies?.comments && { threadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }),
...(postAuthor && { adminTitle: postAuthor }), ...(postAuthor && { adminTitle: postAuthor }),
...(mtpMessage.noforwards && { isProtected: true }),
}; };
} }

View File

@ -1130,3 +1130,14 @@ export async function importChatInvite({ hash }: { hash: string }) {
return buildApiChatFromPreview(updates.chats[0]); return buildApiChatFromPreview(updates.chats[0]);
} }
export function toggleIsProtected({
chat, isProtected,
}: { chat: ApiChat; isProtected: boolean }) {
const { id, accessHash } = chat;
return invokeRequest(new GramJs.messages.ToggleNoForwards({
peer: buildInputPeer(id, accessHash),
enabled: isProtected,
}), true);
}

View File

@ -14,7 +14,7 @@ export {
fetchChatFolders, editChatFolder, deleteChatFolder, fetchRecommendedChatFolders, fetchChatFolders, editChatFolder, deleteChatFolder, fetchRecommendedChatFolders,
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights, getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup, updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
} from './chats'; } from './chats';
export { export {

View File

@ -61,6 +61,9 @@ export function init(_onUpdate: OnApiUpdate) {
const sentMessageIds = new Set(); const sentMessageIds = new Set();
let serverTimeOffset = 0; let serverTimeOffset = 0;
// Workaround for a situation when an incorrect update comes with an undefined property `adminRights`
let shouldIgnoreNextChannelUpdate = false;
const IGNORE_NEXT_CHANNEL_UPDATE_TIMEOUT = 2000;
function dispatchUserAndChatUpdates(entities: (GramJs.TypeUser | GramJs.TypeChat)[]) { function dispatchUserAndChatUpdates(entities: (GramJs.TypeUser | GramJs.TypeChat)[]) {
entities entities
@ -636,6 +639,16 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
)); ));
if (channel instanceof GramJs.Channel) { if (channel instanceof GramJs.Channel) {
if (shouldIgnoreNextChannelUpdate) {
shouldIgnoreNextChannelUpdate = false;
return;
}
if (originRequest instanceof GramJs.messages.ToggleNoForwards) {
shouldIgnoreNextChannelUpdate = true;
setTimeout(() => { shouldIgnoreNextChannelUpdate = false; }, IGNORE_NEXT_CHANNEL_UPDATE_TIMEOUT);
}
const chat = buildApiChatFromPreview(channel); const chat = buildApiChatFromPreview(channel);
if (chat) { if (chat) {
onUpdate({ onUpdate({

View File

@ -31,6 +31,7 @@ export interface ApiChat {
isSupport?: boolean; isSupport?: boolean;
photos?: ApiPhoto[]; photos?: ApiPhoto[];
draftDate?: number; draftDate?: number;
isProtected?: boolean;
// Calls // Calls
isCallActive?: boolean; isCallActive?: boolean;

View File

@ -265,6 +265,7 @@ export interface ApiMessage {
shouldHideKeyboardButtons?: boolean; shouldHideKeyboardButtons?: boolean;
isFromScheduled?: boolean; isFromScheduled?: boolean;
seenByUserIds?: string[]; seenByUserIds?: string[];
isProtected?: boolean;
} }
export interface ApiThreadInfo { export interface ApiThreadInfo {

View File

@ -28,6 +28,7 @@ type OwnProps = {
sender?: ApiUser | ApiChat; sender?: ApiUser | ApiChat;
title?: string; title?: string;
customText?: string; customText?: string;
isProtected?: boolean;
onClick: NoneToVoidFunction; onClick: NoneToVoidFunction;
}; };
@ -39,6 +40,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
sender, sender,
title, title,
customText, customText,
isProtected,
observeIntersection, observeIntersection,
onClick, onClick,
}) => { }) => {
@ -61,7 +63,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
className={buildClassName('EmbeddedMessage', className)} className={buildClassName('EmbeddedMessage', className)}
onClick={message ? onClick : undefined} onClick={message ? onClick : undefined}
> >
{mediaThumbnail && renderPictogram(pictogramId, mediaThumbnail, mediaBlobUrl, isRoundVideo)} {mediaThumbnail && renderPictogram(pictogramId, mediaThumbnail, mediaBlobUrl, isRoundVideo, isProtected)}
<div className="message-text"> <div className="message-text">
<p dir="auto"> <p dir="auto">
{!message ? ( {!message ? (
@ -83,18 +85,23 @@ function renderPictogram(
thumbDataUri: string, thumbDataUri: string,
blobUrl?: string, blobUrl?: string,
isRoundVideo?: boolean, isRoundVideo?: boolean,
isProtected?: boolean,
) { ) {
const { width, height } = getPictogramDimensions(); const { width, height } = getPictogramDimensions();
return ( return (
<img <>
id={id} <img
src={blobUrl || thumbDataUri} id={id}
width={width} src={blobUrl || thumbDataUri}
height={height} width={width}
alt="" height={height}
className={isRoundVideo ? 'round' : ''} alt=""
/> className={isRoundVideo ? 'round' : ''}
draggable={!isProtected}
/>
{isProtected && <span className="protector" />}
</>
); );
} }

View File

@ -3,6 +3,7 @@ import React, { FC, memo, useCallback } from '../../lib/teact/teact';
import { ApiMessage } from '../../api/types'; import { ApiMessage } from '../../api/types';
import { formatMediaDuration } from '../../util/dateFormat'; import { formatMediaDuration } from '../../util/dateFormat';
import stopEvent from '../../util/stopEvent';
import { import {
getMessageMediaHash, getMessageMediaHash,
getMessageMediaThumbDataUri, getMessageMediaThumbDataUri,
@ -17,10 +18,16 @@ import './Media.scss';
type OwnProps = { type OwnProps = {
message: ApiMessage; message: ApiMessage;
idPrefix?: string; idPrefix?: string;
isProtected?: boolean;
onClick?: (messageId: number, chatId: string) => void; onClick?: (messageId: number, chatId: string) => void;
}; };
const Media: FC<OwnProps> = ({ message, idPrefix = 'shared-media', onClick }) => { const Media: FC<OwnProps> = ({
message,
idPrefix = 'shared-media',
isProtected,
onClick,
}) => {
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
onClick!(message.id, message.chatId); onClick!(message.id, message.chatId);
}, [message.id, message.chatId, onClick]); }, [message.id, message.chatId, onClick]);
@ -33,9 +40,16 @@ const Media: FC<OwnProps> = ({ message, idPrefix = 'shared-media', onClick }) =>
return ( return (
<div id={`${idPrefix}${message.id}`} className="Media scroll-item" onClick={onClick ? handleClick : undefined}> <div id={`${idPrefix}${message.id}`} className="Media scroll-item" onClick={onClick ? handleClick : undefined}>
<img src={thumbDataUri} alt="" /> <img src={thumbDataUri} alt="" draggable={!isProtected} onContextMenu={isProtected ? stopEvent : undefined} />
<img src={mediaBlobUrl} className={buildClassName('full-media', transitionClassNames)} alt="" /> <img
src={mediaBlobUrl}
className={buildClassName('full-media', transitionClassNames)}
alt=""
draggable={!isProtected}
onContextMenu={isProtected ? stopEvent : undefined}
/>
{video && <span className="video-duration">{video.isGif ? 'GIF' : formatMediaDuration(video.duration)}</span>} {video && <span className="video-duration">{video.isGif ? 'GIF' : formatMediaDuration(video.duration)}</span>}
{isProtected && <span className="protector" />}
</div> </div>
); );
}; };

View File

@ -20,11 +20,12 @@ const MAX_TEXT_LENGTH = 170; // symbols
type OwnProps = { type OwnProps = {
message: ApiMessage; message: ApiMessage;
senderTitle?: string; senderTitle?: string;
isProtected?: boolean;
onMessageClick: (messageId: number, chatId: string) => void; onMessageClick: (messageId: number, chatId: string) => void;
}; };
const WebLink: FC<OwnProps> = ({ const WebLink: FC<OwnProps> = ({
message, senderTitle, onMessageClick, message, senderTitle, isProtected, onMessageClick,
}) => { }) => {
const lang = useLang(); const lang = useLang();
@ -76,7 +77,7 @@ const WebLink: FC<OwnProps> = ({
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
> >
{photo && ( {photo && (
<Media message={message} /> <Media message={message} isProtected={isProtected} />
)} )}
<div className="content"> <div className="content">
<Link isRtl={lang.isRtl} className="site-title" onClick={handleMessageClick}> <Link isRtl={lang.isRtl} className="site-title" onClick={handleMessageClick}>

View File

@ -35,6 +35,7 @@ const LinkResults: FC<OwnProps & StateProps> = ({
globalMessagesByChatId, globalMessagesByChatId,
foundIds, foundIds,
lastSyncTime, lastSyncTime,
isChatProtected,
}) => { }) => {
const { const {
searchMessagesGlobal, searchMessagesGlobal,
@ -89,6 +90,7 @@ const LinkResults: FC<OwnProps & StateProps> = ({
key={message.id} key={message.id}
message={message} message={message}
senderTitle={getSenderName(lang, message, chatsById, usersById)} senderTitle={getSenderName(lang, message, chatsById, usersById)}
isProtected={isChatProtected || message.isProtected}
onMessageClick={handleMessageFocus} onMessageClick={handleMessageFocus}
/> />
</div> </div>

View File

@ -33,6 +33,7 @@ const MediaResults: FC<OwnProps & StateProps> = ({
globalMessagesByChatId, globalMessagesByChatId,
foundIds, foundIds,
lastSyncTime, lastSyncTime,
isChatProtected,
}) => { }) => {
const { const {
searchMessagesGlobal, searchMessagesGlobal,
@ -81,6 +82,7 @@ const MediaResults: FC<OwnProps & StateProps> = ({
key={message.id} key={message.id}
idPrefix="search-media" idPrefix="search-media"
message={message} message={message}
isProtected={isChatProtected || message.isProtected}
onClick={handleSelectMedia} onClick={handleSelectMedia}
/> />
))} ))}

View File

@ -4,7 +4,7 @@ import {
} from '../../../../api/types'; } from '../../../../api/types';
import { ISettings } from '../../../../types'; import { ISettings } from '../../../../types';
import { selectTheme } from '../../../../modules/selectors'; import { selectChat, selectTheme } from '../../../../modules/selectors';
export type StateProps = { export type StateProps = {
theme: ISettings['theme']; theme: ISettings['theme'];
@ -16,6 +16,7 @@ export type StateProps = {
lastSyncTime?: number; lastSyncTime?: number;
searchChatId?: string; searchChatId?: string;
activeDownloads: Record<string, number[]>; activeDownloads: Record<string, number[]>;
isChatProtected?: boolean;
}; };
export function createMapStateToProps(type: ApiGlobalMessageSearchType) { export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
@ -46,6 +47,7 @@ export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
foundIds, foundIds,
searchChatId: chatId, searchChatId: chatId,
activeDownloads, activeDownloads,
isChatProtected: chatId ? selectChat(global, chatId)?.isProtected : undefined,
lastSyncTime: global.lastSyncTime, lastSyncTime: global.lastSyncTime,
}; };
}; };

View File

@ -23,6 +23,7 @@ import buildClassName from '../../util/buildClassName';
import { fastRaf } from '../../util/schedulers'; import { fastRaf } from '../../util/schedulers';
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners'; import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
import { processDeepLink } from '../../util/deeplink'; import { processDeepLink } from '../../util/deeplink';
import stopEvent from '../../util/stopEvent';
import windowSize from '../../util/windowSize'; import windowSize from '../../util/windowSize';
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import useBackgroundMode from '../../hooks/useBackgroundMode'; import useBackgroundMode from '../../hooks/useBackgroundMode';
@ -273,11 +274,6 @@ const Main: FC<StateProps> = ({
usePreventPinchZoomGesture(isMediaViewerOpen); usePreventPinchZoomGesture(isMediaViewerOpen);
function stopEvent(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
e.preventDefault();
e.stopPropagation();
}
return ( return (
<div id="Main" className={className} onDrop={stopEvent} onDragOver={stopEvent}> <div id="Main" className={className} onDrop={stopEvent} onDragOver={stopEvent}>
<LeftColumn /> <LeftColumn />

View File

@ -12,7 +12,7 @@ import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { getMessageMediaHash } from '../../modules/helpers'; import { getMessageMediaHash } from '../../modules/helpers';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress'; import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
import { selectIsDownloading } from '../../modules/selectors'; import { selectIsDownloading, selectIsMessageProtected } from '../../modules/selectors';
import Button from '../ui/Button'; import Button from '../ui/Button';
import DropdownMenu from '../ui/DropdownMenu'; import DropdownMenu from '../ui/DropdownMenu';
@ -23,6 +23,7 @@ import './MediaViewerActions.scss';
type StateProps = { type StateProps = {
isDownloading: boolean; isDownloading: boolean;
isProtected?: boolean;
}; };
type OwnProps = { type OwnProps = {
@ -45,6 +46,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
fileName, fileName,
isAvatar, isAvatar,
isDownloading, isDownloading,
isProtected,
onCloseMediaViewer, onCloseMediaViewer,
onForward, onForward,
onZoomToggle, onZoomToggle,
@ -84,7 +86,44 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
); );
}, []); }, []);
function renderDownloadButton() {
if (isProtected) {
return undefined;
}
return isVideo ? (
<Button
round
size="smaller"
color="translucent-white"
ariaLabel={lang('AccActionDownload')}
onClick={handleDownloadClick}
>
{isDownloading ? (
<ProgressSpinner progress={downloadProgress} size="s" onClick={handleDownloadClick} />
) : (
<i className="icon-download" />
)}
</Button>
) : (
<Button
href={mediaData}
download={fileName}
round
size="smaller"
color="translucent-white"
ariaLabel={lang('AccActionDownload')}
>
<i className="icon-download" />
</Button>
);
}
if (IS_SINGLE_COLUMN_LAYOUT) { if (IS_SINGLE_COLUMN_LAYOUT) {
if (isProtected) {
return undefined;
}
return ( return (
<div className="MediaViewerActions-mobile"> <div className="MediaViewerActions-mobile">
<DropdownMenu <DropdownMenu
@ -123,7 +162,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
return ( return (
<div className="MediaViewerActions"> <div className="MediaViewerActions">
{!isAvatar && ( {!isAvatar && !isProtected && (
<> <>
<Button <Button
round round
@ -136,32 +175,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
</Button> </Button>
</> </>
)} )}
{isVideo ? ( {renderDownloadButton()}
<Button
round
size="smaller"
color="translucent-white"
ariaLabel={lang('AccActionDownload')}
onClick={handleDownloadClick}
>
{isDownloading ? (
<ProgressSpinner progress={downloadProgress} size="s" onClick={handleDownloadClick} />
) : (
<i className="icon-download" />
)}
</Button>
) : (
<Button
href={mediaData}
download={fileName}
round
size="smaller"
color="translucent-white"
ariaLabel={lang('AccActionDownload')}
>
<i className="icon-download" />
</Button>
)}
<Button <Button
round round
size="smaller" size="smaller"
@ -187,9 +201,11 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { message }): StateProps => { (global, { message }): StateProps => {
const isDownloading = message ? selectIsDownloading(global, message) : false; const isDownloading = message ? selectIsDownloading(global, message) : false;
const isProtected = selectIsMessageProtected(global, message);
return { return {
isDownloading, isDownloading,
isProtected,
}; };
}, },
)(MediaViewerActions)); )(MediaViewerActions));

View File

@ -6,6 +6,7 @@ import {
} from '../../api/types'; } from '../../api/types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import useBlurSync from '../../hooks/useBlurSync'; import useBlurSync from '../../hooks/useBlurSync';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../hooks/useMedia';
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress'; import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
@ -26,10 +27,11 @@ import {
isMessageDocumentVideo, isMessageDocumentVideo,
} from '../../modules/helpers'; } from '../../modules/helpers';
import { import {
selectChat, selectChatMessage, selectScheduledMessage, selectUser, selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectUser,
} from '../../modules/selectors'; } from '../../modules/selectors';
import { AVATAR_FULL_DIMENSIONS, calculateMediaViewerDimensions } from '../common/helpers/mediaDimensions'; import { AVATAR_FULL_DIMENSIONS, calculateMediaViewerDimensions } from '../common/helpers/mediaDimensions';
import { renderMessageText } from '../common/helpers/renderMessageText'; import { renderMessageText } from '../common/helpers/renderMessageText';
import stopEvent from '../../util/stopEvent';
import Spinner from '../ui/Spinner'; import Spinner from '../ui/Spinner';
import MediaViewerFooter from './MediaViewerFooter'; import MediaViewerFooter from './MediaViewerFooter';
@ -60,6 +62,7 @@ type StateProps = {
profilePhotoIndex?: number; profilePhotoIndex?: number;
message?: ApiMessage; message?: ApiMessage;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
isProtected?: boolean;
}; };
const ANIMATION_DURATION = 350; const ANIMATION_DURATION = 350;
@ -77,6 +80,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
onClose, onClose,
onFooterClick, onFooterClick,
isFooterHidden, isFooterHidden,
isProtected,
} = props; } = props;
/* Content */ /* Content */
const photo = message ? getMessagePhoto(message) : undefined; const photo = message ? getMessagePhoto(message) : undefined;
@ -163,7 +167,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
{renderPhoto( {renderPhoto(
fullMediaBlobUrl || previewBlobUrl, fullMediaBlobUrl || previewBlobUrl,
calculateMediaViewerDimensions(AVATAR_FULL_DIMENSIONS, false), calculateMediaViewerDimensions(AVATAR_FULL_DIMENSIONS, false),
false, !IS_SINGLE_COLUMN_LAYOUT && !isProtected,
)} )}
</div> </div>
); );
@ -176,10 +180,11 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
<div <div
className={`MediaViewerContent ${hasFooter ? 'has-footer' : ''}`} className={`MediaViewerContent ${hasFooter ? 'has-footer' : ''}`}
> >
{isProtected && <div onContextMenu={stopEvent} className="protector" />}
{isPhoto && renderPhoto( {isPhoto && renderPhoto(
localBlobUrl || fullMediaBlobUrl || previewBlobUrl || pictogramBlobUrl, localBlobUrl || fullMediaBlobUrl || previewBlobUrl || pictogramBlobUrl,
message && calculateMediaViewerDimensions(dimensions!, hasFooter), message && calculateMediaViewerDimensions(dimensions!, hasFooter),
false, !IS_SINGLE_COLUMN_LAYOUT && !isProtected,
)} )}
{isVideo && (isActive ? ( {isVideo && (isActive ? (
<VideoPlayer <VideoPlayer
@ -197,7 +202,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
) : renderVideoPreview( ) : renderVideoPreview(
bestImageData, bestImageData,
message && calculateMediaViewerDimensions(dimensions!, hasFooter, true), message && calculateMediaViewerDimensions(dimensions!, hasFooter, true),
false, !IS_SINGLE_COLUMN_LAYOUT && !isProtected,
))} ))}
{textParts && ( {textParts && (
<MediaViewerFooter <MediaViewerFooter
@ -238,6 +243,7 @@ export default memo(withGlobal<OwnProps>(
senderId: message.senderId, senderId: message.senderId,
origin, origin,
message, message,
isProtected: selectIsMessageProtected(global, message),
}; };
} }
@ -275,6 +281,7 @@ export default memo(withGlobal<OwnProps>(
senderId: message.senderId, senderId: message.senderId,
origin, origin,
message, message,
isProtected: selectIsMessageProtected(global, message),
}; };
}, },
)(MediaViewerContent)); )(MediaViewerContent));

View File

@ -10,6 +10,7 @@ import {
REM, REM,
} from '../../common/helpers/mediaDimensions'; } from '../../common/helpers/mediaDimensions';
import windowSize from '../../../util/windowSize'; import windowSize from '../../../util/windowSize';
import stopEvent from '../../../util/stopEvent';
const ANIMATION_DURATION = 200; const ANIMATION_DURATION = 200;
@ -208,6 +209,8 @@ function createGhost(source: string | HTMLImageElement | HTMLVideoElement, origi
ghost.classList.add('ghost'); ghost.classList.add('ghost');
const img = new Image(); const img = new Image();
img.draggable = false;
img.oncontextmenu = stopEvent;
if (typeof source === 'string') { if (typeof source === 'string') {
img.src = source; img.src = source;

View File

@ -30,6 +30,7 @@ type OwnProps = {
hasCustomAppendix?: boolean; hasCustomAppendix?: boolean;
lastSyncTime?: number; lastSyncTime?: number;
isOwn: boolean; isOwn: boolean;
isProtected?: boolean;
albumLayout: IAlbumLayout; albumLayout: IAlbumLayout;
onMediaClick: (messageId: number) => void; onMediaClick: (messageId: number) => void;
}; };
@ -46,6 +47,7 @@ const Album: FC<OwnProps & StateProps> = ({
hasCustomAppendix, hasCustomAppendix,
lastSyncTime, lastSyncTime,
isOwn, isOwn,
isProtected,
albumLayout, albumLayout,
onMediaClick, onMediaClick,
uploadsById, uploadsById,
@ -85,6 +87,7 @@ const Album: FC<OwnProps & StateProps> = ({
shouldAffectAppendix={shouldAffectAppendix} shouldAffectAppendix={shouldAffectAppendix}
uploadProgress={uploadProgress} uploadProgress={uploadProgress}
dimensions={dimensions} dimensions={dimensions}
isProtected={isProtected}
onClick={onMediaClick} onClick={onMediaClick}
onCancelUpload={handleCancelUpload} onCancelUpload={handleCancelUpload}
isDownloading={activeDownloadIds.includes(message.id)} isDownloading={activeDownloadIds.includes(message.id)}
@ -102,6 +105,7 @@ const Album: FC<OwnProps & StateProps> = ({
uploadProgress={uploadProgress} uploadProgress={uploadProgress}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
dimensions={dimensions} dimensions={dimensions}
isProtected={isProtected}
onClick={onMediaClick} onClick={onMediaClick}
onCancelUpload={handleCancelUpload} onCancelUpload={handleCancelUpload}
isDownloading={activeDownloadIds.includes(message.id)} isDownloading={activeDownloadIds.includes(message.id)}

View File

@ -11,6 +11,7 @@ import {
selectAllowedMessageActions, selectAllowedMessageActions,
selectChat, selectChat,
selectCurrentMessageList, selectCurrentMessageList,
selectIsMessageProtected,
} from '../../../modules/selectors'; } from '../../../modules/selectors';
import { isChatGroup, isOwnMessage } from '../../../modules/helpers'; import { isChatGroup, isOwnMessage } from '../../../modules/helpers';
import { SEEN_BY_MEMBERS_EXPIRE, SEEN_BY_MEMBERS_CHAT_MAX } from '../../../config'; import { SEEN_BY_MEMBERS_EXPIRE, SEEN_BY_MEMBERS_CHAT_MAX } from '../../../config';
@ -366,6 +367,7 @@ export default memo(withGlobal<OwnProps>(
&& chat.membersCount && chat.membersCount
&& chat.membersCount < SEEN_BY_MEMBERS_CHAT_MAX && chat.membersCount < SEEN_BY_MEMBERS_CHAT_MAX
&& message.date > Date.now() / 1000 - SEEN_BY_MEMBERS_EXPIRE); && message.date > Date.now() / 1000 - SEEN_BY_MEMBERS_EXPIRE);
const isProtected = selectIsMessageProtected(global, message);
return { return {
noOptions, noOptions,
@ -377,13 +379,13 @@ export default memo(withGlobal<OwnProps>(
canDelete, canDelete,
canReport, canReport,
canEdit: !isPinned && canEdit, canEdit: !isPinned && canEdit,
canForward: !isScheduled && canForward, canForward: !isProtected && !isScheduled && canForward,
canFaveSticker: !isScheduled && canFaveSticker, canFaveSticker: !isScheduled && canFaveSticker,
canUnfaveSticker: !isScheduled && canUnfaveSticker, canUnfaveSticker: !isScheduled && canUnfaveSticker,
canCopy, canCopy: !isProtected && canCopy,
canCopyLink: !isScheduled && canCopyLink, canCopyLink: !isProtected && !isScheduled && canCopyLink,
canSelect, canSelect,
canDownload, canDownload: !isProtected && canDownload,
activeDownloads, activeDownloads,
canShowSeenBy, canShowSeenBy,
}; };

View File

@ -27,6 +27,10 @@
transform: translateX(-2.5rem) !important; transform: translateX(-2.5rem) !important;
} }
&.is-protected {
user-select: none;
}
> .Avatar, > .Avatar,
> .message-content-wrapper { > .message-content-wrapper {
opacity: 1; opacity: 1;

View File

@ -45,6 +45,7 @@ import {
selectAllowedMessageActions, selectAllowedMessageActions,
selectIsDownloading, selectIsDownloading,
selectThreadInfo, selectThreadInfo,
selectIsMessageProtected,
} from '../../../modules/selectors'; } from '../../../modules/selectors';
import { import {
getMessageContent, getMessageContent,
@ -138,6 +139,7 @@ type StateProps = {
replyMessageSender?: ApiUser | ApiChat; replyMessageSender?: ApiUser | ApiChat;
outgoingStatus?: ApiMessageOutgoingStatus; outgoingStatus?: ApiMessageOutgoingStatus;
uploadProgress?: number; uploadProgress?: number;
isProtected?: boolean;
isFocused?: boolean; isFocused?: boolean;
focusDirection?: FocusDirection; focusDirection?: FocusDirection;
noFocusHighlight?: boolean; noFocusHighlight?: boolean;
@ -201,6 +203,7 @@ const Message: FC<OwnProps & StateProps> = ({
replyMessageSender, replyMessageSender,
outgoingStatus, outgoingStatus,
uploadProgress, uploadProgress,
isProtected,
isFocused, isFocused,
focusDirection, focusDirection,
noFocusHighlight, noFocusHighlight,
@ -325,6 +328,7 @@ const Message: FC<OwnProps & StateProps> = ({
isAlbum, isAlbum,
Boolean(isInSelectMode), Boolean(isInSelectMode),
Boolean(canReply), Boolean(canReply),
Boolean(isProtected),
onContextMenu, onContextMenu,
handleBeforeContextMenu, handleBeforeContextMenu,
); );
@ -364,6 +368,7 @@ const Message: FC<OwnProps & StateProps> = ({
const containerClassName = buildClassName( const containerClassName = buildClassName(
'Message message-list-item', 'Message message-list-item',
isFirstInGroup && 'first-in-group', isFirstInGroup && 'first-in-group',
isProtected && 'is-protected',
isLastInGroup && 'last-in-group', isLastInGroup && 'last-in-group',
isFirstInDocumentGroup && 'first-in-document-group', isFirstInDocumentGroup && 'first-in-document-group',
isLastInDocumentGroup && 'last-in-document-group', isLastInDocumentGroup && 'last-in-document-group',
@ -485,6 +490,7 @@ const Message: FC<OwnProps & StateProps> = ({
{hasReply && ( {hasReply && (
<EmbeddedMessage <EmbeddedMessage
message={replyMessage} message={replyMessage}
isProtected={isProtected}
sender={replyMessageSender} sender={replyMessageSender}
observeIntersection={observeIntersectionForMedia} observeIntersection={observeIntersectionForMedia}
onClick={handleReplyClick} onClick={handleReplyClick}
@ -514,6 +520,7 @@ const Message: FC<OwnProps & StateProps> = ({
albumLayout={albumLayout!} albumLayout={albumLayout!}
observeIntersection={observeIntersectionForMedia} observeIntersection={observeIntersectionForMedia}
isOwn={isOwn} isOwn={isOwn}
isProtected={isProtected}
hasCustomAppendix={hasCustomAppendix} hasCustomAppendix={hasCustomAppendix}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
onMediaClick={handleAlbumMediaClick} onMediaClick={handleAlbumMediaClick}
@ -530,6 +537,7 @@ const Message: FC<OwnProps & StateProps> = ({
onClick={handleMediaClick} onClick={handleMediaClick}
onCancelUpload={handleCancelUpload} onCancelUpload={handleCancelUpload}
isDownloading={isDownloading} isDownloading={isDownloading}
isProtected={isProtected}
theme={theme} theme={theme}
/> />
)} )}
@ -554,6 +562,7 @@ const Message: FC<OwnProps & StateProps> = ({
onClick={handleMediaClick} onClick={handleMediaClick}
onCancelUpload={handleCancelUpload} onCancelUpload={handleCancelUpload}
isDownloading={isDownloading} isDownloading={isDownloading}
isProtected={isProtected}
/> />
)} )}
{(audio || voice) && ( {(audio || voice) && (
@ -615,6 +624,7 @@ const Message: FC<OwnProps & StateProps> = ({
onMediaClick={handleMediaClick} onMediaClick={handleMediaClick}
onCancelMediaTransfer={handleCancelUpload} onCancelMediaTransfer={handleCancelUpload}
isDownloading={isDownloading} isDownloading={isDownloading}
isProtected={isProtected}
theme={theme} theme={theme}
/> />
)} )}
@ -880,6 +890,7 @@ export default memo(withGlobal<OwnProps>(
isThreadTop, isThreadTop,
replyMessage, replyMessage,
replyMessageSender, replyMessageSender,
isProtected: selectIsMessageProtected(global, message),
isFocused, isFocused,
isForwarding, isForwarding,
isChatWithSelf, isChatWithSelf,

View File

@ -39,6 +39,7 @@ export type OwnProps = {
dimensions?: IMediaDimensions & { isSmall?: boolean }; dimensions?: IMediaDimensions & { isSmall?: boolean };
nonInteractive?: boolean; nonInteractive?: boolean;
isDownloading: boolean; isDownloading: boolean;
isProtected?: boolean;
theme: ISettings['theme']; theme: ISettings['theme'];
onClick?: (id: number) => void; onClick?: (id: number) => void;
onCancelUpload?: (message: ApiMessage) => void; onCancelUpload?: (message: ApiMessage) => void;
@ -60,6 +61,7 @@ const Photo: FC<OwnProps> = ({
nonInteractive, nonInteractive,
shouldAffectAppendix, shouldAffectAppendix,
isDownloading, isDownloading,
isProtected,
theme, theme,
onClick, onClick,
onCancelUpload, onCancelUpload,
@ -167,7 +169,9 @@ const Photo: FC<OwnProps> = ({
width={width} width={width}
height={height} height={height}
alt="" alt=""
draggable={!isProtected}
/> />
{isProtected && <span className="protector" />}
{shouldRenderSpinner && !shouldRenderDownloadButton && ( {shouldRenderSpinner && !shouldRenderDownloadButton && (
<div className={`media-loading ${spinnerClassNames}`}> <div className={`media-loading ${spinnerClassNames}`}>
<ProgressSpinner progress={transferProgress} onClick={isUploading ? handleClick : undefined} /> <ProgressSpinner progress={transferProgress} onClick={isUploading ? handleClick : undefined} />

View File

@ -42,6 +42,7 @@ export type OwnProps = {
dimensions?: IMediaDimensions; dimensions?: IMediaDimensions;
lastSyncTime?: number; lastSyncTime?: number;
isDownloading: boolean; isDownloading: boolean;
isProtected?: boolean;
onClick?: (id: number) => void; onClick?: (id: number) => void;
onCancelUpload?: (message: ApiMessage) => void; onCancelUpload?: (message: ApiMessage) => void;
}; };
@ -59,6 +60,7 @@ const Video: FC<OwnProps> = ({
onClick, onClick,
onCancelUpload, onCancelUpload,
isDownloading, isDownloading,
isProtected,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -173,6 +175,7 @@ const Video: FC<OwnProps> = ({
// @ts-ignore teact feature // @ts-ignore teact feature
style={`width: ${width}px; height: ${height}px;`} style={`width: ${width}px; height: ${height}px;`}
alt="" alt=""
draggable={!isProtected}
/> />
{isInline && ( {isInline && (
<video <video
@ -186,11 +189,13 @@ const Video: FC<OwnProps> = ({
playsInline playsInline
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...bufferingHandlers} {...bufferingHandlers}
draggable={!isProtected}
onTimeUpdate={handleTimeUpdate} onTimeUpdate={handleTimeUpdate}
> >
<source src={fullMediaData} /> <source src={fullMediaData} />
</video> </video>
)} )}
{isProtected && <span className="protector" />}
{shouldRenderPlayButton && <i className={buildClassName('icon-large-play', playButtonClassNames)} />} {shouldRenderPlayButton && <i className={buildClassName('icon-large-play', playButtonClassNames)} />}
{shouldRenderSpinner && ( {shouldRenderSpinner && (
<div className={buildClassName('media-loading', spinnerClassNames)}> <div className={buildClassName('media-loading', spinnerClassNames)}>

View File

@ -27,6 +27,7 @@ type OwnProps = {
inPreview?: boolean; inPreview?: boolean;
lastSyncTime?: number; lastSyncTime?: number;
isDownloading?: boolean; isDownloading?: boolean;
isProtected?: boolean;
theme: ISettings['theme']; theme: ISettings['theme'];
onMediaClick?: () => void; onMediaClick?: () => void;
onCancelMediaTransfer?: () => void; onCancelMediaTransfer?: () => void;
@ -41,6 +42,7 @@ const WebPage: FC<OwnProps> = ({
inPreview, inPreview,
lastSyncTime, lastSyncTime,
isDownloading = false, isDownloading = false,
isProtected,
theme, theme,
onMediaClick, onMediaClick,
onCancelMediaTransfer, onCancelMediaTransfer,
@ -97,6 +99,7 @@ const WebPage: FC<OwnProps> = ({
onClick={isMediaInteractive ? handleMediaClick : undefined} onClick={isMediaInteractive ? handleMediaClick : undefined}
onCancelUpload={onCancelMediaTransfer} onCancelUpload={onCancelMediaTransfer}
isDownloading={isDownloading} isDownloading={isDownloading}
isProtected={isProtected}
theme={theme} theme={theme}
/> />
)} )}
@ -120,6 +123,7 @@ const WebPage: FC<OwnProps> = ({
onClick={isMediaInteractive ? handleMediaClick : undefined} onClick={isMediaInteractive ? handleMediaClick : undefined}
onCancelUpload={onCancelMediaTransfer} onCancelUpload={onCancelMediaTransfer}
isDownloading={isDownloading} isDownloading={isDownloading}
isProtected={isProtected}
/> />
)} )}
</div> </div>

View File

@ -7,6 +7,7 @@ import windowSize from '../../../../util/windowSize';
import { captureEvents, SwipeDirection } from '../../../../util/captureEvents'; import { captureEvents, SwipeDirection } from '../../../../util/captureEvents';
import useFlag from '../../../../hooks/useFlag'; import useFlag from '../../../../hooks/useFlag';
import { preventMessageInputBlur } from '../../helpers/preventMessageInputBlur'; import { preventMessageInputBlur } from '../../helpers/preventMessageInputBlur';
import stopEvent from '../../../../util/stopEvent';
const ANDROID_KEYBOARD_HIDE_DELAY_MS = 350; const ANDROID_KEYBOARD_HIDE_DELAY_MS = 350;
const SWIPE_ANIMATION_DURATION = 150; const SWIPE_ANIMATION_DURATION = 150;
@ -18,6 +19,7 @@ export default function useOuterHandlers(
isAlbum: boolean, isAlbum: boolean,
isInSelectMode: boolean, isInSelectMode: boolean,
canReply: boolean, canReply: boolean,
isProtected: boolean,
onContextMenu: (e: React.MouseEvent) => void, onContextMenu: (e: React.MouseEvent) => void,
handleBeforeContextMenu: (e: React.MouseEvent) => void, handleBeforeContextMenu: (e: React.MouseEvent) => void,
) { ) {
@ -107,7 +109,7 @@ export default function useOuterHandlers(
return { return {
handleMouseDown: !isInSelectMode ? handleMouseDown : undefined, handleMouseDown: !isInSelectMode ? handleMouseDown : undefined,
handleClick, handleClick,
handleContextMenu: !isInSelectMode ? handleContextMenu : undefined, handleContextMenu: !isInSelectMode ? handleContextMenu : (isProtected ? stopEvent : undefined),
handleDoubleClick: !isInSelectMode ? handleContainerDoubleClick : undefined, handleDoubleClick: !isInSelectMode ? handleContainerDoubleClick : undefined,
handleContentDoubleClick: !IS_TOUCH_ENV ? stopPropagation : undefined, handleContentDoubleClick: !IS_TOUCH_ENV ? stopPropagation : undefined,
isSwiped, isSwiped,

View File

@ -92,6 +92,7 @@ type StateProps = {
lastSyncTime?: number; lastSyncTime?: number;
serverTimeOffset: number; serverTimeOffset: number;
activeDownloadIds: number[]; activeDownloadIds: number[];
isChatProtected?: boolean;
}; };
const TABS = [ const TABS = [
@ -130,6 +131,7 @@ const Profile: FC<OwnProps & StateProps> = ({
lastSyncTime, lastSyncTime,
activeDownloadIds, activeDownloadIds,
serverTimeOffset, serverTimeOffset,
isChatProtected,
}) => { }) => {
const { const {
setLocalMediaSearchType, setLocalMediaSearchType,
@ -322,6 +324,7 @@ const Profile: FC<OwnProps & StateProps> = ({
<Media <Media
key={id} key={id}
message={chatMessages[id]} message={chatMessages[id]}
isProtected={isChatProtected || chatMessages[id].isProtected}
onClick={handleSelectMedia} onClick={handleSelectMedia}
/> />
)) ))
@ -342,6 +345,7 @@ const Profile: FC<OwnProps & StateProps> = ({
<WebLink <WebLink
key={id} key={id}
message={chatMessages[id]} message={chatMessages[id]}
isProtected={isChatProtected || chatMessages[id].isProtected}
onMessageClick={handleMessageFocus} onMessageClick={handleMessageFocus}
/> />
)) ))
@ -533,6 +537,7 @@ export default memo(withGlobal<OwnProps>(
usersById, usersById,
userStatusesById, userStatusesById,
chatsById, chatsById,
isChatProtected: chat?.isProtected,
...(hasMembersTab && members && { members }), ...(hasMembersTab && members && { members }),
...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }), ...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }),
}; };

View File

@ -21,9 +21,7 @@ import FloatingActionButton from '../../ui/FloatingActionButton';
import UsernameInput from '../../common/UsernameInput'; import UsernameInput from '../../common/UsernameInput';
import ConfirmDialog from '../../ui/ConfirmDialog'; import ConfirmDialog from '../../ui/ConfirmDialog';
type PrivacyType = type PrivacyType = 'private' | 'public';
'private'
| 'public';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
@ -36,6 +34,7 @@ type StateProps = {
isChannel: boolean; isChannel: boolean;
progress?: ManagementProgress; progress?: ManagementProgress;
isUsernameAvailable?: boolean; isUsernameAvailable?: boolean;
isProtected?: boolean;
}; };
const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
@ -45,11 +44,13 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
isChannel, isChannel,
progress, progress,
isUsernameAvailable, isUsernameAvailable,
isProtected,
}) => { }) => {
const { const {
checkPublicLink, checkPublicLink,
updatePublicLink, updatePublicLink,
updatePrivateLink, updatePrivateLink,
toggleIsProtected,
} = getDispatch(); } = getDispatch();
const isPublic = Boolean(chat.username); const isPublic = Boolean(chat.username);
@ -76,6 +77,13 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
setPrivacyType(value as PrivacyType); setPrivacyType(value as PrivacyType);
}, []); }, []);
const handleForwardingOptionChange = useCallback((value: string) => {
toggleIsProtected({
chatId: chat.id,
isProtected: value === 'protected',
});
}, [chat.id, toggleIsProtected]);
const handleSave = useCallback(() => { const handleSave = useCallback(() => {
updatePublicLink({ username: privacyType === 'public' ? username : '' }); updatePublicLink({ username: privacyType === 'public' ? username : '' });
}, [privacyType, updatePublicLink, username]); }, [privacyType, updatePublicLink, username]);
@ -94,6 +102,14 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
{ value: 'public', label: lang(`${langPrefix1}Public`), subLabel: lang(`${langPrefix1}PublicInfo`) }, { value: 'public', label: lang(`${langPrefix1}Public`), subLabel: lang(`${langPrefix1}PublicInfo`) },
]; ];
const forwardingOptions = [{
value: 'allowed',
label: lang('ChannelVisibility.Forwarding.Enabled'),
}, {
value: 'protected',
label: lang('ChannelVisibility.Forwarding.Disabled'),
}];
const isLoading = progress === ManagementProgress.InProgress; const isLoading = progress === ManagementProgress.InProgress;
return ( return (
@ -148,6 +164,22 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
</p> </p>
</div> </div>
)} )}
<div className="section" dir={lang.isRtl ? 'rtl' : undefined}>
<h3 className="section-heading">
{lang(isChannel ? 'ChannelVisibility.Forwarding.ChannelTitle' : 'ChannelVisibility.Forwarding.GroupTitle')}
</h3>
<RadioGroup
selected={isProtected ? 'protected' : 'allowed'}
name="channel-type"
options={forwardingOptions}
onChange={handleForwardingOptionChange}
/>
<p className="section-info">
{isChannel
? lang('ChannelVisibility.Forwarding.ChannelInfo')
: lang('ChannelVisibility.Forwarding.GroupInfo')}
</p>
</div>
</div> </div>
<FloatingActionButton <FloatingActionButton
isShown={canUpdate} isShown={canUpdate}
@ -175,6 +207,7 @@ export default memo(withGlobal<OwnProps>(
isChannel: isChatChannel(chat), isChannel: isChatChannel(chat),
progress: global.management.progress, progress: global.management.progress,
isUsernameAvailable, isUsernameAvailable,
isProtected: chat?.isProtected,
}; };
}, },
)(ManageChatPrivacyType)); )(ManageChatPrivacyType));

View File

@ -30,7 +30,7 @@ export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive'; export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg'; export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
export const LANG_CACHE_NAME = 'tt-lang-packs-v6'; export const LANG_CACHE_NAME = 'tt-lang-packs-v7';
export const ASSET_CACHE_NAME = 'tt-assets'; export const ASSET_CACHE_NAME = 'tt-assets';
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500]; export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];

View File

@ -496,7 +496,7 @@ export type ActionTypes = (
'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' | 'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' |
'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' | 'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' |
'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' | 'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' |
'addChatMembers' | 'deleteChatMember' | 'openPreviousChat' | 'editChatFolders' | 'addChatMembers' | 'deleteChatMember' | 'openPreviousChat' | 'editChatFolders' | 'toggleIsProtected' |
// messages // messages
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' | 'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' | 'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' |

View File

@ -1080,6 +1080,7 @@ messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Boo
messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory; messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
messages.deleteChat#5bd0ee50 chat_id:long = Bool; messages.deleteChat#5bd0ee50 chat_id:long = Bool;
messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector<long>; messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector<long>;
messages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates;
messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool; messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool;
updates.getState#edd4882a = updates.State; updates.getState#edd4882a = updates.State;
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;

View File

@ -1081,6 +1081,7 @@ messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Boo
messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory; messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
messages.deleteChat#5bd0ee50 chat_id:long = Bool; messages.deleteChat#5bd0ee50 chat_id:long = Bool;
messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector<long>; messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector<long>;
messages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates;
messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool; messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool;
updates.getState#edd4882a = updates.State; updates.getState#edd4882a = updates.State;
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;

View File

@ -993,6 +993,17 @@ addReducer('deleteChatMember', (global, actions, payload) => {
})(); })();
}); });
addReducer('toggleIsProtected', (global, actions, payload) => {
const { chatId, isProtected } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
void callApi('toggleIsProtected', { chat, isProtected });
});
async function loadChats(listType: 'active' | 'archived', offsetId?: string, offsetDate?: number) { async function loadChats(listType: 'active' | 'archived', offsetId?: string, offsetDate?: number) {
let global = getGlobal(); let global = getGlobal();

View File

@ -856,6 +856,10 @@ export function selectLastServiceNotification(global: GlobalState) {
return serviceNotifications.find(({ id }) => id === maxId); return serviceNotifications.find(({ id }) => id === maxId);
} }
export function selectIsMessageProtected(global: GlobalState, message?: ApiMessage) {
return message ? message.isProtected || selectChat(global, message.chatId)?.isProtected : false;
}
export function selectSponsoredMessage(global: GlobalState, chatId: string) { export function selectSponsoredMessage(global: GlobalState, chatId: string) {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const message = chat && isChatChannel(chat) ? global.messages.sponsoredByChatId[chatId] : undefined; const message = chat && isChatChannel(chat) ? global.messages.sponsoredByChatId[chatId] : undefined;

View File

@ -225,6 +225,15 @@ div[role="button"] {
color: var(--color-text-secondary) !important; color: var(--color-text-secondary) !important;
} }
.protector {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 2;
}
.for-ios-autocapitalization-fix { .for-ios-autocapitalization-fix {
position: fixed; position: fixed;
font-size: 16px; font-size: 16px;

View File

@ -1821,4 +1821,28 @@ export default {
key: 'AutoDownloadFilesTitle', key: 'AutoDownloadFilesTitle',
value: 'Auto-download files and music', value: 'Auto-download files and music',
}, },
'ChannelVisibility.Forwarding.ChannelTitle': {
key: 'ChannelVisibility.Forwarding.ChannelTitle',
value: 'Forwarding From This Channel',
},
'ChannelVisibility.Forwarding.GroupTitle': {
key: 'ChannelVisibility.Forwarding.GroupTitle',
value: 'Forwarding From This Group',
},
'ChannelVisibility.Forwarding.ChannelInfo': {
key: 'ChannelVisibility.Forwarding.ChannelInfo',
value: 'Subscribers can forward messages from this channel and save media files.',
},
'ChannelVisibility.Forwarding.GroupInfo': {
key: 'ChannelVisibility.Forwarding.GroupInfo',
value: 'Members can forward messages from this group and save media files.',
},
'ChannelVisibility.Forwarding.Enabled': {
key: 'ChannelVisibility.Forwarding.Enabled',
value: 'Allow Forwarding',
},
'ChannelVisibility.Forwarding.Disabled': {
key: 'ChannelVisibility.Forwarding.Disabled',
value: 'Restrict Forwarding',
},
} as ApiLangPack; } as ApiLangPack;

6
src/util/stopEvent.ts Normal file
View File

@ -0,0 +1,6 @@
import React from '../lib/teact/teact';
export default (e: React.UIEvent | Event) => {
e.stopPropagation();
e.preventDefault();
};