Refactor state & Custom editor (#1190)

* Fix eslint

* Enable ts strict mode

* install folds, jotai & immer

* Enable immer map/set

* change cross-signing alert anim to 30 iteration

* Add function to access matrix client

* Add new types

* Add disposable util

* Add room utils

* Add mDirect list atom

* Add invite list atom

* add room list atom

* add utils for jotai atoms

* Add room id to parents atom

* Add mute list atom

* Add room to unread atom

* Use hook to bind atoms with sdk

* Add settings atom

* Add settings hook

* Extract set settings hook

* Add Sidebar components

* WIP

* Add bind atoms hook

* Fix init muted room list atom

* add navigation atoms

* Add custom editor

* Fix hotkeys

* Update folds

* Add editor output function

* Add matrix client context

* Add tooltip to editor toolbar items

* WIP - Add editor to room input

* Refocus editor on toolbar item click

* Add Mentions - WIP

* update folds

* update mention focus outline

* rename emoji element type

* Add auto complete menu

* add autocomplete query functions

* add index file for editor

* fix bug in getPrevWord function

* Show room mention autocomplete

* Add async search function

* add use async search hook

* use async search in room mention autocomplete

* remove folds prefer font for now

* allow number array in async search

* reset search with empty query

* Autocomplete unknown room mention

* Autocomplete first room mention on tab

* fix roomAliasFromQueryText

* change mention color to primary

* add isAlive hook

* add getMxIdLocalPart to mx utils

* fix getRoomAvatarUrl size

* fix types

* add room members hook

* fix bug in room mention

* add user mention autocomplete

* Fix async search giving prev result after no match

* update folds

* add twemoji font

* add use state provider hook

* add prevent scroll with arrow key util

* add ts to custom-emoji and emoji files

* add types

* add hook for emoji group labels

* add hook for emoji group icons

* add emoji board with basic emoji

* add emojiboard in room input

* select multiple emoji with shift press

* display custom emoji in emojiboard

* Add emoji preview

* focus element on hover

* update folds

* position emojiboard properly

* convert recent-emoji.js to ts

* add use recent emoji hook

* add io.element.recent_emoji to account data evt

* Render recent emoji in emoji board

* show custom emoji from parent spaces

* show room emoji

* improve emoji sidebar

* update folds

* fix pack avatar and name fallback in emoji board

* add stickers to emoji board

* fix bug in emoji preview

* Add sticker icon in room input

* add debounce hook

* add search in emoji board

* Optimize emoji board

* fix emoji board sidebar divider

* sync emojiboard sidebar with scroll & update ui

* Add use throttle hook

* support custom emoji in editor

* remove duplicate emoji selection function

* fix emoji and mention spacing

* add emoticon autocomplete in editor

* fix string

* makes emoji size relative to font size in editor

* add option to render link element

* add spoiler in editor

* fix sticker in emoji board search using wrong type

* render custom placeholder

* update hotkey for block quote and block code

* add terminate search function in async search

* add getImageInfo to matrix utils

* send stickers

* add resize observer hook

* move emoji board component hooks in hooks dir

* prevent editor expand hides room timeline

* send typing notifications

* improve emoji style and performance

* fix imports

* add on paste param to editor

* add selectFile utils

* add file picker hook

* add file paste handler hook

* add file drop handler

* update folds

* Add file upload card

* add bytes to size util

* add blurHash util

* add await to js lib

* add browser-encrypt-attachment types

* add list atom

* convert mimetype file to ts

* add matrix types

* add matrix file util

* add file related dom utils

* add common utils

* add upload atom

* add room input draft atom

* add upload card renderer component

* add upload board component

* add support for file upload in editor

* send files with message / enter

* fix circular deps

* store editor toolbar state in local store

* move msg content util to separate file

* store msg draft on room switch

* fix following member not updating on msg sent

* add theme for folds component

* fix system default theme

* Add reply support in editor

* prevent initMatrix to init multiple time

* add state event hooks

* add async callback hook

* Show tombstone info for tombstone room

* fix room tombstone component border

* add power level hook

* Add room input placeholder component

* Show input placeholder for muted member
This commit is contained in:
Ajay Bura 2023-06-12 21:15:23 +10:00 committed by GitHub
parent 2055d7a07f
commit 0b06bed1db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
128 changed files with 8799 additions and 409 deletions

15
src/app/hooks/useAlive.ts Normal file
View file

@ -0,0 +1,15 @@
import { useCallback, useEffect, useRef } from 'react';
export const useAlive = (): (() => boolean) => {
const aliveRef = useRef<boolean>(true);
useEffect(() => {
aliveRef.current = true;
return () => {
aliveRef.current = false;
};
}, []);
const alive = useCallback(() => aliveRef.current, []);
return alive;
};

View file

@ -0,0 +1,70 @@
import { useCallback, useState } from 'react';
import { useAlive } from './useAlive';
export enum AsyncStatus {
Idle = 'idle',
Loading = 'loading',
Success = 'success',
Error = 'error',
}
export type AsyncIdle = {
status: AsyncStatus.Idle;
};
export type AsyncLoading = {
status: AsyncStatus.Loading;
};
export type AsyncSuccess<T> = {
status: AsyncStatus.Success;
data: T;
};
export type AsyncError = {
status: AsyncStatus.Error;
error: unknown;
};
export type AsyncState<T> = AsyncIdle | AsyncLoading | AsyncSuccess<T> | AsyncError;
export type AsyncCallback<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData>;
export const useAsyncCallback = <TArgs extends unknown[], TData>(
asyncCallback: AsyncCallback<TArgs, TData>
): [AsyncState<TData>, AsyncCallback<TArgs, TData>] => {
const [state, setState] = useState<AsyncState<TData>>({
status: AsyncStatus.Idle,
});
const alive = useAlive();
const callback: AsyncCallback<TArgs, TData> = useCallback(
async (...args) => {
setState({
status: AsyncStatus.Loading,
});
try {
const data = await asyncCallback(...args);
if (alive()) {
setState({
status: AsyncStatus.Success,
data,
});
}
return data;
} catch (e) {
if (alive()) {
setState({
status: AsyncStatus.Error,
error: e,
});
}
throw e;
}
},
[asyncCallback, alive]
);
return [state, callback];
};

View file

@ -0,0 +1,81 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
MatchHandler,
AsyncSearch,
AsyncSearchHandler,
AsyncSearchOption,
MatchQueryOption,
NormalizeOption,
normalize,
matchQuery,
ResultHandler,
} from '../utils/AsyncSearch';
export type UseAsyncSearchOptions = AsyncSearchOption & {
matchOptions?: MatchQueryOption;
normalizeOptions?: NormalizeOption;
};
export type SearchItemStrGetter<TSearchItem extends object | string | number> = (
searchItem: TSearchItem
) => string | string[];
export type UseAsyncSearchResult<TSearchItem extends object | string | number> = {
query: string;
items: TSearchItem[];
};
export const useAsyncSearch = <TSearchItem extends object | string | number>(
list: TSearchItem[],
getItemStr: SearchItemStrGetter<TSearchItem>,
options?: UseAsyncSearchOptions
): [UseAsyncSearchResult<TSearchItem> | undefined, AsyncSearchHandler] => {
const [result, setResult] = useState<UseAsyncSearchResult<TSearchItem>>();
const [searchCallback, terminateSearch] = useMemo(() => {
setResult(undefined);
const handleMatch: MatchHandler<TSearchItem> = (item, query) => {
const itemStr = getItemStr(item);
if (Array.isArray(itemStr))
return !!itemStr.find((i) =>
matchQuery(normalize(i, options?.normalizeOptions), query, options?.matchOptions)
);
return matchQuery(
normalize(itemStr, options?.normalizeOptions),
query,
options?.matchOptions
);
};
const handleResult: ResultHandler<TSearchItem> = (results, query) =>
setResult({
query,
items: results,
});
return AsyncSearch(list, handleMatch, handleResult, options);
}, [list, options, getItemStr]);
const searchHandler: AsyncSearchHandler = useCallback(
(query) => {
const normalizedQuery = normalize(query, options?.normalizeOptions);
if (!normalizedQuery) {
setResult(undefined);
return;
}
searchCallback(normalizedQuery);
},
[searchCallback, options?.normalizeOptions]
);
useEffect(
() => () => {
// terminate any ongoing search request on unmount.
terminateSearch();
},
[terminateSearch]
);
return [result, searchHandler];
};

