Skip to content

Commit

Permalink
Merge branch 'develop' into MKP-246-enable-unlimited-apps-button-on-i…
Browse files Browse the repository at this point in the history
…nstallation-modal-doesnt-do-anything
  • Loading branch information
kodiakhq[bot] committed Feb 28, 2023
2 parents 4ff673a + 92a6187 commit 06cb96b
Show file tree
Hide file tree
Showing 27 changed files with 528 additions and 123 deletions.
2 changes: 1 addition & 1 deletion apps/meteor/app/theme/client/imports/general/base_old.css
Expand Up @@ -1821,7 +1821,7 @@
}
}

@media (width <= 780px) {
@media (max-width: 767px) {
.rc-old.main-content {
transition: right 0.25s cubic-bezier(0.5, 0, 0.1, 1), transform 0.1s linear;
will-change: transform;
Expand Down
Expand Up @@ -21,7 +21,14 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string)
input.dispatchEvent(event);
};

const emitter = new Emitter<{ quotedMessagesUpdate: void; editing: void; recording: void; recordingVideo: void; formatting: void }>();
const emitter = new Emitter<{
quotedMessagesUpdate: void;
editing: void;
recording: void;
recordingVideo: void;
formatting: void;
mircophoneDenied: void;
}>();

let _quotedMessages: IMessage[] = [];

Expand Down Expand Up @@ -167,6 +174,21 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string)
];
})();

const [isMicrophoneDenied, setIsMicrophoneDenied] = (() => {
let isMicrophoneDenied = false;

return [
{
get: () => isMicrophoneDenied,
subscribe: (callback: () => void) => emitter.on('mircophoneDenied', callback),
},
(value: boolean) => {
isMicrophoneDenied = value;
emitter.emit('mircophoneDenied');
},
];
})();

const setEditingMode = (editing: boolean): void => {
setEditing(editing);
};
Expand Down Expand Up @@ -317,5 +339,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string)
dismissAllQuotedMessages,
quotedMessages,
formatters,
isMicrophoneDenied,
setIsMicrophoneDenied,
};
};
3 changes: 3 additions & 0 deletions apps/meteor/client/lib/chats/ChatAPI.ts
Expand Up @@ -53,6 +53,9 @@ export type ComposerAPI = {
setRecordingVideo(recording: boolean): void;
readonly recordingVideo: Subscribable<boolean>;

setIsMicrophoneDenied(isMicrophoneDenied: boolean): void;
readonly isMicrophoneDenied: Subscribable<boolean>;

readonly formatters: Subscribable<FormattingButton[]>;
};

Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/sidebar/RoomList/RoomList.tsx
Expand Up @@ -104,7 +104,7 @@ const RoomList = (): ReactElement => {
padding-block-start: 12px;
}
@media (width <= 400px) {
@media (max-width: 400px) {
padding: 0 calc(var(--sidebar-small-default-padding) - 4px);
&__type,
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/sidebar/Sidebar.tsx
Expand Up @@ -37,7 +37,7 @@ const Sidebar = () => {
transform: translate3d(0px, 0px, 0px);
}
@media (width < 768px) {
@media (max-width: 767px) {
position: absolute;
user-select: none;
transform: translate3d(-100%, 0, 0);
Expand Down
@@ -1,9 +1,9 @@
import type { IMessage, IRoom } from '@rocket.chat/core-typings';
import type { IRoom } from '@rocket.chat/core-typings';
import { Box, Throbber } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { MessageComposerAction } from '@rocket.chat/ui-composer';
import { useSetting, useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement, AllHTMLAttributes } from 'react';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useEffect, useMemo, useState } from 'react';

import { AudioRecorder } from '../../../../app/ui/client';
Expand All @@ -14,16 +14,15 @@ const audioRecorder = new AudioRecorder();

type AudioMessageRecorderProps = {
rid: IRoom['_id'];
tmid?: IMessage['_id'];
chatContext?: ChatAPI; // TODO: remove this when the composer is migrated to React
} & Omit<AllHTMLAttributes<HTMLDivElement>, 'is'>;
isMicrophoneDenied?: boolean;
};

const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps): ReactElement | null => {
const AudioMessageRecorder = ({ rid, chatContext, isMicrophoneDenied }: AudioMessageRecorderProps): ReactElement | null => {
const t = useTranslation();

const [state, setState] = useState<'loading' | 'recording'>('recording');
const [time, setTime] = useState('00:00');
const [isMicrophoneDenied, setIsMicrophoneDenied] = useState(false);
const [recordingInterval, setRecordingInterval] = useState<ReturnType<typeof setInterval> | null>(null);
const [recordingRoomId, setRecordingRoomId] = useState<IRoom['_id'] | null>(null);

Expand All @@ -36,42 +35,13 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps):

setTime('00:00');

const blob = await new Promise<Blob>((resolve) => audioRecorder.stop(resolve));

chat?.action.stop('recording');

chat?.composer?.setRecordingMode(false);

return blob;
});

