Clean frontend files

This commit is contained in:
Romaric Mourgues
2023-04-18 11:22:14 +02:00
parent 1d83a583dc
commit 0ff1cc97d4
547 changed files with 1271 additions and 27036 deletions
@@ -1,12 +1,9 @@
import Strings from 'features/global/utils/strings';
import { Message, MessageFileType } from 'features/messages/types/message';
import FileUploadService from 'features/files/services/file-upload-service';
import routerService from 'app/features/router/services/router-service';
import { ChannelType } from 'app/features/channels/types/channel';
import ChannelAPIClient from 'app/features/channels/api/channel-api-client';
import { DriveApiClient } from 'app/features/drive/api-client/api-client';
import { DriveItem } from 'app/features/drive/types';
import DriveService from 'app/deprecated/Apps/Drive/Drive.js';
import routerService from '@features/router/services/router-service';
import { DriveApiClient } from '@features/drive/api-client/api-client';
import { DriveItem } from '@features/drive/types';
import { AttachedFileType } from '@features/files/types/file';
export const highlightText = (text?: string, highlight?: string) => {
if (!text) {
@@ -19,7 +16,7 @@ export const highlightText = (text?: string, highlight?: string) => {
return Strings.removeAccents(text).replace(reg, "<span class='highlight'>$1</span>");
};
export const getFileMessageDownloadRoute = (file: MessageFileType): string => {
export const getFileMessageDownloadRoute = (file: AttachedFileType): string => {
if (file?.metadata?.source === 'internal')
return FileUploadService.getDownloadRoute({
companyId: file.metadata?.external_id?.company_id,
@@ -28,61 +25,24 @@ export const getFileMessageDownloadRoute = (file: MessageFileType): string => {
return '';
};
export const onFileDownloadClick = (file: MessageFileType) => {
export const onFileDownloadClick = (file: AttachedFileType) => {
const url = getFileMessageDownloadRoute(file);
url && (window.location.href = url);
};
export const openChannel = async (channel: ChannelType, currentWorkspaceId: string) => {
if (channel.workspace_id === 'direct') {
const direct = await ChannelAPIClient.getDirect(
channel.company_id || '',
channel.members || [],
);
if (direct) channel = { ...channel, id: direct.id };
}
routerService.push(
routerService.generateRouteFromState({
companyId: channel.company_id,
workspaceId:
(channel.workspace_id === 'direct' ? undefined : channel.workspace_id) ||
currentWorkspaceId,
channelId: channel.id,
}),
);
};
export const openMessage = async (message: Message, currentWorkspaceId: string) => {
routerService.push(
routerService.generateRouteFromState({
companyId: message?.cache?.company_id,
workspaceId:
message?.cache?.workspace_id === 'direct'
? currentWorkspaceId
: message?.cache?.workspace_id,
channelId: message?.cache?.channel_id,
threadId: message?.thread_id,
...(message.id !== message?.thread_id ? { messageId: message.id } : {}),
}),
);
};
export const openDriveItem = (driveItem: DriveItem, workspace_id: string, drive_app_id: string) => {
routerService.push(
routerService.generateRouteFromState({
companyId: driveItem.company_id,
workspaceId: workspace_id ,
workspaceId: workspace_id,
channelId: drive_app_id,
}),
);
DriveService.changeCurrentDirectory(drive_app_id, driveItem.parent_id);
}
};
export const onDriveItemDownloadClick = async (driveItem: DriveItem) => {
const url = await DriveApiClient.getDownloadUrl(driveItem.company_id, driveItem.id);
url && (window.location.href = url);
};
};
@@ -1,83 +0,0 @@
.component-channel-avatar {
display: inline-flex;
flex-direction: column;
.channel-icon-container{
display: flex;
flex-direction: row;
}
$iconWidth: 64px;
.channel-icon-wrapper {
height: $iconWidth;
width: $iconWidth;
border-radius: 50%;
margin-bottom:2px;
align-items: center;
text-align: center;
display: flex;
.channel-icon-text {
line-height: 64px;
width:100%;
font-size: 24px;
color:white;
font-weight: 700;
}
.channel-icon-img {
width:$iconWidth;
height:$iconWidth;
object-fit: cover;
object-position: 50% 15%;
border-radius: 50%;
}
}
.channel-icon-wrapper:not(:first-child){
margin-left:calc($iconWidth*2/5*-1);
}
.channel-label {
text-align: center;
font-size:10px;
line-height: 1.2em;
padding-top:4px;
padding-bottom:4px;
width:$iconWidth;
-webkit-mask-image: linear-gradient(to right, black 85%, transparent 100%);
mask-image: linear-gradient(to right, black 85%, transparent 100%);
overflow: hidden;
-webkit-mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
overflow-y: hidden;
height: 40px;
}
.channel-icon-wrapper.style0{
background: var(--messagebubblelight);
}
.channel-icon-wrapper.style1 {background: linear-gradient(129.62deg, #9F2EF4 15.98%, #F97D64 50.17%, #F6C533 82.97%);}
.channel-icon-wrapper.style2 {background: linear-gradient(124.19deg, #0099C0 12.87%, #0099C0 49.65%, #3DD5A8 84.94%);}
.channel-icon-wrapper.style3 {background: linear-gradient(137.24deg, #D3A6FF 15.1%, #B966B5 50.52%, #7E72C6 85.13%);}
.channel-icon-wrapper.style4 {background: linear-gradient(124.08deg, #E2EF54 14.56%, #FF5C66 47.57%, #6248D5 83.84%);}
.channel-icon-wrapper.style5 {background: linear-gradient(120.32deg, #FF5CF1 14.48%, #B38ADE 51.66%, #56CDDF 84.61%);}
.channel-icon-wrapper.style6 {background: linear-gradient(120.32deg, #75C192 14.48%, #70BDA0 51.66%, #21B59C 84.61%);}
.channel-icon-wrapper.style7 {background: linear-gradient(136.93deg, #FFA6AF 14.87%, #E62E40 50.07%, #AA0909 84.36%);}
.channel-icon-wrapper.style8 {background: linear-gradient(136.93deg, #335F50 14.87%, #47888C 50.07%, #08C992 84.36%);}
.channel-icon-wrapper.style9 {background: linear-gradient(135.32deg, #6CD97E 15.23%, #12B312 56.97%, #117600 84.49%);}
.channel-icon-wrapper.style10 {background: linear-gradient(135deg, #7DF1FA 14.84%, #2BB4D6 49.93%, #008AA2 82.63%);}
.channel-icon-wrapper.style11 {background: linear-gradient(136.26deg, #FFBF80 13.66%, #E66B2E 51.13%, #A64300 84.79%);}
}
@@ -1,108 +0,0 @@
import { ChannelType } from 'features/channels/types/channel';
import React from 'react';
import './channel-avatar.scss';
import UsersService from 'features/users/services/current-user-service';
import UserService from 'features/users/services/current-user-service';
import { UserType } from 'features/users/types/user';
import { addApiUrlIfNeeded } from 'features/global/utils/URLUtils';
import emoji from 'emoji-name-map';
import _ from 'lodash';
type PropsType = {
channel: ChannelType;
showLabel: boolean;
collapseToOne?: boolean;
};
export default ({ channel, showLabel, collapseToOne }: PropsType): JSX.Element => {
if (channel.visibility === 'direct') {
let channelUsers = _.uniqBy(channel.users || [], 'id')
.filter(e => e.id !== UsersService.getCurrentUserId() || channel.users?.length === 1)
.filter(a => a);
if (channelUsers.length === 0) {
channelUsers = [UsersService.getCurrentUser()];
}
const channelName: string[] = [];
const icons = [] as JSX.Element[];
channelUsers.forEach(member => {
channelName.push(UserService.getFullName(member));
if (!(collapseToOne && icons.length > 0)) {
icons.push(getThumbnail(member));
}
});
let width = 64;
if (!collapseToOne && channelUsers.length > 1) {
const shift = 64 - (64 * 2) / 5;
width = 64 + (channelUsers.length - 1) * shift;
}
return (
<div className="component-channel-avatar">
<div className="channel-icon-container">{icons.map(icon => icon)}</div>
{showLabel && (
<div className="channel-label" style={{ width: width }}>
{channelName.join(', ')}
</div>
)}
</div>
);
}
let thumbnail;
const label = channel?.name;
if (channel.icon) {
thumbnail = emoji.get(channel.icon);
}
if (!thumbnail && channel.name) {
thumbnail = '';
const channelName =
channel.name.indexOf(' ') < 0 ? channel.name.replace('_', ' ') : channel.name;
const split = channelName.toUpperCase().split(' ');
thumbnail += (split?.[0]?.[0] || '') + (split?.[1]?.[0] || '');
}
const style = getStyle(channel.id);
return (
<div className="component-channel-avatar">
<div className={'channel-icon-wrapper' + style}>
<div className="channel-icon-text">{thumbnail}</div>
</div>
{showLabel && <div className="channel-label">{label}</div>}
</div>
);
};
const getStyle = (id?: string) => {
if (!id) return '';
let output = 0;
for (let i = 0; i < id.length; i++) {
output += id[i].charCodeAt(0);
}
const i = (output % 10) + 1;
return ` style${i}`;
};
const getThumbnail = (user: UserType) => {
let inner;
let style = '';
if (user && (user.thumbnail || user.picture)) {
const thumbnail = addApiUrlIfNeeded(user.picture || user.thumbnail || '');
inner = <img alt={user.first_name} className="channel-icon-img" src={thumbnail} />;
} else {
const fullName = UserService.getFullName(user).toUpperCase().split(' ');
const letters = (fullName?.[0]?.[0] || '?') + (fullName?.[1]?.[0] || '');
inner = <div className="channel-icon-text">{letters}</div>;
style = getStyle(user.id);
}
return <div className={`channel-icon-wrapper${style}`}>{inner}</div>;
};
@@ -1,90 +0,0 @@
import * as Text from '@atoms/text';
import { ChannelType } from 'app/features/channels/types/channel';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { useSearchModal } from 'app/features/search/hooks/use-search';
import { SearchInputState } from 'app/features/search/state/search-input';
import ChannelAvatar from 'app/components/search-popup/parts/channel-avatar/channel-avatar';
import Highlighter from 'react-highlight-words';
import { useRecoilValue } from 'recoil';
import { openChannel } from '../common';
import { Button } from 'app/atoms/button/button';
import Languages from 'app/features/global/services/languages-service';
import { useChannel } from 'app/features/channels/hooks/use-channel';
import { useChannelNotifications } from 'app/features/users/hooks/use-notifications';
import { Badge } from '@atoms/badge';
import UserService from 'features/users/services/current-user-service';
import ResultContext from './result-context';
import _ from 'lodash';
type PropsType = {
channel: ChannelType;
};
export default ({ channel: _channel }: PropsType): JSX.Element => {
const channel =
(_channel.visibility !== 'direct'
? useChannel(_channel.id || '', {
companyId: _channel.company_id || '',
workspaceId: _channel.workspace_id || '',
})?.channel
: undefined) || _channel;
const currentWorkspaceId = useRouterWorkspace();
const input = useRecoilValue(SearchInputState);
const name =
channel.name ||
_.uniqBy(channel.users || [], 'id')
.filter(u => u.id != UserService.getCurrentUserId() || channel.users?.length === 1)
.map(u => UserService.getFullName(u).trim())
.join(', ');
const { setOpen } = useSearchModal();
const notifications = useChannelNotifications(channel.id || '');
return (
<div
className="flex items-center p-2 hover:bg-zinc-50 rounded-md cursor-pointer"
onClick={() => {
openChannel(channel, currentWorkspaceId);
setOpen(false);
}}
>
<div className="mr-3">
<ChannelAvatar channel={channel} showLabel={false} collapseToOne={true} />
</div>
<div className="grow mr-3 overflow-hidden text-ellipsis whitespace-nowrap">
<Text.Base className="block overflow-hidden text-ellipsis whitespace-nowrap">
<Highlighter
highlightClassName="text-blue-500 p-0 bg-blue-50 overflow-hidden text-ellipsis whitespace-nowrap"
searchWords={input?.query?.split(' ')}
autoEscape={true}
textToHighlight={name}
/>
</Text.Base>
<Text.Info className="block">
{channel?.stats?.members || channel.users?.length || 0} members
</Text.Info>
<ResultContext context={{ workspaceId: channel.workspace_id || '' }} />
</div>
<div className="text-right">
{!channel.user_member && (
<>
<Button theme="outline">Preview</Button>
<br />
<Text.Info>
{Languages.t(
'scenes.client.channelsbar.modals.workspace_channel_list.workspace_channel_row.tag',
)}
</Text.Info>
</>
)}
{!!channel.user_member && (notifications?.badges?.length || 0) > 0 && (
<>
<Badge size="sm" className="rounded-full px-2.5" theme="primary">
{notifications.badges.length}
</Badge>
</>
)}
</div>
</div>
);
};
@@ -1,45 +0,0 @@
import { ChannelType } from 'app/features/channels/types/channel';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { useSearchModal } from 'app/features/search/hooks/use-search';
import ChannelAvatar from 'app/components/search-popup/parts/channel-avatar/channel-avatar';
import { openChannel } from '../common';
import { useChannelNotifications } from 'app/features/users/hooks/use-notifications';
import { useChannel } from 'app/features/channels/hooks/use-channel';
import { Badge } from 'app/atoms/badge';
type PropsType = {
channel: ChannelType;
};
export default ({ channel: _channel }: PropsType): JSX.Element => {
const channel =
(_channel.visibility !== 'direct'
? useChannel(_channel.id || '', {
companyId: _channel.company_id || '',
workspaceId: _channel.workspace_id || '',
})?.channel
: undefined) || _channel;
const currentWorkspaceId = useRouterWorkspace();
const { setOpen } = useSearchModal();
const notifications = useChannelNotifications(channel.id || '');
return (
<div
className="relative"
onClick={() => {
openChannel(channel, currentWorkspaceId);
setOpen(false);
}}
>
<ChannelAvatar channel={channel} showLabel={true} collapseToOne={true} />
{!!channel.user_member && !!notifications?.badges?.length && (
<>
<Badge size="sm" className="rounded-full px-2.5 absolute -right-2 top-0" theme="primary">
{notifications.badges.length}
</Badge>
</>
)}
</div>
);
};
@@ -1,8 +1,11 @@
import * as Text from '@atoms/text';
import FileUploadAPIClient from '@features/files/api/file-upload-api-client';
import { Button } from 'app/atoms/button/button';
import { DownloadIcon } from 'app/atoms/icons-agnostic';
import { FolderIcon } from '@heroicons/react/solid';
import { ArrowRight } from 'react-feather';
import Highlighter from 'react-highlight-words';
import { useRecoilState, useRecoilValue } from 'recoil';
import { onDriveItemDownloadClick, openDriveItem } from '../common';
import ResultContext from './result-context';
import { Button } from '@atoms/button/button';
import { DownloadIcon } from '@atoms/icons-agnostic';
import {
FileTypeArchiveIcon,
FileTypeDocumentIcon,
@@ -10,23 +13,20 @@ import {
FileTypeSlidesIcon,
FileTypeSpreadsheetIcon,
FileTypeUnknownIcon,
} from 'app/atoms/icons-colored';
import { formatDate } from 'app/features/global/utils/format-date';
import { formatSize } from 'app/features/global/utils/format-file-size';
import { DriveItem } from 'app/features/drive/types';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { useSearchModal } from 'app/features/search/hooks/use-search';
import { SearchInputState } from 'app/features/search/state/search-input';
import { UserType } from 'app/features/users/types/user';
import { useFileViewerModal } from 'app/features/viewer/hooks/use-viewer';
import Media from 'app/molecules/media';
import Highlighter from 'react-highlight-words';
import { useRecoilValue, useRecoilState } from 'recoil';
import { openDriveItem, onDriveItemDownloadClick } from '../common';
import ResultContext from './result-context';
import { useCompanyApplications } from 'app/features/applications/hooks/use-company-applications';
import { DriveCurrentFolderAtom } from 'app/views/client/body/drive/browser';
import { FolderIcon } from '@heroicons/react/solid';
} from '@atoms/icons-colored';
import * as Text from '@atoms/text';
import { useCompanyApplications } from '@features/applications/hooks/use-company-applications';
import { DriveItem } from '@features/drive/types';
import FileUploadAPIClient from '@features/files/api/file-upload-api-client';
import { formatDate } from '@features/global/utils/format-date';
import { formatSize } from '@features/global/utils/format-file-size';
import useRouterWorkspace from '@features/router/hooks/use-router-workspace';
import { useSearchModal } from '@features/search/hooks/use-search';
import { SearchInputState } from '@features/search/state/search-input';
import { UserType } from '@features/users/types/user';
import { useFileViewerModal } from '@features/viewer/hooks/use-viewer';
import Media from '@molecules/media';
import { DriveCurrentFolderAtom } from '@views/client/body/drive/browser';
export default (props: { driveItem: DriveItem & { user?: UserType } }) => {
const input = useRecoilValue(SearchInputState);
@@ -63,12 +63,7 @@ export default (props: { driveItem: DriveItem & { user?: UserType } }) => {
{extension?.toLocaleUpperCase()} {formatDate(parseInt(file?.last_modified))} {' '}
{formatSize(file?.size)}
</Text.Info>
<ResultContext
user={file.user}
context={{
companyId: file?.company_id,
}}
/>
<ResultContext user={file.user} />
</div>
<div
className="whitespace-nowrap"
@@ -1,139 +0,0 @@
import * as Text from '@atoms/text';
import FileUploadAPIClient from '@features/files/api/file-upload-api-client';
import { Button } from 'app/atoms/button/button';
import { DownloadIcon } from 'app/atoms/icons-agnostic';
import {
FileTypeArchiveIcon,
FileTypeDocumentIcon,
FileTypePdfIcon,
FileTypeSlidesIcon,
FileTypeSpreadsheetIcon,
FileTypeUnknownIcon,
} from 'app/atoms/icons-colored';
import { formatDate } from 'app/features/global/utils/format-date';
import { formatSize } from 'app/features/global/utils/format-file-size';
import { Message, MessageFileType } from 'app/features/messages/types/message';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { useSearchModal } from 'app/features/search/hooks/use-search';
import { SearchInputState } from 'app/features/search/state/search-input';
import { UserType } from 'app/features/users/types/user';
import { useFileViewerModal } from 'app/features/viewer/hooks/use-viewer';
import Media from 'app/molecules/media';
import { ArrowRight } from 'react-feather';
import Highlighter from 'react-highlight-words';
import { useRecoilValue } from 'recoil';
import { onFileDownloadClick, openMessage } from '../common';
import ResultContext from './result-context';
export default (props: { file: MessageFileType & { message?: Message } & { user?: UserType } }) => {
const input = useRecoilValue(SearchInputState);
const currentWorkspaceId = useRouterWorkspace();
const file = props.file;
const name = file?.metadata?.name;
const extension = name?.split('.').pop();
const { setOpen } = useSearchModal();
const { open: openViewer } = useFileViewerModal();
return (
<div
className="flex items-center p-2 hover:bg-zinc-50 rounded-md cursor-pointer"
onClick={() => openViewer(file)}
>
<FileResultMedia file={file} className="w-16 h-16 mr-3" />
<div className="grow mr-3 overflow-hidden">
<Text.Base className="block whitespace-nowrap overflow-hidden text-ellipsis">
<Highlighter
highlightClassName="text-blue-500 p-0 bg-blue-50"
searchWords={input?.query?.split(' ')}
autoEscape={true}
textToHighlight={name}
/>
</Text.Base>
<Text.Info className="block">
{extension?.toLocaleUpperCase()} {formatDate(file?.message?.created_at)} {' '}
{formatSize(file?.metadata?.size)}
</Text.Info>
<ResultContext
user={file.user}
context={{
channelId: file.message?.cache?.channel_id,
workspaceId: file.message?.cache?.workspace_id,
companyId: file.message?.cache?.company_id,
}}
/>
</div>
<div
className="whitespace-nowrap"
onClick={e => {
e.preventDefault();
e.stopPropagation();
}}
>
<Button
theme="outline"
className="w-9 px-1.5 ml-2 rounded-full border-none"
onClick={() => onFileDownloadClick(file)}
>
<DownloadIcon className="text-blue-500 w-6 h-6" />
</Button>
{!!file.message && (
<Button
theme="outline"
className="w-9 px-1.5 ml-2 rounded-full border-none"
onClick={() => {
openMessage(file.message as Message, currentWorkspaceId);
setOpen(false);
}}
>
<ArrowRight className="text-blue-500 w-6 h-6" />
</Button>
)}
</div>
</div>
);
};
export const FileResultMedia = (props: {
className?: string;
size?: 'md' | 'lg' | 'sm';
file: MessageFileType & { message?: Message } & { user?: UserType };
}) => {
const file = props.file;
const name = file?.metadata?.name;
const type = FileUploadAPIClient.mimeToType(file?.metadata?.mime || '');
const url = FileUploadAPIClient.getFileThumbnailUrlFromMessageFile(file);
const extension = name?.split('.').pop();
let iconClassName = 'absolute left-0 top-0 bottom-0 right-0 m-auto w-8 h-8';
if (url) iconClassName = 'absolute bottom-1 left-1 w-6 h-6';
return (
<div className={'relative flex bg-zinc-200 rounded-md ' + (props.className || '')}>
<Media
size={props.size || 'md'}
url={url}
duration={type === 'video' ? extension : undefined}
/>
{(!['image', 'video'].includes(type) || !url) && (
<>
{type === 'archive' ? (
<FileTypeArchiveIcon className={iconClassName} />
) : type === 'pdf' ? (
<FileTypePdfIcon className={iconClassName} />
) : type === 'document' ? (
<FileTypeDocumentIcon className={iconClassName} />
) : type === 'spreadsheet' ? (
<FileTypeSpreadsheetIcon className={iconClassName} />
) : type === 'slides' ? (
<FileTypeSlidesIcon className={iconClassName} />
) : (
<FileTypeUnknownIcon className={iconClassName} />
)}
</>
)}
</div>
);
};
@@ -1,29 +0,0 @@
import FileUploadAPIClient from '@features/files/api/file-upload-api-client';
import { MessageFileType } from '@features/messages/types/message';
import Media from '@molecules/media';
import { useFileViewerModal } from 'app/features/viewer/hooks/use-viewer';
export default (props: { file: MessageFileType }) => {
const file = props.file;
const url = FileUploadAPIClient.getFileThumbnailUrlFromMessageFile(file);
const type = FileUploadAPIClient.mimeToType(file?.metadata?.mime || '');
const { open: openViewer } = useFileViewerModal();
return (
<div
className="cursor-pointer hover:opacity-75 inline-block m-2"
onClick={() => openViewer(file)}
>
<Media
key={file.id}
size="lg"
url={url}
duration={
type === 'video'
? file?.metadata?.name?.split('.').slice(-1)?.[0]?.toLocaleUpperCase()
: undefined
}
/>
</div>
);
};
@@ -1,79 +0,0 @@
import * as Text from '@atoms/text';
import { Button } from 'app/atoms/button/button';
import { DownloadIcon } from 'app/atoms/icons-agnostic';
import { formatDate } from 'app/features/global/utils/format-date';
import { formatSize } from 'app/features/global/utils/format-file-size';
import { Message, MessageFileType } from 'app/features/messages/types/message';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { useSearchModal } from 'app/features/search/hooks/use-search';
import { SearchInputState } from 'app/features/search/state/search-input';
import { UserType } from 'app/features/users/types/user';
import { useFileViewerModal } from 'app/features/viewer/hooks/use-viewer';
import { ArrowRight } from 'react-feather';
import Highlighter from 'react-highlight-words';
import { useRecoilValue } from 'recoil';
import { onFileDownloadClick, openMessage } from '../common';
import { FileResultMedia } from './file-result';
export default (props: { file: MessageFileType & { message?: Message } & { user?: UserType } }) => {
const input = useRecoilValue(SearchInputState);
const currentWorkspaceId = useRouterWorkspace();
const file = props.file;
const name = file?.metadata?.name;
const extension = name?.split('.').pop();
const { setOpen } = useSearchModal();
const { open: openViewer } = useFileViewerModal();
return (
<div
className="flex items-center p-2 rounded-md cursor-pointer"
onClick={() => openViewer(file)}
>
<FileResultMedia size="sm" file={file} className=" w-12 h-12 mr-3" />
<div className="grow mr-3 overflow-hidden">
<Text.Base className="block whitespace-nowrap overflow-hidden text-ellipsis">
<Highlighter
highlightClassName="text-blue-500 p-0 bg-blue-50"
searchWords={input?.query?.split(' ')}
autoEscape={true}
textToHighlight={name}
/>
</Text.Base>
<Text.Info className="block">
{extension?.toLocaleUpperCase()} {formatDate(file?.message?.created_at)} {' '}
{formatSize(file?.metadata?.size)}
</Text.Info>
</div>
<div
className="whitespace-nowrap"
onClick={e => {
e.preventDefault();
e.stopPropagation();
}}
>
<Button
theme="outline"
className="w-9 px-1.5 ml-2 rounded-full border-none"
onClick={() => onFileDownloadClick(file)}
>
<DownloadIcon className="text-blue-500 w-6 h-6" />
</Button>
{!!file.message && (
<Button
theme="outline"
className="w-9 px-1.5 ml-2 rounded-full border-none"
onClick={() => {
openMessage(file.message as Message, currentWorkspaceId);
setOpen(false);
}}
>
<ArrowRight className="text-blue-500 w-6 h-6" />
</Button>
)}
</div>
</div>
);
};
@@ -1,90 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import A from '@atoms/link';
import * as Text from '@atoms/text';
import { formatDate } from '@features/global/utils/format-date';
import { Message } from '@features/messages/types/message';
import ChannelAvatar from 'app/components/search-popup/parts/channel-avatar/channel-avatar';
import { useChannel } from 'app/features/channels/hooks/use-channel';
import { ChannelType } from 'app/features/channels/types/channel';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { useSearchModal } from 'app/features/search/hooks/use-search';
import { SearchInputState } from 'app/features/search/state/search-input';
import { useUser } from 'app/features/users/hooks/use-user';
import { UserType } from 'app/features/users/types/user';
import { useState } from 'react';
import Highlighter from 'react-highlight-words';
import { useRecoilValue } from 'recoil';
import { openMessage } from '../common';
import MessageResultFile from './message-result-file';
import ResultContext from './result-context';
export default ({
message,
}: {
message: Message & { users?: UserType[]; channel?: ChannelType };
}) => {
const input = useRecoilValue(SearchInputState);
const currentWorkspaceId = useRouterWorkspace();
const user =
(message.users || []).find(u => u.id === message.user_id) || useUser(message.user_id || '');
const channel = message.channel || useChannel(message?.cache?.channel_id || '').channel;
const [truncated, setTruncated] = useState(true);
const { setOpen } = useSearchModal();
return (
<div
className="w-full flex items-start p-2 hover:bg-zinc-50 rounded-md cursor-pointer"
onClick={() => {
openMessage(message, currentWorkspaceId);
setOpen(false);
}}
>
<div className="mr-3">
{channel && <ChannelAvatar channel={channel} showLabel={false} collapseToOne={true} />}
</div>
<div className="grow mr-3 overflow-hidden">
<div className="messages-title overflow-hidden whitespace-nowrap text-ellipsis">
<ResultContext
user={user}
context={{
companyId: message?.cache?.company_id,
workspaceId: message?.cache?.workspace_id,
channelId: message?.cache?.channel_id,
}}
/>
</div>
<div className="messages-text">
<Text.BaseSmall className="block whitespace-pre-line">
<Highlighter
highlightClassName="text-blue-500 p-0 bg-blue-50"
searchWords={input?.query?.split(' ')}
autoEscape={true}
textToHighlight={message.text?.substring(0, truncated ? 500 : message.text.length)}
/>
{truncated && (message.text?.length || 0) > 500 && (
<A
className="ml-3"
onClick={(e: any) => {
e.stopPropagation();
setTruncated(false);
}}
>
See more
</A>
)}
</Text.BaseSmall>
{(message.files || []).map(file => (
<div
key={file.id}
onClick={(e: any) => e.stopPropagation()}
className="rounded-md border bg-white border-gray-200 hover:border-gray-300 mb-1"
>
<MessageResultFile file={{ ...file, message, user }} />
</div>
))}
</div>
<Text.Info className="block">{formatDate(message.created_at)}</Text.Info>
</div>
</div>
);
};
@@ -1,6 +1,6 @@
import { NotFoundIcon } from 'app/atoms/icons-colored';
import Languages from 'app/features/global/services/languages-service';
import { SearchInputState } from 'app/features/search/state/search-input';
import { NotFoundIcon } from '@atoms/icons-colored';
import Languages from '@features/global/services/languages-service';
import { SearchInputState } from '@features/search/state/search-input';
import { useRecoilValue } from 'recoil';
import * as Text from '@atoms/text';
@@ -1,5 +1,5 @@
import { NotFoundIcon } from 'app/atoms/icons-colored';
import Languages from 'app/features/global/services/languages-service';
import { NotFoundIcon } from '@atoms/icons-colored';
import Languages from '@features/global/services/languages-service';
import * as Text from '@atoms/text';
export default () => {
@@ -1,62 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import A from '@atoms/link';
import { getChannel } from '@features/channels/hooks/use-channel';
import { UserType } from '@features/users/types/user';
import { useWorkspace } from '@features/workspaces/hooks/use-workspaces';
import UsersService from '@features/users/services/current-user-service';
import RouterServices from '@features/router/services/router-service';
import useRouterCompany from 'app/features/router/hooks/use-router-company';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { useSearchModal } from 'app/features/search/hooks/use-search';
export default ({
user,
context,
}: {
user?: UserType;
context?: { channelId?: string; workspaceId?: string; companyId?: string };
}) => {
const channel = getChannel(context?.channelId || '');
const routerWorkspace = useRouterWorkspace();
const routerCompany = useRouterCompany();
const workspaceId =
(context?.workspaceId !== 'direct' ? context?.workspaceId : '') || routerWorkspace;
const { workspace } = useWorkspace(workspaceId || '');
const companyId = context?.companyId || routerCompany;
const { setOpen } = useSearchModal();
const workspaceName =
context?.workspaceId === 'direct' ? 'Direct' : workspace?.name ? workspace?.name : '';
import { UserType } from '@features/users/types/user';
export default ({ user }: { user?: UserType }) => {
return (
<div className="flex overflow-hidden whitespace-nowrap text-ellipsis">
{!!user && <>{UsersService.getFullName(user)}</>}
{!!user && !!(channel || workspaceName) && <span className="mx-2"> {'->'} </span>}
{(channel || workspaceName) && (
<>
{!!workspaceName && <span className="opacity-50">{workspaceName}</span>}
{!!workspaceName && !!channel?.name && <span className="mx-2"> / </span>}
{!!channel?.name && (
<A
onClick={(e: any) => {
e.stopPropagation();
setOpen(false);
RouterServices.push(
RouterServices.generateRouteFromState({
companyId: companyId || '',
workspaceId: workspaceId || '',
channelId: context?.channelId || '',
}),
);
}}
>
{channel?.name}
</A>
)}
</>
)}
</div>
);
};
@@ -1,19 +1,12 @@
import { InputDecorationIcon } from '@atoms/input/input-decoration-icon';
import { SearchIcon } from '@heroicons/react/solid';
import { Input } from 'app/atoms/input/input-text';
import { Loader } from 'app/atoms/loader';
import Languages from 'app/features/global/services/languages-service';
import { useSearchChannelsLoading } from 'app/features/search/hooks/use-search-channels';
import {
useSearchMessagesFilesLoading,
useSearchMessagesMediasLoading,
} from 'app/features/search/hooks/use-search-files-or-medias';
import { useSearchDriveItemsLoading } from 'app/features/search/hooks/use-search-drive-items';
import { useSearchMessagesLoading } from 'app/features/search/hooks/use-search-messages';
import { SearchInputState } from 'app/features/search/state/search-input';
import _ from 'lodash';
import { useEffect, useRef } from 'react';
import { useRecoilState } from 'recoil';
import { InputDecorationIcon } from '@atoms/input/input-decoration-icon';
import { Input } from '@atoms/input/input-text';
import { Loader } from '@atoms/loader';
import Languages from '@features/global/services/languages-service';
import { useSearchDriveItemsLoading } from '@features/search/hooks/use-search-drive-items';
import { SearchInputState } from '@features/search/state/search-input';
export const SearchInput = () => {
const [input, setInput] = useRecoilState(SearchInputState);
@@ -23,14 +16,9 @@ export const SearchInput = () => {
if (inputElement.current) inputElement.current.focus();
}, []);
const channelsLoading = useSearchChannelsLoading();
const messagesLoading = useSearchMessagesLoading();
const filesLoading = useSearchMessagesFilesLoading();
const mediasLoading = useSearchMessagesMediasLoading();
const driveItemsLoading = useSearchDriveItemsLoading();
const loading =
channelsLoading || messagesLoading || filesLoading || mediasLoading || driveItemsLoading;
const loading = driveItemsLoading;
return (
<div className="relative flex mt-2 w-full">
@@ -1,6 +1,6 @@
import { Modal, ModalContent } from '@atoms/modal';
import Languages from 'app/features/global/services/languages-service';
import { useSearchModal } from 'app/features/search/hooks/use-search';
import Languages from '@features/global/services/languages-service';
import { useSearchModal } from '@features/search/hooks/use-search';
import { SearchInput } from './search-input';
import { SearchResultsIndex } from './search-tabs';
@@ -1,117 +0,0 @@
import * as Text from '@atoms/text';
import A from 'app/atoms/link';
import Languages from 'app/features/global/services/languages-service';
import { useSearchChannels } from 'app/features/search/hooks/use-search-channels';
import {
useSearchMessagesFiles,
useSearchMessagesMedias,
} from 'app/features/search/hooks/use-search-files-or-medias';
import { useSearchMessages } from 'app/features/search/hooks/use-search-messages';
import { SearchInputState, SearchTabsState } from 'app/features/search/state/search-input';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { ChannelsResults, ChannelsRowResults } from './channels';
import { FilesResults } from './files';
import { MediasResults } from './medias';
import { MessagesResults } from './messages';
export default () => {
const setTab = useSetRecoilState(SearchTabsState);
const input = useRecoilValue(SearchInputState);
const isRecent = input?.query?.trim()?.length === 0;
const { channels } = useSearchChannels();
const { messages } = useSearchMessages();
const { files } = useSearchMessagesFiles();
const { files: medias } = useSearchMessagesMedias();
return (
<div className="-mt-4">
{channels.length > 0 && (
<>
<div className="flex mt-4">
<Text.Subtitle className="block grow">
{isRecent
? Languages.t('components.searchpopup.recent_channels_and_contacts')
: Languages.t('components.searchpopup.channels')}
</Text.Subtitle>
{channels.length > 6 && (
<div className="w-auto">
<A onClick={() => setTab('channels')}>
{Languages.t('components.searchpopup.see_more')}
</A>
</div>
)}
</div>
{isRecent && (
<div className="-mx-2 mt-2">
<ChannelsRowResults max={6} />
</div>
)}
{!isRecent && (
<div className="-mx-2">
<ChannelsResults max={6} />
</div>
)}
</>
)}
{medias.length > 0 && (
<>
<div className="flex mt-4">
<Text.Subtitle className="block grow">
{isRecent
? Languages.t('components.searchpopup.recent_media')
: Languages.t('components.searchpopup.media')}
</Text.Subtitle>
{medias.length > 6 && (
<div className="w-auto">
<A onClick={() => setTab('medias')}>
{Languages.t('components.searchpopup.see_more')}
</A>
</div>
)}
</div>
<div className={'-mx-2'}>
<MediasResults max={6} showAsFiles={!isRecent} />
</div>
</>
)}
{files.length > 0 && (
<>
<div className="flex mt-4">
<Text.Subtitle className="block grow">
{isRecent
? Languages.t('components.searchpopup.recent_files')
: Languages.t('components.searchpopup.files')}
</Text.Subtitle>
{files.length > 6 && (
<div className="w-auto">
<A onClick={() => setTab('files')}>
{Languages.t('components.searchpopup.see_more')}
</A>
</div>
)}
</div>
<div className={'-mx-2'}>
<FilesResults max={6} />
</div>
</>
)}
{messages.length > 0 && (
<>
<div className="flex mt-4">
<Text.Subtitle className="block grow">
{Languages.t('components.searchpopup.messages')}
</Text.Subtitle>
</div>
<div className="-mx-2">
<MessagesResults />
</div>
</>
)}
</div>
);
};
@@ -1,87 +0,0 @@
import * as Text from '@atoms/text';
import Languages from 'app/features/global/services/languages-service';
import { useSearchChannels } from 'app/features/search/hooks/use-search-channels';
import { SearchInputState } from 'app/features/search/state/search-input';
import { useRecoilValue } from 'recoil';
import ChannelLineResult from '../parts/channel-line-result';
import ChannelResult from '../parts/channel-result';
import NothingFound from '../parts/nothing-found';
export default () => {
const input = useRecoilValue(SearchInputState);
const isRecent = input?.query?.trim()?.length === 0;
const { channels, loading } = useSearchChannels();
if (channels.length === 0 && !loading) return <NothingFound />;
const topChannels = channels
.slice()
.filter(a => Date.now() - (a.last_activity || 0) < 1000 * 60 * 60 * 24 * 31)
.sort((a, b) => Math.min(50, b?.stats?.messages || 0) - Math.min(50, a?.stats?.messages || 0))
.slice(0, 5);
return (
<div>
{!!isRecent && !!topChannels.length && (
<div className="mb-4">
<Text.Subtitle className="block">
{Languages.t('components.searchpopup.recent_channels_and_contacts')}
</Text.Subtitle>
<div className="-mx-2 mt-2 items-top">
{topChannels.map(channel => (
<div
className="mx-2 inline-block align-top hover:opacity-75 cursor-pointer"
key={channel.id}
>
<ChannelResult channel={channel} />
</div>
))}
</div>
</div>
)}
{!!isRecent && (
<Text.Subtitle className="block">
{Languages.t('components.searchpopup.channels')}
</Text.Subtitle>
)}
<div className={'-mx-2'}>
<ChannelsResults />
</div>
</div>
);
};
export const ChannelsRowResults = (props: { max?: number }) => {
const { channels, loading } = useSearchChannels();
if (channels.length === 0 && !loading) return <NothingFound />;
return (
<>
{channels.slice(0, props?.max || channels.length).map(channel => (
<div
className="mx-2 inline-block align-top hover:opacity-75 cursor-pointer"
key={channel.id}
>
<ChannelResult channel={channel} />
</div>
))}
</>
);
};
export const ChannelsResults = (props: { max?: number }) => {
const { channels, loading } = useSearchChannels();
if (channels.length === 0 && !loading) return <NothingFound />;
return (
<>
{channels.slice(0, props?.max || channels.length).map(channel => (
<ChannelLineResult key={channel.id} channel={channel} />
))}
</>
);
};
@@ -1,7 +1,7 @@
import * as Text from '@atoms/text';
import Languages from 'app/features/global/services/languages-service';
import { useSearchDriveItems } from 'app/features/search/hooks/use-search-drive-items';
import { SearchInputState } from 'app/features/search/state/search-input';
import Languages from '@features/global/services/languages-service';
import { useSearchDriveItems } from '@features/search/hooks/use-search-drive-items';
import { SearchInputState } from '@features/search/state/search-input';
import { useRecoilValue } from 'recoil';
import DriveItemResult from '../parts/drive-item-result';
import NothingFound from '../parts/nothing-found';
@@ -1,43 +0,0 @@
import * as Text from '@atoms/text';
import Languages from 'app/features/global/services/languages-service';
import { useSearchMessagesFiles } from 'app/features/search/hooks/use-search-files-or-medias';
import { SearchInputState } from 'app/features/search/state/search-input';
import { useRecoilValue } from 'recoil';
import FileResult from '../parts/file-result';
import NothingFound from '../parts/nothing-found';
export default () => {
const input = useRecoilValue(SearchInputState);
const isRecent = input?.query?.trim()?.length === 0;
const { files, loading } = useSearchMessagesFiles();
if (files.length === 0 && !loading) return <NothingFound />;
return (
<div>
{!!isRecent && (
<Text.Subtitle className="block">
{Languages.t('components.searchpopup.recent_files')}
</Text.Subtitle>
)}
<div className={'-mx-2'}>
<FilesResults />
</div>
</div>
);
};
export const FilesResults = (props: { max?: number }) => {
const { files, loading } = useSearchMessagesFiles();
if (files.length === 0 && !loading) return <NothingFound />;
return (
<>
{files.slice(0, props?.max || files.length).map(file => (
<FileResult key={file.id} file={file} />
))}
</>
);
};
@@ -1,51 +0,0 @@
import * as Text from '@atoms/text';
import FileUploadAPIClient from '@features/files/api/file-upload-api-client';
import Languages from '@features/global/services/languages-service';
import { useSearchMessagesMedias } from '@features/search/hooks/use-search-files-or-medias';
import { SearchInputState } from '@features/search/state/search-input';
import { useRecoilValue } from 'recoil';
import FileResult from '../parts/file-result';
import MediaResult from '../parts/media-result';
import NothingFound from '../parts/nothing-found';
export default () => {
const input = useRecoilValue(SearchInputState);
const isRecent = input?.query?.trim()?.length === 0;
return (
<div>
{!!isRecent && (
<Text.Subtitle className="block">
{Languages.t('components.searchpopup.recent_media')}
</Text.Subtitle>
)}
<div className="-mx-2">
<MediasResults showAsFiles={!isRecent} />
</div>
</div>
);
};
export const MediasResults = (props: { max?: number; showAsFiles?: boolean }) => {
const { files, loading } = useSearchMessagesMedias();
if (files.length === 0 && !loading) return <NothingFound />;
return (
<>
{files
.slice(0, props?.max || files.length)
.map(file => {
const url = FileUploadAPIClient.getFileThumbnailUrlFromMessageFile(file);
if (url || props.showAsFiles)
return props.showAsFiles ? (
<FileResult key={file.id} file={file} />
) : (
<MediaResult key={file.id} file={file} />
);
})
.filter(a => a)}
</>
);
};
@@ -1,41 +0,0 @@
import { SearchInputState } from '@features/search/state/search-input';
import { useSearchMessages } from 'app/features/search/hooks/use-search-messages';
import { useRecoilValue } from 'recoil';
import MessageResult from '../parts/message-result';
import NothingFound from '../parts/nothing-found';
import NothingSearched from '../parts/nothing-searched';
export default () => {
return (
<div>
<div className="-mx-2">
<MessagesResults />
</div>
</div>
);
};
export const MessagesResults = () => {
const input = useRecoilValue(SearchInputState);
const isRecent = input?.query?.trim()?.length === 0;
const { messages, loading } = useSearchMessages();
if (isRecent) return <NothingSearched />;
if (messages.length === 0 && !loading) return <NothingFound />;
return (
<>
<div className="">
{messages
.map(message => {
return (
<div key={message.id} className="cursor-pointer block w-full">
<MessageResult message={message} />
</div>
);
})
.filter(a => a)}
</div>
</>
);
};