Support protected ("no forwards") chats and messages (#1602)
This commit is contained in:
parent
72ae33139a
commit
d1d463c7d2
@ -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 }),
|
||||||
|
|||||||
@ -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 }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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" />}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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} />
|
||||||
|
|||||||
@ -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)}>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 }),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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];
|
||||||
|
|
||||||
|
|||||||
@ -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' |
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
6
src/util/stopEvent.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from '../lib/teact/teact';
|
||||||
|
|
||||||
|
export default (e: React.UIEvent | Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user