View file

@ -0,0 +1,34 @@
import { useCallback, useRef } from 'react';
export interface DebounceOptions {
wait?: number;
immediate?: boolean;
}
export type DebounceCallback<T extends unknown[]> = (...args: T) => void;
export function useDebounce<T extends unknown[]>(
callback: DebounceCallback<T>,
options?: DebounceOptions
): DebounceCallback<T> {
const timeoutIdRef = useRef<number>();
const { wait, immediate } = options ?? {};
const debounceCallback = useCallback(
(...cbArgs: T) => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = undefined;
} else if (immediate) {
callback(...cbArgs);
}
timeoutIdRef.current = window.setTimeout(() => {
callback(...cbArgs);
timeoutIdRef.current = undefined;
}, wait);
},
[callback, wait, immediate]
);
return debounceCallback;
}

View file

@ -0,0 +1,66 @@
import { useCallback, DragEventHandler, RefObject, useState, useEffect, useRef } from 'react';
import { getDataTransferFiles } from '../utils/dom';
export const useFileDropHandler = (onDrop: (file: File[]) => void): DragEventHandler =>
useCallback(
(evt) => {
const files = getDataTransferFiles(evt.dataTransfer);
if (files) onDrop(files);
},
[onDrop]
);
export const useFileDropZone = (
zoneRef: RefObject<HTMLElement>,
onDrop: (file: File[]) => void
): boolean => {
const dragStateRef = useRef<'start' | 'leave' | 'over'>();
const [active, setActive] = useState(false);
useEffect(() => {
const target = zoneRef.current;
const handleDrop = (evt: DragEvent) => {
evt.preventDefault();
dragStateRef.current = undefined;
setActive(false);
if (!evt.dataTransfer) return;
const files = getDataTransferFiles(evt.dataTransfer);
if (files) onDrop(files);
};
target?.addEventListener('drop', handleDrop);
return () => {
target?.removeEventListener('drop', handleDrop);
};
}, [zoneRef, onDrop]);
useEffect(() => {
const target = zoneRef.current;
const handleDragEnter = (evt: DragEvent) => {
if (evt.dataTransfer?.types.includes('Files')) {
dragStateRef.current = 'start';
setActive(true);
}
};
const handleDragLeave = () => {
if (dragStateRef.current !== 'over') return;
dragStateRef.current = 'leave';
setActive(false);
};
const handleDragOver = (evt: DragEvent) => {
evt.preventDefault();
dragStateRef.current = 'over';
};
target?.addEventListener('dragenter', handleDragEnter);
target?.addEventListener('dragleave', handleDragLeave);
target?.addEventListener('dragover', handleDragOver);
return () => {
target?.removeEventListener('dragenter', handleDragEnter);
target?.removeEventListener('dragleave', handleDragLeave);
target?.removeEventListener('dragover', handleDragOver);
};
}, [zoneRef]);
return active;
};

