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 }),
|
||||
joinDate: peerEntity.date,
|
||||
}),
|
||||
...((peerEntity instanceof GramJs.Chat || peerEntity instanceof GramJs.Channel) && {
|
||||
isProtected: Boolean('noforwards' in peerEntity && peerEntity.noforwards),
|
||||
}),
|
||||
...(isSupport && { isSupport: true }),
|
||||
...buildApiChatPermissions(peerEntity),
|
||||
...(('creator' in peerEntity) && { isCreator: peerEntity.creator }),
|
||||
|
||||
@ -140,7 +140,7 @@ type UniversalMessage = (
|
||||
& Pick<Partial<GramJs.Message & GramJs.MessageService>, (
|
||||
'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' |
|
||||
'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') }),
|
||||
...(replies?.comments && { threadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }),
|
||||
...(postAuthor && { adminTitle: postAuthor }),
|
||||
...(mtpMessage.noforwards && { isProtected: true }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1130,3 +1130,14 @@ export async function importChatInvite({ hash }: { hash: string }) {
|
||||
|
||||
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,
|
||||
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
|
||||
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
||||
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember,
|
||||
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
|
||||
} from './chats';
|
||||
|
||||
export {
|
||||
|
||||
@ -61,6 +61,9 @@ export function init(_onUpdate: OnApiUpdate) {
|
||||
|
||||
const sentMessageIds = new Set();
|
||||
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)[]) {
|
||||
entities
|
||||
@ -636,6 +639,16 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
||||
));
|
||||
|
||||
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);
|
||||
if (chat) {
|
||||
onUpdate({
|
||||
|
||||
@ -31,6 +31,7 @@ export interface ApiChat {
|
||||
isSupport?: boolean;
|
||||
photos?: ApiPhoto[];
|
||||
draftDate?: number;
|
||||
isProtected?: boolean;
|
||||
|
||||
// Calls
|
||||
isCallActive?: boolean;
|
||||
|
||||
@ -265,6 +265,7 @@ export interface ApiMessage {
|
||||
shouldHideKeyboardButtons?: boolean;
|
||||
isFromScheduled?: boolean;
|
||||
seenByUserIds?: string[];
|
||||
isProtected?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiThreadInfo {
|
||||
|
||||
@ -28,6 +28,7 @@ type OwnProps = {
|
||||
sender?: ApiUser | ApiChat;
|
||||
title?: string;
|
||||
customText?: string;
|
||||
isProtected?: boolean;
|
||||
onClick: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
@ -39,6 +40,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
sender,
|
||||
title,
|
||||
customText,
|
||||
isProtected,
|
||||
observeIntersection,
|
||||
onClick,
|
||||
}) => {
|
||||
@ -61,7 +63,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
className={buildClassName('EmbeddedMessage', className)}
|
||||
onClick={message ? onClick : undefined}
|
||||
>
|
||||
{mediaThumbnail && renderPictogram(pictogramId, mediaThumbnail, mediaBlobUrl, isRoundVideo)}
|
||||
{mediaThumbnail && renderPictogram(pictogramId, mediaThumbnail, mediaBlobUrl, isRoundVideo, isProtected)}
|
||||
<div className="message-text">
|
||||
<p dir="auto">
|
||||
{!message ? (
|
||||
@ -83,18 +85,23 @@ function renderPictogram(
|
||||
thumbDataUri: string,
|
||||
blobUrl?: string,
|
||||
isRoundVideo?: boolean,
|
||||
isProtected?: boolean,
|
||||
) {
|
||||
const { width, height } = getPictogramDimensions();
|
||||
|
||||
return (
|
||||
<img
|
||||
id={id}
|
||||
src={blobUrl || thumbDataUri}
|
||||
width={width}
|
||||
height={height}
|
||||
alt=""
|
||||
className={isRoundVideo ? 'round' : ''}
|
||||
/>
|
||||
<>
|
||||
<img
|
||||
id={id}
|
||||
src={blobUrl || thumbDataUri}
|
||||
width={width}
|
||||
height={height}
|
||||
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 { formatMediaDuration } from '../../util/dateFormat';
|
||||
import stopEvent from '../../util/stopEvent';
|
||||
import {
|
||||
getMessageMediaHash,
|
||||
getMessageMediaThumbDataUri,
|
||||
@ -17,10 +18,16 @@ import './Media.scss';
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
idPrefix?: string;
|
||||
isProtected?: boolean;
|
||||
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(() => {
|
||||
onClick!(message.id, message.chatId);
|
||||
}, [message.id, message.chatId, onClick]);
|
||||
@ -33,9 +40,16 @@ const Media: FC<OwnProps> = ({ message, idPrefix = 'shared-media', onClick }) =>
|
||||
|
||||
return (
|
||||
<div id={`${idPrefix}${message.id}`} className="Media scroll-item" onClick={onClick ? handleClick : undefined}>
|
||||
<img src={thumbDataUri} alt="" />
|
||||
<img src={mediaBlobUrl} className={buildClassName('full-media', transitionClassNames)} alt="" />
|
||||
<img src={thumbDataUri} alt="" draggable={!isProtected} onContextMenu={isProtected ? stopEvent : undefined} />
|
||||
<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>}
|
||||
{isProtected && <span className="protector" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -20,11 +20,12 @@ const MAX_TEXT_LENGTH = 170; // symbols
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
senderTitle?: string;
|
||||
isProtected?: boolean;
|
||||
onMessageClick: (messageId: number, chatId: string) => void;
|
||||
};
|
||||
|
||||
const WebLink: FC<OwnProps> = ({
|
||||
message, senderTitle, onMessageClick,
|
||||
message, senderTitle, isProtected, onMessageClick,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
@ -76,7 +77,7 @@ const WebLink: FC<OwnProps> = ({
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
{photo && (
|
||||
<Media message={message} />
|
||||
<Media message={message} isProtected={isProtected} />
|
||||
)}
|
||||
<div className="content">
|
||||
<Link isRtl={lang.isRtl} className="site-title" onClick={handleMessageClick}>
|
||||
|
||||
@ -35,6 +35,7 @@ const LinkResults: FC<OwnProps & StateProps> = ({
|
||||
globalMessagesByChatId,
|
||||
foundIds,
|
||||
lastSyncTime,
|
||||
isChatProtected,
|
||||
}) => {
|
||||
const {
|
||||
searchMessagesGlobal,
|
||||
@ -89,6 +90,7 @@ const LinkResults: FC<OwnProps & StateProps> = ({
|
||||
key={message.id}
|
||||
message={message}
|
||||
senderTitle={getSenderName(lang, message, chatsById, usersById)}
|
||||
isProtected={isChatProtected || message.isProtected}
|
||||
onMessageClick={handleMessageFocus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -33,6 +33,7 @@ const MediaResults: FC<OwnProps & StateProps> = ({
|
||||
globalMessagesByChatId,
|
||||
foundIds,
|
||||
lastSyncTime,
|
||||
isChatProtected,
|
||||
}) => {
|
||||
const {
|
||||
searchMessagesGlobal,
|
||||
@ -81,6 +82,7 @@ const MediaResults: FC<OwnProps & StateProps> = ({
|
||||
key={message.id}
|
||||
idPrefix="search-media"
|
||||
message={message}
|
||||
isProtected={isChatProtected || message.isProtected}
|
||||
onClick={handleSelectMedia}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
} from '../../../../api/types';
|
||||
import { ISettings } from '../../../../types';
|
||||
|
||||
import { selectTheme } from '../../../../modules/selectors';
|
||||
import { selectChat, selectTheme } from '../../../../modules/selectors';
|
||||
|
||||
export type StateProps = {
|
||||
theme: ISettings['theme'];
|
||||
@ -16,6 +16,7 @@ export type StateProps = {
|
||||
lastSyncTime?: number;
|
||||
searchChatId?: string;
|
||||
activeDownloads: Record<string, number[]>;
|
||||
isChatProtected?: boolean;
|
||||
};
|
||||
|
||||
export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
|
||||
@ -46,6 +47,7 @@ export function createMapStateToProps(type: ApiGlobalMessageSearchType) {
|
||||
foundIds,
|
||||
searchChatId: chatId,
|
||||
activeDownloads,
|
||||
isChatProtected: chatId ? selectChat(global, chatId)?.isProtected : undefined,
|
||||
lastSyncTime: global.lastSyncTime,
|
||||
};
|
||||
};
|
||||
|
||||
@ -23,6 +23,7 @@ import buildClassName from '../../util/buildClassName';
|
||||
import { fastRaf } from '../../util/schedulers';
|
||||
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
|
||||
import { processDeepLink } from '../../util/deeplink';
|
||||
import stopEvent from '../../util/stopEvent';
|
||||
import windowSize from '../../util/windowSize';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
import useBackgroundMode from '../../hooks/useBackgroundMode';
|
||||
@ -273,11 +274,6 @@ const Main: FC<StateProps> = ({
|
||||
|
||||
usePreventPinchZoomGesture(isMediaViewerOpen);
|
||||
|
||||
function stopEvent(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="Main" className={className} onDrop={stopEvent} onDragOver={stopEvent}>
|
||||
<LeftColumn />
|
||||
|
||||
@ -12,7 +12,7 @@ import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import { getMessageMediaHash } from '../../modules/helpers';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
||||
import { selectIsDownloading } from '../../modules/selectors';
|
||||
import { selectIsDownloading, selectIsMessageProtected } from '../../modules/selectors';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import DropdownMenu from '../ui/DropdownMenu';
|
||||
@ -23,6 +23,7 @@ import './MediaViewerActions.scss';
|
||||
|
||||
type StateProps = {
|
||||
isDownloading: boolean;
|
||||
isProtected?: boolean;
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
@ -45,6 +46,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
fileName,
|
||||
isAvatar,
|
||||
isDownloading,
|
||||
isProtected,
|
||||
onCloseMediaViewer,
|
||||
onForward,
|
||||
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 (isProtected) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="MediaViewerActions-mobile">
|
||||
<DropdownMenu
|
||||
@ -123,7 +162,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div className="MediaViewerActions">
|
||||
{!isAvatar && (
|
||||
{!isAvatar && !isProtected && (
|
||||
<>
|
||||
<Button
|
||||
round
|
||||
@ -136,32 +175,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{renderDownloadButton()}
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
@ -187,9 +201,11 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { message }): StateProps => {
|
||||
const isDownloading = message ? selectIsDownloading(global, message) : false;
|
||||
const isProtected = selectIsMessageProtected(global, message);
|
||||
|
||||
return {
|
||||
isDownloading,
|
||||
isProtected,
|
||||
};
|
||||
},
|
||||
)(MediaViewerActions));
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
} from '../../api/types';
|
||||
import { MediaViewerOrigin } from '../../types';
|
||||
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import useBlurSync from '../../hooks/useBlurSync';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
||||
@ -26,10 +27,11 @@ import {
|
||||
isMessageDocumentVideo,
|
||||
} from '../../modules/helpers';
|
||||
import {
|
||||
selectChat, selectChatMessage, selectScheduledMessage, selectUser,
|
||||
selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectUser,
|
||||
} from '../../modules/selectors';
|
||||
import { AVATAR_FULL_DIMENSIONS, calculateMediaViewerDimensions } from '../common/helpers/mediaDimensions';
|
||||
import { renderMessageText } from '../common/helpers/renderMessageText';
|
||||
import stopEvent from '../../util/stopEvent';
|
||||
|
||||
import Spinner from '../ui/Spinner';
|
||||
import MediaViewerFooter from './MediaViewerFooter';
|
||||
@ -60,6 +62,7 @@ type StateProps = {
|
||||
profilePhotoIndex?: number;
|
||||
message?: ApiMessage;
|
||||
origin?: MediaViewerOrigin;
|
||||
isProtected?: boolean;
|
||||
};
|
||||
|
||||
const ANIMATION_DURATION = 350;
|
||||
@ -77,6 +80,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
onClose,
|
||||
onFooterClick,
|
||||
isFooterHidden,
|
||||
isProtected,
|
||||
} = props;
|
||||
/* Content */
|
||||
const photo = message ? getMessagePhoto(message) : undefined;
|
||||
@ -163,7 +167,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
{renderPhoto(
|
||||
fullMediaBlobUrl || previewBlobUrl,
|
||||
calculateMediaViewerDimensions(AVATAR_FULL_DIMENSIONS, false),
|
||||
false,
|
||||
!IS_SINGLE_COLUMN_LAYOUT && !isProtected,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@ -176,10 +180,11 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
<div
|
||||
className={`MediaViewerContent ${hasFooter ? 'has-footer' : ''}`}
|
||||
>
|
||||
{isProtected && <div onContextMenu={stopEvent} className="protector" />}
|
||||
{isPhoto && renderPhoto(
|
||||
localBlobUrl || fullMediaBlobUrl || previewBlobUrl || pictogramBlobUrl,
|
||||
message && calculateMediaViewerDimensions(dimensions!, hasFooter),
|
||||
false,
|
||||
!IS_SINGLE_COLUMN_LAYOUT && !isProtected,
|
||||
)}
|
||||
{isVideo && (isActive ? (
|
||||
<VideoPlayer
|
||||
@ -197,7 +202,7 @@ const MediaViewerContent: FC<OwnProps & StateProps> = (props) => {
|
||||
) : renderVideoPreview(
|
||||
bestImageData,
|
||||
message && calculateMediaViewerDimensions(dimensions!, hasFooter, true),
|
||||
false,
|
||||
!IS_SINGLE_COLUMN_LAYOUT && !isProtected,
|
||||
))}
|
||||
{textParts && (
|
||||
<MediaViewerFooter
|
||||
@ -238,6 +243,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
senderId: message.senderId,
|
||||
origin,
|
||||
message,
|
||||
isProtected: selectIsMessageProtected(global, message),
|
||||
};
|
||||
}
|
||||
|
||||
@ -275,6 +281,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
senderId: message.senderId,
|
||||
origin,
|
||||
message,
|
||||
isProtected: selectIsMessageProtected(global, message),
|
||||
};
|
||||
},
|
||||
)(MediaViewerContent));
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
REM,
|
||||
} from '../../common/helpers/mediaDimensions';
|
||||
import windowSize from '../../../util/windowSize';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
|
||||
const ANIMATION_DURATION = 200;
|
||||
|
||||
@ -208,6 +209,8 @@ function createGhost(source: string | HTMLImageElement | HTMLVideoElement, origi
|
||||
ghost.classList.add('ghost');
|
||||
|
||||
const img = new Image();
|
||||
img.draggable = false;
|
||||
img.oncontextmenu = stopEvent;
|
||||
|
||||
if (typeof source === 'string') {
|
||||
img.src = source;
|
||||
|
||||
@ -30,6 +30,7 @@ type OwnProps = {
|
||||
hasCustomAppendix?: boolean;
|
||||
lastSyncTime?: number;
|
||||
isOwn: boolean;
|
||||
isProtected?: boolean;
|
||||
albumLayout: IAlbumLayout;
|
||||
onMediaClick: (messageId: number) => void;
|
||||
};
|
||||
@ -46,6 +47,7 @@ const Album: FC<OwnProps & StateProps> = ({
|
||||
hasCustomAppendix,
|
||||
lastSyncTime,
|
||||
isOwn,
|
||||
isProtected,
|
||||
albumLayout,
|
||||
onMediaClick,
|
||||
uploadsById,
|
||||
@ -85,6 +87,7 @@ const Album: FC<OwnProps & StateProps> = ({
|
||||
shouldAffectAppendix={shouldAffectAppendix}
|
||||
uploadProgress={uploadProgress}
|
||||
dimensions={dimensions}
|
||||
isProtected={isProtected}
|
||||
onClick={onMediaClick}
|
||||
onCancelUpload={handleCancelUpload}
|
||||
isDownloading={activeDownloadIds.includes(message.id)}
|
||||
@ -102,6 +105,7 @@ const Album: FC<OwnProps & StateProps> = ({
|
||||
uploadProgress={uploadProgress}
|
||||
lastSyncTime={lastSyncTime}
|
||||
dimensions={dimensions}
|
||||
isProtected={isProtected}
|
||||
onClick={onMediaClick}
|
||||
onCancelUpload={handleCancelUpload}
|
||||
isDownloading={activeDownloadIds.includes(message.id)}
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
selectAllowedMessageActions,
|
||||
selectChat,
|
||||
selectCurrentMessageList,
|
||||
selectIsMessageProtected,
|
||||
} from '../../../modules/selectors';
|
||||
import { isChatGroup, isOwnMessage } from '../../../modules/helpers';
|
||||
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 < SEEN_BY_MEMBERS_CHAT_MAX
|
||||
&& message.date > Date.now() / 1000 - SEEN_BY_MEMBERS_EXPIRE);
|
||||
const isProtected = selectIsMessageProtected(global, message);
|
||||
|
||||
return {
|
||||
noOptions,
|
||||
@ -377,13 +379,13 @@ export default memo(withGlobal<OwnProps>(
|
||||
canDelete,
|
||||
canReport,
|
||||
canEdit: !isPinned && canEdit,
|
||||
canForward: !isScheduled && canForward,
|
||||
canForward: !isProtected && !isScheduled && canForward,
|
||||
canFaveSticker: !isScheduled && canFaveSticker,
|
||||
canUnfaveSticker: !isScheduled && canUnfaveSticker,
|
||||
canCopy,
|
||||
canCopyLink: !isScheduled && canCopyLink,
|
||||
canCopy: !isProtected && canCopy,
|
||||
canCopyLink: !isProtected && !isScheduled && canCopyLink,
|
||||
canSelect,
|
||||
canDownload,
|
||||
canDownload: !isProtected && canDownload,
|
||||
activeDownloads,
|
||||
canShowSeenBy,
|
||||
};
|
||||
|
||||
@ -27,6 +27,10 @@
|
||||
transform: translateX(-2.5rem) !important;
|
||||
}
|
||||
|
||||
&.is-protected {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> .Avatar,
|
||||
> .message-content-wrapper {
|
||||
opacity: 1;
|
||||
|
||||
@ -45,6 +45,7 @@ import {
|
||||
selectAllowedMessageActions,
|
||||
selectIsDownloading,
|
||||
selectThreadInfo,
|
||||
selectIsMessageProtected,
|
||||
} from '../../../modules/selectors';
|
||||
import {
|
||||
getMessageContent,
|
||||
@ -138,6 +139,7 @@ type StateProps = {
|
||||
replyMessageSender?: ApiUser | ApiChat;
|
||||
outgoingStatus?: ApiMessageOutgoingStatus;
|
||||
uploadProgress?: number;
|
||||
isProtected?: boolean;
|
||||
isFocused?: boolean;
|
||||
focusDirection?: FocusDirection;
|
||||
noFocusHighlight?: boolean;
|
||||
@ -201,6 +203,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
replyMessageSender,
|
||||
outgoingStatus,
|
||||
uploadProgress,
|
||||
isProtected,
|
||||
isFocused,
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
@ -325,6 +328,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
isAlbum,
|
||||
Boolean(isInSelectMode),
|
||||
Boolean(canReply),
|
||||
Boolean(isProtected),
|
||||
onContextMenu,
|
||||
handleBeforeContextMenu,
|
||||
);
|
||||
@ -364,6 +368,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
const containerClassName = buildClassName(
|
||||
'Message message-list-item',
|
||||
isFirstInGroup && 'first-in-group',
|
||||
isProtected && 'is-protected',
|
||||
isLastInGroup && 'last-in-group',
|
||||
isFirstInDocumentGroup && 'first-in-document-group',
|
||||
isLastInDocumentGroup && 'last-in-document-group',
|
||||
@ -485,6 +490,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
{hasReply && (
|
||||
<EmbeddedMessage
|
||||
message={replyMessage}
|
||||
isProtected={isProtected}
|
||||
sender={replyMessageSender}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
onClick={handleReplyClick}
|
||||
@ -514,6 +520,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
albumLayout={albumLayout!}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
isOwn={isOwn}
|
||||
isProtected={isProtected}
|
||||
hasCustomAppendix={hasCustomAppendix}
|
||||
lastSyncTime={lastSyncTime}
|
||||
onMediaClick={handleAlbumMediaClick}
|
||||
@ -530,6 +537,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
onClick={handleMediaClick}
|
||||
onCancelUpload={handleCancelUpload}
|
||||
isDownloading={isDownloading}
|
||||
isProtected={isProtected}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
@ -554,6 +562,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
onClick={handleMediaClick}
|
||||
onCancelUpload={handleCancelUpload}
|
||||
isDownloading={isDownloading}
|
||||
isProtected={isProtected}
|
||||
/>
|
||||
)}
|
||||
{(audio || voice) && (
|
||||
@ -615,6 +624,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
onMediaClick={handleMediaClick}
|
||||
onCancelMediaTransfer={handleCancelUpload}
|
||||
isDownloading={isDownloading}
|
||||
isProtected={isProtected}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
@ -880,6 +890,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isThreadTop,
|
||||
replyMessage,
|
||||
replyMessageSender,
|
||||
isProtected: selectIsMessageProtected(global, message),
|
||||
isFocused,
|
||||
isForwarding,
|
||||
isChatWithSelf,
|
||||
|
||||
@ -39,6 +39,7 @@ export type OwnProps = {
|
||||
dimensions?: IMediaDimensions & { isSmall?: boolean };
|
||||
nonInteractive?: boolean;
|
||||
isDownloading: boolean;
|
||||
isProtected?: boolean;
|
||||
theme: ISettings['theme'];
|
||||
onClick?: (id: number) => void;
|
||||
onCancelUpload?: (message: ApiMessage) => void;
|
||||
@ -60,6 +61,7 @@ const Photo: FC<OwnProps> = ({
|
||||
nonInteractive,
|
||||
shouldAffectAppendix,
|
||||
isDownloading,
|
||||
isProtected,
|
||||
theme,
|
||||
onClick,
|
||||
onCancelUpload,
|
||||
@ -167,7 +169,9 @@ const Photo: FC<OwnProps> = ({
|
||||
width={width}
|
||||
height={height}
|
||||
alt=""
|
||||
draggable={!isProtected}
|
||||
/>
|
||||
{isProtected && <span className="protector" />}
|
||||
{shouldRenderSpinner && !shouldRenderDownloadButton && (
|
||||
<div className={`media-loading ${spinnerClassNames}`}>
|
||||
<ProgressSpinner progress={transferProgress} onClick={isUploading ? handleClick : undefined} />
|
||||
|
||||
@ -42,6 +42,7 @@ export type OwnProps = {
|
||||
dimensions?: IMediaDimensions;
|
||||
lastSyncTime?: number;
|
||||
isDownloading: boolean;
|
||||
isProtected?: boolean;
|
||||
onClick?: (id: number) => void;
|
||||
onCancelUpload?: (message: ApiMessage) => void;
|
||||
};
|
||||
@ -59,6 +60,7 @@ const Video: FC<OwnProps> = ({
|
||||
onClick,
|
||||
onCancelUpload,
|
||||
isDownloading,
|
||||
isProtected,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@ -173,6 +175,7 @@ const Video: FC<OwnProps> = ({
|
||||
// @ts-ignore teact feature
|
||||
style={`width: ${width}px; height: ${height}px;`}
|
||||
alt=""
|
||||
draggable={!isProtected}
|
||||
/>
|
||||
{isInline && (
|
||||
<video
|
||||
@ -186,11 +189,13 @@ const Video: FC<OwnProps> = ({
|
||||
playsInline
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...bufferingHandlers}
|
||||
draggable={!isProtected}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
>
|
||||
<source src={fullMediaData} />
|
||||
</video>
|
||||
)}
|
||||
{isProtected && <span className="protector" />}
|
||||
{shouldRenderPlayButton && <i className={buildClassName('icon-large-play', playButtonClassNames)} />}
|
||||
{shouldRenderSpinner && (
|
||||
<div className={buildClassName('media-loading', spinnerClassNames)}>
|
||||
|
||||
@ -27,6 +27,7 @@ type OwnProps = {
|
||||
inPreview?: boolean;
|
||||
lastSyncTime?: number;
|
||||
isDownloading?: boolean;
|
||||
isProtected?: boolean;
|
||||
theme: ISettings['theme'];
|
||||
onMediaClick?: () => void;
|
||||
onCancelMediaTransfer?: () => void;
|
||||
@ -41,6 +42,7 @@ const WebPage: FC<OwnProps> = ({
|
||||
inPreview,
|
||||
lastSyncTime,
|
||||
isDownloading = false,
|
||||
isProtected,
|
||||
theme,
|
||||
onMediaClick,
|
||||
onCancelMediaTransfer,
|
||||
@ -97,6 +99,7 @@ const WebPage: FC<OwnProps> = ({
|
||||
onClick={isMediaInteractive ? handleMediaClick : undefined}
|
||||
onCancelUpload={onCancelMediaTransfer}
|
||||
isDownloading={isDownloading}
|
||||
isProtected={isProtected}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
@ -120,6 +123,7 @@ const WebPage: FC<OwnProps> = ({
|
||||
onClick={isMediaInteractive ? handleMediaClick : undefined}
|
||||
onCancelUpload={onCancelMediaTransfer}
|
||||
isDownloading={isDownloading}
|
||||
isProtected={isProtected}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -7,6 +7,7 @@ import windowSize from '../../../../util/windowSize';
|
||||
import { captureEvents, SwipeDirection } from '../../../../util/captureEvents';
|
||||
import useFlag from '../../../../hooks/useFlag';
|
||||
import { preventMessageInputBlur } from '../../helpers/preventMessageInputBlur';
|
||||
import stopEvent from '../../../../util/stopEvent';
|
||||
|
||||
const ANDROID_KEYBOARD_HIDE_DELAY_MS = 350;
|
||||
const SWIPE_ANIMATION_DURATION = 150;
|
||||
@ -18,6 +19,7 @@ export default function useOuterHandlers(
|
||||
isAlbum: boolean,
|
||||
isInSelectMode: boolean,
|
||||
canReply: boolean,
|
||||
isProtected: boolean,
|
||||
onContextMenu: (e: React.MouseEvent) => void,
|
||||
handleBeforeContextMenu: (e: React.MouseEvent) => void,
|
||||
) {
|
||||
@ -107,7 +109,7 @@ export default function useOuterHandlers(
|
||||
return {
|
||||
handleMouseDown: !isInSelectMode ? handleMouseDown : undefined,
|
||||
handleClick,
|
||||
handleContextMenu: !isInSelectMode ? handleContextMenu : undefined,
|
||||
handleContextMenu: !isInSelectMode ? handleContextMenu : (isProtected ? stopEvent : undefined),
|
||||
handleDoubleClick: !isInSelectMode ? handleContainerDoubleClick : undefined,
|
||||
handleContentDoubleClick: !IS_TOUCH_ENV ? stopPropagation : undefined,
|
||||
isSwiped,
|
||||
|
||||
@ -92,6 +92,7 @@ type StateProps = {
|
||||
lastSyncTime?: number;
|
||||
serverTimeOffset: number;
|
||||
activeDownloadIds: number[];
|
||||
isChatProtected?: boolean;
|
||||
};
|
||||
|
||||
const TABS = [
|
||||
@ -130,6 +131,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
lastSyncTime,
|
||||
activeDownloadIds,
|
||||
serverTimeOffset,
|
||||
isChatProtected,
|
||||
}) => {
|
||||
const {
|
||||
setLocalMediaSearchType,
|
||||
@ -322,6 +324,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
<Media
|
||||
key={id}
|
||||
message={chatMessages[id]}
|
||||
isProtected={isChatProtected || chatMessages[id].isProtected}
|
||||
onClick={handleSelectMedia}
|
||||
/>
|
||||
))
|
||||
@ -342,6 +345,7 @@ const Profile: FC<OwnProps & StateProps> = ({
|
||||
<WebLink
|
||||
key={id}
|
||||
message={chatMessages[id]}
|
||||
isProtected={isChatProtected || chatMessages[id].isProtected}
|
||||
onMessageClick={handleMessageFocus}
|
||||
/>
|
||||
))
|
||||
@ -533,6 +537,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
usersById,
|
||||
userStatusesById,
|
||||
chatsById,
|
||||
isChatProtected: chat?.isProtected,
|
||||
...(hasMembersTab && members && { members }),
|
||||
...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }),
|
||||
};
|
||||
|
||||
@ -21,9 +21,7 @@ import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
import UsernameInput from '../../common/UsernameInput';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
|
||||
type PrivacyType =
|
||||
'private'
|
||||
| 'public';
|
||||
type PrivacyType = 'private' | 'public';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
@ -36,6 +34,7 @@ type StateProps = {
|
||||
isChannel: boolean;
|
||||
progress?: ManagementProgress;
|
||||
isUsernameAvailable?: boolean;
|
||||
isProtected?: boolean;
|
||||
};
|
||||
|
||||
const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
|
||||
@ -45,11 +44,13 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
|
||||
isChannel,
|
||||
progress,
|
||||
isUsernameAvailable,
|
||||
isProtected,
|
||||
}) => {
|
||||
const {
|
||||
checkPublicLink,
|
||||
updatePublicLink,
|
||||
updatePrivateLink,
|
||||
toggleIsProtected,
|
||||
} = getDispatch();
|
||||
|
||||
const isPublic = Boolean(chat.username);
|
||||
@ -76,6 +77,13 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
|
||||
setPrivacyType(value as PrivacyType);
|
||||
}, []);
|
||||
|
||||
const handleForwardingOptionChange = useCallback((value: string) => {
|
||||
toggleIsProtected({
|
||||
chatId: chat.id,
|
||||
isProtected: value === 'protected',
|
||||
});
|
||||
}, [chat.id, toggleIsProtected]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
updatePublicLink({ username: privacyType === 'public' ? username : '' });
|
||||
}, [privacyType, updatePublicLink, username]);
|
||||
@ -94,6 +102,14 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
|
||||
{ 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;
|
||||
|
||||
return (
|
||||
@ -148,6 +164,22 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
|
||||
</p>
|
||||
</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>
|
||||
<FloatingActionButton
|
||||
isShown={canUpdate}
|
||||
@ -175,6 +207,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isChannel: isChatChannel(chat),
|
||||
progress: global.management.progress,
|
||||
isUsernameAvailable,
|
||||
isProtected: chat?.isProtected,
|
||||
};
|
||||
},
|
||||
)(ManageChatPrivacyType));
|
||||
|
||||
@ -30,7 +30,7 @@ export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
|
||||
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
|
||||
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
||||
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 AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
|
||||
|
||||
|
||||
@ -496,7 +496,7 @@ export type ActionTypes = (
|
||||
'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' |
|
||||
'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' |
|
||||
'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' |
|
||||
'addChatMembers' | 'deleteChatMember' | 'openPreviousChat' | 'editChatFolders' |
|
||||
'addChatMembers' | 'deleteChatMember' | 'openPreviousChat' | 'editChatFolders' | 'toggleIsProtected' |
|
||||
// messages
|
||||
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
|
||||
'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.deleteChat#5bd0ee50 chat_id:long = Bool;
|
||||
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;
|
||||
updates.getState#edd4882a = updates.State;
|
||||
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.deleteChat#5bd0ee50 chat_id:long = Bool;
|
||||
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;
|
||||
updates.getState#edd4882a = updates.State;
|
||||
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) {
|
||||
let global = getGlobal();
|
||||
|
||||
|
||||
@ -856,6 +856,10 @@ export function selectLastServiceNotification(global: GlobalState) {
|
||||
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) {
|
||||
const chat = selectChat(global, chatId);
|
||||
const message = chat && isChatChannel(chat) ? global.messages.sponsoredByChatId[chatId] : undefined;
|
||||
|
||||
@ -225,6 +225,15 @@ div[role="button"] {
|
||||
color: var(--color-text-secondary) !important;
|
||||
}
|
||||
|
||||
.protector {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.for-ios-autocapitalization-fix {
|
||||
position: fixed;
|
||||
font-size: 16px;
|
||||
|
||||
@ -1821,4 +1821,28 @@ export default {
|
||||
key: 'AutoDownloadFilesTitle',
|
||||
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;
|
||||
|
||||
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