feat: Hide UI elements through window postmessage (#31184)
parent
2fa8055d06
commit
b2b0035162
@ -0,0 +1,6 @@ |
||||
--- |
||||
"@rocket.chat/meteor": minor |
||||
"@rocket.chat/ui-contexts": minor |
||||
--- |
||||
|
||||
Add the possibility to hide some elements through postMessage events. |
||||
@ -0,0 +1,23 @@ |
||||
import { useRef, useEffect } from 'react'; |
||||
import type { AllHTMLAttributes } from 'react'; |
||||
|
||||
export const useFileInput = (props: AllHTMLAttributes<HTMLInputElement>) => { |
||||
const ref = useRef<HTMLInputElement>(); |
||||
|
||||
useEffect(() => { |
||||
const fileInput = document.createElement('input'); |
||||
fileInput.setAttribute('style', 'display: none;'); |
||||
Object.entries(props).forEach(([key, value]) => { |
||||
fileInput.setAttribute(key, value); |
||||
}); |
||||
document.body.appendChild(fileInput); |
||||
ref.current = fileInput; |
||||
|
||||
return (): void => { |
||||
ref.current = undefined; |
||||
fileInput.remove(); |
||||
}; |
||||
}, [props]); |
||||
|
||||
return ref; |
||||
}; |
||||
@ -1,75 +0,0 @@ |
||||
import { Option, OptionContent, OptionIcon } from '@rocket.chat/fuselage'; |
||||
import { MessageComposerAction } from '@rocket.chat/ui-composer'; |
||||
import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; |
||||
import type { ChangeEvent, AllHTMLAttributes } from 'react'; |
||||
import React, { useRef } from 'react'; |
||||
|
||||
import type { ChatAPI } from '../../../../../../lib/chats/ChatAPI'; |
||||
import { useChat } from '../../../../contexts/ChatContext'; |
||||
|
||||
type FileUploadActionProps = { |
||||
collapsed?: boolean; |
||||
chatContext?: ChatAPI; // TODO: remove this when the composer is migrated to React
|
||||
} & Omit<AllHTMLAttributes<HTMLButtonElement>, 'is'>; |
||||
|
||||
const FileUploadAction = ({ collapsed, chatContext, disabled, ...props }: FileUploadActionProps) => { |
||||
const t = useTranslation(); |
||||
const fileUploadEnabled = useSetting('FileUpload_Enabled'); |
||||
const fileInputRef = useRef<HTMLInputElement>(null); |
||||
const chat = useChat() ?? chatContext; |
||||
|
||||
const resetFileInput = () => { |
||||
if (!fileInputRef.current) { |
||||
return; |
||||
} |
||||
|
||||
fileInputRef.current.value = ''; |
||||
}; |
||||
|
||||
const handleUploadChange = async (e: ChangeEvent<HTMLInputElement>) => { |
||||
const { mime } = await import('../../../../../../../app/utils/lib/mimeTypes'); |
||||
const filesToUpload = Array.from(e.target.files ?? []).map((file) => { |
||||
Object.defineProperty(file, 'type', { |
||||
value: mime.lookup(file.name), |
||||
}); |
||||
return file; |
||||
}); |
||||
chat?.flows.uploadFiles(filesToUpload, resetFileInput); |
||||
}; |
||||
|
||||
const handleUpload = () => { |
||||
fileInputRef.current?.click(); |
||||
}; |
||||
|
||||
if (collapsed) { |
||||
return ( |
||||
<> |
||||
<Option |
||||
{...((!fileUploadEnabled || disabled) && { title: t('Not_Available') })} |
||||
disabled={!fileUploadEnabled || disabled} |
||||
onClick={handleUpload} |
||||
> |
||||
<OptionIcon name='clip' /> |
||||
<OptionContent>{t('File')}</OptionContent> |
||||
</Option> |
||||
<input ref={fileInputRef} type='file' onChange={handleUploadChange} multiple style={{ display: 'none' }} /> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<MessageComposerAction |
||||
data-qa-id='file-upload' |
||||
icon='clip' |
||||
disabled={!fileUploadEnabled || disabled} |
||||
onClick={handleUpload} |
||||
title={t('File')} |
||||
{...props} |
||||
/> |
||||
<input ref={fileInputRef} type='file' onChange={handleUploadChange} multiple style={{ display: 'none' }} /> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default FileUploadAction; |
||||
@ -0,0 +1,10 @@ |
||||
import type { Keys as IconName } from '@rocket.chat/icons'; |
||||
|
||||
export type ToolbarAction = { |
||||
title?: string; |
||||
disabled?: boolean; |
||||
onClick: (...params: any) => unknown; |
||||
icon: IconName; |
||||
label: string; |
||||
id: string; |
||||
}; |
||||
@ -0,0 +1,52 @@ |
||||
import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; |
||||
import { useEffect } from 'react'; |
||||
|
||||
import { useFileInput } from '../../../../../../hooks/useFileInput'; |
||||
import { useChat } from '../../../../contexts/ChatContext'; |
||||
import type { ToolbarAction } from './ToolbarAction'; |
||||
|
||||
const fileInputProps = { type: 'file', multiple: true }; |
||||
|
||||
export const useFileUploadAction = (disabled: boolean): ToolbarAction => { |
||||
const t = useTranslation(); |
||||
const fileUploadEnabled = useSetting('FileUpload_Enabled'); |
||||
const fileInputRef = useFileInput(fileInputProps); |
||||
const chat = useChat(); |
||||
|
||||
useEffect(() => { |
||||
const resetFileInput = () => { |
||||
if (!fileInputRef?.current) { |
||||
return; |
||||
} |
||||
|
||||
fileInputRef.current.value = ''; |
||||
}; |
||||
|
||||
const handleUploadChange = async () => { |
||||
const { mime } = await import('../../../../../../../app/utils/lib/mimeTypes'); |
||||
const filesToUpload = Array.from(fileInputRef?.current?.files ?? []).map((file) => { |
||||
Object.defineProperty(file, 'type', { |
||||
value: mime.lookup(file.name), |
||||
}); |
||||
return file; |
||||
}); |
||||
chat?.flows.uploadFiles(filesToUpload, resetFileInput); |
||||
}; |
||||
|
||||
fileInputRef.current?.addEventListener('change', handleUploadChange); |
||||
return () => fileInputRef?.current?.removeEventListener('change', handleUploadChange); |
||||
}, [chat, fileInputRef]); |
||||
|
||||
const handleUpload = () => { |
||||
fileInputRef?.current?.click(); |
||||
}; |
||||
|
||||
return { |
||||
id: 'file-upload', |
||||
icon: 'clip', |
||||
label: t('File'), |
||||
title: t('File'), |
||||
onClick: handleUpload, |
||||
disabled: !fileUploadEnabled || disabled, |
||||
}; |
||||
}; |
||||
@ -0,0 +1,112 @@ |
||||
import { useUserRoom, useTranslation, useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; |
||||
|
||||
import { messageBox } from '../../../../../../../app/ui-utils/client'; |
||||
import { isTruthy } from '../../../../../../../lib/isTruthy'; |
||||
import { useMessageboxAppsActionButtons } from '../../../../../../hooks/useAppActionButtons'; |
||||
import type { ToolbarAction } from './ToolbarAction'; |
||||
import { useAudioMessageAction } from './useAudioMessageAction'; |
||||
import { useCreateDiscussionAction } from './useCreateDiscussionAction'; |
||||
import { useFileUploadAction } from './useFileUploadAction'; |
||||
import { useShareLocationAction } from './useShareLocationAction'; |
||||
import { useVideoMessageAction } from './useVideoMessageAction'; |
||||
import { useWebdavActions } from './useWebdavActions'; |
||||
|
||||
type ToolbarActionsOptions = { |
||||
variant: 'small' | 'large'; |
||||
canSend: boolean; |
||||
typing: boolean; |
||||
isRecording: boolean; |
||||
isMicrophoneDenied: boolean; |
||||
rid: string; |
||||
tmid?: string; |
||||
}; |
||||
|
||||
const isHidden = (hiddenActions: Array<string>, action: ToolbarAction) => { |
||||
if (!action) { |
||||
return true; |
||||
} |
||||
return hiddenActions.includes(action.id); |
||||
}; |
||||
|
||||
export const useToolbarActions = ({ canSend, typing, isRecording, isMicrophoneDenied, rid, tmid, variant }: ToolbarActionsOptions) => { |
||||
const room = useUserRoom(rid); |
||||
const t = useTranslation(); |
||||
|
||||
const videoMessageAction = useVideoMessageAction(!canSend || typing || isRecording); |
||||
const audioMessageAction = useAudioMessageAction(!canSend || typing || isRecording || isMicrophoneDenied, isMicrophoneDenied); |
||||
const fileUploadAction = useFileUploadAction(!canSend || typing || isRecording); |
||||
const webdavActions = useWebdavActions(); |
||||
const createDiscussionAction = useCreateDiscussionAction(room); |
||||
const shareLocationAction = useShareLocationAction(room, tmid); |
||||
|
||||
const apps = useMessageboxAppsActionButtons(); |
||||
const { composerToolbox: hiddenActions } = useLayoutHiddenActions(); |
||||
|
||||
const allActions = { |
||||
...(!isHidden(hiddenActions, videoMessageAction) && { videoMessageAction }), |
||||
...(!isHidden(hiddenActions, audioMessageAction) && { audioMessageAction }), |
||||
...(!isHidden(hiddenActions, fileUploadAction) && { fileUploadAction }), |
||||
...(!isHidden(hiddenActions, createDiscussionAction) && { createDiscussionAction }), |
||||
...(!isHidden(hiddenActions, shareLocationAction) && { shareLocationAction }), |
||||
...(!hiddenActions.includes('webdav-add') && { webdavActions }), |
||||
}; |
||||
|
||||
const data: { featured: ToolbarAction[]; menu: Array<string | ToolbarAction> } = (() => { |
||||
const featured: Array<ToolbarAction | undefined> = []; |
||||
const createNew = []; |
||||
const share = []; |
||||
|
||||
if (variant === 'small') { |
||||
featured.push(allActions.audioMessageAction); |
||||
createNew.push(allActions.videoMessageAction, allActions.fileUploadAction); |
||||
} else { |
||||
featured.push(allActions.videoMessageAction, allActions.audioMessageAction, allActions.fileUploadAction); |
||||
} |
||||
|
||||
if (allActions.webdavActions) { |
||||
createNew.push(...allActions.webdavActions); |
||||
} |
||||
|
||||
share.push(allActions.shareLocationAction); |
||||
|
||||
const groups = { |
||||
...(apps.isSuccess && |
||||
apps.data.length > 0 && { |
||||
Apps: apps.data, |
||||
}), |
||||
...messageBox.actions.get(), |
||||
}; |
||||
|
||||
const messageBoxActions = Object.entries(groups).reduce<Array<string | ToolbarAction>>((acc, [name, group]) => { |
||||
const items = group |
||||
.filter((item) => !hiddenActions.includes(item.id)) |
||||
.map( |
||||
(item): ToolbarAction => ({ |
||||
id: item.id, |
||||
icon: item.icon, |
||||
label: t(item.label), |
||||
onClick: item.action, |
||||
}), |
||||
); |
||||
|
||||
if (items.length === 0) { |
||||
return acc; |
||||
} |
||||
return [...acc, (t.has(name) && t(name)) || name, ...items]; |
||||
}, []); |
||||
|
||||
const createNewFiltered = createNew.filter(isTruthy); |
||||
const shareFiltered = share.filter(isTruthy); |
||||
|
||||
return { |
||||
featured: featured.filter(isTruthy), |
||||
menu: [ |
||||
...(createNewFiltered.length > 0 ? ['Create_new', ...createNewFiltered] : []), |
||||
...(shareFiltered.length > 0 ? ['Share', ...shareFiltered] : []), |
||||
...messageBoxActions, |
||||
], |
||||
}; |
||||
})(); |
||||
|
||||
return data; |
||||
}; |
||||
@ -0,0 +1,6 @@ |
||||
import { useContext } from 'react'; |
||||
|
||||
import type { LayoutContextValue } from '../LayoutContext'; |
||||
import { LayoutContext } from '../LayoutContext'; |
||||
|
||||
export const useLayoutHiddenActions = (): LayoutContextValue['hiddenActions'] => useContext(LayoutContext).hiddenActions; |
||||
Loading…
Reference in new issue