Support saving GIFs, support scheduling GIFs and stickers (#1739)
This commit is contained in:
parent
a348fda4dd
commit
d05fecddf4
@ -98,13 +98,15 @@ export async function fetchInlineBotResults({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function sendInlineBotResult({
|
export async function sendInlineBotResult({
|
||||||
chat, resultId, queryId, replyingTo, sendAs,
|
chat, resultId, queryId, replyingTo, sendAs, isSilent, scheduleDate,
|
||||||
}: {
|
}: {
|
||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
resultId: string;
|
resultId: string;
|
||||||
queryId: string;
|
queryId: string;
|
||||||
replyingTo?: number;
|
replyingTo?: number;
|
||||||
sendAs?: ApiUser | ApiChat;
|
sendAs?: ApiUser | ApiChat;
|
||||||
|
isSilent?: boolean;
|
||||||
|
scheduleDate?: number;
|
||||||
}) {
|
}) {
|
||||||
const randomId = generateRandomBigInt();
|
const randomId = generateRandomBigInt();
|
||||||
|
|
||||||
@ -114,6 +116,8 @@ export async function sendInlineBotResult({
|
|||||||
queryId: BigInt(queryId),
|
queryId: BigInt(queryId),
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
id: resultId,
|
id: resultId,
|
||||||
|
scheduleDate,
|
||||||
|
...(isSilent && { silent: true }),
|
||||||
...(replyingTo && { replyToMsgId: replyingTo }),
|
...(replyingTo && { replyToMsgId: replyingTo }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
}), true);
|
}), true);
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
fetchStickerSets, fetchRecentStickers, fetchFavoriteStickers, fetchFeaturedStickers,
|
fetchStickerSets, fetchRecentStickers, fetchFavoriteStickers, fetchFeaturedStickers,
|
||||||
faveSticker, fetchStickers, fetchSavedGifs, searchStickers, installStickerSet, uninstallStickerSet,
|
faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet,
|
||||||
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
|
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
|
||||||
} from './symbols';
|
} from './symbols';
|
||||||
|
|
||||||
|
|||||||
@ -174,6 +174,15 @@ export async function fetchSavedGifs({ hash = '0' }: { hash?: string }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function saveGif({ gif, shouldUnsave }: { gif: ApiVideo; shouldUnsave?: boolean }) {
|
||||||
|
const request = new GramJs.messages.SaveGif({
|
||||||
|
id: buildInputDocument(gif),
|
||||||
|
unsave: shouldUnsave,
|
||||||
|
});
|
||||||
|
|
||||||
|
return invokeRequest(request, true);
|
||||||
|
}
|
||||||
|
|
||||||
export async function installStickerSet({ stickerSetId, accessHash }: { stickerSetId: string; accessHash: string }) {
|
export async function installStickerSet({ stickerSetId, accessHash }: { stickerSetId: string; accessHash: string }) {
|
||||||
const result = await invokeRequest(new GramJs.messages.InstallStickerSet({
|
const result = await invokeRequest(new GramJs.messages.InstallStickerSet({
|
||||||
stickerset: buildInputStickerSet(stickerSetId, accessHash),
|
stickerset: buildInputStickerSet(stickerSetId, accessHash),
|
||||||
|
|||||||
@ -15,20 +15,25 @@ export { default as SeenByModal } from '../components/common/SeenByModal';
|
|||||||
export { default as ReactorListModal } from '../components/middle/ReactorListModal';
|
export { default as ReactorListModal } from '../components/middle/ReactorListModal';
|
||||||
export { default as EmojiInteractionAnimation } from '../components/middle/EmojiInteractionAnimation';
|
export { default as EmojiInteractionAnimation } from '../components/middle/EmojiInteractionAnimation';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
export { default as LeftSearch } from '../components/left/search/LeftSearch';
|
export { default as LeftSearch } from '../components/left/search/LeftSearch';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
export { default as Settings } from '../components/left/settings/Settings';
|
export { default as Settings } from '../components/left/settings/Settings';
|
||||||
export { default as ContactList } from '../components/left/main/ContactList';
|
export { default as ContactList } from '../components/left/main/ContactList';
|
||||||
export { default as NewChat } from '../components/left/newChat/NewChat';
|
export { default as NewChat } from '../components/left/newChat/NewChat';
|
||||||
export { default as NewChatStep1 } from '../components/left/newChat/NewChatStep1';
|
export { default as NewChatStep1 } from '../components/left/newChat/NewChatStep1';
|
||||||
export { default as NewChatStep2 } from '../components/left/newChat/NewChatStep2';
|
export { default as NewChatStep2 } from '../components/left/newChat/NewChatStep2';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
export { default as ArchivedChats } from '../components/left/ArchivedChats';
|
export { default as ArchivedChats } from '../components/left/ArchivedChats';
|
||||||
export { default as ChatFolderModal } from '../components/left/ChatFolderModal';
|
export { default as ChatFolderModal } from '../components/left/ChatFolderModal';
|
||||||
|
|
||||||
export { default as ContextMenuContainer } from '../components/middle/message/ContextMenuContainer';
|
export { default as ContextMenuContainer } from '../components/middle/message/ContextMenuContainer';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
export { default as StickerSetModal } from '../components/common/StickerSetModal';
|
export { default as StickerSetModal } from '../components/common/StickerSetModal';
|
||||||
export { default as HeaderMenuContainer } from '../components/middle/HeaderMenuContainer';
|
export { default as HeaderMenuContainer } from '../components/middle/HeaderMenuContainer';
|
||||||
export { default as MobileSearch } from '../components/middle/MobileSearch';
|
export { default as MobileSearch } from '../components/middle/MobileSearch';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
|
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
|
||||||
export { default as PollModal } from '../components/middle/composer/PollModal';
|
export { default as PollModal } from '../components/middle/composer/PollModal';
|
||||||
export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu';
|
export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu';
|
||||||
@ -44,7 +49,9 @@ export { default as InlineBotTooltip } from '../components/middle/composer/Inlin
|
|||||||
export { default as SendAsMenu } from '../components/middle/composer/SendAsMenu';
|
export { default as SendAsMenu } from '../components/middle/composer/SendAsMenu';
|
||||||
|
|
||||||
export { default as RightSearch } from '../components/right/RightSearch';
|
export { default as RightSearch } from '../components/right/RightSearch';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
export { default as StickerSearch } from '../components/right/StickerSearch';
|
export { default as StickerSearch } from '../components/right/StickerSearch';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
export { default as GifSearch } from '../components/right/GifSearch';
|
export { default as GifSearch } from '../components/right/GifSearch';
|
||||||
export { default as Statistics } from '../components/right/statistics/Statistics';
|
export { default as Statistics } from '../components/right/statistics/Statistics';
|
||||||
export { default as PollResults } from '../components/right/PollResults';
|
export { default as PollResults } from '../components/right/PollResults';
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { getActions, getGlobal } from '../global';
|
|||||||
|
|
||||||
import { DEBUG } from '../config';
|
import { DEBUG } from '../config';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
export { default as Main } from '../components/main/Main';
|
export { default as Main } from '../components/main/Main';
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
|||||||
@ -4,10 +4,14 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 6.25rem;
|
height: 6.25rem;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
cursor: pointer;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.gif-unsave-button {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
@ -20,6 +24,10 @@
|
|||||||
grid-column-end: span 2;
|
grid-column-end: span 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -34,10 +42,39 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Spinner {
|
.Spinner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gif-unsave-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.25rem;
|
||||||
|
right: 0.25rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
padding: 0.125rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: 0.15s opacity ease-in-out;
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gif-context-menu {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,26 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, memo, useCallback, useRef,
|
FC, memo, useCallback, useEffect, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
import { ApiMediaFormat, ApiVideo } from '../../api/types';
|
import { ApiMediaFormat, ApiVideo } from '../../api/types';
|
||||||
|
|
||||||
|
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
|
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
||||||
|
|
||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
import useVideoCleanup from '../../hooks/useVideoCleanup';
|
import useVideoCleanup from '../../hooks/useVideoCleanup';
|
||||||
import useBuffering from '../../hooks/useBuffering';
|
import useBuffering from '../../hooks/useBuffering';
|
||||||
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
||||||
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
import useLang from '../../hooks/useLang';
|
||||||
|
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
||||||
|
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||||
|
|
||||||
import Spinner from '../ui/Spinner';
|
import Spinner from '../ui/Spinner';
|
||||||
|
import Button from '../ui/Button';
|
||||||
|
import Menu from '../ui/Menu';
|
||||||
|
import MenuItem from '../ui/MenuItem';
|
||||||
|
|
||||||
import './GifButton.scss';
|
import './GifButton.scss';
|
||||||
|
|
||||||
@ -21,17 +29,27 @@ type OwnProps = {
|
|||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick: (gif: ApiVideo) => void;
|
onClick?: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
|
onUnsaveClick?: (gif: ApiVideo) => void;
|
||||||
|
isSavedMessages?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GifButton: FC<OwnProps> = ({
|
const GifButton: FC<OwnProps> = ({
|
||||||
gif, observeIntersection, isDisabled, className, onClick,
|
gif,
|
||||||
|
isDisabled,
|
||||||
|
className,
|
||||||
|
observeIntersection,
|
||||||
|
onClick,
|
||||||
|
onUnsaveClick,
|
||||||
|
isSavedMessages,
|
||||||
}) => {
|
}) => {
|
||||||
// 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);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
const hasThumbnail = Boolean(gif.thumbnail?.dataUri);
|
const hasThumbnail = Boolean(gif.thumbnail?.dataUri);
|
||||||
const localMediaHash = `gif${gif.id}`;
|
const localMediaHash = `gif${gif.id}`;
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
@ -46,17 +64,78 @@ const GifButton: FC<OwnProps> = ({
|
|||||||
|
|
||||||
useVideoCleanup(videoRef, [shouldRenderVideo]);
|
useVideoCleanup(videoRef, [shouldRenderVideo]);
|
||||||
|
|
||||||
const handleClick = useCallback(
|
const {
|
||||||
() => onClick({
|
isContextMenuOpen, contextMenuPosition,
|
||||||
|
handleBeforeContextMenu, handleContextMenu,
|
||||||
|
handleContextMenuClose, handleContextMenuHide,
|
||||||
|
} = useContextMenuHandlers(ref);
|
||||||
|
|
||||||
|
const getTriggerElement = useCallback(() => ref.current, []);
|
||||||
|
|
||||||
|
const getRootElement = useCallback(
|
||||||
|
() => ref.current!.closest('.custom-scroll, .no-scrollbar'),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getMenuElement = useCallback(
|
||||||
|
() => ref.current!.querySelector('.gif-context-menu .bubble'),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||||
|
} = useContextMenuPosition(
|
||||||
|
contextMenuPosition,
|
||||||
|
getTriggerElement,
|
||||||
|
getRootElement,
|
||||||
|
getMenuElement,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (isContextMenuOpen || !onClick) return;
|
||||||
|
onClick({
|
||||||
...gif,
|
...gif,
|
||||||
blobUrl: videoData,
|
blobUrl: videoData,
|
||||||
}),
|
});
|
||||||
[onClick, gif, videoData],
|
}, [isContextMenuOpen, onClick, gif, videoData]);
|
||||||
);
|
|
||||||
|
const handleUnsaveClick = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onUnsaveClick!(gif);
|
||||||
|
}, [onUnsaveClick, gif]);
|
||||||
|
|
||||||
|
const handleContextDelete = useCallback(() => {
|
||||||
|
onUnsaveClick?.(gif);
|
||||||
|
}, [gif, onUnsaveClick]);
|
||||||
|
|
||||||
|
const handleSendQuiet = useCallback(() => {
|
||||||
|
onClick!({
|
||||||
|
...gif,
|
||||||
|
blobUrl: videoData,
|
||||||
|
}, true);
|
||||||
|
}, [gif, onClick, videoData]);
|
||||||
|
|
||||||
|
const handleSendScheduled = useCallback(() => {
|
||||||
|
onClick!({
|
||||||
|
...gif,
|
||||||
|
blobUrl: videoData,
|
||||||
|
}, undefined, true);
|
||||||
|
}, [gif, onClick, videoData]);
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||||
|
preventMessageInputBlurWithBubbling(e);
|
||||||
|
handleBeforeContextMenu(e);
|
||||||
|
}, [handleBeforeContextMenu]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDisabled) handleContextMenuClose();
|
||||||
|
}, [handleContextMenuClose, isDisabled]);
|
||||||
|
|
||||||
const fullClassName = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
'GifButton',
|
'GifButton',
|
||||||
gif.width && gif.height && gif.width < gif.height ? 'vertical' : 'horizontal',
|
gif.width && gif.height && gif.width < gif.height ? 'vertical' : 'horizontal',
|
||||||
|
onClick && 'interactive',
|
||||||
localMediaHash,
|
localMediaHash,
|
||||||
className,
|
className,
|
||||||
);
|
);
|
||||||
@ -65,9 +144,20 @@ const GifButton: FC<OwnProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={fullClassName}
|
className={fullClassName}
|
||||||
onMouseDown={preventMessageInputBlurWithBubbling}
|
onMouseDown={handleMouseDown}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
|
{!IS_TOUCH_ENV && onUnsaveClick && (
|
||||||
|
<Button
|
||||||
|
className="gif-unsave-button"
|
||||||
|
color="dark"
|
||||||
|
pill
|
||||||
|
onClick={handleUnsaveClick}
|
||||||
|
>
|
||||||
|
<i className="icon-close gif-unsave-button-icon" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{hasThumbnail && (
|
{hasThumbnail && (
|
||||||
<canvas
|
<canvas
|
||||||
ref={thumbRef}
|
ref={thumbRef}
|
||||||
@ -100,6 +190,28 @@ const GifButton: FC<OwnProps> = ({
|
|||||||
{shouldRenderSpinner && (
|
{shouldRenderSpinner && (
|
||||||
<Spinner color={previewBlobUrl || hasThumbnail ? 'white' : 'black'} />
|
<Spinner color={previewBlobUrl || hasThumbnail ? 'white' : 'black'} />
|
||||||
)}
|
)}
|
||||||
|
{onClick && contextMenuPosition !== undefined && (
|
||||||
|
<Menu
|
||||||
|
isOpen={isContextMenuOpen}
|
||||||
|
transformOriginX={transformOriginX}
|
||||||
|
transformOriginY={transformOriginY}
|
||||||
|
positionX={positionX}
|
||||||
|
positionY={positionY}
|
||||||
|
style={menuStyle}
|
||||||
|
className="gif-context-menu"
|
||||||
|
autoClose
|
||||||
|
onClose={handleContextMenuClose}
|
||||||
|
onCloseAnimationEnd={handleContextMenuHide}
|
||||||
|
>
|
||||||
|
{!isSavedMessages && <MenuItem onClick={handleSendQuiet} icon="mute">{lang('SendWithoutSound')}</MenuItem>}
|
||||||
|
<MenuItem onClick={handleSendScheduled} icon="calendar">
|
||||||
|
{lang(isSavedMessages ? 'SetReminder' : 'ScheduleMessage')}
|
||||||
|
</MenuItem>
|
||||||
|
{onUnsaveClick && (
|
||||||
|
<MenuItem destructive icon="delete" onClick={handleContextDelete}>{lang('Delete')}</MenuItem>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -50,6 +50,8 @@
|
|||||||
img,
|
img,
|
||||||
video {
|
video {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticker-unfave-button {
|
.sticker-unfave-button {
|
||||||
@ -66,4 +68,12 @@
|
|||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticker-context-menu {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,41 +1,62 @@
|
|||||||
import { MouseEvent as ReactMouseEvent } from 'react';
|
import { MouseEvent as ReactMouseEvent } from 'react';
|
||||||
import React, {
|
import React, {
|
||||||
FC, memo, useEffect, useRef,
|
memo, useCallback, useEffect, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
import { ApiMediaFormat, ApiSticker } from '../../api/types';
|
import { ApiBotInlineMediaResult, ApiMediaFormat, ApiSticker } from '../../api/types';
|
||||||
|
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
||||||
|
import safePlay from '../../util/safePlay';
|
||||||
|
import { IS_TOUCH_ENV, IS_WEBM_SUPPORTED } from '../../util/environment';
|
||||||
|
|
||||||
import { useIsIntersecting, ObserveFn } from '../../hooks/useIntersectionObserver';
|
import { useIsIntersecting, ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
import useFlag from '../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import useLang from '../../hooks/useLang';
|
||||||
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||||
import safePlay from '../../util/safePlay';
|
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
||||||
import { IS_WEBM_SUPPORTED } from '../../util/environment';
|
|
||||||
|
|
||||||
import AnimatedSticker from './AnimatedSticker';
|
import AnimatedSticker from './AnimatedSticker';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
|
import Menu from '../ui/Menu';
|
||||||
|
import MenuItem from '../ui/MenuItem';
|
||||||
|
|
||||||
import './StickerButton.scss';
|
import './StickerButton.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps<T> = {
|
||||||
sticker: ApiSticker;
|
sticker: ApiSticker;
|
||||||
size: number;
|
size: number;
|
||||||
observeIntersection: ObserveFn;
|
|
||||||
noAnimate?: boolean;
|
noAnimate?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (arg: any) => void;
|
clickArg: T;
|
||||||
clickArg?: any;
|
noContextMenu?: boolean;
|
||||||
|
isSavedMessages?: boolean;
|
||||||
|
observeIntersection: ObserveFn;
|
||||||
|
onClick?: (arg: OwnProps<T>['clickArg'], isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
|
onFaveClick?: (sticker: ApiSticker) => void;
|
||||||
onUnfaveClick?: (sticker: ApiSticker) => void;
|
onUnfaveClick?: (sticker: ApiSticker) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StickerButton: FC<OwnProps> = ({
|
const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult | undefined = undefined>({
|
||||||
sticker, size, observeIntersection, noAnimate, title, className, onClick, clickArg, onUnfaveClick,
|
sticker,
|
||||||
}) => {
|
size,
|
||||||
|
noAnimate,
|
||||||
|
title,
|
||||||
|
className,
|
||||||
|
clickArg,
|
||||||
|
noContextMenu,
|
||||||
|
isSavedMessages,
|
||||||
|
observeIntersection,
|
||||||
|
onClick,
|
||||||
|
onFaveClick,
|
||||||
|
onUnfaveClick,
|
||||||
|
}: OwnProps<T>) => {
|
||||||
// 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);
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
const localMediaHash = `sticker${sticker.id}`;
|
const localMediaHash = `sticker${sticker.id}`;
|
||||||
const stickerSelector = `sticker-button-${sticker.id}`;
|
const stickerSelector = `sticker-button-${sticker.id}`;
|
||||||
@ -60,6 +81,33 @@ const StickerButton: FC<OwnProps> = ({
|
|||||||
'slow',
|
'slow',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isContextMenuOpen, contextMenuPosition,
|
||||||
|
handleBeforeContextMenu, handleContextMenu,
|
||||||
|
handleContextMenuClose, handleContextMenuHide,
|
||||||
|
} = useContextMenuHandlers(ref);
|
||||||
|
|
||||||
|
const getTriggerElement = useCallback(() => ref.current, []);
|
||||||
|
|
||||||
|
const getRootElement = useCallback(
|
||||||
|
() => ref.current!.closest('.custom-scroll, .no-scrollbar'),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getMenuElement = useCallback(
|
||||||
|
() => ref.current!.querySelector('.sticker-context-menu .bubble'),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||||
|
} = useContextMenuPosition(
|
||||||
|
contextMenuPosition,
|
||||||
|
getTriggerElement,
|
||||||
|
getRootElement,
|
||||||
|
getMenuElement,
|
||||||
|
);
|
||||||
|
|
||||||
// To avoid flickering
|
// To avoid flickering
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldPlay) {
|
if (!shouldPlay) {
|
||||||
@ -78,18 +126,42 @@ const StickerButton: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}, [isVideo, canVideoPlay]);
|
}, [isVideo, canVideoPlay]);
|
||||||
|
|
||||||
function handleClick() {
|
useEffect(() => {
|
||||||
if (onClick) {
|
if (!isIntersecting) handleContextMenuClose();
|
||||||
onClick(clickArg);
|
}, [handleContextMenuClose, isIntersecting]);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUnfaveClick(e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) {
|
const handleClick = () => {
|
||||||
|
if (isContextMenuOpen) return;
|
||||||
|
onClick?.(clickArg);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
|
preventMessageInputBlurWithBubbling(e);
|
||||||
|
handleBeforeContextMenu(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnfaveClick = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
onUnfaveClick!(sticker);
|
onUnfaveClick!(sticker);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const handleContextUnfave = () => {
|
||||||
|
onUnfaveClick!(sticker);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContextFave = () => {
|
||||||
|
onFaveClick!(sticker);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendQuiet = () => {
|
||||||
|
onClick?.(clickArg, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendScheduled = () => {
|
||||||
|
onClick?.(clickArg, undefined, true);
|
||||||
|
};
|
||||||
|
|
||||||
const fullClassName = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
'StickerButton',
|
'StickerButton',
|
||||||
@ -107,8 +179,9 @@ const StickerButton: FC<OwnProps> = ({
|
|||||||
title={title || (sticker?.emoji)}
|
title={title || (sticker?.emoji)}
|
||||||
style={style}
|
style={style}
|
||||||
data-sticker-id={sticker.id}
|
data-sticker-id={sticker.id}
|
||||||
onMouseDown={preventMessageInputBlurWithBubbling}
|
onMouseDown={handleMouseDown}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
{!canLottiePlay && !canVideoPlay && (
|
{!canLottiePlay && !canVideoPlay && (
|
||||||
// eslint-disable-next-line jsx-a11y/alt-text
|
// eslint-disable-next-line jsx-a11y/alt-text
|
||||||
@ -134,7 +207,7 @@ const StickerButton: FC<OwnProps> = ({
|
|||||||
onLoad={markLoaded}
|
onLoad={markLoaded}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{onUnfaveClick && (
|
{!IS_TOUCH_ENV && onUnfaveClick && (
|
||||||
<Button
|
<Button
|
||||||
className="sticker-unfave-button"
|
className="sticker-unfave-button"
|
||||||
color="dark"
|
color="dark"
|
||||||
@ -144,6 +217,35 @@ const StickerButton: FC<OwnProps> = ({
|
|||||||
<i className="icon-close" />
|
<i className="icon-close" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{!noContextMenu && onClick && contextMenuPosition !== undefined && (
|
||||||
|
<Menu
|
||||||
|
isOpen={isContextMenuOpen}
|
||||||
|
transformOriginX={transformOriginX}
|
||||||
|
transformOriginY={transformOriginY}
|
||||||
|
positionX={positionX}
|
||||||
|
positionY={positionY}
|
||||||
|
style={menuStyle}
|
||||||
|
className="sticker-context-menu"
|
||||||
|
autoClose
|
||||||
|
onClose={handleContextMenuClose}
|
||||||
|
onCloseAnimationEnd={handleContextMenuHide}
|
||||||
|
>
|
||||||
|
{onUnfaveClick && (
|
||||||
|
<MenuItem icon="favorite" onClick={handleContextUnfave}>
|
||||||
|
{lang('Stickers.RemoveFromFavorites')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{onFaveClick && (
|
||||||
|
<MenuItem icon="favorite" onClick={handleContextFave}>
|
||||||
|
{lang('AddToFavorites')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{!isSavedMessages && <MenuItem onClick={handleSendQuiet} icon="muted">{lang('SendWithoutSound')}</MenuItem>}
|
||||||
|
<MenuItem onClick={handleSendScheduled} icon="calendar">
|
||||||
|
{lang(isSavedMessages ? 'SetReminder' : 'ScheduleMessage')}
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,12 +7,19 @@ import { ApiSticker, ApiStickerSet } from '../../api/types';
|
|||||||
|
|
||||||
import { STICKER_SIZE_MODAL } from '../../config';
|
import { STICKER_SIZE_MODAL } from '../../config';
|
||||||
import {
|
import {
|
||||||
selectChat, selectCurrentMessageList, selectStickerSet, selectStickerSetByShortName,
|
selectCanScheduleUntilOnline,
|
||||||
|
selectChat,
|
||||||
|
selectCurrentMessageList,
|
||||||
|
selectIsChatWithSelf,
|
||||||
|
selectShouldSchedule,
|
||||||
|
selectStickerSet,
|
||||||
|
selectStickerSetByShortName,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import renderText from './helpers/renderText';
|
import renderText from './helpers/renderText';
|
||||||
import { getAllowedAttachmentOptions, getCanPostInChat } from '../../global/helpers';
|
import { getAllowedAttachmentOptions, getCanPostInChat } from '../../global/helpers';
|
||||||
|
import useSchedule from '../../hooks/useSchedule';
|
||||||
|
|
||||||
import Modal from '../ui/Modal';
|
import Modal from '../ui/Modal';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
@ -31,6 +38,9 @@ export type OwnProps = {
|
|||||||
type StateProps = {
|
type StateProps = {
|
||||||
canSendStickers?: boolean;
|
canSendStickers?: boolean;
|
||||||
stickerSet?: ApiStickerSet;
|
stickerSet?: ApiStickerSet;
|
||||||
|
canScheduleUntilOnline?: boolean;
|
||||||
|
shouldSchedule?: boolean;
|
||||||
|
isSavedMessages?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const INTERSECTION_THROTTLE = 200;
|
const INTERSECTION_THROTTLE = 200;
|
||||||
@ -41,6 +51,9 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
stickerSetShortName,
|
stickerSetShortName,
|
||||||
stickerSet,
|
stickerSet,
|
||||||
canSendStickers,
|
canSendStickers,
|
||||||
|
canScheduleUntilOnline,
|
||||||
|
shouldSchedule,
|
||||||
|
isSavedMessages,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -53,6 +66,8 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
|
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
observe: observeIntersection,
|
observe: observeIntersection,
|
||||||
} = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: !isOpen });
|
} = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: !isOpen });
|
||||||
@ -73,15 +88,22 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [isOpen, fromSticker, loadStickers, stickerSetShortName]);
|
}, [isOpen, fromSticker, loadStickers, stickerSetShortName]);
|
||||||
|
|
||||||
const handleSelect = useCallback((sticker: ApiSticker) => {
|
const handleSelect = useCallback((sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean) => {
|
||||||
sticker = {
|
sticker = {
|
||||||
...sticker,
|
...sticker,
|
||||||
isPreloadedGlobally: true,
|
isPreloadedGlobally: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
sendMessage({ sticker });
|
if (shouldSchedule || isScheduleRequested) {
|
||||||
onClose();
|
requestCalendar((scheduledAt) => {
|
||||||
}, [onClose, sendMessage]);
|
sendMessage({ sticker, isSilent, scheduledAt });
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendMessage({ sticker, isSilent });
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [onClose, requestCalendar, sendMessage, shouldSchedule]);
|
||||||
|
|
||||||
const handleButtonClick = useCallback(() => {
|
const handleButtonClick = useCallback(() => {
|
||||||
if (stickerSet) {
|
if (stickerSet) {
|
||||||
@ -108,6 +130,7 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
onClick={canSendStickers ? handleSelect : undefined}
|
onClick={canSendStickers ? handleSelect : undefined}
|
||||||
clickArg={sticker}
|
clickArg={sticker}
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -129,6 +152,7 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<Loading />
|
<Loading />
|
||||||
)}
|
)}
|
||||||
|
{calendar}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -142,9 +166,13 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const canSendStickers = Boolean(
|
const canSendStickers = Boolean(
|
||||||
chat && threadId && getCanPostInChat(chat, threadId) && sendOptions?.canSendStickers,
|
chat && threadId && getCanPostInChat(chat, threadId) && sendOptions?.canSendStickers,
|
||||||
);
|
);
|
||||||
|
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
canScheduleUntilOnline: Boolean(chatId) && selectCanScheduleUntilOnline(global, chatId),
|
||||||
canSendStickers,
|
canSendStickers,
|
||||||
|
isSavedMessages,
|
||||||
|
shouldSchedule: selectShouldSchedule(global),
|
||||||
stickerSet: fromSticker
|
stickerSet: fromSticker
|
||||||
? selectStickerSet(global, fromSticker.stickerSetId)
|
? selectStickerSet(global, fromSticker.stickerSetId)
|
||||||
: stickerSetShortName
|
: stickerSetShortName
|
||||||
|
|||||||
@ -78,6 +78,8 @@ const SettingsStickerSet: FC<OwnProps> = ({
|
|||||||
size={STICKER_SIZE_GENERAL_SETTINGS}
|
size={STICKER_SIZE_GENERAL_SETTINGS}
|
||||||
title={stickerSet.title}
|
title={stickerSet.title}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
|
clickArg={undefined}
|
||||||
|
noContextMenu
|
||||||
/>
|
/>
|
||||||
<div className="multiline-menu-item">
|
<div className="multiline-menu-item">
|
||||||
<div className="title">{stickerSet.title}</div>
|
<div className="title">{stickerSet.title}</div>
|
||||||
|
|||||||
@ -83,6 +83,7 @@ const ContactGreeting: FC<OwnProps & StateProps> = ({
|
|||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
size={160}
|
size={160}
|
||||||
className="large"
|
className="large"
|
||||||
|
noContextMenu
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { getFileExtension } from '../../common/helpers/documentInfo';
|
import { getFileExtension } from '../../common/helpers/documentInfo';
|
||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
|
|
||||||
import usePrevious from '../../../hooks/usePrevious';
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
import useMentionTooltip from './hooks/useMentionTooltip';
|
import useMentionTooltip from './hooks/useMentionTooltip';
|
||||||
import useEmojiTooltip from './hooks/useEmojiTooltip';
|
import useEmojiTooltip from './hooks/useEmojiTooltip';
|
||||||
@ -43,13 +44,14 @@ export type OwnProps = {
|
|||||||
recentEmojis: string[];
|
recentEmojis: string[];
|
||||||
baseEmojiKeywords?: Record<string, string[]>;
|
baseEmojiKeywords?: Record<string, string[]>;
|
||||||
emojiKeywords?: Record<string, string[]>;
|
emojiKeywords?: Record<string, string[]>;
|
||||||
|
shouldSchedule?: boolean;
|
||||||
addRecentEmoji: AnyToVoidFunction;
|
addRecentEmoji: AnyToVoidFunction;
|
||||||
onCaptionUpdate: (html: string) => void;
|
onCaptionUpdate: (html: string) => void;
|
||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
onFileAppend: (files: File[], isQuick: boolean) => void;
|
onFileAppend: (files: File[], isQuick: boolean) => void;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
onSilentSend: () => void;
|
onSendSilent: () => void;
|
||||||
openCalendar: () => void;
|
onSendScheduled: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DROP_LEAVE_TIMEOUT_MS = 150;
|
const DROP_LEAVE_TIMEOUT_MS = 150;
|
||||||
@ -67,13 +69,14 @@ const AttachmentModal: FC<OwnProps> = ({
|
|||||||
recentEmojis,
|
recentEmojis,
|
||||||
baseEmojiKeywords,
|
baseEmojiKeywords,
|
||||||
emojiKeywords,
|
emojiKeywords,
|
||||||
|
shouldSchedule,
|
||||||
addRecentEmoji,
|
addRecentEmoji,
|
||||||
onCaptionUpdate,
|
onCaptionUpdate,
|
||||||
onSend,
|
onSend,
|
||||||
onFileAppend,
|
onFileAppend,
|
||||||
onClear,
|
onClear,
|
||||||
onSilentSend,
|
onSendSilent,
|
||||||
openCalendar,
|
onSendScheduled,
|
||||||
}) => {
|
}) => {
|
||||||
const captionRef = useStateRef(caption);
|
const captionRef = useStateRef(caption);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -121,9 +124,13 @@ const AttachmentModal: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const sendAttachments = useCallback(() => {
|
const sendAttachments = useCallback(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
onSend();
|
if (shouldSchedule) {
|
||||||
|
onSendScheduled();
|
||||||
|
} else {
|
||||||
|
onSend();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [isOpen, onSend]);
|
}, [isOpen, onSendScheduled, onSend, shouldSchedule]);
|
||||||
|
|
||||||
const handleDragLeave = (e: React.DragEvent<HTMLElement>) => {
|
const handleDragLeave = (e: React.DragEvent<HTMLElement>) => {
|
||||||
const { relatedTarget: toTarget, target: fromTarget } = e;
|
const { relatedTarget: toTarget, target: fromTarget } = e;
|
||||||
@ -217,10 +224,11 @@ const AttachmentModal: FC<OwnProps> = ({
|
|||||||
<CustomSendMenu
|
<CustomSendMenu
|
||||||
isOpen={isCustomSendMenuOpen}
|
isOpen={isCustomSendMenuOpen}
|
||||||
isOpenToBottom
|
isOpenToBottom
|
||||||
onSilentSend={!isChatWithSelf ? onSilentSend : undefined}
|
onSendSilent={!isChatWithSelf ? onSendSilent : undefined}
|
||||||
onScheduleSend={openCalendar}
|
onSendSchedule={onSendScheduled}
|
||||||
onClose={handleContextMenuClose}
|
onClose={handleContextMenuClose}
|
||||||
onCloseAnimationEnd={handleContextMenuHide}
|
onCloseAnimationEnd={handleContextMenuHide}
|
||||||
|
isSavedMessages={isChatWithSelf}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -288,7 +296,7 @@ const AttachmentModal: FC<OwnProps> = ({
|
|||||||
editableInputId={EDITABLE_INPUT_MODAL_ID}
|
editableInputId={EDITABLE_INPUT_MODAL_ID}
|
||||||
placeholder={lang('Caption')}
|
placeholder={lang('Caption')}
|
||||||
onUpdate={onCaptionUpdate}
|
onUpdate={onCaptionUpdate}
|
||||||
onSend={onSend}
|
onSend={sendAttachments}
|
||||||
canAutoFocus={Boolean(isReady && attachments.length)}
|
canAutoFocus={Boolean(isReady && attachments.length)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
import { InlineBotSettings } from '../../../types';
|
import { InlineBotSettings } from '../../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_ID, REPLIES_USER_ID, SCHEDULED_WHEN_ONLINE, SEND_MESSAGE_ACTION_INTERVAL,
|
BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_ID, REPLIES_USER_ID, SEND_MESSAGE_ACTION_INTERVAL,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment';
|
import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
@ -36,20 +36,18 @@ import {
|
|||||||
selectEditingMessage,
|
selectEditingMessage,
|
||||||
selectIsChatWithSelf,
|
selectIsChatWithSelf,
|
||||||
selectChatBot,
|
selectChatBot,
|
||||||
selectChatUser,
|
|
||||||
selectChatMessage,
|
selectChatMessage,
|
||||||
selectUser,
|
selectUser,
|
||||||
selectUserStatus,
|
selectCanScheduleUntilOnline,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import {
|
import {
|
||||||
getAllowedAttachmentOptions,
|
getAllowedAttachmentOptions,
|
||||||
getChatSlowModeOptions,
|
getChatSlowModeOptions,
|
||||||
isUserId,
|
|
||||||
isChatAdmin,
|
isChatAdmin,
|
||||||
isChatSuperGroup,
|
isChatSuperGroup,
|
||||||
isChatChannel,
|
isChatChannel,
|
||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
import { formatMediaDuration, formatVoiceRecordDuration, getDayStartAt } from '../../../util/dateFormat';
|
import { formatMediaDuration, formatVoiceRecordDuration } from '../../../util/dateFormat';
|
||||||
import focusEditableElement from '../../../util/focusEditableElement';
|
import focusEditableElement from '../../../util/focusEditableElement';
|
||||||
import parseMessageInput from '../../../util/parseMessageInput';
|
import parseMessageInput from '../../../util/parseMessageInput';
|
||||||
import buildAttachment from './helpers/buildAttachment';
|
import buildAttachment from './helpers/buildAttachment';
|
||||||
@ -79,12 +77,12 @@ import useEmojiTooltip from './hooks/useEmojiTooltip';
|
|||||||
import useMentionTooltip from './hooks/useMentionTooltip';
|
import useMentionTooltip from './hooks/useMentionTooltip';
|
||||||
import useInlineBotTooltip from './hooks/useInlineBotTooltip';
|
import useInlineBotTooltip from './hooks/useInlineBotTooltip';
|
||||||
import useBotCommandTooltip from './hooks/useBotCommandTooltip';
|
import useBotCommandTooltip from './hooks/useBotCommandTooltip';
|
||||||
|
import useSchedule from '../../../hooks/useSchedule';
|
||||||
|
|
||||||
import DeleteMessageModal from '../../common/DeleteMessageModal.async';
|
import DeleteMessageModal from '../../common/DeleteMessageModal.async';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
|
import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
|
||||||
import Spinner from '../../ui/Spinner';
|
import Spinner from '../../ui/Spinner';
|
||||||
import CalendarModal from '../../common/CalendarModal.async';
|
|
||||||
import AttachMenu from './AttachMenu';
|
import AttachMenu from './AttachMenu';
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
import SymbolMenu from './SymbolMenu.async';
|
import SymbolMenu from './SymbolMenu.async';
|
||||||
@ -159,6 +157,10 @@ enum MainButtonState {
|
|||||||
Schedule = 'schedule',
|
Schedule = 'schedule',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScheduledMessageArgs = GlobalState['messages']['contentToBeScheduled'] | {
|
||||||
|
id: string; queryId: string; isSilent?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const VOICE_RECORDING_FILENAME = 'wonderful-voice-message.ogg';
|
const VOICE_RECORDING_FILENAME = 'wonderful-voice-message.ogg';
|
||||||
// When voice recording is active, composer placeholder will hide to prevent overlapping
|
// When voice recording is active, composer placeholder will hide to prevent overlapping
|
||||||
const SCREEN_WIDTH_TO_HIDE_PLACEHOLDER = 600; // px
|
const SCREEN_WIDTH_TO_HIDE_PLACEHOLDER = 600; // px
|
||||||
@ -236,15 +238,18 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
const htmlRef = useStateRef(html);
|
const htmlRef = useStateRef(html);
|
||||||
const lastMessageSendTimeSeconds = useRef<number>();
|
const lastMessageSendTimeSeconds = useRef<number>();
|
||||||
const prevDropAreaState = usePrevious(dropAreaState);
|
const prevDropAreaState = usePrevious(dropAreaState);
|
||||||
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
|
|
||||||
const [
|
|
||||||
scheduledMessageArgs, setScheduledMessageArgs,
|
|
||||||
] = useState<GlobalState['messages']['contentToBeScheduled'] | undefined>();
|
|
||||||
const { width: windowWidth } = windowSize.get();
|
const { width: windowWidth } = windowSize.get();
|
||||||
const sendAsIds = chat?.sendAsIds;
|
const sendAsIds = chat?.sendAsIds;
|
||||||
const canShowSendAs = sendAsIds && (sendAsIds.length > 1 || !sendAsIds.includes(currentUserId!));
|
const canShowSendAs = sendAsIds && (sendAsIds.length > 1 || !sendAsIds.includes(currentUserId!));
|
||||||
|
// Prevent Symbol Menu from closing when calendar is open
|
||||||
|
const [isSymbolMenuForced, forceShowSymbolMenu, cancelForceShowSymbolMenu] = useFlag();
|
||||||
const sendMessageAction = useSendMessageAction(chatId, threadId);
|
const sendMessageAction = useSendMessageAction(chatId, threadId);
|
||||||
|
|
||||||
|
const handleScheduleCancel = useCallback(() => {
|
||||||
|
cancelForceShowSymbolMenu();
|
||||||
|
}, [cancelForceShowSymbolMenu]);
|
||||||
|
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline, handleScheduleCancel);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
lastMessageSendTimeSeconds.current = undefined;
|
lastMessageSendTimeSeconds.current = undefined;
|
||||||
}, [chatId]);
|
}, [chatId]);
|
||||||
@ -279,13 +284,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
appendixRef.current.innerHTML = APPENDIX;
|
appendixRef.current.innerHTML = APPENDIX;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (contentToBeScheduled) {
|
|
||||||
setScheduledMessageArgs(contentToBeScheduled);
|
|
||||||
openCalendar();
|
|
||||||
}
|
|
||||||
}, [contentToBeScheduled, openCalendar]);
|
|
||||||
|
|
||||||
const [attachments, setAttachments] = useState<ApiAttachment[]>([]);
|
const [attachments, setAttachments] = useState<ApiAttachment[]>([]);
|
||||||
|
|
||||||
const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag();
|
const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag();
|
||||||
@ -438,8 +436,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
setAttachments(MEMO_EMPTY_ARRAY);
|
setAttachments(MEMO_EMPTY_ARRAY);
|
||||||
closeStickerTooltip();
|
closeStickerTooltip();
|
||||||
closeCalendar();
|
|
||||||
setScheduledMessageArgs(undefined);
|
|
||||||
closeMentionTooltip();
|
closeMentionTooltip();
|
||||||
closeEmojiTooltip();
|
closeEmojiTooltip();
|
||||||
|
|
||||||
@ -449,7 +445,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
closeSymbolMenu();
|
closeSymbolMenu();
|
||||||
}
|
}
|
||||||
}, [closeStickerTooltip, closeCalendar, closeMentionTooltip, closeEmojiTooltip, closeSymbolMenu]);
|
}, [closeStickerTooltip, closeMentionTooltip, closeEmojiTooltip, closeSymbolMenu]);
|
||||||
|
|
||||||
// Handle chat change (ref is used to avoid redundant effect calls)
|
// Handle chat change (ref is used to avoid redundant effect calls)
|
||||||
const stopRecordingVoiceRef = useRef<typeof stopRecordingVoice>();
|
const stopRecordingVoiceRef = useRef<typeof stopRecordingVoice>();
|
||||||
@ -600,44 +596,111 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
openSymbolMenu();
|
openSymbolMenu();
|
||||||
}, [closeBotCommandMenu, closeSendAsMenu, openSymbolMenu]);
|
}, [closeBotCommandMenu, closeSendAsMenu, openSymbolMenu]);
|
||||||
|
|
||||||
const handleStickerSelect = useCallback((sticker: ApiSticker, shouldPreserveInput = false) => {
|
const handleMessageSchedule = useCallback((
|
||||||
|
args: ScheduledMessageArgs, scheduledAt: number,
|
||||||
|
) => {
|
||||||
|
if (args && 'queryId' in args) {
|
||||||
|
const { id, queryId, isSilent } = args;
|
||||||
|
sendInlineBotResult({
|
||||||
|
id,
|
||||||
|
queryId,
|
||||||
|
scheduledAt,
|
||||||
|
isSilent,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isSilent, ...restArgs } = args || {};
|
||||||
|
|
||||||
|
if (!args || Object.keys(restArgs).length === 0) {
|
||||||
|
void handleSend(Boolean(isSilent), scheduledAt);
|
||||||
|
} else {
|
||||||
|
sendMessage({
|
||||||
|
...args,
|
||||||
|
scheduledAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [handleSend, sendInlineBotResult, sendMessage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (contentToBeScheduled) {
|
||||||
|
requestCalendar((scheduledAt) => {
|
||||||
|
handleMessageSchedule(contentToBeScheduled, scheduledAt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [contentToBeScheduled, handleMessageSchedule, requestCalendar]);
|
||||||
|
|
||||||
|
const handleStickerSelect = useCallback((
|
||||||
|
sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean, shouldPreserveInput = false,
|
||||||
|
) => {
|
||||||
sticker = {
|
sticker = {
|
||||||
...sticker,
|
...sticker,
|
||||||
isPreloadedGlobally: true,
|
isPreloadedGlobally: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (shouldSchedule) {
|
if (shouldSchedule || isScheduleRequested) {
|
||||||
setScheduledMessageArgs({ sticker });
|
forceShowSymbolMenu();
|
||||||
openCalendar();
|
requestCalendar((scheduledAt) => {
|
||||||
|
cancelForceShowSymbolMenu();
|
||||||
|
handleMessageSchedule({ sticker, isSilent }, scheduledAt);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
resetComposer(shouldPreserveInput);
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
sendMessage({ sticker });
|
sendMessage({ sticker, isSilent });
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
resetComposer(shouldPreserveInput);
|
resetComposer(shouldPreserveInput);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [shouldSchedule, openCalendar, sendMessage, resetComposer]);
|
}, [
|
||||||
|
shouldSchedule, forceShowSymbolMenu, requestCalendar, cancelForceShowSymbolMenu, handleMessageSchedule,
|
||||||
|
resetComposer, sendMessage,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleGifSelect = useCallback((gif: ApiVideo) => {
|
const handleGifSelect = useCallback((gif: ApiVideo, isSilent?: boolean, isScheduleRequested?: boolean) => {
|
||||||
if (shouldSchedule) {
|
if (shouldSchedule || isScheduleRequested) {
|
||||||
setScheduledMessageArgs({ gif });
|
forceShowSymbolMenu();
|
||||||
openCalendar();
|
requestCalendar((scheduledAt) => {
|
||||||
|
cancelForceShowSymbolMenu();
|
||||||
|
handleMessageSchedule({ gif, isSilent }, scheduledAt);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
resetComposer(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
sendMessage({ gif });
|
sendMessage({ gif, isSilent });
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
resetComposer(true);
|
resetComposer(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [shouldSchedule, openCalendar, sendMessage, resetComposer]);
|
}, [
|
||||||
|
shouldSchedule, forceShowSymbolMenu, requestCalendar, cancelForceShowSymbolMenu, handleMessageSchedule,
|
||||||
|
resetComposer, sendMessage,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleInlineBotSelect = useCallback((inlineResult: ApiBotInlineResult | ApiBotInlineMediaResult) => {
|
const handleInlineBotSelect = useCallback((
|
||||||
|
inlineResult: ApiBotInlineResult | ApiBotInlineMediaResult, isSilent?: boolean, isScheduleRequested?: boolean,
|
||||||
|
) => {
|
||||||
if (connectionState !== 'connectionStateReady') {
|
if (connectionState !== 'connectionStateReady') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendInlineBotResult({
|
if (shouldSchedule || isScheduleRequested) {
|
||||||
id: inlineResult.id,
|
requestCalendar((scheduledAt) => {
|
||||||
queryId: inlineResult.queryId,
|
handleMessageSchedule({
|
||||||
});
|
id: inlineResult.id,
|
||||||
|
queryId: inlineResult.queryId,
|
||||||
|
isSilent,
|
||||||
|
}, scheduledAt);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendInlineBotResult({
|
||||||
|
id: inlineResult.id,
|
||||||
|
queryId: inlineResult.queryId,
|
||||||
|
isSilent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const messageInput = document.getElementById(EDITABLE_INPUT_ID)!;
|
const messageInput = document.getElementById(EDITABLE_INPUT_ID)!;
|
||||||
if (IS_IOS && messageInput === document.activeElement) {
|
if (IS_IOS && messageInput === document.activeElement) {
|
||||||
@ -648,7 +711,10 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
resetComposer();
|
resetComposer();
|
||||||
});
|
});
|
||||||
}, [chatId, clearDraft, connectionState, resetComposer, sendInlineBotResult]);
|
}, [
|
||||||
|
chatId, clearDraft, connectionState, handleMessageSchedule, requestCalendar, resetComposer, sendInlineBotResult,
|
||||||
|
shouldSchedule,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleBotCommandSelect = useCallback(() => {
|
const handleBotCommandSelect = useCallback(() => {
|
||||||
clearDraft({ chatId, localOnly: true });
|
clearDraft({ chatId, localOnly: true });
|
||||||
@ -659,56 +725,25 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const handlePollSend = useCallback((poll: ApiNewPoll) => {
|
const handlePollSend = useCallback((poll: ApiNewPoll) => {
|
||||||
if (shouldSchedule) {
|
if (shouldSchedule) {
|
||||||
setScheduledMessageArgs({ poll });
|
requestCalendar((scheduledAt) => {
|
||||||
|
handleMessageSchedule({ poll }, scheduledAt);
|
||||||
|
});
|
||||||
closePollModal();
|
closePollModal();
|
||||||
openCalendar();
|
|
||||||
} else {
|
} else {
|
||||||
sendMessage({ poll });
|
sendMessage({ poll });
|
||||||
closePollModal();
|
closePollModal();
|
||||||
}
|
}
|
||||||
}, [closePollModal, openCalendar, sendMessage, shouldSchedule]);
|
}, [closePollModal, handleMessageSchedule, requestCalendar, sendMessage, shouldSchedule]);
|
||||||
|
|
||||||
const handleSilentSend = useCallback(() => {
|
const handleSendSilent = useCallback(() => {
|
||||||
if (shouldSchedule) {
|
if (shouldSchedule) {
|
||||||
setScheduledMessageArgs({ isSilent: true });
|
requestCalendar((scheduledAt) => {
|
||||||
openCalendar();
|
handleMessageSchedule({ isSilent: true }, scheduledAt);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
void handleSend(true);
|
void handleSend(true);
|
||||||
}
|
}
|
||||||
}, [handleSend, openCalendar, shouldSchedule]);
|
}, [handleMessageSchedule, handleSend, requestCalendar, shouldSchedule]);
|
||||||
|
|
||||||
const handleMessageSchedule = useCallback((date: Date, isWhenOnline = false) => {
|
|
||||||
const { isSilent, ...restArgs } = scheduledMessageArgs || {};
|
|
||||||
|
|
||||||
// No need to subscribe on updates in `mapStateToProps`
|
|
||||||
const { serverTimeOffset } = getGlobal();
|
|
||||||
|
|
||||||
// Scheduled time can not be less than 10 seconds in future
|
|
||||||
const scheduledAt = Math.round(Math.max(date.getTime(), Date.now() + 60 * 1000) / 1000)
|
|
||||||
+ (isWhenOnline ? 0 : serverTimeOffset);
|
|
||||||
|
|
||||||
if (!scheduledMessageArgs || Object.keys(restArgs).length === 0) {
|
|
||||||
void handleSend(Boolean(isSilent), scheduledAt);
|
|
||||||
} else {
|
|
||||||
sendMessage({
|
|
||||||
...scheduledMessageArgs,
|
|
||||||
scheduledAt,
|
|
||||||
});
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
resetComposer();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
closeCalendar();
|
|
||||||
}, [closeCalendar, handleSend, resetComposer, scheduledMessageArgs, sendMessage]);
|
|
||||||
|
|
||||||
const handleMessageScheduleUntilOnline = useCallback(() => {
|
|
||||||
handleMessageSchedule(new Date(SCHEDULED_WHEN_ONLINE * 1000), true);
|
|
||||||
}, [handleMessageSchedule]);
|
|
||||||
|
|
||||||
const handleCloseCalendar = useCallback(() => {
|
|
||||||
closeCalendar();
|
|
||||||
setScheduledMessageArgs(undefined);
|
|
||||||
}, [closeCalendar]);
|
|
||||||
|
|
||||||
const handleSearchOpen = useCallback((type: 'stickers' | 'gifs') => {
|
const handleSearchOpen = useCallback((type: 'stickers' | 'gifs') => {
|
||||||
if (type === 'stickers') {
|
if (type === 'stickers') {
|
||||||
@ -790,14 +825,16 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
if (activeVoiceRecording) {
|
if (activeVoiceRecording) {
|
||||||
pauseRecordingVoice();
|
pauseRecordingVoice();
|
||||||
}
|
}
|
||||||
openCalendar();
|
requestCalendar((scheduledAt) => {
|
||||||
|
handleMessageSchedule({}, scheduledAt);
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
mainButtonState, handleSend, startRecordingVoice, handleEditComplete,
|
mainButtonState, handleSend, startRecordingVoice, handleEditComplete, activeVoiceRecording, requestCalendar,
|
||||||
activeVoiceRecording, openCalendar, pauseRecordingVoice,
|
pauseRecordingVoice, handleMessageSchedule,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const areVoiceMessagesNotAllowed = mainButtonState === MainButtonState.Record && !canAttachMedia;
|
const areVoiceMessagesNotAllowed = mainButtonState === MainButtonState.Record && !canAttachMedia;
|
||||||
@ -837,9 +874,15 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
: (isSymbolMenuOpen && 'is-loading'),
|
: (isSymbolMenuOpen && 'is-loading'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSendScheduled = useCallback(() => {
|
||||||
|
requestCalendar((scheduledAt) => {
|
||||||
|
handleMessageSchedule({}, scheduledAt);
|
||||||
|
});
|
||||||
|
}, [handleMessageSchedule, requestCalendar]);
|
||||||
|
|
||||||
const onSend = mainButtonState === MainButtonState.Edit
|
const onSend = mainButtonState === MainButtonState.Edit
|
||||||
? handleEditComplete
|
? handleEditComplete
|
||||||
: mainButtonState === MainButtonState.Schedule ? openCalendar
|
: mainButtonState === MainButtonState.Schedule ? handleSendScheduled
|
||||||
: handleSend;
|
: handleSend;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -867,9 +910,10 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
baseEmojiKeywords={baseEmojiKeywords}
|
baseEmojiKeywords={baseEmojiKeywords}
|
||||||
emojiKeywords={emojiKeywords}
|
emojiKeywords={emojiKeywords}
|
||||||
addRecentEmoji={addRecentEmoji}
|
addRecentEmoji={addRecentEmoji}
|
||||||
onSilentSend={handleSilentSend}
|
shouldSchedule={shouldSchedule}
|
||||||
openCalendar={openCalendar}
|
onSendSilent={handleSendSilent}
|
||||||
onSend={shouldSchedule ? openCalendar : handleSend}
|
onSend={handleSend}
|
||||||
|
onSendScheduled={handleSendScheduled}
|
||||||
onFileAppend={handleAppendFiles}
|
onFileAppend={handleAppendFiles}
|
||||||
onClear={handleClearAttachment}
|
onClear={handleClearAttachment}
|
||||||
/>
|
/>
|
||||||
@ -909,6 +953,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
onSelectResult={handleInlineBotSelect}
|
onSelectResult={handleInlineBotSelect}
|
||||||
loadMore={loadMoreForInlineBot}
|
loadMore={loadMoreForInlineBot}
|
||||||
onClose={closeInlineBotTooltip}
|
onClose={closeInlineBotTooltip}
|
||||||
|
isSavedMessages={isChatWithSelf}
|
||||||
|
canSendGifs={canSendGifs}
|
||||||
/>
|
/>
|
||||||
<BotCommandTooltip
|
<BotCommandTooltip
|
||||||
isOpen={isBotCommandTooltipOpen}
|
isOpen={isBotCommandTooltipOpen}
|
||||||
@ -1063,7 +1109,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
<SymbolMenu
|
<SymbolMenu
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
isOpen={isSymbolMenuOpen}
|
isOpen={isSymbolMenuOpen || isSymbolMenuForced}
|
||||||
canSendGifs={canSendGifs}
|
canSendGifs={canSendGifs}
|
||||||
canSendStickers={canSendStickers}
|
canSendStickers={canSendStickers}
|
||||||
onLoad={onSymbolMenuLoadingComplete}
|
onLoad={onSymbolMenuLoadingComplete}
|
||||||
@ -1108,23 +1154,14 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
{canShowCustomSendMenu && (
|
{canShowCustomSendMenu && (
|
||||||
<CustomSendMenu
|
<CustomSendMenu
|
||||||
isOpen={isCustomSendMenuOpen}
|
isOpen={isCustomSendMenuOpen}
|
||||||
onSilentSend={!isChatWithSelf ? handleSilentSend : undefined}
|
onSendSilent={!isChatWithSelf ? handleSendSilent : undefined}
|
||||||
onScheduleSend={!shouldSchedule ? openCalendar : undefined}
|
onSendSchedule={!shouldSchedule ? handleSendScheduled : undefined}
|
||||||
onClose={handleContextMenuClose}
|
onClose={handleContextMenuClose}
|
||||||
onCloseAnimationEnd={handleContextMenuHide}
|
onCloseAnimationEnd={handleContextMenuHide}
|
||||||
|
isSavedMessages={isChatWithSelf}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CalendarModal
|
{calendar}
|
||||||
isOpen={isCalendarOpen}
|
|
||||||
withTimePicker
|
|
||||||
selectedAt={scheduledDefaultDate.getTime()}
|
|
||||||
maxAt={getDayStartAt(scheduledMaxDate)}
|
|
||||||
isFutureMode
|
|
||||||
secondButtonLabel={canScheduleUntilOnline ? lang('Schedule.SendWhenOnline') : undefined}
|
|
||||||
onClose={handleCloseCalendar}
|
|
||||||
onSubmit={handleMessageSchedule}
|
|
||||||
onSecondButtonClick={canScheduleUntilOnline ? handleMessageScheduleUntilOnline : undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1132,7 +1169,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { chatId, threadId, messageListType }): StateProps => {
|
(global, { chatId, threadId, messageListType }): StateProps => {
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
const chatUser = chat && selectChatUser(global, chat);
|
|
||||||
const chatBot = chatId !== REPLIES_USER_ID ? selectChatBot(global, chatId) : undefined;
|
const chatBot = chatId !== REPLIES_USER_ID ? selectChatBot(global, chatId) : undefined;
|
||||||
const isChatWithBot = Boolean(chatBot);
|
const isChatWithBot = Boolean(chatBot);
|
||||||
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
||||||
@ -1158,11 +1194,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
chat,
|
chat,
|
||||||
isChatWithBot,
|
isChatWithBot,
|
||||||
isChatWithSelf,
|
isChatWithSelf,
|
||||||
|
canScheduleUntilOnline: selectCanScheduleUntilOnline(global, chatId),
|
||||||
isChannel: chat ? isChatChannel(chat) : undefined,
|
isChannel: chat ? isChatChannel(chat) : undefined,
|
||||||
canScheduleUntilOnline: Boolean(
|
|
||||||
!isChatWithSelf && !isChatWithBot && chat && chatUser
|
|
||||||
&& isUserId(chatId) && selectUserStatus(global, chatId)?.wasOnline,
|
|
||||||
),
|
|
||||||
isRightColumnShown: selectIsRightColumnShown(global),
|
isRightColumnShown: selectIsRightColumnShown(global),
|
||||||
isSelectModeActive: selectIsInSelectMode(global),
|
isSelectModeActive: selectIsInSelectMode(global),
|
||||||
withScheduledButton: (
|
withScheduledButton: (
|
||||||
|
|||||||
@ -18,11 +18,12 @@ import {
|
|||||||
selectEditingMessage,
|
selectEditingMessage,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { isUserId } from '../../../global/helpers';
|
import { isUserId } from '../../../global/helpers';
|
||||||
|
|
||||||
|
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||||
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import EmbeddedMessage from '../../common/EmbeddedMessage';
|
import EmbeddedMessage from '../../common/EmbeddedMessage';
|
||||||
|
|
||||||
|
|||||||
@ -12,14 +12,21 @@ import './CustomSendMenu.scss';
|
|||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isOpenToBottom?: boolean;
|
isOpenToBottom?: boolean;
|
||||||
onSilentSend?: NoneToVoidFunction;
|
isSavedMessages?: boolean;
|
||||||
onScheduleSend?: NoneToVoidFunction;
|
onSendSilent?: NoneToVoidFunction;
|
||||||
|
onSendSchedule?: NoneToVoidFunction;
|
||||||
onClose: NoneToVoidFunction;
|
onClose: NoneToVoidFunction;
|
||||||
onCloseAnimationEnd?: NoneToVoidFunction;
|
onCloseAnimationEnd?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomSendMenu: FC<OwnProps> = ({
|
const CustomSendMenu: FC<OwnProps> = ({
|
||||||
isOpen, isOpenToBottom = false, onSilentSend, onScheduleSend, onClose, onCloseAnimationEnd,
|
isOpen,
|
||||||
|
isOpenToBottom = false,
|
||||||
|
isSavedMessages,
|
||||||
|
onSendSilent,
|
||||||
|
onSendSchedule,
|
||||||
|
onClose,
|
||||||
|
onCloseAnimationEnd,
|
||||||
}) => {
|
}) => {
|
||||||
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose);
|
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose);
|
||||||
|
|
||||||
@ -38,8 +45,12 @@ const CustomSendMenu: FC<OwnProps> = ({
|
|||||||
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
|
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
|
||||||
noCloseOnBackdrop={!IS_TOUCH_ENV}
|
noCloseOnBackdrop={!IS_TOUCH_ENV}
|
||||||
>
|
>
|
||||||
{onSilentSend && <MenuItem icon="mute" onClick={onSilentSend}>{lang('SendWithoutSound')}</MenuItem>}
|
{onSendSilent && <MenuItem icon="mute" onClick={onSendSilent}>{lang('SendWithoutSound')}</MenuItem>}
|
||||||
{onScheduleSend && <MenuItem icon="schedule" onClick={onScheduleSend}>{lang('ScheduleMessage')}</MenuItem>}
|
{onSendSchedule && (
|
||||||
|
<MenuItem icon="schedule" onClick={onSendSchedule}>
|
||||||
|
{lang(isSavedMessages ? 'SetReminder' : 'ScheduleMessage')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, useEffect, memo, useRef,
|
FC, useEffect, memo, useRef, useCallback,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
@ -8,6 +8,8 @@ import { ApiVideo } from '../../../api/types';
|
|||||||
import { SLIDE_TRANSITION_DURATION } from '../../../config';
|
import { SLIDE_TRANSITION_DURATION } from '../../../config';
|
||||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { selectCurrentMessageList, selectIsChatWithSelf } from '../../../global/selectors';
|
||||||
|
|
||||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||||
|
|
||||||
@ -20,11 +22,12 @@ type OwnProps = {
|
|||||||
className: string;
|
className: string;
|
||||||
loadAndPlay: boolean;
|
loadAndPlay: boolean;
|
||||||
canSendGifs: boolean;
|
canSendGifs: boolean;
|
||||||
onGifSelect: (gif: ApiVideo) => void;
|
onGifSelect: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
savedGifs?: ApiVideo[];
|
savedGifs?: ApiVideo[];
|
||||||
|
isSavedMessages?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const INTERSECTION_DEBOUNCE = 300;
|
const INTERSECTION_DEBOUNCE = 300;
|
||||||
@ -34,9 +37,10 @@ const GifPicker: FC<OwnProps & StateProps> = ({
|
|||||||
loadAndPlay,
|
loadAndPlay,
|
||||||
canSendGifs,
|
canSendGifs,
|
||||||
savedGifs,
|
savedGifs,
|
||||||
|
isSavedMessages,
|
||||||
onGifSelect,
|
onGifSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const { loadSavedGifs } = getActions();
|
const { loadSavedGifs, saveGif } = getActions();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -51,6 +55,10 @@ const GifPicker: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [loadAndPlay, loadSavedGifs]);
|
}, [loadAndPlay, loadSavedGifs]);
|
||||||
|
|
||||||
|
const handleUnsaveClick = useCallback((gif: ApiVideo) => {
|
||||||
|
saveGif({ gif, shouldUnsave: true });
|
||||||
|
}, [saveGif]);
|
||||||
|
|
||||||
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -67,7 +75,9 @@ const GifPicker: FC<OwnProps & StateProps> = ({
|
|||||||
gif={gif}
|
gif={gif}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
isDisabled={!loadAndPlay}
|
isDisabled={!loadAndPlay}
|
||||||
onClick={onGifSelect}
|
onClick={canSendGifs ? onGifSelect : undefined}
|
||||||
|
onUnsaveClick={handleUnsaveClick}
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : canRenderContents && savedGifs ? (
|
) : canRenderContents && savedGifs ? (
|
||||||
@ -81,8 +91,11 @@ const GifPicker: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
|
const { chatId } = selectCurrentMessageList(global) || {};
|
||||||
|
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
|
||||||
return {
|
return {
|
||||||
savedGifs: global.gifs.saved.gifs,
|
savedGifs: global.gifs.saved.gifs,
|
||||||
|
isSavedMessages,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(GifPicker));
|
)(GifPicker));
|
||||||
|
|||||||
@ -4,8 +4,6 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
--border-radius-default: 0;
|
|
||||||
|
|
||||||
&.gallery {
|
&.gallery {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, memo, useCallback, useEffect, useRef,
|
FC, memo, useCallback, useEffect, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
import { ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm } from '../../../api/types';
|
import { ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm } from '../../../api/types';
|
||||||
import { LoadMoreDirection } from '../../../types';
|
import { LoadMoreDirection } from '../../../types';
|
||||||
@ -22,7 +23,6 @@ import ListItem from '../../ui/ListItem';
|
|||||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
|
|
||||||
import './InlineBotTooltip.scss';
|
import './InlineBotTooltip.scss';
|
||||||
import { getActions } from '../../../global';
|
|
||||||
|
|
||||||
const INTERSECTION_DEBOUNCE_MS = 200;
|
const INTERSECTION_DEBOUNCE_MS = 200;
|
||||||
const runThrottled = throttle((cb) => cb(), 500, true);
|
const runThrottled = throttle((cb) => cb(), 500, true);
|
||||||
@ -33,7 +33,11 @@ export type OwnProps = {
|
|||||||
isGallery?: boolean;
|
isGallery?: boolean;
|
||||||
inlineBotResults?: (ApiBotInlineResult | ApiBotInlineMediaResult)[];
|
inlineBotResults?: (ApiBotInlineResult | ApiBotInlineMediaResult)[];
|
||||||
switchPm?: ApiBotInlineSwitchPm;
|
switchPm?: ApiBotInlineSwitchPm;
|
||||||
onSelectResult: (inlineResult: ApiBotInlineMediaResult | ApiBotInlineResult) => void;
|
isSavedMessages?: boolean;
|
||||||
|
canSendGifs?: boolean;
|
||||||
|
onSelectResult: (
|
||||||
|
inlineResult: ApiBotInlineMediaResult | ApiBotInlineResult, isSilent?: boolean, shouldSchedule?: boolean
|
||||||
|
) => void;
|
||||||
loadMore: NoneToVoidFunction;
|
loadMore: NoneToVoidFunction;
|
||||||
onClose: NoneToVoidFunction;
|
onClose: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
@ -44,6 +48,8 @@ const InlineBotTooltip: FC<OwnProps> = ({
|
|||||||
isGallery,
|
isGallery,
|
||||||
inlineBotResults,
|
inlineBotResults,
|
||||||
switchPm,
|
switchPm,
|
||||||
|
isSavedMessages,
|
||||||
|
canSendGifs,
|
||||||
loadMore,
|
loadMore,
|
||||||
onClose,
|
onClose,
|
||||||
onSelectResult,
|
onSelectResult,
|
||||||
@ -127,6 +133,8 @@ const InlineBotTooltip: FC<OwnProps> = ({
|
|||||||
inlineResult={inlineBotResult}
|
inlineResult={inlineBotResult}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
onClick={onSelectResult}
|
onClick={onSelectResult}
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
|
canSendGifs={canSendGifs}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -147,6 +155,7 @@ const InlineBotTooltip: FC<OwnProps> = ({
|
|||||||
inlineResult={inlineBotResult}
|
inlineResult={inlineBotResult}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
onClick={onSelectResult}
|
onClick={onSelectResult}
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
|||||||
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
||||||
|
import { selectIsChatWithSelf } from '../../../global/selectors';
|
||||||
|
|
||||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||||
@ -33,7 +35,7 @@ type OwnProps = {
|
|||||||
className: string;
|
className: string;
|
||||||
loadAndPlay: boolean;
|
loadAndPlay: boolean;
|
||||||
canSendStickers: boolean;
|
canSendStickers: boolean;
|
||||||
onStickerSelect: (sticker: ApiSticker) => void;
|
onStickerSelect: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -42,6 +44,7 @@ type StateProps = {
|
|||||||
stickerSetsById: Record<string, ApiStickerSet>;
|
stickerSetsById: Record<string, ApiStickerSet>;
|
||||||
addedSetIds?: string[];
|
addedSetIds?: string[];
|
||||||
shouldPlay?: boolean;
|
shouldPlay?: boolean;
|
||||||
|
isSavedMessages?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SMOOTH_SCROLL_DISTANCE = 500;
|
const SMOOTH_SCROLL_DISTANCE = 500;
|
||||||
@ -61,12 +64,14 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
addedSetIds,
|
addedSetIds,
|
||||||
stickerSetsById,
|
stickerSetsById,
|
||||||
shouldPlay,
|
shouldPlay,
|
||||||
|
isSavedMessages,
|
||||||
onStickerSelect,
|
onStickerSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadRecentStickers,
|
loadRecentStickers,
|
||||||
addRecentSticker,
|
addRecentSticker,
|
||||||
unfaveSticker,
|
unfaveSticker,
|
||||||
|
faveSticker,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -164,8 +169,8 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
fastSmoothScroll(containerRef.current!, stickerSetEl, 'start', undefined, SMOOTH_SCROLL_DISTANCE);
|
fastSmoothScroll(containerRef.current!, stickerSetEl, 'start', undefined, SMOOTH_SCROLL_DISTANCE);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleStickerSelect = useCallback((sticker: ApiSticker) => {
|
const handleStickerSelect = useCallback((sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => {
|
||||||
onStickerSelect(sticker);
|
onStickerSelect(sticker, isSilent, shouldSchedule);
|
||||||
addRecentSticker({ sticker });
|
addRecentSticker({ sticker });
|
||||||
}, [addRecentSticker, onStickerSelect]);
|
}, [addRecentSticker, onStickerSelect]);
|
||||||
|
|
||||||
@ -173,6 +178,10 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
unfaveSticker({ sticker });
|
unfaveSticker({ sticker });
|
||||||
}, [unfaveSticker]);
|
}, [unfaveSticker]);
|
||||||
|
|
||||||
|
const handleStickerFave = useCallback((sticker: ApiSticker) => {
|
||||||
|
faveSticker({ sticker });
|
||||||
|
}, [faveSticker]);
|
||||||
|
|
||||||
const handleMouseMove = useCallback(() => {
|
const handleMouseMove = useCallback(() => {
|
||||||
sendMessageAction({ type: 'chooseSticker' });
|
sendMessageAction({ type: 'chooseSticker' });
|
||||||
}, [sendMessageAction]);
|
}, [sendMessageAction]);
|
||||||
@ -225,6 +234,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
observeIntersection={observeIntersectionForCovers}
|
observeIntersection={observeIntersectionForCovers}
|
||||||
onClick={selectStickerSet}
|
onClick={selectStickerSet}
|
||||||
clickArg={index}
|
clickArg={index}
|
||||||
|
noContextMenu
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -269,6 +279,9 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
||||||
onStickerSelect={handleStickerSelect}
|
onStickerSelect={handleStickerSelect}
|
||||||
onStickerUnfave={handleStickerUnfave}
|
onStickerUnfave={handleStickerUnfave}
|
||||||
|
onStickerFave={handleStickerFave}
|
||||||
|
favoriteStickers={favoriteStickers}
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -277,7 +290,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global): StateProps => {
|
(global, { chatId }): StateProps => {
|
||||||
const {
|
const {
|
||||||
setsById,
|
setsById,
|
||||||
added,
|
added,
|
||||||
@ -285,12 +298,15 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
favorite,
|
favorite,
|
||||||
} = global.stickers;
|
} = global.stickers;
|
||||||
|
|
||||||
|
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
recentStickers: recent.stickers,
|
recentStickers: recent.stickers,
|
||||||
favoriteStickers: favorite.stickers,
|
favoriteStickers: favorite.stickers,
|
||||||
stickerSetsById: setsById,
|
stickerSetsById: setsById,
|
||||||
addedSetIds: added.setIds,
|
addedSetIds: added.setIds,
|
||||||
shouldPlay: global.settings.byKey.shouldLoopStickers,
|
shouldPlay: global.settings.byKey.shouldLoopStickers,
|
||||||
|
isSavedMessages,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(StickerPicker));
|
)(StickerPicker));
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import React, { FC, memo, useRef } from '../../../lib/teact/teact';
|
import React, {
|
||||||
|
FC, memo, useMemo, useRef,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import { ApiSticker } from '../../../api/types';
|
import { ApiSticker } from '../../../api/types';
|
||||||
import { StickerSetOrRecent } from '../../../types';
|
import { StickerSetOrRecent } from '../../../types';
|
||||||
@ -7,18 +9,23 @@ import { ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserve
|
|||||||
import { STICKER_SIZE_PICKER } from '../../../config';
|
import { STICKER_SIZE_PICKER } from '../../../config';
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||||
import windowSize from '../../../util/windowSize';
|
import windowSize from '../../../util/windowSize';
|
||||||
import StickerButton from '../../common/StickerButton';
|
|
||||||
import useMediaTransition from '../../../hooks/useMediaTransition';
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
import useMediaTransition from '../../../hooks/useMediaTransition';
|
||||||
|
|
||||||
|
import StickerButton from '../../common/StickerButton';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
stickerSet: StickerSetOrRecent;
|
stickerSet: StickerSetOrRecent;
|
||||||
loadAndPlay: boolean;
|
loadAndPlay: boolean;
|
||||||
index: number;
|
index: number;
|
||||||
observeIntersection: ObserveFn;
|
|
||||||
shouldRender: boolean;
|
shouldRender: boolean;
|
||||||
onStickerSelect: (sticker: ApiSticker) => void;
|
favoriteStickers?: ApiSticker[];
|
||||||
|
isSavedMessages?: boolean;
|
||||||
|
observeIntersection: ObserveFn;
|
||||||
|
onStickerSelect: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
onStickerUnfave: (sticker: ApiSticker) => void;
|
onStickerUnfave: (sticker: ApiSticker) => void;
|
||||||
|
onStickerFave: (sticker: ApiSticker) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const STICKERS_PER_ROW_ON_DESKTOP = 5;
|
const STICKERS_PER_ROW_ON_DESKTOP = 5;
|
||||||
@ -29,10 +36,13 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
stickerSet,
|
stickerSet,
|
||||||
loadAndPlay,
|
loadAndPlay,
|
||||||
index,
|
index,
|
||||||
observeIntersection,
|
|
||||||
shouldRender,
|
shouldRender,
|
||||||
|
favoriteStickers,
|
||||||
|
isSavedMessages,
|
||||||
|
observeIntersection,
|
||||||
onStickerSelect,
|
onStickerSelect,
|
||||||
onStickerUnfave,
|
onStickerUnfave,
|
||||||
|
onStickerFave,
|
||||||
}) => {
|
}) => {
|
||||||
// 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);
|
||||||
@ -46,6 +56,10 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
: STICKERS_PER_ROW_ON_DESKTOP;
|
: STICKERS_PER_ROW_ON_DESKTOP;
|
||||||
const height = Math.ceil(stickerSet.count / stickersPerRow) * (STICKER_SIZE_PICKER + STICKER_MARGIN);
|
const height = Math.ceil(stickerSet.count / stickersPerRow) * (STICKER_SIZE_PICKER + STICKER_MARGIN);
|
||||||
|
|
||||||
|
const favoriteStickerIdsSet = useMemo(() => (
|
||||||
|
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
||||||
|
), [favoriteStickers]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -67,7 +81,9 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
noAnimate={!loadAndPlay}
|
noAnimate={!loadAndPlay}
|
||||||
onClick={onStickerSelect}
|
onClick={onStickerSelect}
|
||||||
clickArg={sticker}
|
clickArg={sticker}
|
||||||
onUnfaveClick={stickerSet.id === 'favorite' ? onStickerUnfave : undefined}
|
onUnfaveClick={favoriteStickerIdsSet?.has(sticker.id) ? onStickerUnfave : undefined}
|
||||||
|
onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined}
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'
|
|||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import usePrevious from '../../../hooks/usePrevious';
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
import useSendMessageAction from '../../../hooks/useSendMessageAction';
|
import useSendMessageAction from '../../../hooks/useSendMessageAction';
|
||||||
|
import { selectIsChatWithSelf } from '../../../global/selectors';
|
||||||
|
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
import StickerButton from '../../common/StickerButton';
|
import StickerButton from '../../common/StickerButton';
|
||||||
@ -22,11 +23,12 @@ export type OwnProps = {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
threadId?: number;
|
threadId?: number;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onStickerSelect: (sticker: ApiSticker) => void;
|
onStickerSelect: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
stickers?: ApiSticker[];
|
stickers?: ApiSticker[];
|
||||||
|
isSavedMessages?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const INTERSECTION_THROTTLE = 200;
|
const INTERSECTION_THROTTLE = 200;
|
||||||
@ -35,8 +37,9 @@ const StickerTooltip: FC<OwnProps & StateProps> = ({
|
|||||||
chatId,
|
chatId,
|
||||||
threadId,
|
threadId,
|
||||||
isOpen,
|
isOpen,
|
||||||
onStickerSelect,
|
|
||||||
stickers,
|
stickers,
|
||||||
|
isSavedMessages,
|
||||||
|
onStickerSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const { clearStickersForEmoji } = getActions();
|
const { clearStickersForEmoji } = getActions();
|
||||||
|
|
||||||
@ -78,6 +81,7 @@ const StickerTooltip: FC<OwnProps & StateProps> = ({
|
|||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
onClick={onStickerSelect}
|
onClick={onStickerSelect}
|
||||||
clickArg={sticker}
|
clickArg={sticker}
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : shouldRender ? (
|
) : shouldRender ? (
|
||||||
@ -88,9 +92,10 @@ const StickerTooltip: FC<OwnProps & StateProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global): StateProps => {
|
(global, { chatId }): StateProps => {
|
||||||
const { stickers } = global.stickers.forEmoji;
|
const { stickers } = global.stickers.forEmoji;
|
||||||
|
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
||||||
|
|
||||||
return { stickers };
|
return { stickers, isSavedMessages };
|
||||||
},
|
},
|
||||||
)(StickerTooltip));
|
)(StickerTooltip));
|
||||||
|
|||||||
@ -34,8 +34,10 @@ export type OwnProps = {
|
|||||||
onLoad: () => void;
|
onLoad: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onEmojiSelect: (emoji: string) => void;
|
onEmojiSelect: (emoji: string) => void;
|
||||||
onStickerSelect: (sticker: ApiSticker, shouldPreserveInput?: boolean) => void;
|
onStickerSelect: (
|
||||||
onGifSelect: (gif: ApiVideo) => void;
|
sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean, shouldPreserveInput?: boolean
|
||||||
|
) => void;
|
||||||
|
onGifSelect: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
onRemoveSymbol: () => void;
|
onRemoveSymbol: () => void;
|
||||||
onSearchOpen: (type: 'stickers' | 'gifs') => void;
|
onSearchOpen: (type: 'stickers' | 'gifs') => void;
|
||||||
addRecentEmoji: AnyToVoidFunction;
|
addRecentEmoji: AnyToVoidFunction;
|
||||||
@ -126,8 +128,8 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
onSearchOpen(type);
|
onSearchOpen(type);
|
||||||
}, [onClose, onSearchOpen]);
|
}, [onClose, onSearchOpen]);
|
||||||
|
|
||||||
const handleStickerSelect = useCallback((sticker: ApiSticker) => {
|
const handleStickerSelect = useCallback((sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => {
|
||||||
onStickerSelect(sticker, true);
|
onStickerSelect(sticker, isSilent, shouldSchedule, true);
|
||||||
}, [onStickerSelect]);
|
}, [onStickerSelect]);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React, {
|
|||||||
FC, memo, useCallback,
|
FC, memo, useCallback,
|
||||||
} from '../../../../lib/teact/teact';
|
} from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
import { ApiBotInlineMediaResult, ApiBotInlineResult } from '../../../../api/types';
|
import { ApiBotInlineMediaResult, ApiBotInlineResult, ApiVideo } from '../../../../api/types';
|
||||||
|
|
||||||
import { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
import { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
@ -10,17 +10,19 @@ import GifButton from '../../../common/GifButton';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
inlineResult: ApiBotInlineMediaResult;
|
inlineResult: ApiBotInlineMediaResult;
|
||||||
|
isSavedMessages?: boolean;
|
||||||
|
canSendGifs?: boolean;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
onClick: (result: ApiBotInlineResult) => void;
|
onClick: (result: ApiBotInlineResult, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GifResult: FC<OwnProps> = ({
|
const GifResult: FC<OwnProps> = ({
|
||||||
inlineResult, observeIntersection, onClick,
|
inlineResult, isSavedMessages, canSendGifs, observeIntersection, onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { gif } = inlineResult;
|
const { gif } = inlineResult;
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback((_gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => {
|
||||||
onClick(inlineResult);
|
onClick(inlineResult, isSilent, shouldSchedule);
|
||||||
}, [inlineResult, onClick]);
|
}, [inlineResult, onClick]);
|
||||||
|
|
||||||
if (!gif) {
|
if (!gif) {
|
||||||
@ -32,7 +34,8 @@ const GifResult: FC<OwnProps> = ({
|
|||||||
gif={gif}
|
gif={gif}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
className="chat-item-clickable"
|
className="chat-item-clickable"
|
||||||
onClick={handleClick}
|
onClick={canSendGifs ? handleClick : undefined}
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,11 +9,17 @@ import StickerButton from '../../../common/StickerButton';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
inlineResult: ApiBotInlineMediaResult;
|
inlineResult: ApiBotInlineMediaResult;
|
||||||
|
isSavedMessages?: boolean;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
onClick: (result: ApiBotInlineResult) => void;
|
onClick: (result: ApiBotInlineResult, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StickerResult: FC<OwnProps> = ({ inlineResult, observeIntersection, onClick }) => {
|
const StickerResult: FC<OwnProps> = ({
|
||||||
|
inlineResult,
|
||||||
|
isSavedMessages,
|
||||||
|
observeIntersection,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
const { sticker } = inlineResult;
|
const { sticker } = inlineResult;
|
||||||
|
|
||||||
if (!sticker) {
|
if (!sticker) {
|
||||||
@ -29,6 +35,7 @@ const StickerResult: FC<OwnProps> = ({ inlineResult, observeIntersection, onClic
|
|||||||
className="chat-item-clickable"
|
className="chat-item-clickable"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
clickArg={inlineResult}
|
clickArg={inlineResult}
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import {
|
import {
|
||||||
isActionMessage, isChatChannel,
|
isActionMessage, isChatChannel,
|
||||||
isChatGroup, isOwnMessage, areReactionsEmpty, isUserId, isMessageLocal,
|
isChatGroup, isOwnMessage, areReactionsEmpty, isUserId, isMessageLocal, getMessageVideo,
|
||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
||||||
import { getDayStartAt } from '../../../util/dateFormat';
|
import { getDayStartAt } from '../../../util/dateFormat';
|
||||||
@ -67,6 +67,7 @@ type StateProps = {
|
|||||||
canCopyLink?: boolean;
|
canCopyLink?: boolean;
|
||||||
canSelect?: boolean;
|
canSelect?: boolean;
|
||||||
canDownload?: boolean;
|
canDownload?: boolean;
|
||||||
|
canSaveGif?: boolean;
|
||||||
activeDownloads: number[];
|
activeDownloads: number[];
|
||||||
canShowSeenBy?: boolean;
|
canShowSeenBy?: boolean;
|
||||||
enabledReactions?: string[];
|
enabledReactions?: string[];
|
||||||
@ -104,6 +105,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
canCopyLink,
|
canCopyLink,
|
||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
|
canSaveGif,
|
||||||
activeDownloads,
|
activeDownloads,
|
||||||
canShowSeenBy,
|
canShowSeenBy,
|
||||||
}) => {
|
}) => {
|
||||||
@ -126,6 +128,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
loadFullChat,
|
loadFullChat,
|
||||||
loadReactors,
|
loadReactors,
|
||||||
copyMessagesByIds,
|
copyMessagesByIds,
|
||||||
|
saveGif,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
||||||
@ -310,6 +313,12 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
}, [album, message, closeMenu, isDownloading, cancelMessageMediaDownload, downloadMessageMedia]);
|
}, [album, message, closeMenu, isDownloading, cancelMessageMediaDownload, downloadMessageMedia]);
|
||||||
|
|
||||||
|
const handleSaveGif = useCallback(() => {
|
||||||
|
const video = getMessageVideo(message);
|
||||||
|
saveGif({ gif: video });
|
||||||
|
closeMenu();
|
||||||
|
}, [closeMenu, message, saveGif]);
|
||||||
|
|
||||||
const handleSendReaction = useCallback((reaction: string | undefined, x: number, y: number) => {
|
const handleSendReaction = useCallback((reaction: string | undefined, x: number, y: number) => {
|
||||||
sendReaction({
|
sendReaction({
|
||||||
chatId: message.chatId, messageId: message.id, reaction, x, y, startSize: START_SIZE,
|
chatId: message.chatId, messageId: message.id, reaction, x, y, startSize: START_SIZE,
|
||||||
@ -355,6 +364,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
canCopyLink={canCopyLink}
|
canCopyLink={canCopyLink}
|
||||||
canSelect={canSelect}
|
canSelect={canSelect}
|
||||||
canDownload={canDownload}
|
canDownload={canDownload}
|
||||||
|
canSaveGif={canSaveGif}
|
||||||
canShowSeenBy={canShowSeenBy}
|
canShowSeenBy={canShowSeenBy}
|
||||||
isDownloading={isDownloading}
|
isDownloading={isDownloading}
|
||||||
seenByRecentUsers={seenByRecentUsers}
|
seenByRecentUsers={seenByRecentUsers}
|
||||||
@ -374,6 +384,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
onCopyLink={handleCopyLink}
|
onCopyLink={handleCopyLink}
|
||||||
onCopyMessages={handleCopyMessages}
|
onCopyMessages={handleCopyMessages}
|
||||||
onDownload={handleDownloadClick}
|
onDownload={handleDownloadClick}
|
||||||
|
onSaveGif={handleSaveGif}
|
||||||
onShowSeenBy={handleOpenSeenByModal}
|
onShowSeenBy={handleOpenSeenByModal}
|
||||||
onSendReaction={handleSendReaction}
|
onSendReaction={handleSendReaction}
|
||||||
onShowReactors={handleOpenReactorListModal}
|
onShowReactors={handleOpenReactorListModal}
|
||||||
@ -432,6 +443,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canCopyLink,
|
canCopyLink,
|
||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
|
canSaveGif,
|
||||||
} = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
} = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
||||||
const isPinned = messageListType === 'pinned';
|
const isPinned = messageListType === 'pinned';
|
||||||
const isScheduled = messageListType === 'scheduled';
|
const isScheduled = messageListType === 'scheduled';
|
||||||
@ -471,6 +483,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canCopyLink: !isProtected && !isScheduled && canCopyLink,
|
canCopyLink: !isProtected && !isScheduled && canCopyLink,
|
||||||
canSelect,
|
canSelect,
|
||||||
canDownload: !isProtected && canDownload,
|
canDownload: !isProtected && canDownload,
|
||||||
|
canSaveGif: !isProtected && canSaveGif,
|
||||||
activeDownloads,
|
activeDownloads,
|
||||||
canShowSeenBy,
|
canShowSeenBy,
|
||||||
enabledReactions: chat?.fullInfo?.enabledReactions,
|
enabledReactions: chat?.fullInfo?.enabledReactions,
|
||||||
|
|||||||
@ -46,6 +46,7 @@ type OwnProps = {
|
|||||||
canSelect?: boolean;
|
canSelect?: boolean;
|
||||||
isPrivate?: boolean;
|
isPrivate?: boolean;
|
||||||
canDownload?: boolean;
|
canDownload?: boolean;
|
||||||
|
canSaveGif?: boolean;
|
||||||
isDownloading?: boolean;
|
isDownloading?: boolean;
|
||||||
canShowSeenBy?: boolean;
|
canShowSeenBy?: boolean;
|
||||||
seenByRecentUsers?: ApiUser[];
|
seenByRecentUsers?: ApiUser[];
|
||||||
@ -66,6 +67,7 @@ type OwnProps = {
|
|||||||
onCopyLink?: () => void;
|
onCopyLink?: () => void;
|
||||||
onCopyMessages?: (messageIds: number[]) => void;
|
onCopyMessages?: (messageIds: number[]) => void;
|
||||||
onDownload?: () => void;
|
onDownload?: () => void;
|
||||||
|
onSaveGif?: () => void;
|
||||||
onShowSeenBy?: () => void;
|
onShowSeenBy?: () => void;
|
||||||
onShowReactors?: () => void;
|
onShowReactors?: () => void;
|
||||||
onSendReaction: (reaction: string | undefined, x: number, y: number) => void;
|
onSendReaction: (reaction: string | undefined, x: number, y: number) => void;
|
||||||
@ -97,6 +99,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
canCopyLink,
|
canCopyLink,
|
||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
|
canSaveGif,
|
||||||
isDownloading,
|
isDownloading,
|
||||||
canShowSeenBy,
|
canShowSeenBy,
|
||||||
canShowReactionsCount,
|
canShowReactionsCount,
|
||||||
@ -119,6 +122,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
onCloseAnimationEnd,
|
onCloseAnimationEnd,
|
||||||
onCopyLink,
|
onCopyLink,
|
||||||
onDownload,
|
onDownload,
|
||||||
|
onSaveGif,
|
||||||
onShowSeenBy,
|
onShowSeenBy,
|
||||||
onShowReactors,
|
onShowReactors,
|
||||||
onSendReaction,
|
onSendReaction,
|
||||||
@ -240,6 +244,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
))}
|
))}
|
||||||
{canPin && <MenuItem icon="pin" onClick={onPin}>{lang('DialogPin')}</MenuItem>}
|
{canPin && <MenuItem icon="pin" onClick={onPin}>{lang('DialogPin')}</MenuItem>}
|
||||||
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
|
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
|
||||||
|
{canSaveGif && <MenuItem icon="gifs" onClick={onSaveGif}>{lang('lng_context_save_gif')}</MenuItem>}
|
||||||
{canDownload && (
|
{canDownload && (
|
||||||
<MenuItem icon="download" onClick={onDownload}>
|
<MenuItem icon="download" onClick={onDownload}>
|
||||||
{isDownloading ? lang('lng_context_cancel_download') : lang('lng_media_download')}
|
{isDownloading ? lang('lng_context_cancel_download') : lang('lng_media_download')}
|
||||||
|
|||||||
@ -11,12 +11,15 @@ import {
|
|||||||
selectChat,
|
selectChat,
|
||||||
selectIsChatWithBot,
|
selectIsChatWithBot,
|
||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
|
selectCanScheduleUntilOnline,
|
||||||
|
selectIsChatWithSelf,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import { getAllowedAttachmentOptions } from '../../global/helpers';
|
import { getAllowedAttachmentOptions, getCanPostInChat } from '../../global/helpers';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||||
|
import useSchedule from '../../hooks/useSchedule';
|
||||||
|
|
||||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||||
import GifButton from '../common/GifButton';
|
import GifButton from '../common/GifButton';
|
||||||
@ -34,18 +37,24 @@ type StateProps = {
|
|||||||
results?: ApiVideo[];
|
results?: ApiVideo[];
|
||||||
chat?: ApiChat;
|
chat?: ApiChat;
|
||||||
isChatWithBot?: boolean;
|
isChatWithBot?: boolean;
|
||||||
|
canScheduleUntilOnline?: boolean;
|
||||||
|
isSavedMessages?: boolean;
|
||||||
|
canPostInChat?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PRELOAD_BACKWARDS = 96; // GIF Search bot results are multiplied by 24
|
const PRELOAD_BACKWARDS = 96; // GIF Search bot results are multiplied by 24
|
||||||
const INTERSECTION_DEBOUNCE = 300;
|
const INTERSECTION_DEBOUNCE = 300;
|
||||||
|
|
||||||
const GifSearch: FC<OwnProps & StateProps> = ({
|
const GifSearch: FC<OwnProps & StateProps> = ({
|
||||||
onClose,
|
|
||||||
isActive,
|
isActive,
|
||||||
query,
|
query,
|
||||||
results,
|
results,
|
||||||
chat,
|
chat,
|
||||||
isChatWithBot,
|
isChatWithBot,
|
||||||
|
canScheduleUntilOnline,
|
||||||
|
isSavedMessages,
|
||||||
|
canPostInChat,
|
||||||
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
searchMoreGifs,
|
searchMoreGifs,
|
||||||
@ -56,21 +65,29 @@ const GifSearch: FC<OwnProps & StateProps> = ({
|
|||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
observe: observeIntersection,
|
observe: observeIntersection,
|
||||||
} = useIntersectionObserver({ rootRef: containerRef, debounceMs: INTERSECTION_DEBOUNCE });
|
} = useIntersectionObserver({ rootRef: containerRef, debounceMs: INTERSECTION_DEBOUNCE });
|
||||||
|
|
||||||
const { canSendGifs } = getAllowedAttachmentOptions(chat, isChatWithBot);
|
const canSendGifs = canPostInChat && getAllowedAttachmentOptions(chat, isChatWithBot).canSendGifs;
|
||||||
|
|
||||||
const handleGifClick = useCallback((gif: ApiVideo) => {
|
const handleGifClick = useCallback((gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => {
|
||||||
if (canSendGifs) {
|
if (canSendGifs) {
|
||||||
sendMessage({ gif });
|
if (shouldSchedule) {
|
||||||
|
requestCalendar((scheduledAt) => {
|
||||||
|
sendMessage({ gif, scheduledAt, isSilent });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendMessage({ gif, isSilent });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_TOUCH_ENV) {
|
if (IS_TOUCH_ENV) {
|
||||||
setGifSearchQuery({ query: undefined });
|
setGifSearchQuery({ query: undefined });
|
||||||
}
|
}
|
||||||
}, [canSendGifs, sendMessage, setGifSearchQuery]);
|
}, [canSendGifs, requestCalendar, sendMessage, setGifSearchQuery]);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
@ -98,7 +115,8 @@ const GifSearch: FC<OwnProps & StateProps> = ({
|
|||||||
key={gif.id}
|
key={gif.id}
|
||||||
gif={gif}
|
gif={gif}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
onClick={handleGifClick}
|
onClick={canSendGifs ? handleGifClick : undefined}
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -118,6 +136,7 @@ const GifSearch: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
|
{calendar}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -126,15 +145,20 @@ export default memo(withGlobal(
|
|||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
const currentSearch = selectCurrentGifSearch(global);
|
const currentSearch = selectCurrentGifSearch(global);
|
||||||
const { query, results } = currentSearch || {};
|
const { query, results } = currentSearch || {};
|
||||||
const { chatId } = selectCurrentMessageList(global) || {};
|
const { chatId, threadId } = selectCurrentMessageList(global) || {};
|
||||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||||
const isChatWithBot = chat ? selectIsChatWithBot(global, chat) : undefined;
|
const isChatWithBot = chat ? selectIsChatWithBot(global, chat) : undefined;
|
||||||
|
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
|
||||||
|
const canPostInChat = Boolean(chat) && Boolean(threadId) && getCanPostInChat(chat, threadId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
results,
|
results,
|
||||||
chat,
|
chat,
|
||||||
isChatWithBot,
|
isChatWithBot,
|
||||||
|
isSavedMessages,
|
||||||
|
canPostInChat,
|
||||||
|
canScheduleUntilOnline: Boolean(chatId) && selectCanScheduleUntilOnline(global, chatId),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(GifSearch));
|
)(GifSearch));
|
||||||
|
|||||||
@ -101,7 +101,9 @@ const StickerSetResult: FC<OwnProps & StateProps> = ({
|
|||||||
size={STICKER_SIZE_SEARCH}
|
size={STICKER_SIZE_SEARCH}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
noAnimate={!shouldPlay || isModalOpen || isSomeModalOpen}
|
noAnimate={!shouldPlay || isModalOpen || isSomeModalOpen}
|
||||||
|
clickArg={undefined}
|
||||||
onClick={openModal}
|
onClick={openModal}
|
||||||
|
noContextMenu
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -194,7 +194,9 @@ addActionHandler('queryInlineBot', async (global, actions, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('sendInlineBotResult', (global, actions, payload) => {
|
addActionHandler('sendInlineBotResult', (global, actions, payload) => {
|
||||||
const { id, queryId } = payload;
|
const {
|
||||||
|
id, queryId, isSilent, scheduledAt,
|
||||||
|
} = payload;
|
||||||
const currentMessageList = selectCurrentMessageList(global);
|
const currentMessageList = selectCurrentMessageList(global);
|
||||||
if (!currentMessageList || !id) {
|
if (!currentMessageList || !id) {
|
||||||
return;
|
return;
|
||||||
@ -213,6 +215,8 @@ addActionHandler('sendInlineBotResult', (global, actions, payload) => {
|
|||||||
queryId,
|
queryId,
|
||||||
replyingTo: selectReplyingToId(global, chatId, threadId),
|
replyingTo: selectReplyingToId(global, chatId, threadId),
|
||||||
sendAs: selectSendAs(global, chatId),
|
sendAs: selectSendAs(global, chatId),
|
||||||
|
isSilent,
|
||||||
|
scheduleDate: scheduledAt,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -108,6 +108,29 @@ addActionHandler('loadSavedGifs', (global) => {
|
|||||||
void loadSavedGifs(hash);
|
void loadSavedGifs(hash);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('saveGif', async (global, actions, payload) => {
|
||||||
|
const { gif, shouldUnsave } = payload!;
|
||||||
|
const result = await callApi('saveGif', { gif, shouldUnsave });
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
const gifs = global.gifs.saved.gifs?.filter(({ id }) => id !== gif.id) || [];
|
||||||
|
const newGifs = shouldUnsave ? gifs : [gif, ...gifs];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
gifs: {
|
||||||
|
...global.gifs,
|
||||||
|
saved: {
|
||||||
|
...global.gifs.saved,
|
||||||
|
gifs: newGifs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('faveSticker', (global, actions, payload) => {
|
addActionHandler('faveSticker', (global, actions, payload) => {
|
||||||
const { sticker } = payload!;
|
const { sticker } = payload!;
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,11 @@ import {
|
|||||||
MAIN_THREAD_ID,
|
MAIN_THREAD_ID,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
|
|
||||||
import { LOCAL_MESSAGE_ID_BASE, SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
|
import { LOCAL_MESSAGE_ID_BASE, REPLIES_USER_ID, SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
|
||||||
import {
|
import {
|
||||||
selectChat, selectIsChatWithBot, selectIsChatWithSelf,
|
selectChat, selectChatBot, selectIsChatWithBot, selectIsChatWithSelf,
|
||||||
} from './chats';
|
} from './chats';
|
||||||
import { selectIsUserOrChatContact, selectUser } from './users';
|
import { selectIsUserOrChatContact, selectUser, selectUserStatus } from './users';
|
||||||
import {
|
import {
|
||||||
getSendingState,
|
getSendingState,
|
||||||
isChatChannel,
|
isChatChannel,
|
||||||
@ -427,6 +427,8 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
const canDownload = Boolean(content.webPage?.document || content.webPage?.video || content.webPage?.photo
|
const canDownload = Boolean(content.webPage?.document || content.webPage?.video || content.webPage?.photo
|
||||||
|| content.audio || content.voice || content.photo || content.video || content.document || content.sticker);
|
|| content.audio || content.voice || content.photo || content.video || content.document || content.sticker);
|
||||||
|
|
||||||
|
const canSaveGif = message.content.video?.isGif;
|
||||||
|
|
||||||
const noOptions = [
|
const noOptions = [
|
||||||
canReply,
|
canReply,
|
||||||
canEdit,
|
canEdit,
|
||||||
@ -442,6 +444,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
canCopyLink,
|
canCopyLink,
|
||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
|
canSaveGif,
|
||||||
].every((ability) => !ability);
|
].every((ability) => !ability);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -460,6 +463,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
canCopyLink,
|
canCopyLink,
|
||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
|
canSaveGif,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -922,3 +926,15 @@ export function selectVisibleUsers(global: GlobalState) {
|
|||||||
return senderId ? selectUser(global, senderId) : undefined;
|
return senderId ? selectUser(global, senderId) : undefined;
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectShouldSchedule(global: GlobalState) {
|
||||||
|
return selectCurrentMessageList(global)?.type === 'scheduled';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectCanScheduleUntilOnline(global: GlobalState, id: string) {
|
||||||
|
const isChatWithSelf = selectIsChatWithSelf(global, id);
|
||||||
|
const chatBot = id === REPLIES_USER_ID && selectChatBot(global, id);
|
||||||
|
return Boolean(
|
||||||
|
!isChatWithSelf && !chatBot && isUserId(id) && selectUserStatus(global, id)?.wasOnline,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -593,7 +593,7 @@ export type NonTypedActionNames = (
|
|||||||
'loadCountryList' | 'ensureTimeFormat' | 'loadAppConfig' |
|
'loadCountryList' | 'ensureTimeFormat' | 'loadAppConfig' |
|
||||||
// stickers & GIFs
|
// stickers & GIFs
|
||||||
'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' |
|
'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' |
|
||||||
'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' |
|
'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'saveGif' | 'setGifSearchQuery' | 'searchMoreGifs' |
|
||||||
'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' | 'loadAnimatedEmojis' |
|
'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' | 'loadAnimatedEmojis' |
|
||||||
'loadStickersForEmoji' | 'clearStickersForEmoji' | 'loadEmojiKeywords' | 'loadGreetingStickers' |
|
'loadStickersForEmoji' | 'clearStickersForEmoji' | 'loadEmojiKeywords' | 'loadGreetingStickers' |
|
||||||
'openStickerSetShortName' |
|
'openStickerSetShortName' |
|
||||||
|
|||||||
@ -3,16 +3,11 @@ import { useState, useEffect, useCallback } from '../lib/teact/teact';
|
|||||||
|
|
||||||
import { IAnchorPosition } from '../types';
|
import { IAnchorPosition } from '../types';
|
||||||
import {
|
import {
|
||||||
IS_TOUCH_ENV, IS_SINGLE_COLUMN_LAYOUT, IS_PWA, IS_IOS,
|
IS_TOUCH_ENV, IS_PWA, IS_IOS,
|
||||||
} from '../util/environment';
|
} from '../util/environment';
|
||||||
|
|
||||||
const LONG_TAP_DURATION_MS = 200;
|
const LONG_TAP_DURATION_MS = 200;
|
||||||
|
|
||||||
function checkIsDisabledForMobile() {
|
|
||||||
return IS_SINGLE_COLUMN_LAYOUT
|
|
||||||
&& window.document.body.classList.contains('enable-symbol-menu-transforms');
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopEvent(e: Event) {
|
function stopEvent(e: Event) {
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -106,7 +101,7 @@ const useContextMenuHandlers = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startLongPressTimer = (e: TouchEvent) => {
|
const startLongPressTimer = (e: TouchEvent) => {
|
||||||
if (isMenuDisabled || checkIsDisabledForMobile()) {
|
if (isMenuDisabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearLongPressTimer();
|
clearLongPressTimer();
|
||||||
|
|||||||
@ -99,11 +99,9 @@ export default function useContextMenuPosition(
|
|||||||
const triggerRect = triggerEl.getBoundingClientRect();
|
const triggerRect = triggerEl.getBoundingClientRect();
|
||||||
const left = horizontalPosition === 'left'
|
const left = horizontalPosition === 'left'
|
||||||
? Math.min(x - triggerRect.left, rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX)
|
? Math.min(x - triggerRect.left, rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX)
|
||||||
: Math.max((x - triggerRect.left), menuRect.width + MENU_POSITION_VISUAL_COMFORT_SPACE_PX);
|
: (x - triggerRect.left);
|
||||||
const top = Math.min(
|
const top = y - triggerRect.top;
|
||||||
rootRect.height - triggerRect.top + triggerRect.height - MENU_POSITION_BOTTOM_MARGIN + (marginTop || 0),
|
|
||||||
y - triggerRect.top,
|
|
||||||
);
|
|
||||||
const menuMaxHeight = rootRect.height - MENU_POSITION_BOTTOM_MARGIN - (marginTop || 0);
|
const menuMaxHeight = rootRect.height - MENU_POSITION_BOTTOM_MARGIN - (marginTop || 0);
|
||||||
|
|
||||||
setWithScroll(menuMaxHeight < menuRect.height);
|
setWithScroll(menuMaxHeight < menuRect.height);
|
||||||
|
|||||||
65
src/hooks/useSchedule.tsx
Normal file
65
src/hooks/useSchedule.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React, { useCallback, useState } from '../lib/teact/teact';
|
||||||
|
import { getGlobal } from '../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { SCHEDULED_WHEN_ONLINE } from '../config';
|
||||||
|
import { getDayStartAt } from '../util/dateFormat';
|
||||||
|
import useLang from './useLang';
|
||||||
|
|
||||||
|
import CalendarModal from '../components/common/CalendarModal.async';
|
||||||
|
|
||||||
|
type OnScheduledCallback = (scheduledAt: number) => void;
|
||||||
|
|
||||||
|
const useSchedule = (
|
||||||
|
canScheduleUntilOnline?: boolean,
|
||||||
|
onCancel?: () => void,
|
||||||
|
) => {
|
||||||
|
const lang = useLang();
|
||||||
|
const [onScheduled, setOnScheduled] = useState<OnScheduledCallback | undefined>();
|
||||||
|
|
||||||
|
const handleMessageSchedule = useCallback((date: Date, isWhenOnline = false) => {
|
||||||
|
const { serverTimeOffset } = getGlobal();
|
||||||
|
// Scheduled time can not be less than 10 seconds in future
|
||||||
|
const scheduledAt = Math.round(Math.max(date.getTime(), Date.now() + 60 * 1000) / 1000)
|
||||||
|
+ (isWhenOnline ? 0 : serverTimeOffset);
|
||||||
|
onScheduled?.(scheduledAt);
|
||||||
|
setOnScheduled(undefined);
|
||||||
|
}, [onScheduled]);
|
||||||
|
|
||||||
|
const handleMessageScheduleUntilOnline = useCallback(() => {
|
||||||
|
handleMessageSchedule(new Date(SCHEDULED_WHEN_ONLINE * 1000), true);
|
||||||
|
}, [handleMessageSchedule]);
|
||||||
|
|
||||||
|
const handleCloseCalendar = useCallback(() => {
|
||||||
|
setOnScheduled(undefined);
|
||||||
|
onCancel?.();
|
||||||
|
}, [onCancel]);
|
||||||
|
|
||||||
|
const requestCalendar = useCallback((whenScheduled: OnScheduledCallback) => {
|
||||||
|
setOnScheduled(() => whenScheduled);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scheduledDefaultDate = new Date();
|
||||||
|
scheduledDefaultDate.setSeconds(0);
|
||||||
|
scheduledDefaultDate.setMilliseconds(0);
|
||||||
|
|
||||||
|
const scheduledMaxDate = new Date();
|
||||||
|
scheduledMaxDate.setFullYear(scheduledMaxDate.getFullYear() + 1);
|
||||||
|
|
||||||
|
const calendar = (
|
||||||
|
<CalendarModal
|
||||||
|
isOpen={Boolean(onScheduled)}
|
||||||
|
withTimePicker
|
||||||
|
selectedAt={scheduledDefaultDate.getTime()}
|
||||||
|
maxAt={getDayStartAt(scheduledMaxDate)}
|
||||||
|
isFutureMode
|
||||||
|
secondButtonLabel={canScheduleUntilOnline ? lang('Schedule.SendWhenOnline') : undefined}
|
||||||
|
onClose={handleCloseCalendar}
|
||||||
|
onSubmit={handleMessageSchedule}
|
||||||
|
onSecondButtonClick={canScheduleUntilOnline ? handleMessageScheduleUntilOnline : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return [requestCalendar, calendar] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSchedule;
|
||||||
@ -1052,6 +1052,7 @@ messages.migrateChat#a2875319 chat_id:long = Updates;
|
|||||||
messages.searchGlobal#4bc6589a flags:# folder_id:flags.0?int q:string filter:MessagesFilter min_date:int max_date:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
|
messages.searchGlobal#4bc6589a flags:# folder_id:flags.0?int q:string filter:MessagesFilter min_date:int max_date:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
|
||||||
messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document;
|
messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document;
|
||||||
messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs;
|
messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs;
|
||||||
|
messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool;
|
||||||
messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults;
|
messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults;
|
||||||
messages.sendInlineBotResult#7aa11297 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
|
messages.sendInlineBotResult#7aa11297 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates;
|
||||||
messages.editMessage#48f71778 flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int = Updates;
|
messages.editMessage#48f71778 flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int = Updates;
|
||||||
|
|||||||
@ -98,6 +98,7 @@
|
|||||||
"messages.searchGlobal",
|
"messages.searchGlobal",
|
||||||
"messages.getDocumentByHash",
|
"messages.getDocumentByHash",
|
||||||
"messages.getSavedGifs",
|
"messages.getSavedGifs",
|
||||||
|
"messages.saveGif",
|
||||||
"messages.getInlineBotResults",
|
"messages.getInlineBotResults",
|
||||||
"messages.sendInlineBotResult",
|
"messages.sendInlineBotResult",
|
||||||
"messages.editMessage",
|
"messages.editMessage",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user