@@ -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);
|
||||
};
|
||||
+83
@@ -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%);}
|
||||
}
|
||||
|
||||
+108
@@ -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>;
|
||||
};
|
||||
+90
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user