View file

@ -0,0 +1,11 @@
import { useCallback, ClipboardEventHandler } from 'react';
import { getDataTransferFiles } from '../utils/dom';
export const useFilePasteHandler = (onPaste: (file: File[]) => void): ClipboardEventHandler =>
useCallback(
(evt) => {
const files = getDataTransferFiles(evt.clipboardData);
if (files) onPaste(files);
},
[onPaste]
);

View file

@ -0,0 +1,15 @@
import { useCallback } from 'react';
import { selectFile } from '../utils/dom';
export const useFilePicker = <M extends boolean | undefined = undefined>(
onSelect: (file: M extends true ? File[] : File) => void,
multiple?: M
) =>
useCallback(
async (accept: string) => {
const file = await selectFile(accept, multiple);
if (!file) return;
onSelect(file);
},
[multiple, onSelect]
);

View file

@ -0,0 +1,9 @@
import { useReducer } from 'react';
const reducer = (prevCount: number): number => prevCount + 1;
export const useForceUpdate = (): [number, () => void] => {
const [state, dispatch] = useReducer<typeof reducer>(reducer, 0);
return [state, dispatch];
};

View file

@ -0,0 +1,48 @@
import { ClientEvent, MatrixClient, MatrixEvent, Room, RoomStateEvent } from 'matrix-js-sdk';
import { useEffect, useMemo } from 'react';
import { getRelevantPacks, ImagePack, PackUsage } from '../plugins/custom-emoji';
import { AccountDataEvent } from '../../types/matrix/accountData';
import { StateEvent } from '../../types/matrix/room';
import { useForceUpdate } from './useForceUpdate';
export const useRelevantImagePacks = (
mx: MatrixClient,
usage: PackUsage,
rooms: Room[]
): ImagePack[] => {
const [forceCount, forceUpdate] = useForceUpdate();
const relevantPacks = useMemo(
() => getRelevantPacks(mx, rooms).filter((pack) => pack.getImagesFor(usage).length > 0),
// eslint-disable-next-line react-hooks/exhaustive-deps
[mx, usage, rooms, forceCount]
);
useEffect(() => {
const handleUpdate = (event: MatrixEvent) => {
if (
event.getType() === AccountDataEvent.PoniesEmoteRooms ||
event.getType() === AccountDataEvent.PoniesUserEmotes
) {
forceUpdate();
}
const eventRoomId = event.getRoomId();
if (
eventRoomId &&
event.getType() === StateEvent.PoniesRoomEmotes &&
rooms.find((room) => room.roomId === eventRoomId)
) {
forceUpdate();
}
};
mx.on(ClientEvent.AccountData, handleUpdate);
mx.on(RoomStateEvent.Events, handleUpdate);
return () => {
mx.removeListener(ClientEvent.AccountData, handleUpdate);
mx.removeListener(RoomStateEvent.Events, handleUpdate);
};
}, [mx, rooms, forceUpdate]);
return relevantPacks;
};

