Send various "typing"-like statuses (#1597)

This commit is contained in:
Alexander Zinchuk 2021-12-31 18:17:32 +01:00
parent ffa5697887
commit cb4749f7c6
16 changed files with 166 additions and 15 deletions

View File

@ -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') {

View File

@ -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 {

View File

@ -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,
}: { }: {

View File

@ -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

View File

@ -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}

View File

@ -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 (

View File

@ -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) => (

View File

@ -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) => (

View File

@ -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:

View File

@ -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) {

View File

@ -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';

View File

@ -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';

View File

@ -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
View 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;

View 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]);
};

View File

@ -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);