📁 Changed TDrive root folder (#16)

📁 Changed TDrive root folder
This commit is contained in:
Montassar Ghanmy
2023-04-11 10:04:29 +01:00
committed by GitHub
parent 3b3f67af07
commit e0615fa867
10548 changed files with 48 additions and 48 deletions
@@ -0,0 +1,88 @@
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';
export const highlightText = (text?: string, highlight?: string) => {
if (!text) {
return '';
}
if (!highlight) {
return text;
}
const reg = new RegExp('(' + Strings.removeAccents(highlight) + ')', 'ig');
return Strings.removeAccents(text).replace(reg, "<span class='highlight'>$1</span>");
};
export const getFileMessageDownloadRoute = (file: MessageFileType): string => {
if (file?.metadata?.source === 'internal')
return FileUploadService.getDownloadRoute({
companyId: file.metadata?.external_id?.company_id,
fileId: file.metadata?.external_id?.id,
});
return '';
};
export const onFileDownloadClick = (file: MessageFileType) => {
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 ,
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);
};
@@ -0,0 +1,83 @@
.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%);}
}
@@ -0,0 +1,108 @@
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>;
};
@@ -0,0 +1,90 @@
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>
);
};
@@ -0,0 +1,45 @@
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>
);
};
@@ -0,0 +1,160 @@
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 { ArrowRight } from 'react-feather';
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 { 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';
export default (props: { driveItem: DriveItem & { user?: UserType } }) => {
const input = useRecoilValue(SearchInputState);
const currentWorkspaceId = useRouterWorkspace();
const companyApplications = useCompanyApplications();
const [_, setParentId] = useRecoilState(DriveCurrentFolderAtom('root'));
const tdriveDriveApplicationId =
companyApplications.applications.find(application => {
return application.identity.code === 'tdrive_drive';
})?.id || '';
const file = props.driveItem;
const name = file?.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(parseInt(file?.last_modified))} {' '}
{formatSize(file?.size)}
</Text.Info>
<ResultContext
user={file.user}
context={{
companyId: file?.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={() => onDriveItemDownloadClick(file)}
>
<DownloadIcon className="text-blue-500 w-6 h-6" />
</Button>
</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={() => {
openDriveItem(file, currentWorkspaceId, tdriveDriveApplicationId);
setParentId(file.parent_id);
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: DriveItem & { user?: UserType };
}) => {
const file = props.file;
const name = file?.last_version_cache?.file_metadata?.name;
const type = FileUploadAPIClient.mimeToType(file?.last_version_cache?.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';
if (file.is_directory) {
return (
<div className={'relative flex bg-blue-100 rounded-md ' + (props.className || '')}>
<FolderIcon className="w-10 h-10 m-auto text-blue-500" />
</div>
);
}
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>
);
};
@@ -0,0 +1,139 @@
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>
);
};
@@ -0,0 +1,29 @@
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>
);
};
@@ -0,0 +1,79 @@
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>
);
};
@@ -0,0 +1,90 @@
/* 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>
);
};
@@ -0,0 +1,19 @@
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 { useRecoilValue } from 'recoil';
import * as Text from '@atoms/text';
export default () => {
const input = useRecoilValue(SearchInputState);
return (
<div className="flex items-center justify-center flex-col h-64">
<NotFoundIcon className="h-8 w-8 mb-2" />
<Text.Info className="p-2">
{Languages.t('components.searchpopup.no_results_for')}
<span className="font-semibold">{input?.query}</span>.{' '}
{Languages.t('components.searchpopup.try_new_search')}
</Text.Info>
</div>
);
};
@@ -0,0 +1,12 @@
import { NotFoundIcon } from 'app/atoms/icons-colored';
import Languages from 'app/features/global/services/languages-service';
import * as Text from '@atoms/text';
export default () => {
return (
<div className="flex items-center justify-center flex-col h-64">
<NotFoundIcon className="h-8 w-8 mb-2" />
<Text.Info className="p-2">{Languages.t('components.searchpopup.start_search')}</Text.Info>
</div>
);
};
@@ -0,0 +1,62 @@
/* 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 : '';
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>
);
};
@@ -0,0 +1,65 @@
import { InputDecorationIcon } from '@atoms/input/input-decoration-icon';
import { SearchIcon } from '@heroicons/react/solid';
import { Select } from 'app/atoms/input/input-select';
import { Input } from 'app/atoms/input/input-text';
import { Loader } from 'app/atoms/loader';
import Languages from 'app/features/global/services/languages-service';
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
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';
export const SearchInput = () => {
const [input, setInput] = useRecoilState(SearchInputState);
const workspaceId = useRouterWorkspace();
const channelId = useRouterChannel();
const inputElement = useRef<HTMLInputElement>(null);
useEffect(() => {
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;
return (
<div className="relative flex mt-2 w-full">
<InputDecorationIcon
className="grow"
prefix={
loading
? ({ className }) => (
<div className={className + ' !h-6'}>
<Loader className="h-4 w-4" />
</div>
)
: SearchIcon
}
input={({ className }) => (
<Input
inputRef={inputElement}
onChange={e => setInput({ ...input, query: e.target.value })}
value={input.query}
className={className}
placeholder={Languages.t('scenes.app.mainview.quick_search_placeholder')}
/>
)}
/>
</div>
);
};
@@ -0,0 +1,516 @@
//@import url("https://fonts.googleapis.com/css?family=Inter:400,500,600,700");
:root {
--white: #fff;
--border-radius-base: 8px;
--accentlight: #007aff;
--backgroundlight: #ffffff;
--iconlight: #818c99;
--messagebubblelight: #ebedf0;
--separatorlight: #d7d8d9;
--textprimary: #ffffff;
--textprimarylight: #000000;
--texttertiarylight: #818c99;
--font-size-l: 15px;
--font-size-m: 14px;
--font-size-s: 13px;
--font-size-xl: 16px;
--font-size-xxl: 17px;
--font-size-xxxl: 21px;
--font-size-xxxxl: 24px;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-family-inter: 'Inter', Helvetica;
}
.screen input:focus {
outline: none;
}
.screen * {
-webkit-font-smoothing: antialiased;
box-sizing: border-box;
}
.screen div {
-webkit-text-size-adjust: none;
}
.container-center-horizontal {
display: flex;
flex-direction: row;
justify-content: center;
pointer-events: none;
width: 100%;
}
.container-center-horizontal > * {
flex-shrink: 0;
pointer-events: auto;
}
@keyframes reveal {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
input:focus {
outline: none;
}
* {
box-sizing: border-box;
}
/* screen - search */
.search-popup .overlay {
position: absolute;
z-index: 8;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #0000002e;
}
.search-popup .component-container {
position: absolute;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 0px;
height: 0px;
z-index: 9;
//transition: opacity 1s easy-out, top 0.2s, bottom 0.2s, left 0.2s, right 0.2s;
margin-top: 30vh;
}
.search-popup {
.searchLoading {
text-align: center;
}
}
.search {
justify-content: flex-start;
mix-blend-mode: normal;
//position: absolute;
height: calc(100vh - 20px);
transform: translate(-50%, -29%);
background: var(--white);
box-shadow: 0 2px 32px rgba(0, 0, 0, 0.2);
.tabs-wrapper {
width: 100%;
align-items: flex-start;
display: flex;
flex-direction: column;
justify-content: flex-start;
mix-blend-mode: normal;
//position: absolute;
//top: 112px;
.tab-items-wrapper {
align-items: flex-start;
display: flex;
justify-content: flex-start;
overflow: hidden;
.tab-item-wrapper {
align-items: center;
display: flex;
height: 48px;
justify-content: flex-start;
min-width: 73px;
padding-left: 16px;
padding-right: 16px;
z-index: 1;
.tab-item-active {
border-bottom: 2px solid var(--accentlight);
}
.tab-item-title {
color: var(--iconlight);
font-family: var(--font-family-inter);
font-size: var(--font-size-xl);
font-style: normal;
font-weight: var(--font-weight-medium);
letter-spacing: -0.16px;
line-height: 20px;
mix-blend-mode: normal;
position: relative;
text-align: center;
white-space: nowrap;
width: auto;
padding: 0px 10px;
}
.tab-item-active .tab-item-title {
color: var(--accentlight);
}
.tab-item-indicator {
align-self: stretch;
height: 6.5px;
position: relative;
}
}
}
.tab-horizontal-separator {
height: 1px;
background-color: var(--separatorlight);
mix-blend-mode: normal;
width: calc(100% - 30px);
margin: 0px 15px;
margin-top: -11px;
}
}
.input-wrapper {
position: relative;
width: 100%;
padding: 0 12px;
.input.big {
height: 36px;
background-color: var(--messagebubblelight);
padding-left: 36px;
font-weight: var(--font-weight-regular);
}
.input_icon.big > i {
padding-top: 6px;
padding-left: 8px;
font-size: 16px;
}
.input-clear-btn {
position: absolute;
top: 10px;
right: 24px;
cursor: pointer;
}
}
.header-wrapper {
width: 100%;
min-height: 52px;
position: relative;
}
.header-title {
margin: 0 auto;
padding-top: 16px;
color: var(--textprimarylight);
font-family: var(--font-family-inter);
font-size: var(--font-size-xxxl);
font-style: normal;
font-weight: 700;
letter-spacing: 0px;
line-height: 26px;
mix-blend-mode: normal;
position: relative;
text-align: center;
white-space: nowrap;
width: 100%;
}
.header-close-btn {
border-radius: 10px;
height: 25px;
top: 17px;
right: 12px;
position: absolute;
width: 25px;
cursor: pointer;
}
.results-wrapper {
//* {box-shadow: 0 0 0 1px gray;}
overflow: hidden;
margin-top: 4px;
margin-left: 18px;
width: calc(100% - 36px);
.results-group {
margin-bottom: 15px;
}
.results-group:last-child {
margin-bottom: 0;
}
.results-group-title {
margin-bottom: 10px;
letter-spacing: -0.15px;
line-height: 18px;
mix-blend-mode: normal;
text-align: left;
white-space: nowrap;
color: var(--textprimarylight);
font-family: var(--font-family-inter);
font-size: var(--font-size-l);
font-style: normal;
font-weight: var(--font-weight-semibold);
}
}
.search-results {
.result-items {
margin-top: 10px;
margin-bottom: 20px;
.result-item {
display: flex;
margin-bottom: 15px;
cursor: pointer;
.component-channel-avatar .channel-icon-wrapper {
width: 60px;
height: 60px;
}
.result-item-icon {
height: 58px;
width: 58px;
position: relative;
}
.result-item-content {
margin-left: 10px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
flex: 1 1 auto;
-webkit-mask-image: linear-gradient(to right, black 85%, transparent 100%);
mask-image: linear-gradient(to right, black 85%, transparent 100%);
overflow: hidden;
.channel-title {
letter-spacing: -0.41px;
line-height: 22px;
mix-blend-mode: normal;
text-align: left;
white-space: nowrap;
color: var(--textprimarylight);
font-family: var(--font-family-inter);
font-size: var(--font-size-xxl);
font-style: normal;
font-weight: var(--font-weight-semibold);
margin-bottom: 1px;
}
.channel-description {
color: #6d7885;
font-family: var(--font-family-inter);
font-size: var(--font-size-l);
font-style: normal;
font-weight: var(--font-weight-regular);
height: 16px;
letter-spacing: -0.24px;
line-height: 20px;
mix-blend-mode: normal;
text-align: left;
white-space: nowrap;
margin-bottom: 5px;
}
.messages-title {
font-family: var(--font-family-inter);
font-style: normal;
font-weight: 600;
font-size: 15px;
line-height: 16px;
display: flex;
align-items: center;
text-align: center;
letter-spacing: -0.352941px;
}
.messages-author {
color: var(--textprimarylight);
font-family: var(--font-family-inter);
font-size: var(--font-size-m);
font-style: normal;
font-weight: var(--font-weight-regular);
letter-spacing: -0.15px;
line-height: 18px;
mix-blend-mode: normal;
text-align: center;
white-space: nowrap;
}
.messages-text {
font-style: normal;
letter-spacing: -0.02px;
color: var(--iconlight);
font-family: var(--font-family-inter);
font-size: var(--font-size-m);
font-weight: var(--font-weight-regular);
line-height: 16px;
margin-bottom: 2px;
}
.highlight {
color: var(--accentlight);
}
.messages-text .highlight {
color: var(--accentlight);
font-family: var(--font-family-inter);
font-style: normal;
font-weight: var(--font-weight-medium);
letter-spacing: -0.02px;
}
}
.result-item-postfix {
color: #b8c1cc;
font-family: var(--font-family-inter);
font-size: var(--font-size-s);
font-style: normal;
font-weight: var(--font-weight-medium);
height: auto;
letter-spacing: -0.14px;
line-height: 20px;
mix-blend-mode: normal;
text-align: right;
white-space: nowrap;
width: auto;
}
.result-item-icon {
.result-item-icon-title {
line-height: 38pt;
font-size: 46pt;
}
.result-item-icon-back {
height: 56px;
mix-blend-mode: normal;
position: absolute;
width: 56px;
visibility: visible;
border-radius: 50%;
background-size: cover;
background-position: center;
}
}
//.result-item-channel-icon {
// .result-item-icon-title {
// line-height: 44pt;
// font-size: 20pt;
// padding-left: 16px;
// }
//}
}
}
}
.search-results {
.result-items-channel {
padding: 4px;
display: flex;
flex-direction: row;
height: 110px;
border-bottom: 0.5px solid #ddd;
-webkit-mask-image: linear-gradient(to right, black 92%, transparent 100%);
mask-image: linear-gradient(to right, black 92%, transparent 100%);
.result-item {
margin-right: 4px;
cursor: pointer;
}
.component-channel-avatar {
.channel-icon-wrapper {
transition: transform 0.15s;
}
}
.component-channel-avatar:hover {
.channel-icon-wrapper {
transform: scale(1.1);
}
}
}
.result-items-files {
display: flex;
flex-direction: column;
max-height: 520px;
.result-item {
display: flex;
flex-direction: row;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 0.5px solid #ddd;
.result-item-icon {
}
.result-item-info {
width: 100%;
margin-left: 10px;
.filename {
font-weight: 400;
font-size: 15px;
line-height: 20px;
letter-spacing: -0.272px;
color: #000000;
}
.details {
font-style: normal;
font-weight: 400;
font-size: 11px;
line-height: 14px;
letter-spacing: -0.192px;
color: #6d7885;
}
.sender {
margin-top: 2px;
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 20px;
letter-spacing: -0.24px;
}
}
}
}
}
.recent-results.tab-all {
.result-items-files {
max-height: 206px;
}
}
.recent-results.tab-media {
.result-items-media {
flex-flow: row wrap;
height: 508px;
}
}
}
@@ -0,0 +1,24 @@
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 { SearchInput } from './search-input';
import { SearchResultsIndex } from './search-tabs';
export default () => {
const { open, setOpen } = useSearchModal();
return (
<Modal open={open} onClose={() => setOpen(false)} className="sm:w-[80vw] sm:max-w-4xl">
<SearchBox />
</Modal>
);
};
const SearchBox = () => {
return (
<ModalContent textCenter title={Languages.t('components.searchpopup.header_title')}>
<SearchInput />
<SearchResultsIndex />
</ModalContent>
);
};
@@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SearchInputState } from 'app/features/search/state/search-input';
import PerfectScrollbar from 'react-perfect-scrollbar';
import { useRecoilValue } from 'recoil';
import SearchResulsDriveItems from './tabs/drive';
export const SearchResultsIndex = () => {
return (
<>
<PerfectScrollbar
className="-mb-4 py-3 overflow-hidden -mx-2 px-2"
style={{ maxHeight: 'calc(80vh - 100px)', minHeight: 'calc(80vh - 100px)' }}
options={{ suppressScrollX: true, suppressScrollY: false }}
component="div"
>
<SearchResulsDriveItems />
</PerfectScrollbar>
</>
);
};
const SearchCounterBadge = (props: { count: number }) => {
const count = props.count < 100 ? props.count : '99+';
return (
<div className="bg-zinc-200 ml-2 px-1.5 text-sm rounded-full text-zinc-500 dark:bg-zing-800 dark:text-zinc-600">
{count}
</div>
);
};
@@ -0,0 +1,117 @@
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>
);
};
@@ -0,0 +1,87 @@
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} />
))}
</>
);
};
@@ -0,0 +1,43 @@
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 { useRecoilValue } from 'recoil';
import DriveItemResult from '../parts/drive-item-result';
import NothingFound from '../parts/nothing-found';
export default () => {
const input = useRecoilValue(SearchInputState);
const isRecent = input?.query?.trim()?.length === 0;
const { driveItems, loading } = useSearchDriveItems();
if (driveItems.length === 0 && !loading) return <NothingFound />;
return (
<div>
{!!isRecent && (
<Text.Subtitle className="block">
{Languages.t('components.searchpopup.recent_files')}
</Text.Subtitle>
)}
<div className={'-mx-2'}>
<DriveItemsResults />
</div>
</div>
);
};
export const DriveItemsResults = (props: { max?: number }) => {
const { driveItems, loading } = useSearchDriveItems();
if (driveItems.length === 0 && !loading) return <NothingFound />;
return (
<>
{driveItems.slice(0, props?.max || driveItems.length).map(item => (
<DriveItemResult key={item.id} driveItem={item} />
))}
</>
);
};
@@ -0,0 +1,43 @@
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} />
))}
</>
);
};
@@ -0,0 +1,51 @@
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)}
</>
);
};
@@ -0,0 +1,41 @@
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>
</>
);
};