View file

@ -0,0 +1,10 @@
import { useEffect } from 'react';
export const useKeyDown = (target: Window, callback: (evt: KeyboardEvent) => void) => {
useEffect(() => {
target.addEventListener('keydown', callback);
return () => {
target.removeEventListener('keydown', callback);
};
}, [target, callback]);
};

View file

@ -0,0 +1,12 @@
import { createContext, useContext } from 'react';
import { MatrixClient } from 'matrix-js-sdk';
const MatrixClientContext = createContext<MatrixClient | null>(null);
export const MatrixClientProvider = MatrixClientContext.Provider;
export function useMatrixClient(): MatrixClient {
const mx = useContext(MatrixClientContext);
if (!mx) throw new Error('MatrixClient not initialized!');
return mx;
}

View file

@ -0,0 +1,86 @@
import { Room } from 'matrix-js-sdk';
import { useCallback } from 'react';
import { useStateEvent } from './useStateEvent';
import { StateEvent } from '../../types/matrix/room';
enum DefaultPowerLevels {
usersDefault = 0,
stateDefault = 50,
eventsDefault = 0,
invite = 0,
redact = 50,
kick = 50,
ban = 50,
historical = 0,
}
interface IPowerLevels {
users_default?: number;
state_default?: number;
events_default?: number;
historical?: number;
invite?: number;
redact?: number;
kick?: number;
ban?: number;
events?: Record<string, number>;
users?: Record<string, number>;
notifications?: Record<string, number>;
}
export function usePowerLevels(room: Room) {
const powerLevelsEvent = useStateEvent(room, StateEvent.RoomPowerLevels);
const powerLevels: IPowerLevels = powerLevelsEvent?.getContent() ?? DefaultPowerLevels;
const getPowerLevel = useCallback(
(userId: string) => {
const { users_default: usersDefault, users } = powerLevels;
if (users && typeof users[userId] === 'number') {
return users[userId];
}
return usersDefault ?? DefaultPowerLevels.usersDefault;
},
[powerLevels]
);
const canSendEvent = useCallback(
(eventType: string | undefined, powerLevel: number) => {
const { events, events_default: eventsDefault } = powerLevels;
if (events && eventType && typeof events[eventType] === 'string') {
return powerLevel >= events[eventType];
}
return powerLevel >= (eventsDefault ?? DefaultPowerLevels.eventsDefault);
},
[powerLevels]
);
const canSendStateEvent = useCallback(
(eventType: string | undefined, powerLevel: number) => {
const { events, state_default: stateDefault } = powerLevels;
if (events && eventType && typeof events[eventType] === 'number') {
return powerLevel >= events[eventType];
}
return powerLevel >= (stateDefault ?? DefaultPowerLevels.stateDefault);
},
[powerLevels]
);
const canDoAction = useCallback(
(action: 'invite' | 'redact' | 'kick' | 'ban' | 'historical', powerLevel: number) => {
const requiredPL = powerLevels[action];
if (typeof requiredPL === 'number') {
return powerLevel >= requiredPL;
}
return powerLevel >= DefaultPowerLevels[action];
},
[powerLevels]
);
return {
getPowerLevel,
canSendEvent,
canSendStateEvent,
canDoAction,
};
}