const handleMount = useMutableCallback(async (): Promise<void> => {
if (navigator.permissions) {
try {
const permissionStatus = await navigator.permissions.query({ name: 'microphone' as PermissionName });
setIsMicrophoneDenied(permissionStatus.state === 'denied');
permissionStatus.onchange = (): void => {
setIsMicrophoneDenied(permissionStatus.state === 'denied');
};
return;
} catch (error) {
console.warn(error);
}
}

if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
setIsMicrophoneDenied(true);
return;
}
const blob = await new Promise<Blob>((resolve) => audioRecorder.stop(resolve));

try {
if (!(await navigator.mediaDevices.enumerateDevices()).some(({ kind }) => kind === 'audioinput')) {
setIsMicrophoneDenied(true);
return;
}
} catch (error) {
console.warn(error);
}
return blob;
});

const handleUnmount = useMutableCallback(async () => {
Expand Down Expand Up @@ -101,7 +71,6 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps):
setRecordingRoomId(rid);
} catch (error) {
console.log(error);
setIsMicrophoneDenied(true);
chat?.composer?.setRecordingMode(false);
}
});
Expand All @@ -124,29 +93,12 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps):
});

useEffect(() => {
handleMount();
handleRecord();

return () => {
handleUnmount();
};
}, [handleMount, handleUnmount, handleRecord]);

const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean;
const isAudioRecorderEnabled = useSetting('Message_AudioRecorderEnabled') as boolean;
const fileUploadMediaTypeBlackList = useSetting('FileUpload_MediaTypeBlackList') as string;
const fileUploadMediaTypeWhiteList = useSetting('FileUpload_MediaTypeWhiteList') as string;

const isAllowed = useMemo(
() =>
audioRecorder.isSupported() &&
!isMicrophoneDenied &&
isFileUploadEnabled &&
isAudioRecorderEnabled &&
(!fileUploadMediaTypeBlackList || !fileUploadMediaTypeBlackList.match(/audio\/mp3|audio\/\*/i)) &&
(!fileUploadMediaTypeWhiteList || fileUploadMediaTypeWhiteList.match(/audio\/mp3|audio\/\*/i)),
[fileUploadMediaTypeBlackList, fileUploadMediaTypeWhiteList, isAudioRecorderEnabled, isFileUploadEnabled, isMicrophoneDenied],
);
}, [handleUnmount, handleRecord]);

