Message: Support swipe-to-reply, double-click on mobiles, refactoring
This commit is contained in:
parent
b1c243777c
commit
851bde515e
@ -20,6 +20,10 @@
|
|||||||
--select-message-scale: 0.9;
|
--select-message-scale: 0.9;
|
||||||
--select-background-color: white;
|
--select-background-color: white;
|
||||||
|
|
||||||
|
&.is-swiped {
|
||||||
|
transform: translateX(-2.5rem) !important;
|
||||||
|
}
|
||||||
|
|
||||||
> .Avatar,
|
> .Avatar,
|
||||||
> .message-content-wrapper {
|
> .message-content-wrapper {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@ -16,11 +16,8 @@ import {
|
|||||||
ApiUser,
|
ApiUser,
|
||||||
ApiChat,
|
ApiChat,
|
||||||
ApiSticker,
|
ApiSticker,
|
||||||
MAIN_THREAD_ID,
|
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import {
|
import { FocusDirection, IAlbum, ISettings } from '../../../types';
|
||||||
FocusDirection, IAlbum, ISettings, MediaViewerOrigin,
|
|
||||||
} from '../../../types';
|
|
||||||
|
|
||||||
import { IS_ANDROID, IS_TOUCH_ENV } from '../../../util/environment';
|
import { IS_ANDROID, IS_TOUCH_ENV } from '../../../util/environment';
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
@ -59,7 +56,6 @@ import {
|
|||||||
getUserColorKey,
|
getUserColorKey,
|
||||||
} from '../../../modules/helpers';
|
} from '../../../modules/helpers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import windowSize from '../../../util/windowSize';
|
|
||||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
||||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||||
import { renderMessageText } from '../../common/helpers/renderMessageText';
|
import { renderMessageText } from '../../common/helpers/renderMessageText';
|
||||||
@ -67,14 +63,15 @@ import { ROUND_VIDEO_DIMENSIONS } from '../../common/helpers/mediaDimensions';
|
|||||||
import { buildContentClassName, isEmojiOnlyMessage } from './helpers/buildContentClassName';
|
import { buildContentClassName, isEmojiOnlyMessage } from './helpers/buildContentClassName';
|
||||||
import { getMinMediaWidth, calculateMediaDimensions } from './helpers/mediaDimensions';
|
import { getMinMediaWidth, calculateMediaDimensions } from './helpers/mediaDimensions';
|
||||||
import { calculateAlbumLayout } from './helpers/calculateAlbumLayout';
|
import { calculateAlbumLayout } from './helpers/calculateAlbumLayout';
|
||||||
import { preventMessageInputBlur } from '../helpers/preventMessageInputBlur';
|
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
import calculateAuthorWidth from './helpers/calculateAuthorWidth';
|
import calculateAuthorWidth from './helpers/calculateAuthorWidth';
|
||||||
import { ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
import { ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
||||||
import useFocusMessage from './hooks/useFocusMessage';
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import useFocusMessage from './hooks/useFocusMessage';
|
||||||
|
import useOuterHandlers from './hooks/useOuterHandlers';
|
||||||
|
import useInnerHandlers from './hooks/useInnerHandlers';
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
@ -157,13 +154,7 @@ type StateProps = {
|
|||||||
shouldLoopStickers?: boolean;
|
shouldLoopStickers?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, (
|
type DispatchProps = Pick<GlobalActions, 'toggleMessageSelection' | 'clickInlineButton' | 'disableContextMenuHint'>;
|
||||||
'focusMessage' | 'openMediaViewer' | 'openAudioPlayer' |
|
|
||||||
'openUserInfo' | 'openChat' |
|
|
||||||
'cancelSendingMessage' | 'markMessagesRead' |
|
|
||||||
'sendPollVote' | 'toggleMessageSelection' | 'setReplyingToId' | 'openForwardMenu' |
|
|
||||||
'clickInlineButton' | 'disableContextMenuHint' | 'showNotification'
|
|
||||||
)>;
|
|
||||||
|
|
||||||
const NBSP = '\u00A0';
|
const NBSP = '\u00A0';
|
||||||
const GROUP_MESSAGE_HOVER_ATTRIBUTE = 'data-is-document-group-hover';
|
const GROUP_MESSAGE_HOVER_ATTRIBUTE = 'data-is-document-group-hover';
|
||||||
@ -173,7 +164,6 @@ const APPENDIX_OWN = '<svg width="9" height="20" xmlns="http://www.w3.org/2000/s
|
|||||||
const APPENDIX_NOT_OWN = '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#000" filter="url(#a)"/><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#FFF" class="corner"/></g></svg>';
|
const APPENDIX_NOT_OWN = '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#000" filter="url(#a)"/><path d="M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z" fill="#FFF" class="corner"/></g></svg>';
|
||||||
const APPEARANCE_DELAY = 10;
|
const APPEARANCE_DELAY = 10;
|
||||||
const NO_MEDIA_CORNERS_THRESHOLD = 18;
|
const NO_MEDIA_CORNERS_THRESHOLD = 18;
|
||||||
const ANDROID_KEYBOARD_HIDE_DELAY_MS = 350;
|
|
||||||
|
|
||||||
const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
message,
|
message,
|
||||||
@ -222,20 +212,9 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
shouldAutoLoadMedia,
|
shouldAutoLoadMedia,
|
||||||
shouldAutoPlayMedia,
|
shouldAutoPlayMedia,
|
||||||
shouldLoopStickers,
|
shouldLoopStickers,
|
||||||
focusMessage,
|
|
||||||
openMediaViewer,
|
|
||||||
openAudioPlayer,
|
|
||||||
openUserInfo,
|
|
||||||
openChat,
|
|
||||||
cancelSendingMessage,
|
|
||||||
markMessagesRead,
|
|
||||||
sendPollVote,
|
|
||||||
toggleMessageSelection,
|
toggleMessageSelection,
|
||||||
setReplyingToId,
|
|
||||||
openForwardMenu,
|
|
||||||
clickInlineButton,
|
clickInlineButton,
|
||||||
disableContextMenuHint,
|
disableContextMenuHint,
|
||||||
showNotification,
|
|
||||||
}) => {
|
}) => {
|
||||||
// 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);
|
||||||
@ -270,7 +249,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
}, [appearanceOrder, markShown, noAppearanceAnimation]);
|
}, [appearanceOrder, markShown, noAppearanceAnimation]);
|
||||||
const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false);
|
const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false);
|
||||||
|
|
||||||
const { chatId, id: messageId, threadInfo } = message;
|
const { id: messageId, chatId, threadInfo } = message;
|
||||||
|
|
||||||
const isLocal = isMessageLocal(message);
|
const isLocal = isMessageLocal(message);
|
||||||
const isOwn = isOwnMessage(message);
|
const isOwn = isOwnMessage(message);
|
||||||
@ -279,7 +258,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const hasThread = Boolean(threadInfo) && messageListType === 'thread';
|
const hasThread = Boolean(threadInfo) && messageListType === 'thread';
|
||||||
const { forwardInfo, viaBotId } = message;
|
const { forwardInfo, viaBotId } = message;
|
||||||
const asForwarded = forwardInfo && !isChatWithSelf && !forwardInfo.isLinkedChannelPost;
|
const asForwarded = forwardInfo && !isChatWithSelf && !forwardInfo.isLinkedChannelPost;
|
||||||
const isInDocumentGroup = !!message.groupedId && !message.isInAlbum;
|
const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum;
|
||||||
const isAlbum = Boolean(album) && album!.messages.length > 1;
|
const isAlbum = Boolean(album) && album!.messages.length > 1;
|
||||||
const {
|
const {
|
||||||
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice,
|
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice,
|
||||||
@ -303,6 +282,68 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const avatarPeer = forwardInfo && (isChatWithSelf || !sender) ? originSender : sender;
|
const avatarPeer = forwardInfo && (isChatWithSelf || !sender) ? originSender : sender;
|
||||||
const senderPeer = forwardInfo ? originSender : sender;
|
const senderPeer = forwardInfo ? originSender : sender;
|
||||||
|
|
||||||
|
const selectMessage = useCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => {
|
||||||
|
if (isLocal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMessageSelection({
|
||||||
|
messageId,
|
||||||
|
groupedId,
|
||||||
|
...(e && e.shiftKey && { withShift: true }),
|
||||||
|
...(isAlbum && { childMessageIds: album!.messages.map(({ id }) => id) }),
|
||||||
|
});
|
||||||
|
}, [isLocal, toggleMessageSelection, messageId, isAlbum, album]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleMouseDown,
|
||||||
|
handleClick,
|
||||||
|
handleContextMenu,
|
||||||
|
handleDoubleClick,
|
||||||
|
handleContentDoubleClick,
|
||||||
|
isSwiped,
|
||||||
|
} = useOuterHandlers(
|
||||||
|
selectMessage,
|
||||||
|
ref,
|
||||||
|
messageId,
|
||||||
|
isLocal,
|
||||||
|
isAlbum,
|
||||||
|
Boolean(isInSelectMode),
|
||||||
|
onContextMenu,
|
||||||
|
handleBeforeContextMenu,
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleAvatarClick,
|
||||||
|
handleSenderClick,
|
||||||
|
handleViaBotClick,
|
||||||
|
handleReplyClick,
|
||||||
|
handleMediaClick,
|
||||||
|
handleAudioPlay,
|
||||||
|
handleAlbumMediaClick,
|
||||||
|
handleMetaClick,
|
||||||
|
handleReadMedia,
|
||||||
|
handleCancelUpload,
|
||||||
|
handleVoteSend,
|
||||||
|
handleGroupForward,
|
||||||
|
handleForward,
|
||||||
|
handleFocus,
|
||||||
|
handleFocusForwarded,
|
||||||
|
handleDocumentGroupSelectAll,
|
||||||
|
} = useInnerHandlers(
|
||||||
|
lang,
|
||||||
|
selectMessage,
|
||||||
|
message,
|
||||||
|
chatId,
|
||||||
|
threadId,
|
||||||
|
isInDocumentGroup,
|
||||||
|
Boolean(isScheduled),
|
||||||
|
album,
|
||||||
|
avatarPeer,
|
||||||
|
senderPeer,
|
||||||
|
botSender,
|
||||||
|
);
|
||||||
|
|
||||||
const containerClassName = buildClassName(
|
const containerClassName = buildClassName(
|
||||||
'Message message-list-item',
|
'Message message-list-item',
|
||||||
isFirstInGroup && 'first-in-group',
|
isFirstInGroup && 'first-in-group',
|
||||||
@ -325,6 +366,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
isInSelectMode && 'is-in-selection-mode',
|
isInSelectMode && 'is-in-selection-mode',
|
||||||
isThreadTop && 'is-thread-top',
|
isThreadTop && 'is-thread-top',
|
||||||
Boolean(message.inlineButtons) && 'has-inline-buttons',
|
Boolean(message.inlineButtons) && 'has-inline-buttons',
|
||||||
|
isSwiped && 'is-swiped',
|
||||||
transitionClassNames,
|
transitionClassNames,
|
||||||
);
|
);
|
||||||
const contentClassName = buildContentClassName(message, {
|
const contentClassName = buildContentClassName(message, {
|
||||||
@ -351,175 +393,6 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
appendixRef.current.innerHTML = isOwn ? APPENDIX_OWN : APPENDIX_NOT_OWN;
|
appendixRef.current.innerHTML = isOwn ? APPENDIX_OWN : APPENDIX_NOT_OWN;
|
||||||
}, [isOwn, withAppendix]);
|
}, [isOwn, withAppendix]);
|
||||||
|
|
||||||
const handleGroupDocumentMessagesSelect = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
toggleMessageSelection({
|
|
||||||
messageId,
|
|
||||||
groupedId: message.groupedId,
|
|
||||||
});
|
|
||||||
}, [messageId, message.groupedId, toggleMessageSelection]);
|
|
||||||
|
|
||||||
const handleMessageSelect = useCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
||||||
if (isLocal) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = isAlbum && album && album.messages
|
|
||||||
? {
|
|
||||||
messageId,
|
|
||||||
childMessageIds: album.messages.map(({ id }) => id),
|
|
||||||
withShift: e && e.shiftKey,
|
|
||||||
}
|
|
||||||
: { messageId, withShift: e && e.shiftKey };
|
|
||||||
toggleMessageSelection(params);
|
|
||||||
}, [isLocal, isAlbum, album, messageId, toggleMessageSelection]);
|
|
||||||
|
|
||||||
const handleContainerDoubleClick = useCallback(() => {
|
|
||||||
setReplyingToId({ messageId });
|
|
||||||
}, [setReplyingToId, messageId]);
|
|
||||||
|
|
||||||
const handleContentDoubleClick = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
||||||
preventMessageInputBlur(e);
|
|
||||||
|
|
||||||
if (!isLocal) {
|
|
||||||
handleBeforeContextMenu(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAvatarClick = useCallback(() => {
|
|
||||||
if (!avatarPeer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isChatPrivate(avatarPeer.id)) {
|
|
||||||
openUserInfo({ id: avatarPeer.id });
|
|
||||||
} else {
|
|
||||||
openChat({ id: avatarPeer.id });
|
|
||||||
}
|
|
||||||
}, [avatarPeer, openUserInfo, openChat]);
|
|
||||||
|
|
||||||
const handleSenderClick = useCallback(() => {
|
|
||||||
if (!senderPeer) {
|
|
||||||
showNotification({ message: lang('HidAccount') });
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isChatPrivate(senderPeer.id)) {
|
|
||||||
openUserInfo({ id: senderPeer.id });
|
|
||||||
} else {
|
|
||||||
openChat({ id: senderPeer.id });
|
|
||||||
}
|
|
||||||
}, [senderPeer, showNotification, lang, openUserInfo, openChat]);
|
|
||||||
|
|
||||||
const handleViaBotClick = useCallback(() => {
|
|
||||||
if (!botSender) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
openUserInfo({ id: botSender.id });
|
|
||||||
}, [botSender, openUserInfo]);
|
|
||||||
|
|
||||||
const handleReplyClick = useCallback((): void => {
|
|
||||||
focusMessage({
|
|
||||||
chatId, threadId, messageId: message.replyToMessageId, replyMessageId: messageId,
|
|
||||||
});
|
|
||||||
}, [focusMessage, chatId, threadId, message.replyToMessageId, messageId]);
|
|
||||||
|
|
||||||
const handleMediaClick = useCallback((): void => {
|
|
||||||
openMediaViewer({
|
|
||||||
chatId, threadId, messageId, origin: isScheduled ? MediaViewerOrigin.ScheduledInline : MediaViewerOrigin.Inline,
|
|
||||||
});
|
|
||||||
}, [chatId, threadId, messageId, openMediaViewer, isScheduled]);
|
|
||||||
|
|
||||||
const handleAudioPlay = useCallback((): void => {
|
|
||||||
openAudioPlayer({ chatId, messageId });
|
|
||||||
}, [chatId, messageId, openAudioPlayer]);
|
|
||||||
|
|
||||||
const handleAlbumMediaClick = useCallback((albumMessageId: number): void => {
|
|
||||||
openMediaViewer({
|
|
||||||
chatId,
|
|
||||||
threadId,
|
|
||||||
messageId: albumMessageId,
|
|
||||||
origin: isScheduled ? MediaViewerOrigin.ScheduledAlbum : MediaViewerOrigin.Album,
|
|
||||||
});
|
|
||||||
}, [chatId, threadId, openMediaViewer, isScheduled]);
|
|
||||||
|
|
||||||
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
||||||
const target = e.target as HTMLDivElement;
|
|
||||||
if (!target.classList.contains('text-content') && !target.classList.contains('Message')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_ANDROID) {
|
|
||||||
if (windowSize.getIsKeyboardVisible()) {
|
|
||||||
setTimeout(() => {
|
|
||||||
onContextMenu(e);
|
|
||||||
}, ANDROID_KEYBOARD_HIDE_DELAY_MS);
|
|
||||||
} else {
|
|
||||||
onContextMenu(e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onContextMenu(e);
|
|
||||||
}
|
|
||||||
}, [onContextMenu]);
|
|
||||||
|
|
||||||
const handleContextMenu = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
||||||
if (IS_ANDROID) {
|
|
||||||
handleMessageSelect(e);
|
|
||||||
} else {
|
|
||||||
onContextMenu(e);
|
|
||||||
}
|
|
||||||
}, [onContextMenu, handleMessageSelect]);
|
|
||||||
|
|
||||||
const handleReadMedia = useCallback((): void => {
|
|
||||||
markMessagesRead({ messageIds: [messageId] });
|
|
||||||
}, [messageId, markMessagesRead]);
|
|
||||||
|
|
||||||
const handleCancelUpload = useCallback(() => {
|
|
||||||
cancelSendingMessage({ chatId, messageId });
|
|
||||||
}, [cancelSendingMessage, chatId, messageId]);
|
|
||||||
|
|
||||||
const handleVoteSend = useCallback((options: string[]) => {
|
|
||||||
sendPollVote({ chatId, messageId, options });
|
|
||||||
}, [chatId, messageId, sendPollVote]);
|
|
||||||
|
|
||||||
const handleGroupForward = useCallback(() => {
|
|
||||||
openForwardMenu({ fromChatId: chatId, groupedId: message.groupedId });
|
|
||||||
}, [openForwardMenu, chatId, message.groupedId]);
|
|
||||||
|
|
||||||
const handleForward = useCallback(() => {
|
|
||||||
if (album && album.messages) {
|
|
||||||
const messageIds = album.messages.map(({ id }) => id);
|
|
||||||
openForwardMenu({ fromChatId: chatId, messageIds });
|
|
||||||
} else {
|
|
||||||
openForwardMenu({ fromChatId: chatId, messageIds: [messageId] });
|
|
||||||
}
|
|
||||||
}, [album, openForwardMenu, chatId, messageId]);
|
|
||||||
|
|
||||||
const handleFocus = useCallback(() => {
|
|
||||||
focusMessage({
|
|
||||||
chatId, threadId: MAIN_THREAD_ID, messageId,
|
|
||||||
});
|
|
||||||
}, [focusMessage, chatId, messageId]);
|
|
||||||
|
|
||||||
const handleFocusForwarded = useCallback(() => {
|
|
||||||
if (isInDocumentGroup) {
|
|
||||||
focusMessage({
|
|
||||||
chatId: forwardInfo!.fromChatId, groupedId: message.groupedId, groupedChatId: chatId,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
focusMessage({
|
|
||||||
chatId: forwardInfo!.fromChatId, messageId: forwardInfo!.fromMessageId,
|
|
||||||
});
|
|
||||||
}, [focusMessage, forwardInfo, message, chatId, isInDocumentGroup]);
|
|
||||||
|
|
||||||
let style = '';
|
let style = '';
|
||||||
let calculatedWidth;
|
let calculatedWidth;
|
||||||
let noMediaCorners = false;
|
let noMediaCorners = false;
|
||||||
@ -704,7 +577,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
message={message}
|
message={message}
|
||||||
outgoingStatus={outgoingStatus}
|
outgoingStatus={outgoingStatus}
|
||||||
signature={signature}
|
signature={signature}
|
||||||
onClick={handleMessageSelect}
|
onClick={handleMetaClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
@ -788,10 +661,10 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
// @ts-ignore teact feature
|
// @ts-ignore teact feature
|
||||||
style={metaSafeAuthorWidth ? `--meta-safe-author-width: ${metaSafeAuthorWidth}px` : undefined}
|
style={metaSafeAuthorWidth ? `--meta-safe-author-width: ${metaSafeAuthorWidth}px` : undefined}
|
||||||
data-message-id={messageId}
|
data-message-id={messageId}
|
||||||
onClick={isInSelectMode ? handleMessageSelect : IS_ANDROID ? handleClick : undefined}
|
onMouseDown={handleMouseDown}
|
||||||
onDoubleClick={!isInSelectMode ? handleContainerDoubleClick : undefined}
|
onClick={handleClick}
|
||||||
onMouseDown={!isInSelectMode ? handleMouseDown : undefined}
|
onContextMenu={handleContextMenu}
|
||||||
onContextMenu={!isInSelectMode && !isLocal ? handleContextMenu : undefined}
|
onDoubleClick={handleDoubleClick}
|
||||||
onMouseEnter={isInDocumentGroup && !isLastInDocumentGroup ? handleDocumentGroupMouseEnter : undefined}
|
onMouseEnter={isInDocumentGroup && !isLastInDocumentGroup ? handleDocumentGroupMouseEnter : undefined}
|
||||||
onMouseLeave={isInDocumentGroup && !isLastInDocumentGroup ? handleDocumentGroupMouseLeave : undefined}
|
onMouseLeave={isInDocumentGroup && !isLastInDocumentGroup ? handleDocumentGroupMouseLeave : undefined}
|
||||||
>
|
>
|
||||||
@ -810,7 +683,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
{!isLocal && isLastInDocumentGroup && (
|
{!isLocal && isLastInDocumentGroup && (
|
||||||
<div
|
<div
|
||||||
className={buildClassName('message-select-control group-select', isGroupSelected && 'is-selected')}
|
className={buildClassName('message-select-control group-select', isGroupSelected && 'is-selected')}
|
||||||
onClick={handleGroupDocumentMessagesSelect}
|
onClick={handleDocumentGroupSelectAll}
|
||||||
>
|
>
|
||||||
{isGroupSelected && (
|
{isGroupSelected && (
|
||||||
<i className="icon-select" />
|
<i className="icon-select" />
|
||||||
@ -820,7 +693,6 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
{withAvatar && renderAvatar()}
|
{withAvatar && renderAvatar()}
|
||||||
<div
|
<div
|
||||||
className={buildClassName('message-content-wrapper', contentClassName.includes('text') && 'can-select-text')}
|
className={buildClassName('message-content-wrapper', contentClassName.includes('text') && 'can-select-text')}
|
||||||
onClick={isInSelectMode && isInDocumentGroup ? handleMessageSelect : undefined}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={contentClassName}
|
className={contentClassName}
|
||||||
@ -837,7 +709,7 @@ const Message: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
message={message}
|
message={message}
|
||||||
outgoingStatus={outgoingStatus}
|
outgoingStatus={outgoingStatus}
|
||||||
signature={signature}
|
signature={signature}
|
||||||
onClick={handleMessageSelect}
|
onClick={handleMetaClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canShowActionButton && canForward ? (
|
{canShowActionButton && canForward ? (
|
||||||
@ -999,19 +871,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
'focusMessage',
|
|
||||||
'openMediaViewer',
|
|
||||||
'openAudioPlayer',
|
|
||||||
'cancelSendingMessage',
|
|
||||||
'openUserInfo',
|
|
||||||
'openChat',
|
|
||||||
'markMessagesRead',
|
|
||||||
'sendPollVote',
|
|
||||||
'toggleMessageSelection',
|
'toggleMessageSelection',
|
||||||
'setReplyingToId',
|
|
||||||
'openForwardMenu',
|
|
||||||
'clickInlineButton',
|
'clickInlineButton',
|
||||||
'disableContextMenuHint',
|
'disableContextMenuHint',
|
||||||
'showNotification',
|
|
||||||
]),
|
]),
|
||||||
)(Message));
|
)(Message));
|
||||||
|
|||||||
@ -15,7 +15,7 @@ type OwnProps = {
|
|||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
outgoingStatus?: ApiMessageOutgoingStatus;
|
outgoingStatus?: ApiMessageOutgoingStatus;
|
||||||
signature?: string;
|
signature?: string;
|
||||||
onClick: () => void;
|
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageMeta: FC<OwnProps> = ({
|
const MessageMeta: FC<OwnProps> = ({
|
||||||
|
|||||||
159
src/components/middle/message/hooks/useInnerHandlers.ts
Normal file
159
src/components/middle/message/hooks/useInnerHandlers.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import React, { useCallback } from '../../../../lib/teact/teact';
|
||||||
|
import { getDispatch } from '../../../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { isChatPrivate } from '../../../../modules/helpers';
|
||||||
|
import { IAlbum, MediaViewerOrigin } from '../../../../types';
|
||||||
|
import {
|
||||||
|
ApiChat, ApiMessage, ApiUser, MAIN_THREAD_ID,
|
||||||
|
} from '../../../../api/types';
|
||||||
|
import { LangFn } from '../../../../hooks/useLang';
|
||||||
|
|
||||||
|
export default function useInnerHandlers(
|
||||||
|
lang: LangFn,
|
||||||
|
selectMessage: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => void,
|
||||||
|
message: ApiMessage,
|
||||||
|
chatId: number,
|
||||||
|
threadId: number,
|
||||||
|
isInDocumentGroup: boolean,
|
||||||
|
isScheduled?: boolean,
|
||||||
|
album?: IAlbum,
|
||||||
|
avatarPeer?: ApiUser | ApiChat,
|
||||||
|
senderPeer?: ApiUser | ApiChat,
|
||||||
|
botSender?: ApiUser,
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
openUserInfo, openChat, showNotification, focusMessage, openMediaViewer, openAudioPlayer,
|
||||||
|
markMessagesRead, cancelSendingMessage, sendPollVote, openForwardMenu,
|
||||||
|
} = getDispatch();
|
||||||
|
|
||||||
|
const {
|
||||||
|
id: messageId, forwardInfo, replyToMessageId, groupedId,
|
||||||
|
} = message;
|
||||||
|
|
||||||
|
const handleAvatarClick = useCallback(() => {
|
||||||
|
if (!avatarPeer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChatPrivate(avatarPeer.id)) {
|
||||||
|
openUserInfo({ id: avatarPeer.id });
|
||||||
|
} else {
|
||||||
|
openChat({ id: avatarPeer.id });
|
||||||
|
}
|
||||||
|
}, [avatarPeer, openUserInfo, openChat]);
|
||||||
|
|
||||||
|
const handleSenderClick = useCallback(() => {
|
||||||
|
if (!senderPeer) {
|
||||||
|
showNotification({ message: lang('HidAccount') });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChatPrivate(senderPeer.id)) {
|
||||||
|
openUserInfo({ id: senderPeer.id });
|
||||||
|
} else {
|
||||||
|
openChat({ id: senderPeer.id });
|
||||||
|
}
|
||||||
|
}, [senderPeer, showNotification, lang, openUserInfo, openChat]);
|
||||||
|
|
||||||
|
const handleViaBotClick = useCallback(() => {
|
||||||
|
if (!botSender) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openUserInfo({ id: botSender.id });
|
||||||
|
}, [botSender, openUserInfo]);
|
||||||
|
|
||||||
|
const handleReplyClick = useCallback((): void => {
|
||||||
|
focusMessage({
|
||||||
|
chatId, threadId, messageId: replyToMessageId, replyMessageId: messageId,
|
||||||
|
});
|
||||||
|
}, [focusMessage, chatId, threadId, replyToMessageId, messageId]);
|
||||||
|
|
||||||
|
const handleMediaClick = useCallback((): void => {
|
||||||
|
openMediaViewer({
|
||||||
|
chatId, threadId, messageId, origin: isScheduled ? MediaViewerOrigin.ScheduledInline : MediaViewerOrigin.Inline,
|
||||||
|
});
|
||||||
|
}, [chatId, threadId, messageId, openMediaViewer, isScheduled]);
|
||||||
|
|
||||||
|
const handleAudioPlay = useCallback((): void => {
|
||||||
|
openAudioPlayer({ chatId, messageId });
|
||||||
|
}, [chatId, messageId, openAudioPlayer]);
|
||||||
|
|
||||||
|
const handleAlbumMediaClick = useCallback((albumMessageId: number): void => {
|
||||||
|
openMediaViewer({
|
||||||
|
chatId,
|
||||||
|
threadId,
|
||||||
|
messageId: albumMessageId,
|
||||||
|
origin: isScheduled ? MediaViewerOrigin.ScheduledAlbum : MediaViewerOrigin.Album,
|
||||||
|
});
|
||||||
|
}, [chatId, threadId, openMediaViewer, isScheduled]);
|
||||||
|
|
||||||
|
const handleReadMedia = useCallback((): void => {
|
||||||
|
markMessagesRead({ messageIds: [messageId] });
|
||||||
|
}, [messageId, markMessagesRead]);
|
||||||
|
|
||||||
|
const handleCancelUpload = useCallback(() => {
|
||||||
|
cancelSendingMessage({ chatId, messageId });
|
||||||
|
}, [cancelSendingMessage, chatId, messageId]);
|
||||||
|
|
||||||
|
const handleVoteSend = useCallback((options: string[]) => {
|
||||||
|
sendPollVote({ chatId, messageId, options });
|
||||||
|
}, [chatId, messageId, sendPollVote]);
|
||||||
|
|
||||||
|
const handleGroupForward = useCallback(() => {
|
||||||
|
openForwardMenu({ fromChatId: chatId, groupedId });
|
||||||
|
}, [openForwardMenu, chatId, groupedId]);
|
||||||
|
|
||||||
|
const handleForward = useCallback(() => {
|
||||||
|
if (album && album.messages) {
|
||||||
|
const messageIds = album.messages.map(({ id }) => id);
|
||||||
|
openForwardMenu({ fromChatId: chatId, messageIds });
|
||||||
|
} else {
|
||||||
|
openForwardMenu({ fromChatId: chatId, messageIds: [messageId] });
|
||||||
|
}
|
||||||
|
}, [album, openForwardMenu, chatId, messageId]);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(() => {
|
||||||
|
focusMessage({
|
||||||
|
chatId, threadId: MAIN_THREAD_ID, messageId,
|
||||||
|
});
|
||||||
|
}, [focusMessage, chatId, messageId]);
|
||||||
|
|
||||||
|
const handleFocusForwarded = useCallback(() => {
|
||||||
|
if (isInDocumentGroup) {
|
||||||
|
focusMessage({
|
||||||
|
chatId: forwardInfo!.fromChatId, groupedId, groupedChatId: chatId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
focusMessage({
|
||||||
|
chatId: forwardInfo!.fromChatId, messageId: forwardInfo!.fromMessageId,
|
||||||
|
});
|
||||||
|
}, [isInDocumentGroup, focusMessage, forwardInfo, groupedId, chatId]);
|
||||||
|
|
||||||
|
const selectWithGroupedId = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
selectMessage(e, groupedId);
|
||||||
|
}, [selectMessage, groupedId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleAvatarClick,
|
||||||
|
handleSenderClick,
|
||||||
|
handleViaBotClick,
|
||||||
|
handleReplyClick,
|
||||||
|
handleMediaClick,
|
||||||
|
handleAudioPlay,
|
||||||
|
handleAlbumMediaClick,
|
||||||
|
handleMetaClick: selectWithGroupedId,
|
||||||
|
handleReadMedia,
|
||||||
|
handleCancelUpload,
|
||||||
|
handleVoteSend,
|
||||||
|
handleGroupForward,
|
||||||
|
handleForward,
|
||||||
|
handleFocus,
|
||||||
|
handleFocusForwarded,
|
||||||
|
handleDocumentGroupSelectAll: selectWithGroupedId,
|
||||||
|
};
|
||||||
|
}
|
||||||
112
src/components/middle/message/hooks/useOuterHandlers.ts
Normal file
112
src/components/middle/message/hooks/useOuterHandlers.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { RefObject } from 'react';
|
||||||
|
import React, { useEffect } from '../../../../lib/teact/teact';
|
||||||
|
import { getDispatch } from '../../../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { IS_ANDROID, IS_TOUCH_ENV } from '../../../../util/environment';
|
||||||
|
import windowSize from '../../../../util/windowSize';
|
||||||
|
import { captureEvents, SwipeDirection } from '../../../../util/captureEvents';
|
||||||
|
import useFlag from '../../../../hooks/useFlag';
|
||||||
|
import { preventMessageInputBlur } from '../../helpers/preventMessageInputBlur';
|
||||||
|
|
||||||
|
const ANDROID_KEYBOARD_HIDE_DELAY_MS = 350;
|
||||||
|
const SWIPE_ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
|
export default function useOuterHandlers(
|
||||||
|
selectMessage: (e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => void,
|
||||||
|
containerRef: RefObject<HTMLDivElement>,
|
||||||
|
messageId: number,
|
||||||
|
isLocal: boolean,
|
||||||
|
isAlbum: boolean,
|
||||||
|
isInSelectMode: boolean,
|
||||||
|
onContextMenu: (e: React.MouseEvent) => void,
|
||||||
|
handleBeforeContextMenu: (e: React.MouseEvent) => void,
|
||||||
|
) {
|
||||||
|
const { setReplyingToId } = getDispatch();
|
||||||
|
|
||||||
|
const [isSwiped, markSwiped, unmarkSwiped] = useFlag();
|
||||||
|
|
||||||
|
function handleMouseDown(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
preventMessageInputBlur(e);
|
||||||
|
|
||||||
|
if (!isLocal) {
|
||||||
|
handleBeforeContextMenu(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
if (isInSelectMode && !isLocal) {
|
||||||
|
selectMessage(e);
|
||||||
|
} else if (IS_ANDROID) {
|
||||||
|
const target = e.target as HTMLDivElement;
|
||||||
|
if (!target.classList.contains('text-content') && !target.classList.contains('Message')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowSize.getIsKeyboardVisible()) {
|
||||||
|
setTimeout(() => {
|
||||||
|
onContextMenu(e);
|
||||||
|
}, ANDROID_KEYBOARD_HIDE_DELAY_MS);
|
||||||
|
} else {
|
||||||
|
onContextMenu(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContextMenu(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
if (IS_ANDROID) {
|
||||||
|
selectMessage();
|
||||||
|
} else {
|
||||||
|
onContextMenu(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContainerDoubleClick() {
|
||||||
|
setReplyingToId({ messageId });
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPropagation(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!IS_TOUCH_ENV || isInSelectMode) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let startedAt: number | undefined;
|
||||||
|
return captureEvents(containerRef.current!, {
|
||||||
|
onSwipe: ((e, direction) => {
|
||||||
|
if (direction === SwipeDirection.Left) {
|
||||||
|
if (!startedAt) {
|
||||||
|
startedAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
markSwiped();
|
||||||
|
} else if (direction === SwipeDirection.Right) {
|
||||||
|
startedAt = undefined;
|
||||||
|
|
||||||
|
unmarkSwiped();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
onRelease: () => {
|
||||||
|
if (!startedAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setReplyingToId({ messageId });
|
||||||
|
|
||||||
|
setTimeout(unmarkSwiped, Math.max(0, SWIPE_ANIMATION_DURATION - (Date.now() - startedAt)));
|
||||||
|
startedAt = undefined;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [containerRef, isInSelectMode, messageId, setReplyingToId, markSwiped, unmarkSwiped]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleMouseDown: !isInSelectMode ? handleMouseDown : undefined,
|
||||||
|
handleClick,
|
||||||
|
handleContextMenu: !isInSelectMode && !isLocal ? handleContextMenu : undefined,
|
||||||
|
handleDoubleClick: !isInSelectMode ? handleContainerDoubleClick : undefined,
|
||||||
|
handleContentDoubleClick: !IS_TOUCH_ENV ? stopPropagation : undefined,
|
||||||
|
isSwiped,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -19,7 +19,6 @@ import {
|
|||||||
ApiSession,
|
ApiSession,
|
||||||
ApiNewPoll,
|
ApiNewPoll,
|
||||||
ApiInviteInfo,
|
ApiInviteInfo,
|
||||||
ApiFieldError,
|
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
import {
|
import {
|
||||||
FocusDirection,
|
FocusDirection,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user