View file

@ -0,0 +1,23 @@
import { useEffect, useState } from 'react';
import { ClientEvent, MatrixClient, MatrixEvent } from 'matrix-js-sdk';
import { getRecentEmojis } from '../plugins/recent-emoji';
import { AccountDataEvent } from '../../types/matrix/accountData';
import { IEmoji } from '../plugins/emoji';
export const useRecentEmoji = (mx: MatrixClient, limit?: number): IEmoji[] => {
const [recentEmoji, setRecentEmoji] = useState(() => getRecentEmojis(mx, limit));
useEffect(() => {
const handleAccountData = (event: MatrixEvent) => {
if (event.getType() !== AccountDataEvent.ElementRecentEmoji) return;
setRecentEmoji(getRecentEmojis(mx, limit));
};
mx.on(ClientEvent.AccountData, handleAccountData);
return () => {
mx.removeListener(ClientEvent.AccountData, handleAccountData);
};
}, [mx, limit]);
return recentEmoji;
};

View file

@ -0,0 +1,24 @@
import { useEffect, useMemo } from 'react';
export type OnResizeCallback = (entries: ResizeObserverEntry[]) => void;
export const getResizeObserverEntry = (
target: Element,
entries: ResizeObserverEntry[]
): ResizeObserverEntry | undefined => entries.find((entry) => entry.target === target);
export const useResizeObserver = (
element: Element | null,
onResizeCallback: OnResizeCallback
): ResizeObserver => {
const resizeObserver = useMemo(() => new ResizeObserver(onResizeCallback), [onResizeCallback]);
useEffect(() => {
if (element) resizeObserver.observe(element);
return () => {
if (element) resizeObserver.unobserve(element);
};
}, [resizeObserver, element]);
return resizeObserver;
};

View file

@ -0,0 +1,34 @@
import { MatrixClient, MatrixEvent, RoomMember, RoomMemberEvent } from 'matrix-js-sdk';
import { useEffect, useState } from 'react';
import { useAlive } from './useAlive';
export const useRoomMembers = (mx: MatrixClient, roomId: string): RoomMember[] => {
const [members, setMembers] = useState<RoomMember[]>([]);
const alive = useAlive();
useEffect(() => {
const room = mx.getRoom(roomId);
const updateMemberList = (event?: MatrixEvent) => {
if (!room || !alive || (event && event.getRoomId() !== roomId)) return;
setMembers(room.getMembers());
};
if (room) {
updateMemberList();
room.loadMembersIfNeeded().then(() => {
if (!alive) return;
updateMemberList();
});
}
mx.on(RoomMemberEvent.Membership, updateMemberList);
mx.on(RoomMemberEvent.PowerLevel, updateMemberList);
return () => {
mx.removeListener(RoomMemberEvent.Membership, updateMemberList);
mx.removeListener(RoomMemberEvent.PowerLevel, updateMemberList);
};
}, [mx, roomId, alive]);
return members;
};

View file

@ -0,0 +1,32 @@
import { Room } from 'matrix-js-sdk';
import { useCallback, useMemo } from 'react';
import { useStateEventCallback } from './useStateEventCallback';
import { useForceUpdate } from './useForceUpdate';
import { getStateEvent } from '../utils/room';
import { StateEvent } from '../../types/matrix/room';
export const useStateEvent = (room: Room, eventType: StateEvent, stateKey = '') => {
const [updateCount, forceUpdate] = useForceUpdate();
useStateEventCallback(
room.client,
useCallback(
(event) => {
if (
event.getRoomId() === room.roomId &&
event.getType() === eventType &&
event.getStateKey() === stateKey
) {
forceUpdate();
}
},
[room, eventType, stateKey, forceUpdate]
)
);
return useMemo(
() => getStateEvent(room, eventType, stateKey),
// eslint-disable-next-line react-hooks/exhaustive-deps
[room, eventType, stateKey, updateCount]
);
};