const stateClass = useMemo(() => {
if (recordingRoomId && recordingRoomId !== rid) {
Expand All @@ -156,7 +108,7 @@ const AudioMessageRecorder = ({ rid, chatContext }: AudioMessageRecorderProps):
return state && `rc-message-box__audio-message--${state}`;
}, [recordingRoomId, rid, state]);

if (!isAllowed) {
if (isMicrophoneDenied) {
return null;
}

Expand Down
Expand Up @@ -20,7 +20,7 @@ const videoContainerClass = css`
transform: scaleX(-1);
filter: FlipH;
@media (width <= 500px) {
@media (max-width: 500px) {
& > video {
width: 100%;
height: 100%;
Expand Down
Expand Up @@ -261,6 +261,11 @@ const MessageBox = ({
subscribe: chat.composer?.recording.subscribe ?? emptySubscribe,
});

const isMicrophoneDenied = useSubscription({
getCurrentValue: chat.composer?.isMicrophoneDenied.get ?? getEmptyFalse,
subscribe: chat.composer?.isMicrophoneDenied.subscribe ?? emptySubscribe,
});

const isRecordingVideo = useSubscription({
getCurrentValue: chat.composer?.recordingVideo.get ?? getEmptyFalse,
subscribe: chat.composer?.recordingVideo.subscribe ?? emptySubscribe,
Expand Down Expand Up @@ -381,7 +386,7 @@ const MessageBox = ({

{isRecordingVideo && <VideoMessageRecorder reference={messageComposerRef} rid={rid} tmid={tmid} />}
<MessageComposer ref={messageComposerRef} variant={isEditing ? 'editing' : undefined}>
{isRecordingAudio && <AudioMessageRecorder rid={rid} tmid={tmid} disabled={!canSend || typing} />}
{isRecordingAudio && <AudioMessageRecorder rid={rid} isMicrophoneDenied={isMicrophoneDenied} />}
<MessageComposerInput
ref={mergedRefs as unknown as Ref<HTMLInputElement>}
aria-label={t('Message')}
Expand Down Expand Up @@ -421,6 +426,7 @@ const MessageBox = ({
canJoin={canJoin}
rid={rid}
tmid={tmid}
isMicrophoneDenied={isMicrophoneDenied}
/>
</MessageComposerToolbarActions>
<MessageComposerToolbarSubmit>
Expand Down
@@ -1,4 +1,4 @@
import type { IRoom } from '@rocket.chat/core-typings';
import type { IRoom, IMessage } from '@rocket.chat/core-typings';
import React, { memo } from 'react';

import ActionsToolbarDropdown from './ActionsToolbarDropdown';
Expand All @@ -13,7 +13,8 @@ type MessageBoxActionsToolbarProps = {
canSend: boolean;
canJoin: boolean;
rid: IRoom['_id'];
tmid?: string;
tmid?: IMessage['_id'];
isMicrophoneDenied?: boolean;
};

const MessageBoxActionsToolbar = ({
Expand All @@ -24,10 +25,15 @@ const MessageBoxActionsToolbar = ({
rid,
tmid,
canJoin,
isMicrophoneDenied,
}: MessageBoxActionsToolbarProps) => {
const actions = [
<VideoMessageAction key='video' collapsed={variant === 'small'} disabled={(!canJoin && !canSend) || typing || isRecording} />,
<AudioMessageAction key='audio' disabled={(!canJoin && !canSend) || typing || isRecording} />,
<AudioMessageAction
key='audio'
disabled={(!canJoin && !canSend) || typing || isRecording || isMicrophoneDenied}
isMicrophoneDenied={isMicrophoneDenied}
/>,
<FileUploadAction key='file' collapsed={variant === 'small'} disabled={!canSend || isRecording} />,
];

Expand Down
@@ -1,25 +1,114 @@
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { MessageComposerAction } from '@rocket.chat/ui-composer';
import { useTranslation } from '@rocket.chat/ui-contexts';
import { useTranslation, useSetting } from '@rocket.chat/ui-contexts';
import type { AllHTMLAttributes } from 'react';
import React from 'react';
import React, { useEffect, useMemo } from 'react';

import { AudioRecorder } from '../../../../../../../../../app/ui/client';
import type { ChatAPI } from '../../../../../../../../lib/chats/ChatAPI';
import { useChat } from '../../../../../../contexts/ChatContext';

const audioRecorder = new AudioRecorder();

type AudioMessageActionProps = {
chatContext?: ChatAPI;
isMicrophoneDenied?: boolean;
} & Omit<AllHTMLAttributes<HTMLButtonElement>, 'is'>;

const AudioMessageAction = ({ chatContext, ...props }: AudioMessageActionProps) => {
const AudioMessageAction = ({ chatContext, disabled, isMicrophoneDenied, ...props }: AudioMessageActionProps) => {
const t = useTranslation();
const chat = useChat() ?? chatContext;

const stopRecording = useMutableCallback(() => {
chat?.action.stop('recording');

chat?.composer?.setRecordingMode(false);
});

const setMicrophoneDenied = useMutableCallback((isDenied) => {
if (isDenied) {
stopRecording();
}

chat?.composer?.setIsMicrophoneDenied(isDenied);
});

const handleRecordButtonClick = () => chat?.composer?.setRecordingMode(true);

const handleMount = useMutableCallback(async (): Promise<void> => {
if (navigator.permissions) {
try {
const permissionStatus = await navigator.permissions.query({ name: 'microphone' as PermissionName });
setMicrophoneDenied(permissionStatus.state === 'denied');
permissionStatus.onchange = (): void => {
setMicrophoneDenied(permissionStatus.state === 'denied');
};
return;
} catch (error) {
console.warn(error);
}
}

if (!navigator.mediaDevices?.enumerateDevices) {
setMicrophoneDenied(true);
return;
}

try {
if (!(await navigator.mediaDevices.enumerateDevices()).some(({ kind }) => kind === 'audioinput')) {
setMicrophoneDenied(true);
return;
}
} catch (error) {
console.warn(error);
}
});

useEffect(() => {
handleMount();
}, [handleMount]);

const isFileUploadEnabled = useSetting('FileUpload_Enabled') as boolean;
const isAudioRecorderEnabled = useSetting('Message_AudioRecorderEnabled') as boolean;
const fileUploadMediaTypeBlackList = useSetting('FileUpload_MediaTypeBlackList') as string;
const fileUploadMediaTypeWhiteList = useSetting('FileUpload_MediaTypeWhiteList') as string;

const isAllowed = useMemo(
() =>
audioRecorder.isSupported() &&
!isMicrophoneDenied &&
isFileUploadEnabled &&
isAudioRecorderEnabled &&
!fileUploadMediaTypeBlackList?.match(/audio\/mp3|audio\/\*/i) &&
(!fileUploadMediaTypeWhiteList || fileUploadMediaTypeWhiteList.match(/audio\/mp3|audio\/\*/i)),
[fileUploadMediaTypeBlackList, fileUploadMediaTypeWhiteList, isAudioRecorderEnabled, isFileUploadEnabled, isMicrophoneDenied],
);

const getTranslationKey = useMemo(() => {
if (isMicrophoneDenied) {
return t('Microphone_access_not_allowed');
}

if (!isFileUploadEnabled) {
return t('File_Upload_Disabled');
}

if (!isAudioRecorderEnabled) {
return t('Message_Audio_Recording_Disabled');
}

if (!isAllowed) {
return t('error-not-allowed');
}

return t('Audio_message');
}, [isMicrophoneDenied, isFileUploadEnabled, isAudioRecorderEnabled, isAllowed, t]);

return (
<MessageComposerAction
title={t('Audio_message')}
title={getTranslationKey}
icon='mic'
disabled={disabled || !isAllowed}
className='rc-message-box__icon rc-message-box__audio-message-mic'
data-qa-id='audio-record'
onClick={handleRecordButtonClick}
Expand Down

0 comments on commit 06cb96b

Please sign in to comment.