diff --git a/.eslintrc.js b/.eslintrc.js index a6e8e749f..0356dd2b8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,6 @@ module.exports = { 'extends': [ '@jitsi/eslint-config' - ] + ], + 'ignorePatterns': [ '*.d.ts' ] }; diff --git a/globals.d.ts b/globals.d.ts new file mode 100644 index 000000000..88dacf7c4 --- /dev/null +++ b/globals.d.ts @@ -0,0 +1,6 @@ +export {}; + +declare global { + const APP: any; + const interfaceConfig: any; +} diff --git a/react/features/base/conference/functions.js b/react/features/base/conference/functions.ts similarity index 82% rename from react/features/base/conference/functions.js rename to react/features/base/conference/functions.ts index e6ec7f6ba..7ef042ea1 100644 --- a/react/features/base/conference/functions.js +++ b/react/features/base/conference/functions.ts @@ -1,25 +1,24 @@ -// @flow - import { sha512_256 as sha512 } from 'js-sha512'; import _ from 'lodash'; +// @ts-ignore import { getName } from '../../app/functions'; +import { IState, IStore } from '../../app/types'; import { determineTranscriptionLanguage } from '../../transcribing/functions'; +import { IStateful } from '../app/types'; import { JitsiTrackErrors } from '../lib-jitsi-meet'; import { - getLocalParticipant, hiddenParticipantJoined, hiddenParticipantLeft, participantJoined, participantLeft -} from '../participants'; -import { toState } from '../redux'; -import { - getBackendSafePath, - getJitsiMeetGlobalNS, - safeDecodeURIComponent -} from '../util'; +} from '../participants/actions'; +import { getLocalParticipant } from '../participants/functions'; +import { toState } from '../redux/functions'; +import { getJitsiMeetGlobalNS } from '../util/helpers'; +import { getBackendSafePath, safeDecodeURIComponent } from '../util/uri'; +// @ts-ignore import { setObfuscatedRoom } from './actions'; import { AVATAR_URL_COMMAND, @@ -27,22 +26,23 @@ import { JITSI_CONFERENCE_URL_KEY } from './constants'; import logger from './logger'; +import { IJitsiConference } from './reducer'; /** * Returns root conference state. * - * @param {Object} state - Global state. + * @param {IState} state - Global state. * @returns {Object} Conference state. */ -export const getConferenceState = (state: Object) => state['features/base/conference']; +export const getConferenceState = (state: IState) => state['features/base/conference']; /** * Is the conference joined or not. * - * @param {Object} state - Global state. + * @param {IState} state - Global state. * @returns {boolean} */ -export const getIsConferenceJoined = (state: Object) => Boolean(getConferenceState(state).conference); +export const getIsConferenceJoined = (state: IState) => Boolean(getConferenceState(state).conference); /** * Attach a set of local tracks to a conference. @@ -53,7 +53,7 @@ export const getIsConferenceJoined = (state: Object) => Boolean(getConferenceSta * @returns {Promise} */ export function _addLocalTracksToConference( - conference: { addTrack: Function, getLocalTracks: Function }, + conference: IJitsiConference, localTracks: Array) { const conferenceLocalTracks = conference.getLocalTracks(); const promises = []; @@ -63,7 +63,7 @@ export function _addLocalTracksToConference( // adding one and the same video track multiple times. if (conferenceLocalTracks.indexOf(track) === -1) { promises.push( - conference.addTrack(track).catch(err => { + conference.addTrack(track).catch((err: Error) => { _reportError( 'Failed to add local track to conference', err); @@ -86,16 +86,16 @@ export function _addLocalTracksToConference( * @returns {void} */ export function commonUserJoinedHandling( - { dispatch }: Object, - conference: Object, - user: Object) { + { dispatch }: IStore, + conference: IJitsiConference, + user: any) { const id = user.getId(); const displayName = user.getDisplayName(); if (user.isHidden()) { dispatch(hiddenParticipantJoined(id, displayName)); } else { - const isReplacing = user.isReplacing && user.isReplacing(); + const isReplacing = user?.isReplacing(); dispatch(participantJoined({ botType: user.getBotType(), @@ -122,15 +122,15 @@ export function commonUserJoinedHandling( * @returns {void} */ export function commonUserLeftHandling( - { dispatch }: Object, - conference: Object, - user: Object) { + { dispatch }: IStore, + conference: IJitsiConference, + user: any) { const id = user.getId(); if (user.isHidden()) { dispatch(hiddenParticipantLeft(id)); } else { - const isReplaced = user.isReplaced && user.isReplaced(); + const isReplaced = user.isReplaced?.(); dispatch(participantLeft(id, conference, { isReplaced })); } @@ -140,7 +140,7 @@ export function commonUserLeftHandling( * Evaluates a specific predicate for each {@link JitsiConference} known to the * redux state features/base/conference while it returns {@code true}. * - * @param {Function | Object} stateful - The redux store, state, or + * @param {IStateful} stateful - The redux store, state, or * {@code getState} function. * @param {Function} predicate - The predicate to evaluate for each * {@code JitsiConference} know to the redux state features/base/conference @@ -150,8 +150,8 @@ export function commonUserLeftHandling( * features/base/conference. */ export function forEachConference( - stateful: Function | Object, - predicate: (Object, URL) => boolean) { + stateful: IStateful, + predicate: (a: any, b: URL) => boolean) { const state = getConferenceState(toState(stateful)); for (const v of Object.values(state)) { @@ -178,48 +178,48 @@ export function forEachConference( /** * Returns the display name of the conference. * - * @param {Function | Object} stateful - Reference that can be resolved to Redux + * @param {IStateful} stateful - Reference that can be resolved to Redux * state with the {@code toState} function. * @returns {string} */ -export function getConferenceName(stateful: Function | Object): string { +export function getConferenceName(stateful: IStateful): string { const state = toState(stateful); const { callee } = state['features/base/jwt']; const { callDisplayName } = state['features/base/config']; const { localSubject, room, subject } = getConferenceState(state); - return localSubject + return (localSubject || subject || callDisplayName - || (callee && callee.name) - || (room && safeStartCase(safeDecodeURIComponent(room))); + || callee?.name + || (room && safeStartCase(safeDecodeURIComponent(room)))) ?? ''; } /** * Returns the name of the conference formatted for the title. * - * @param {Function | Object} stateful - Reference that can be resolved to Redux state with the {@code toState} + * @param {IStateful} stateful - Reference that can be resolved to Redux state with the {@code toState} * function. * @returns {string} - The name of the conference formatted for the title. */ -export function getConferenceNameForTitle(stateful: Function | Object) { - return safeStartCase(safeDecodeURIComponent(getConferenceState(toState(stateful)).room)); +export function getConferenceNameForTitle(stateful: IStateful) { + return safeStartCase(safeDecodeURIComponent(getConferenceState(toState(stateful)).room ?? '')); } /** * Returns an object aggregating the conference options. * - * @param {Object|Function} stateful - The redux store state. + * @param {IStateful} stateful - The redux store state. * @returns {Object} - Options object. */ -export function getConferenceOptions(stateful: Function | Object) { +export function getConferenceOptions(stateful: IStateful) { const state = toState(stateful); const config = state['features/base/config']; const { locationURL } = state['features/base/connection']; const { tenant } = state['features/base/jwt']; - const { email, name: nick } = getLocalParticipant(state); - const options = { ...config }; + const { email, name: nick } = getLocalParticipant(state) ?? {}; + const options: any = { ...config }; if (tenant) { options.siteID = tenant; @@ -257,11 +257,11 @@ export function getConferenceOptions(stateful: Function | Object) { /** * Returns the UTC timestamp when the first participant joined the conference. * -* @param {Function | Object} stateful - Reference that can be resolved to Redux +* @param {IStateful} stateful - Reference that can be resolved to Redux * state with the {@code toState} function. * @returns {number} */ -export function getConferenceTimestamp(stateful: Function | Object): number { +export function getConferenceTimestamp(stateful: IStateful) { const state = toState(stateful); const { conferenceTimestamp } = getConferenceState(state); @@ -274,11 +274,11 @@ export function getConferenceTimestamp(stateful: Function | Object): number { * {@code conference} state of the feature base/conference which is not joining * but may be leaving already. * - * @param {Function|Object} stateful - The redux store, state, or + * @param {IStateful} stateful - The redux store, state, or * {@code getState} function. * @returns {JitsiConference|undefined} */ -export function getCurrentConference(stateful: Function | Object) { +export function getCurrentConference(stateful: IStateful): any { const { conference, joining, leaving, membersOnly, passwordRequired } = getConferenceState(toState(stateful)); @@ -293,25 +293,29 @@ export function getCurrentConference(stateful: Function | Object) { /** * Returns the stored room name. * - * @param {Object} state - The current state of the app. + * @param {IState} state - The current state of the app. * @returns {string} */ -export function getRoomName(state: Object): string { +export function getRoomName(state: IState) { return getConferenceState(state).room; } /** * Get an obfuscated room name or create and persist it if it doesn't exists. * - * @param {Object} state - The current state of the app. + * @param {IState} state - The current state of the app. * @param {Function} dispatch - The Redux dispatch function. * @returns {string} - Obfuscated room name. */ -export function getOrCreateObfuscatedRoomName(state: Object, dispatch: Function) { +export function getOrCreateObfuscatedRoomName(state: IState, dispatch: IStore['dispatch']) { let { obfuscatedRoom } = getConferenceState(state); const { obfuscatedRoomSource } = getConferenceState(state); const room = getRoomName(state); + if (!room) { + return; + } + // On native mobile the store doesn't clear when joining a new conference so we might have the obfuscatedRoom // stored even though a different room was joined. // Check if the obfuscatedRoom was already computed for the current room. @@ -327,11 +331,11 @@ export function getOrCreateObfuscatedRoomName(state: Object, dispatch: Function) * Analytics may require an obfuscated room name, this functions decides based on a config if the normal or * obfuscated room name should be returned. * - * @param {Object} state - The current state of the app. + * @param {IState} state - The current state of the app. * @param {Function} dispatch - The Redux dispatch function. * @returns {string} - Analytics room name. */ -export function getAnalyticsRoomName(state: Object, dispatch: Function) { +export function getAnalyticsRoomName(state: IState, dispatch: IStore['dispatch']) { const { analysis: { obfuscateRoomName = false } = {} } = state['features/base/config']; if (obfuscateRoomName) { @@ -365,7 +369,7 @@ function getWiFiStatsMethod() { * @protected * @returns {void} */ -export function _handleParticipantError(err: { message: ?string }) { +export function _handleParticipantError(err: Error) { // XXX DataChannels are initialized at some later point when the conference // has multiple participants, but code that pins or selects a participant // might be executed before. So here we're swallowing a particular error. @@ -384,7 +388,7 @@ export function _handleParticipantError(err: { message: ?string }) { * @returns {boolean} If the specified room name is valid, then true; otherwise, * false. */ -export function isRoomValid(room: ?string) { +export function isRoomValid(room?: string) { return typeof room === 'string' && room !== ''; } @@ -397,11 +401,11 @@ export function isRoomValid(room: ?string) { * @returns {Promise} */ export function _removeLocalTracksFromConference( - conference: { removeTrack: Function }, + conference: IJitsiConference, localTracks: Array) { return Promise.all(localTracks.map(track => conference.removeTrack(track) - .catch(err => { + .catch((err: Error) => { // Local track might be already disposed by direct // JitsiTrack#dispose() call. So we should ignore this error // here. @@ -425,7 +429,7 @@ export function _removeLocalTracksFromConference( * @private * @returns {void} */ -function _reportError(msg, err) { +function _reportError(msg: string, err: Error) { // TODO This is a good point to call some global error handler when we have // one. logger.error(msg, err); @@ -443,17 +447,14 @@ function _reportError(msg, err) { * @returns {void} */ export function sendLocalParticipant( - stateful: Function | Object, - conference: { - sendCommand: Function, - setDisplayName: Function, - setLocalParticipantProperty: Function }) { + stateful: IStateful, + conference: IJitsiConference) { const { avatarURL, email, features, name - } = getLocalParticipant(stateful); + } = getLocalParticipant(stateful) ?? {}; avatarURL && conference.sendCommand(AVATAR_URL_COMMAND, { value: avatarURL diff --git a/react/features/base/conference/logger.js b/react/features/base/conference/logger.ts similarity index 91% rename from react/features/base/conference/logger.js rename to react/features/base/conference/logger.ts index bc08bb126..61af7ad1a 100644 --- a/react/features/base/conference/logger.js +++ b/react/features/base/conference/logger.ts @@ -1,5 +1,3 @@ -// @flow - import { getLogger } from '../logging/functions'; export default getLogger('features/base/conference'); diff --git a/react/features/base/conference/reducer.ts b/react/features/base/conference/reducer.ts index 42d71308f..361c0c6b1 100644 --- a/react/features/base/conference/reducer.ts +++ b/react/features/base/conference/reducer.ts @@ -40,11 +40,26 @@ const DEFAULT_STATE = { passwordRequired: undefined }; +export interface IJitsiConference { + addTrack: Function; + getBreakoutRooms: Function; + getLocalTracks: Function; + isAVModerationSupported: Function; + isEndConferenceSupported: Function; + isLobbySupported: Function; + removeTrack: Function; + sendCommand: Function; + sendEndpointMessage: Function; + sessionId: string; + setDisplayName: Function; + setLocalParticipantProperty: Function; +} + export interface IConferenceState { authEnabled?: boolean; authLogin?: string; authRequired?: Object; - conference?: any; + conference?: IJitsiConference; conferenceTimestamp?: number; e2eeSupported?: boolean; error?: Error; diff --git a/react/features/base/config/configType.ts b/react/features/base/config/configType.ts index 43e1d585a..93636905d 100644 --- a/react/features/base/config/configType.ts +++ b/react/features/base/config/configType.ts @@ -125,6 +125,7 @@ export interface IConfig { key: ButtonsWithNotifyClick; preventExecution: boolean; }>; + callDisplayName?: string; callStatsConfigParams?: { additionalIDs?: { customerID?: string; diff --git a/react/features/base/config/reducer.ts b/react/features/base/config/reducer.ts index 13476cb81..e8ed0f742 100644 --- a/react/features/base/config/reducer.ts +++ b/react/features/base/config/reducer.ts @@ -71,7 +71,11 @@ const CONFERENCE_HEADER_MAPPING: any = { }; export interface IConfigState extends IConfig { + analysis?: { + obfuscateRoomName?: boolean; + }; error?: Error; + firefox_fake_device?: string; } ReducerRegistry.register('features/base/config', (state = _getInitialState(), action): IConfigState => { diff --git a/react/features/base/jwt/reducer.ts b/react/features/base/jwt/reducer.ts index 481ebf6ea..0db8ead01 100644 --- a/react/features/base/jwt/reducer.ts +++ b/react/features/base/jwt/reducer.ts @@ -4,9 +4,13 @@ import { equals } from '../redux/functions'; import { SET_JWT } from './actionTypes'; export interface IJwtState { + callee?: { + name: string; + }; group?: string; jwt?: string; server?: string; + tenant?: string; } /** diff --git a/react/features/base/media/actions.js b/react/features/base/media/actions.ts similarity index 96% rename from react/features/base/media/actions.js rename to react/features/base/media/actions.ts index 76f2e75f0..76dfc38fc 100644 --- a/react/features/base/media/actions.js +++ b/react/features/base/media/actions.ts @@ -1,10 +1,8 @@ -/* @flow */ - -import type { Dispatch } from 'redux'; +import { Dispatch } from 'redux'; import { showModeratedNotification } from '../../av-moderation/actions'; import { shouldShowModeratedNotification } from '../../av-moderation/functions'; -import { isModerationNotificationDisplayed } from '../../notifications'; +import { isModerationNotificationDisplayed } from '../../notifications/functions'; import { SET_AUDIO_MUTED, @@ -55,7 +53,7 @@ export function setAudioAvailable(available: boolean) { * muted: boolean * }} */ -export function setAudioMuted(muted: boolean, ensureTrack: boolean = false) { +export function setAudioMuted(muted: boolean, ensureTrack = false) { return { type: SET_AUDIO_MUTED, ensureTrack, @@ -70,7 +68,7 @@ export function setAudioMuted(muted: boolean, ensureTrack: boolean = false) { * @param {boolean|undefined} skipNotification - True if we want to skip showing the notification. * @returns {Function} */ -export function setAudioUnmutePermissions(blocked: boolean, skipNotification: boolean = false) { +export function setAudioUnmutePermissions(blocked: boolean, skipNotification = false) { return { type: SET_AUDIO_UNMUTE_PERMISSIONS, blocked, @@ -107,7 +105,7 @@ export function setScreenshareMuted( muted: boolean, mediaType: MediaType = MEDIA_TYPE.SCREENSHARE, authority: number = SCREENSHARE_MUTISM_AUTHORITY.USER, - ensureTrack: boolean = false) { + ensureTrack = false) { return (dispatch: Dispatch, getState: Function) => { const state = getState(); @@ -168,7 +166,7 @@ export function setVideoMuted( muted: boolean, mediaType: string = MEDIA_TYPE.VIDEO, authority: number = VIDEO_MUTISM_AUTHORITY.USER, - ensureTrack: boolean = false) { + ensureTrack = false) { return (dispatch: Dispatch, getState: Function) => { const state = getState(); @@ -203,7 +201,7 @@ export function setVideoMuted( * @param {boolean|undefined} skipNotification - True if we want to skip showing the notification. * @returns {Function} */ -export function setVideoUnmutePermissions(blocked: boolean, skipNotification: boolean = false) { +export function setVideoUnmutePermissions(blocked: boolean, skipNotification = false) { return { type: SET_VIDEO_UNMUTE_PERMISSIONS, blocked, diff --git a/react/features/base/participants/types.ts b/react/features/base/participants/types.ts index 6f2c140cf..bf699eac5 100644 --- a/react/features/base/participants/types.ts +++ b/react/features/base/participants/types.ts @@ -8,7 +8,7 @@ export interface Participant { e2eeSupported?: boolean; email?: string; features?: { - 'screen-sharing'?: boolean; + 'screen-sharing'?: boolean | string; }; getId?: Function; id: string; diff --git a/react/features/base/testing/actions.js b/react/features/base/testing/actions.ts similarity index 100% rename from react/features/base/testing/actions.js rename to react/features/base/testing/actions.ts diff --git a/react/features/base/testing/functions.js b/react/features/base/testing/functions.ts similarity index 75% rename from react/features/base/testing/functions.js rename to react/features/base/testing/functions.ts index 9907ddd8d..0086e2209 100644 --- a/react/features/base/testing/functions.js +++ b/react/features/base/testing/functions.ts @@ -1,8 +1,9 @@ -// @flow - -import { getMultipleVideoSupportFeatureFlag } from '../config'; -import { MEDIA_TYPE, VIDEO_TYPE } from '../media'; -import { getParticipantById } from '../participants'; +import { IState, IStore } from '../../app/types'; +import { getMultipleVideoSupportFeatureFlag } from '../config/functions.any'; +import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants'; +import { getParticipantById } from '../participants/functions'; +// eslint-disable-next-line lines-around-comment +// @ts-ignore import { getTrackByMediaTypeAndParticipant, getVirtualScreenshareParticipantTrack } from '../tracks'; /** @@ -10,23 +11,23 @@ import { getTrackByMediaTypeAndParticipant, getVirtualScreenshareParticipantTrac * {@link TestHint} and other components from the testing package will be * rendered in various places across the app to help with automatic testing. * - * @param {Object} state - The redux store state. + * @param {IState} state - The redux store state. * @returns {boolean} */ -export function isTestModeEnabled(state: Object): boolean { +export function isTestModeEnabled(state: IState): boolean { const testingConfig = state['features/base/config'].testing; - return Boolean(testingConfig && testingConfig.testMode); + return Boolean(testingConfig?.testMode); } /** * Returns the video type of the remote participant's video. * - * @param {Store} store - The redux store. + * @param {IStore} store - The redux store. * @param {string} id - The participant ID for the remote video. * @returns {VIDEO_TYPE} */ -export function getRemoteVideoType({ getState }: Object, id: String): VIDEO_TYPE { +export function getRemoteVideoType({ getState }: IStore, id: string) { const state = getState(); const participant = getParticipantById(state, id); @@ -40,13 +41,13 @@ export function getRemoteVideoType({ getState }: Object, id: String): VIDEO_TYPE /** * Returns whether the last media event received for large video indicates that the video is playing, if not muted. * - * @param {Store} store - The redux store. + * @param {IStore} store - The redux store. * @returns {boolean} */ -export function isLargeVideoReceived({ getState }: Object): boolean { +export function isLargeVideoReceived({ getState }: IStore): boolean { const state = getState(); const largeVideoParticipantId = state['features/large-video'].participantId; - const largeVideoParticipant = getParticipantById(state, largeVideoParticipantId); + const largeVideoParticipant = getParticipantById(state, largeVideoParticipantId ?? ''); const tracks = state['features/base/tracks']; let videoTrack; @@ -64,11 +65,11 @@ export function isLargeVideoReceived({ getState }: Object): boolean { /** * Returns whether the last media event received for a remote video indicates that the video is playing, if not muted. * - * @param {Store} store - The redux store. + * @param {IStore} store - The redux store. * @param {string} id - The participant ID for the remote video. * @returns {boolean} */ -export function isRemoteVideoReceived({ getState }: Object, id: String): boolean { +export function isRemoteVideoReceived({ getState }: IStore, id: string): boolean { const state = getState(); const tracks = state['features/base/tracks']; const participant = getParticipantById(state, id); diff --git a/react/features/face-landmarks/functions.ts b/react/features/face-landmarks/functions.ts index af134d942..456ce60b3 100644 --- a/react/features/face-landmarks/functions.ts +++ b/react/features/face-landmarks/functions.ts @@ -111,7 +111,7 @@ export async function sendFaceExpressionsWebhook(state: IState) { const reqBody = { meetingFqn: extractFqnFromPath(), - sessionId: conference.sessionId, + sessionId: conference?.sessionId, submitted: Date.now(), emotions: faceExpressionsBuffer, participantId: localParticipant?.jwtId, diff --git a/react/features/notifications/functions.js b/react/features/notifications/functions.ts similarity index 71% rename from react/features/notifications/functions.js rename to react/features/notifications/functions.ts index 11cf3ed7c..586012205 100644 --- a/react/features/notifications/functions.js +++ b/react/features/notifications/functions.ts @@ -1,19 +1,16 @@ -// @flow - import { MODERATION_NOTIFICATIONS } from '../av-moderation/constants'; -import { MEDIA_TYPE } from '../base/media'; -import { toState } from '../base/redux'; - -declare var interfaceConfig: Object; +import { IStateful } from '../base/app/types'; +import { MediaType } from '../base/media/constants'; +import { toState } from '../base/redux/functions'; /** * Tells whether or not the notifications are enabled and if there are any * notifications to be displayed based on the current Redux state. * - * @param {Object|Function} stateful - The redux store state. + * @param {IStateful} stateful - The redux store state. * @returns {boolean} */ -export function areThereNotifications(stateful: Object | Function) { +export function areThereNotifications(stateful: IStateful) { const state = toState(stateful); const { enabled, notifications } = state['features/notifications']; @@ -33,10 +30,10 @@ export function joinLeaveNotificationsDisabled() { * Returns whether or not the moderation notification for the given type is displayed. * * @param {MEDIA_TYPE} mediaType - The media type to check. - * @param {Object | Function} stateful - The redux store state. + * @param {IStateful} stateful - The redux store state. * @returns {boolean} */ -export function isModerationNotificationDisplayed(mediaType: MEDIA_TYPE, stateful: Object | Function) { +export function isModerationNotificationDisplayed(mediaType: MediaType, stateful: IStateful) { const state = toState(stateful); const { notifications } = state['features/notifications']; diff --git a/react/features/prejoin/components/Prejoin.native.tsx b/react/features/prejoin/components/Prejoin.native.tsx index 900a5330f..2fc7b2dee 100644 --- a/react/features/prejoin/components/Prejoin.native.tsx +++ b/react/features/prejoin/components/Prejoin.native.tsx @@ -65,7 +65,7 @@ const Prejoin: React.FC = ({ navigation }: PrejoinProps) => { ); const localParticipant = useSelector((state: IState) => getLocalParticipant(state)); const isDisplayNameMandatory = useSelector(state => isDisplayNameRequired(state)); - const roomName = useSelector(state => getConferenceName(state)); + const roomName = useSelector((state: IState) => getConferenceName(state)); const participantName = localParticipant?.name; const [ displayName, setDisplayName ] = useState(participantName || ''); diff --git a/react/features/reactions/functions.any.ts b/react/features/reactions/functions.any.ts index a88cd3c9b..ab6d1a9a3 100644 --- a/react/features/reactions/functions.any.ts +++ b/react/features/reactions/functions.any.ts @@ -69,7 +69,7 @@ export async function sendReactionsWebhook(state: IState, reactions: Array