View file

@ -0,0 +1,17 @@
import { MatrixClient, MatrixEvent, RoomState, RoomStateEvent } from 'matrix-js-sdk';
import { useEffect } from 'react';
export type StateEventCallback = (
event: MatrixEvent,
state: RoomState,
lastStateEvent: MatrixEvent | null
) => void;
export const useStateEventCallback = (mx: MatrixClient, onStateEvent: StateEventCallback) => {
useEffect(() => {
mx.on(RoomStateEvent.Events, onStateEvent);
return () => {
mx.removeListener(RoomStateEvent.Events, onStateEvent);
};
}, [mx, onStateEvent]);
};

View file

@ -0,0 +1,28 @@
import { useCallback, useMemo } from 'react';
import { Room } from 'matrix-js-sdk';
import { StateEvent } from '../../types/matrix/room';
import { useForceUpdate } from './useForceUpdate';
import { useStateEventCallback } from './useStateEventCallback';
import { getStateEvents } from '../utils/room';
export const useStateEvents = (room: Room, eventType: StateEvent) => {
const [updateCount, forceUpdate] = useForceUpdate();
useStateEventCallback(
room.client,
useCallback(
(event) => {
if (event.getRoomId() === room.roomId && event.getType() === eventType) {
forceUpdate();
}
},
[room, eventType, forceUpdate]
)
);
return useMemo(
() => getStateEvents(room, eventType),
// eslint-disable-next-line react-hooks/exhaustive-deps
[room, eventType, updateCount]
);
};

View file

@ -0,0 +1,41 @@
import { useCallback, useRef } from 'react';
export interface ThrottleOptions {
wait?: number;
immediate?: boolean;
}
export type ThrottleCallback<T extends unknown[]> = (...args: T) => void;
export function useThrottle<T extends unknown[]>(
callback: ThrottleCallback<T>,
options?: ThrottleOptions
): ThrottleCallback<T> {
const timeoutIdRef = useRef<number>();
const argsRef = useRef<T>();
const { wait, immediate } = options ?? {};
const debounceCallback = useCallback(
(...cbArgs: T) => {
argsRef.current = cbArgs;
if (timeoutIdRef.current) {
return;
}
if (immediate) {
callback(...cbArgs);
}
timeoutIdRef.current = window.setTimeout(() => {
if (argsRef.current) {
callback(...argsRef.current);
}
argsRef.current = undefined;
timeoutIdRef.current = undefined;
}, wait);
},
[callback, wait, immediate]
);
return debounceCallback;
}

View file

@ -0,0 +1,42 @@
import { MatrixClient } from 'matrix-js-sdk';
import { useMemo, useRef } from 'react';
type TypingStatusUpdater = (typing: boolean) => void;
const TYPING_TIMEOUT_MS = 5000; // 5 seconds
export const useTypingStatusUpdater = (mx: MatrixClient, roomId: string): TypingStatusUpdater => {
const statusSentTsRef = useRef<number>(0);
const sendTypingStatus: TypingStatusUpdater = useMemo(() => {
statusSentTsRef.current = 0;
return (typing) => {
if (typing) {
if (Date.now() - statusSentTsRef.current < TYPING_TIMEOUT_MS) {
return;
}
mx.sendTyping(roomId, true, TYPING_TIMEOUT_MS);
const sentTs = Date.now();
statusSentTsRef.current = sentTs;
// Don't believe server will timeout typing status;
// Clear typing status after timeout if already not;
setTimeout(() => {
if (statusSentTsRef.current === sentTs) {
mx.sendTyping(roomId, false, TYPING_TIMEOUT_MS);
statusSentTsRef.current = 0;
}
}, TYPING_TIMEOUT_MS);
return;
}
if (Date.now() - statusSentTsRef.current < TYPING_TIMEOUT_MS) {
mx.sendTyping(roomId, false, TYPING_TIMEOUT_MS);
}
statusSentTsRef.current = 0;
};
}, [mx, roomId]);
return sendTypingStatus;
};