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