Send various "typing"-like statuses (#1597)
This commit is contained in:
parent
ffa5697887
commit
cb4749f7c6
@ -13,6 +13,7 @@ import {
|
|||||||
ApiMessageEntityTypes,
|
ApiMessageEntityTypes,
|
||||||
ApiNewPoll,
|
ApiNewPoll,
|
||||||
ApiReportReason,
|
ApiReportReason,
|
||||||
|
ApiSendMessageAction,
|
||||||
ApiSticker,
|
ApiSticker,
|
||||||
ApiVideo,
|
ApiVideo,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
@ -421,6 +422,20 @@ export function buildInputReportReason(reason: ApiReportReason) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildSendMessageAction(action: ApiSendMessageAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'cancel':
|
||||||
|
return new GramJs.SendMessageCancelAction();
|
||||||
|
case 'typing':
|
||||||
|
return new GramJs.SendMessageTypingAction();
|
||||||
|
case 'recordAudio':
|
||||||
|
return new GramJs.SendMessageRecordAudioAction();
|
||||||
|
case 'chooseSticker':
|
||||||
|
return new GramJs.SendMessageChooseStickerAction();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildMtpPeerId(id: string, type: 'user' | 'chat' | 'channel') {
|
export function buildMtpPeerId(id: string, type: 'user' | 'chat' | 'channel') {
|
||||||
// Workaround for old-fashioned IDs stored locally
|
// Workaround for old-fashioned IDs stored locally
|
||||||
if (typeof id === 'number') {
|
if (typeof id === 'number') {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export {
|
|||||||
markMessageListRead, markMessagesRead, requestThreadInfoUpdate, searchMessagesLocal, searchMessagesGlobal,
|
markMessageListRead, markMessagesRead, requestThreadInfoUpdate, searchMessagesLocal, searchMessagesGlobal,
|
||||||
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
||||||
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
||||||
reportMessages, fetchSeenBy,
|
reportMessages, sendMessageAction, fetchSeenBy,
|
||||||
} from './messages';
|
} from './messages';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
MESSAGE_DELETED,
|
MESSAGE_DELETED,
|
||||||
ApiGlobalMessageSearchType,
|
ApiGlobalMessageSearchType,
|
||||||
ApiReportReason,
|
ApiReportReason,
|
||||||
|
ApiSendMessageAction,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -44,6 +45,7 @@ import {
|
|||||||
isMessageWithMedia,
|
isMessageWithMedia,
|
||||||
isServiceMessageWithMedia,
|
isServiceMessageWithMedia,
|
||||||
buildInputReportReason,
|
buildInputReportReason,
|
||||||
|
buildSendMessageAction,
|
||||||
} from '../gramjsBuilders';
|
} from '../gramjsBuilders';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||||
@ -662,6 +664,28 @@ export async function reportMessages({
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendMessageAction({
|
||||||
|
peer, threadId, action,
|
||||||
|
}: {
|
||||||
|
peer: ApiChat | ApiUser; threadId?: number; action: ApiSendMessageAction;
|
||||||
|
}) {
|
||||||
|
const gramAction = buildSendMessageAction(action);
|
||||||
|
if (!gramAction) {
|
||||||
|
if (DEBUG) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('Unsupported message action', action);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await invokeRequest(new GramJs.messages.SetTyping({
|
||||||
|
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||||
|
topMsgId: threadId,
|
||||||
|
action: gramAction,
|
||||||
|
}));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export async function markMessageListRead({
|
export async function markMessageListRead({
|
||||||
chat, threadId, maxId, serverTimeOffset,
|
chat, threadId, maxId, serverTimeOffset,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@ -301,6 +301,10 @@ export type ApiGlobalMessageSearchType = 'text' | 'media' | 'documents' | 'links
|
|||||||
export type ApiReportReason = 'spam' | 'violence' | 'pornography' | 'childAbuse'
|
export type ApiReportReason = 'spam' | 'violence' | 'pornography' | 'childAbuse'
|
||||||
| 'copyright' | 'geoIrrelevant' | 'fake' | 'other';
|
| 'copyright' | 'geoIrrelevant' | 'fake' | 'other';
|
||||||
|
|
||||||
|
export type ApiSendMessageAction = {
|
||||||
|
type: 'cancel' | 'typing' | 'recordAudio' | 'chooseSticker';
|
||||||
|
};
|
||||||
|
|
||||||
export const MAIN_THREAD_ID = -1;
|
export const MAIN_THREAD_ID = -1;
|
||||||
|
|
||||||
// `Symbol` can not be transferred from worker
|
// `Symbol` can not be transferred from worker
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
import { InlineBotSettings } from '../../../types';
|
import { InlineBotSettings } from '../../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_ID, REPLIES_USER_ID, SCHEDULED_WHEN_ONLINE,
|
BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_ID, REPLIES_USER_ID, SCHEDULED_WHEN_ONLINE, SEND_MESSAGE_ACTION_INTERVAL,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment';
|
import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment';
|
||||||
import {
|
import {
|
||||||
@ -71,6 +71,8 @@ import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
|||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useInlineBotTooltip from './hooks/useInlineBotTooltip';
|
import useInlineBotTooltip from './hooks/useInlineBotTooltip';
|
||||||
import useBotCommandTooltip from './hooks/useBotCommandTooltip';
|
import useBotCommandTooltip from './hooks/useBotCommandTooltip';
|
||||||
|
import useSendMessageAction from '../../../hooks/useSendMessageAction';
|
||||||
|
import useInterval from '../../../hooks/useInterval';
|
||||||
|
|
||||||
import DeleteMessageModal from '../../common/DeleteMessageModal.async';
|
import DeleteMessageModal from '../../common/DeleteMessageModal.async';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
@ -225,6 +227,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
scheduledMessageArgs, setScheduledMessageArgs,
|
scheduledMessageArgs, setScheduledMessageArgs,
|
||||||
] = useState<GlobalState['messages']['contentToBeScheduled'] | undefined>();
|
] = useState<GlobalState['messages']['contentToBeScheduled'] | undefined>();
|
||||||
const { width: windowWidth } = windowSize.get();
|
const { width: windowWidth } = windowSize.get();
|
||||||
|
const sendMessageAction = useSendMessageAction(chatId, threadId);
|
||||||
|
|
||||||
// Cache for frequently updated state
|
// Cache for frequently updated state
|
||||||
const htmlRef = useRef<string>(html);
|
const htmlRef = useRef<string>(html);
|
||||||
@ -275,6 +278,16 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
startRecordTimeRef,
|
startRecordTimeRef,
|
||||||
} = useVoiceRecording();
|
} = useVoiceRecording();
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
sendMessageAction({ type: 'recordAudio' });
|
||||||
|
}, activeVoiceRecording && SEND_MESSAGE_ACTION_INTERVAL);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeVoiceRecording) {
|
||||||
|
sendMessageAction({ type: 'cancel' });
|
||||||
|
}
|
||||||
|
}, [activeVoiceRecording, sendMessageAction]);
|
||||||
|
|
||||||
const mainButtonState = editingMessage
|
const mainButtonState = editingMessage
|
||||||
? MainButtonState.Edit
|
? MainButtonState.Edit
|
||||||
: !IS_VOICE_RECORDING_SUPPORTED || activeVoiceRecording || (html && !attachments.length) || isForwarding
|
: !IS_VOICE_RECORDING_SUPPORTED || activeVoiceRecording || (html && !attachments.length) || isForwarding
|
||||||
@ -952,6 +965,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<StickerTooltip
|
<StickerTooltip
|
||||||
|
chatId={chatId}
|
||||||
|
threadId={threadId}
|
||||||
isOpen={isStickerTooltipOpen}
|
isOpen={isStickerTooltipOpen}
|
||||||
onStickerSelect={handleStickerSelect}
|
onStickerSelect={handleStickerSelect}
|
||||||
/>
|
/>
|
||||||
@ -984,6 +999,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SymbolMenu
|
<SymbolMenu
|
||||||
|
chatId={chatId}
|
||||||
|
threadId={threadId}
|
||||||
isOpen={isSymbolMenuOpen}
|
isOpen={isSymbolMenuOpen}
|
||||||
allowedAttachmentOptions={allowedAttachmentOptions}
|
allowedAttachmentOptions={allowedAttachmentOptions}
|
||||||
onLoad={onSymbolMenuLoadingComplete}
|
onLoad={onSymbolMenuLoadingComplete}
|
||||||
|
|||||||
@ -18,9 +18,10 @@ import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
|
|||||||
import useLayoutEffectWithPrevDeps from '../../../hooks/useLayoutEffectWithPrevDeps';
|
import useLayoutEffectWithPrevDeps from '../../../hooks/useLayoutEffectWithPrevDeps';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import { isHeavyAnimating } from '../../../hooks/useHeavyAnimationCheck';
|
import { isHeavyAnimating } from '../../../hooks/useHeavyAnimationCheck';
|
||||||
|
import useSendMessageAction from '../../../hooks/useSendMessageAction';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
import parseEmojiOnlyString from '../../common/helpers/parseEmojiOnlyString';
|
import parseEmojiOnlyString from '../../common/helpers/parseEmojiOnlyString';
|
||||||
import { isSelectionInsideInput } from './helpers/selection';
|
import { isSelectionInsideInput } from './helpers/selection';
|
||||||
import useLang from '../../../hooks/useLang';
|
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
import TextFormatter from './TextFormatter';
|
import TextFormatter from './TextFormatter';
|
||||||
@ -48,7 +49,6 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
chatId?: string;
|
|
||||||
replyingToId?: number;
|
replyingToId?: number;
|
||||||
noTabCapture?: boolean;
|
noTabCapture?: boolean;
|
||||||
messageSendKeyCombo?: ISettings['messageSendKeyCombo'];
|
messageSendKeyCombo?: ISettings['messageSendKeyCombo'];
|
||||||
@ -76,6 +76,7 @@ function clearSelection() {
|
|||||||
const MessageInput: FC<OwnProps & StateProps> = ({
|
const MessageInput: FC<OwnProps & StateProps> = ({
|
||||||
id,
|
id,
|
||||||
chatId,
|
chatId,
|
||||||
|
threadId,
|
||||||
isAttachmentModalInput,
|
isAttachmentModalInput,
|
||||||
editableInputId,
|
editableInputId,
|
||||||
html,
|
html,
|
||||||
@ -84,12 +85,12 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
canAutoFocus,
|
canAutoFocus,
|
||||||
shouldSuppressFocus,
|
shouldSuppressFocus,
|
||||||
shouldSuppressTextFormatter,
|
shouldSuppressTextFormatter,
|
||||||
onUpdate,
|
|
||||||
onSuppressedFocus,
|
|
||||||
onSend,
|
|
||||||
replyingToId,
|
replyingToId,
|
||||||
noTabCapture,
|
noTabCapture,
|
||||||
messageSendKeyCombo,
|
messageSendKeyCombo,
|
||||||
|
onUpdate,
|
||||||
|
onSuppressedFocus,
|
||||||
|
onSend,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
editLastMessage,
|
editLastMessage,
|
||||||
@ -107,6 +108,8 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
const [textFormatterAnchorPosition, setTextFormatterAnchorPosition] = useState<IAnchorPosition>();
|
const [textFormatterAnchorPosition, setTextFormatterAnchorPosition] = useState<IAnchorPosition>();
|
||||||
const [selectedRange, setSelectedRange] = useState<Range>();
|
const [selectedRange, setSelectedRange] = useState<Range>();
|
||||||
|
|
||||||
|
const sendMessageAction = useSendMessageAction(chatId, threadId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAttachmentModalInput) return;
|
if (!isAttachmentModalInput) return;
|
||||||
updateInputHeight(false);
|
updateInputHeight(false);
|
||||||
@ -280,6 +283,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
const { innerHTML, textContent } = e.currentTarget;
|
const { innerHTML, textContent } = e.currentTarget;
|
||||||
|
|
||||||
onUpdate(innerHTML === SAFARI_BR ? '' : innerHTML);
|
onUpdate(innerHTML === SAFARI_BR ? '' : innerHTML);
|
||||||
|
sendMessageAction({ type: 'typing' });
|
||||||
|
|
||||||
// Reset focus on the input to remove any active styling when input is cleared
|
// Reset focus on the input to remove any active styling when input is cleared
|
||||||
if (
|
if (
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
|||||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useSendMessageAction from '../../../hooks/useSendMessageAction';
|
||||||
|
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
@ -27,6 +28,8 @@ import StickerSetCoverAnimated from './StickerSetCoverAnimated';
|
|||||||
import './StickerPicker.scss';
|
import './StickerPicker.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
|
chatId: string;
|
||||||
|
threadId?: number;
|
||||||
className: string;
|
className: string;
|
||||||
loadAndPlay: boolean;
|
loadAndPlay: boolean;
|
||||||
canSendStickers: boolean;
|
canSendStickers: boolean;
|
||||||
@ -48,6 +51,8 @@ const STICKER_INTERSECTION_THROTTLE = 200;
|
|||||||
const stickerSetIntersections: boolean[] = [];
|
const stickerSetIntersections: boolean[] = [];
|
||||||
|
|
||||||
const StickerPicker: FC<OwnProps & StateProps> = ({
|
const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||||
|
chatId,
|
||||||
|
threadId,
|
||||||
className,
|
className,
|
||||||
loadAndPlay,
|
loadAndPlay,
|
||||||
canSendStickers,
|
canSendStickers,
|
||||||
@ -72,6 +77,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const headerRef = useRef<HTMLDivElement>(null);
|
const headerRef = useRef<HTMLDivElement>(null);
|
||||||
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
||||||
|
const sendMessageAction = useSendMessageAction(chatId, threadId);
|
||||||
|
|
||||||
const { observe: observeIntersection } = useIntersectionObserver({
|
const { observe: observeIntersection } = useIntersectionObserver({
|
||||||
rootRef: containerRef,
|
rootRef: containerRef,
|
||||||
@ -135,8 +141,9 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
loadStickerSets();
|
loadStickerSets();
|
||||||
loadRecentStickers();
|
loadRecentStickers();
|
||||||
loadFavoriteStickers();
|
loadFavoriteStickers();
|
||||||
|
sendMessageAction({ type: 'chooseSticker' });
|
||||||
}
|
}
|
||||||
}, [loadAndPlay, loadFavoriteStickers, loadRecentStickers, loadStickerSets]);
|
}, [loadAndPlay, loadFavoriteStickers, loadRecentStickers, loadStickerSets, sendMessageAction]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (addedSetIds?.length) {
|
if (addedSetIds?.length) {
|
||||||
@ -177,6 +184,10 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
unfaveSticker({ sticker });
|
unfaveSticker({ sticker });
|
||||||
}, [unfaveSticker]);
|
}, [unfaveSticker]);
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback(() => {
|
||||||
|
sendMessageAction({ type: 'chooseSticker' });
|
||||||
|
}, [sendMessageAction]);
|
||||||
|
|
||||||
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
||||||
|
|
||||||
function renderCover(stickerSet: StickerSetOrRecent, index: number) {
|
function renderCover(stickerSet: StickerSetOrRecent, index: number) {
|
||||||
@ -256,6 +267,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
className={buildClassName('StickerPicker-main no-selection', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
|
className={buildClassName('StickerPicker-main no-selection', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
|
||||||
>
|
>
|
||||||
{allSets.map((stickerSet, i) => (
|
{allSets.map((stickerSet, i) => (
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
|||||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import usePrevious from '../../../hooks/usePrevious';
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
|
import useSendMessageAction from '../../../hooks/useSendMessageAction';
|
||||||
|
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
import StickerButton from '../../common/StickerButton';
|
import StickerButton from '../../common/StickerButton';
|
||||||
@ -19,6 +20,8 @@ import StickerButton from '../../common/StickerButton';
|
|||||||
import './StickerTooltip.scss';
|
import './StickerTooltip.scss';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
|
chatId: string;
|
||||||
|
threadId?: number;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onStickerSelect: (sticker: ApiSticker) => void;
|
onStickerSelect: (sticker: ApiSticker) => void;
|
||||||
};
|
};
|
||||||
@ -30,6 +33,8 @@ type StateProps = {
|
|||||||
const INTERSECTION_THROTTLE = 200;
|
const INTERSECTION_THROTTLE = 200;
|
||||||
|
|
||||||
const StickerTooltip: FC<OwnProps & StateProps> = ({
|
const StickerTooltip: FC<OwnProps & StateProps> = ({
|
||||||
|
chatId,
|
||||||
|
threadId,
|
||||||
isOpen,
|
isOpen,
|
||||||
onStickerSelect,
|
onStickerSelect,
|
||||||
stickers,
|
stickers,
|
||||||
@ -41,6 +46,7 @@ const StickerTooltip: FC<OwnProps & StateProps> = ({
|
|||||||
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
|
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
|
||||||
const prevStickers = usePrevious(stickers, true);
|
const prevStickers = usePrevious(stickers, true);
|
||||||
const displayedStickers = stickers || prevStickers;
|
const displayedStickers = stickers || prevStickers;
|
||||||
|
const sendMessageAction = useSendMessageAction(chatId, threadId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
observe: observeIntersection,
|
observe: observeIntersection,
|
||||||
@ -52,6 +58,10 @@ const StickerTooltip: FC<OwnProps & StateProps> = ({
|
|||||||
document.body.classList.add('no-select');
|
document.body.classList.add('no-select');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = () => {
|
||||||
|
sendMessageAction({ type: 'chooseSticker' });
|
||||||
|
};
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
document.body.classList.remove('no-select');
|
document.body.classList.remove('no-select');
|
||||||
};
|
};
|
||||||
@ -68,6 +78,7 @@ const StickerTooltip: FC<OwnProps & StateProps> = ({
|
|||||||
className={className}
|
className={className}
|
||||||
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
|
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
|
||||||
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
|
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
>
|
>
|
||||||
{shouldRender && displayedStickers ? (
|
{shouldRender && displayedStickers ? (
|
||||||
displayedStickers.map((sticker) => (
|
displayedStickers.map((sticker) => (
|
||||||
|
|||||||
@ -27,6 +27,8 @@ import './SymbolMenu.scss';
|
|||||||
const ANIMATION_DURATION = 350;
|
const ANIMATION_DURATION = 350;
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
|
chatId: string;
|
||||||
|
threadId?: number;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
allowedAttachmentOptions: IAllowedAttachmentOptions;
|
allowedAttachmentOptions: IAllowedAttachmentOptions;
|
||||||
onLoad: () => void;
|
onLoad: () => void;
|
||||||
@ -46,10 +48,19 @@ type StateProps = {
|
|||||||
let isActivated = false;
|
let isActivated = false;
|
||||||
|
|
||||||
const SymbolMenu: FC<OwnProps & StateProps> = ({
|
const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||||
isOpen, allowedAttachmentOptions, isLeftColumnShown,
|
chatId,
|
||||||
onLoad, onClose,
|
threadId,
|
||||||
onEmojiSelect, onStickerSelect, onGifSelect,
|
isOpen,
|
||||||
onRemoveSymbol, onSearchOpen, addRecentEmoji,
|
allowedAttachmentOptions,
|
||||||
|
isLeftColumnShown,
|
||||||
|
onLoad,
|
||||||
|
onClose,
|
||||||
|
onEmojiSelect,
|
||||||
|
onStickerSelect,
|
||||||
|
onGifSelect,
|
||||||
|
onRemoveSymbol,
|
||||||
|
onSearchOpen,
|
||||||
|
addRecentEmoji,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState<number>(0);
|
const [activeTab, setActiveTab] = useState<number>(0);
|
||||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||||
@ -138,6 +149,8 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
loadAndPlay={canSendStickers ? isOpen && (isActive || isFrom) : false}
|
loadAndPlay={canSendStickers ? isOpen && (isActive || isFrom) : false}
|
||||||
canSendStickers={canSendStickers}
|
canSendStickers={canSendStickers}
|
||||||
onStickerSelect={handleStickerSelect}
|
onStickerSelect={handleStickerSelect}
|
||||||
|
chatId={chatId}
|
||||||
|
threadId={threadId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case SymbolMenuTabs.GIFs:
|
case SymbolMenuTabs.GIFs:
|
||||||
|
|||||||
@ -50,7 +50,6 @@ export default () => {
|
|||||||
if (recordButtonRef.current) {
|
if (recordButtonRef.current) {
|
||||||
recordButtonRef.current.style.boxShadow = 'none';
|
recordButtonRef.current.style.boxShadow = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return activeVoiceRecording!.pause();
|
return activeVoiceRecording!.pause();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, memo, useCallback, useMemo,
|
FC, memo, useMemo,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import { ApiMessage, ApiMessageOutgoingStatus } from '../../../api/types';
|
import { ApiMessage, ApiMessageOutgoingStatus } from '../../../api/types';
|
||||||
|
|||||||
@ -75,6 +75,7 @@ export const IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX = 17;
|
|||||||
export const MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX = 15;
|
export const MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX = 15;
|
||||||
|
|
||||||
export const DRAFT_DEBOUNCE = 10000; // 10s
|
export const DRAFT_DEBOUNCE = 10000; // 10s
|
||||||
|
export const SEND_MESSAGE_ACTION_INTERVAL = 3000; // 3s
|
||||||
|
|
||||||
export const EDITABLE_INPUT_ID = 'editable-message-text';
|
export const EDITABLE_INPUT_ID = 'editable-message-text';
|
||||||
export const EDITABLE_INPUT_MODAL_ID = 'editable-message-text-modal';
|
export const EDITABLE_INPUT_MODAL_ID = 'editable-message-text-modal';
|
||||||
|
|||||||
@ -502,7 +502,7 @@ export type ActionTypes = (
|
|||||||
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
||||||
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
||||||
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
||||||
'reportMessages' | 'focusNextReply' | 'openChatByInvite' | 'loadSeenBy' |
|
'reportMessages' | 'sendMessageAction' | 'focusNextReply' | 'openChatByInvite' | 'loadSeenBy' |
|
||||||
// downloads
|
// downloads
|
||||||
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
|
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
|
||||||
// scheduled messages
|
// scheduled messages
|
||||||
|
|||||||
22
src/hooks/useInterval.ts
Normal file
22
src/hooks/useInterval.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect, useLayoutEffect, useRef } from '../lib/teact/teact';
|
||||||
|
|
||||||
|
function useInterval(callback: NoneToVoidFunction, delay?: number, noFirst = false) {
|
||||||
|
const savedCallback = useRef(callback);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
savedCallback.current = callback;
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (delay === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = setInterval(() => savedCallback.current(), delay);
|
||||||
|
if (!noFirst) savedCallback.current();
|
||||||
|
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [delay, noFirst]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useInterval;
|
||||||
15
src/hooks/useSendMessageAction.ts
Normal file
15
src/hooks/useSendMessageAction.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useMemo } from '../lib/teact/teact';
|
||||||
|
import { getDispatch } from '../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { ApiSendMessageAction } from '../api/types';
|
||||||
|
|
||||||
|
import { SEND_MESSAGE_ACTION_INTERVAL } from '../config';
|
||||||
|
import { throttle } from '../util/schedulers';
|
||||||
|
|
||||||
|
export default (chatId: string, threadId?: number) => {
|
||||||
|
return useMemo(() => {
|
||||||
|
return throttle((action: ApiSendMessageAction) => {
|
||||||
|
getDispatch().sendMessageAction({ chatId, threadId, action });
|
||||||
|
}, SEND_MESSAGE_ACTION_INTERVAL);
|
||||||
|
}, [chatId, threadId]);
|
||||||
|
};
|
||||||
@ -459,6 +459,20 @@ addReducer('reportMessages', (global, actions, payload) => {
|
|||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addReducer('sendMessageAction', (global, actions, payload) => {
|
||||||
|
(async () => {
|
||||||
|
const { action, chatId, threadId } = payload!;
|
||||||
|
if (chatId === global.currentUserId) return; // Message actions are disabled in Saved Messages
|
||||||
|
|
||||||
|
const chat = selectChat(global, chatId)!;
|
||||||
|
if (!chat) return;
|
||||||
|
|
||||||
|
await callApi('sendMessageAction', {
|
||||||
|
peer: chat, threadId, action,
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
addReducer('markMessageListRead', (global, actions, payload) => {
|
addReducer('markMessageListRead', (global, actions, payload) => {
|
||||||
const { serverTimeOffset } = global;
|
const { serverTimeOffset } = global;
|
||||||
const currentMessageList = selectCurrentMessageList(global);
|
const currentMessageList = selectCurrentMessageList(global);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user