This commit is contained in:
Romaric Mourgues
2023-03-24 17:04:08 +01:00
parent 8f4c059cb9
commit fcb0f993fe
38 changed files with 51 additions and 2799 deletions
@@ -62,6 +62,11 @@
"flushInterval": "SEARCH_ES_FLUSHINTERVAL" "flushInterval": "SEARCH_ES_FLUSHINTERVAL"
} }
}, },
"storage": {
"local": {
"path": "STORAGE_LOCAL_PATH"
}
},
"message-queue": { "message-queue": {
"type": "PUBSUB_TYPE", "type": "PUBSUB_TYPE",
"amqp": { "amqp": {
+2 -1
View File
@@ -52,7 +52,7 @@
} }
}, },
"webserver": { "webserver": {
"port": 3000, "port": 4000,
"logger": { "logger": {
"level": "info" "level": "info"
}, },
@@ -154,6 +154,7 @@
"api_key": "secret", "api_key": "secret",
"sender": "noreply@twake.app" "sender": "noreply@twake.app"
}, },
"applications": [],
"services": [ "services": [
"auth", "auth",
"push", "push",
@@ -34,7 +34,8 @@ export const SearchInput = () => {
const mediasLoading = useSearchMessagesMediasLoading(); const mediasLoading = useSearchMessagesMediasLoading();
const driveItemsLoading = useSearchDriveItemsLoading(); const driveItemsLoading = useSearchDriveItemsLoading();
const loading = channelsLoading || messagesLoading || filesLoading || mediasLoading || driveItemsLoading; const loading =
channelsLoading || messagesLoading || filesLoading || mediasLoading || driveItemsLoading;
return ( return (
<div className="relative flex mt-2 w-full"> <div className="relative flex mt-2 w-full">
@@ -54,32 +55,11 @@ export const SearchInput = () => {
inputRef={inputElement} inputRef={inputElement}
onChange={e => setInput({ ...input, query: e.target.value })} onChange={e => setInput({ ...input, query: e.target.value })}
value={input.query} value={input.query}
className={className + ' rounded-tr-none rounded-br-none'} className={className}
placeholder={Languages.t('scenes.app.mainview.quick_search_placeholder')} placeholder={Languages.t('scenes.app.mainview.quick_search_placeholder')}
/> />
)} )}
/> />
<Select
onChange={e => {
if (e.target.value === 'company') {
setInput({
..._.omit(input, 'workspaceId', 'channelId'),
});
} else {
setInput({
...input,
workspaceId,
channelId,
});
}
if (inputElement.current) inputElement.current.focus();
}}
value={input.channelId ? 'channel' : 'company'}
className="w-auto rounded-tl-none ml-px rounded-bl-none text-sm text-opacity-50"
>
<option value="company">{Languages.t('components.searchpopup.scope.company')}</option>
<option value="channel">{Languages.t('components.searchpopup.scope.channel')}</option>
</Select>
</div> </div>
); );
}; };
@@ -1,106 +1,24 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import Tabs from '@molecules/tabs'; import { SearchInputState } from 'app/features/search/state/search-input';
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 { useSearchDriveItems } from 'app/features/search/hooks/use-search-drive-items';
import { SearchInputState, SearchTabsState } from 'app/features/search/state/search-input';
import PerfectScrollbar from 'react-perfect-scrollbar'; import PerfectScrollbar from 'react-perfect-scrollbar';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import SearchResultsAll from './tabs/all';
import SearchResultsChannels from './tabs/channels';
import SearchResultsFiles from './tabs/files';
import SearchResultsMedias from './tabs/medias';
import SearchResultsMessages from './tabs/messages';
import SearchResulsDriveItems from './tabs/drive'; import SearchResulsDriveItems from './tabs/drive';
const orderedTabs = ['all', 'messages', 'medias', 'files', 'channels', 'drive'];
export const SearchResultsIndex = () => { export const SearchResultsIndex = () => {
const input = useRecoilValue(SearchInputState);
const hasInput = input?.query?.length > 0;
const [tab, setTab] = useRecoilState(SearchTabsState);
const { messages } = useSearchMessages();
const { files } = useSearchMessagesFiles();
const { files: medias } = useSearchMessagesMedias();
const { driveItems } = useSearchDriveItems();
return ( return (
<> <>
<Tabs
tabs={[
<div key="all">{Languages.t('components.searchpopup.all')}</div>,
<div key="messages">
<div className="flex">
{Languages.t('components.searchpopup.messages')}
{hasInput && <SearchCounterBadge count={messages.length} />}
</div>
</div>,
<div key="media">
<div className="flex">
{Languages.t('components.searchpopup.media')}
{hasInput && <SearchCounterBadge count={medias.length} />}
</div>
</div>,
<div key="files">
<div className="flex">
{Languages.t('components.searchpopup.files')}
{hasInput && <SearchCounterBadge count={files.length} />}
</div>
</div>,
.../*!input.channelId
?*/ [
<div key="channels">
<ChannelsTab />
</div>,
],
/*: []*/
<div key="drive">
<div className="flex">
{Languages.t('components.searchpopup.drive')}
{hasInput && <SearchCounterBadge count={driveItems.length} />}
</div>
</div>,
]}
selected={orderedTabs.indexOf(tab)}
onClick={idx => setTab(orderedTabs[idx] as any)}
/>
<PerfectScrollbar <PerfectScrollbar
className="-mb-4 py-3 overflow-hidden -mx-2 px-2" className="-mb-4 py-3 overflow-hidden -mx-2 px-2"
style={{ maxHeight: 'calc(80vh - 100px)', minHeight: 'calc(80vh - 100px)' }} style={{ maxHeight: 'calc(80vh - 100px)', minHeight: 'calc(80vh - 100px)' }}
options={{ suppressScrollX: true, suppressScrollY: false }} options={{ suppressScrollX: true, suppressScrollY: false }}
component="div" component="div"
> >
{tab === 'all' && <SearchResultsAll />} <SearchResulsDriveItems />
{tab === 'messages' && <SearchResultsMessages />}
{tab === 'medias' && <SearchResultsMedias />}
{tab === 'files' && <SearchResultsFiles />}
{tab === 'channels' && <SearchResultsChannels />}
{tab === 'drive' && <SearchResulsDriveItems />}
</PerfectScrollbar> </PerfectScrollbar>
</> </>
); );
}; };
const ChannelsTab = () => {
const input = useRecoilValue(SearchInputState);
const hasInput = input?.query?.length > 0;
const { channels } = useSearchChannels();
return (
<div className="flex">
{Languages.t('components.searchpopup.channels')}
{hasInput && <SearchCounterBadge count={channels.length} />}
</div>
);
};
const SearchCounterBadge = (props: { count: number }) => { const SearchCounterBadge = (props: { count: number }) => {
const count = props.count < 100 ? props.count : '99+'; const count = props.count < 100 ? props.count : '99+';
return ( return (
@@ -1,132 +0,0 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, { Component } from 'react';
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
import Languages from 'app/features/global/services/languages-service';
import Workspaces from 'app/deprecated/workspaces/workspaces.jsx';
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
import ChannelUI from 'app/views/client/channels-bar/Parts/Channel/Channel';
import {
useCompanyApplications,
useCompanyApplicationsRealtime,
} from 'app/features/applications/hooks/use-company-applications';
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
// This should be deleted
export default class ChannelsApps extends Component<unknown> {
constructor(props: unknown) {
super(props);
this.state = {
i18n: Languages,
};
Collections.get('channels').addSource(
{
http_base_url: 'channels',
http_options: {
workspace_id: Workspaces.currentWorkspaceId,
},
websockets: [
{
uri: 'channels/workspace/' + Workspaces.currentWorkspaceId,
options: { type: 'channels/workspace' },
},
],
},
'channels_' + Workspaces.currentWorkspaceId,
);
Collections.get('channels').addListener(this);
Collections.get('applications').addListener(this);
Languages.addListener(this);
WorkspacesApps.addListener(this);
}
componentWillUnmount() {
Collections.get('channels').removeListener(this);
Collections.get('applications').removeListener(this);
Languages.removeListener(this);
WorkspacesApps.removeListener(this);
}
render() {
//@ts-ignore
const apps = WorkspacesApps.apps_by_group[Workspaces.currentGroupId] || {};
return (
<div className="applications_channels" style={{ marginTop: 8 }}>
{Object.keys(apps).map(id => {
const groupApp = apps[id];
if (!groupApp) {
return '';
}
const app = groupApp.app;
if (groupApp && !(!app || !(app.display || {}).app)) {
const name = Languages.t(
'app.identity?.name.' + app?.identity?.code,
[],
app.identity?.name,
);
let icon = WorkspacesApps.getAppIcon(app);
if ((icon || '').indexOf('http') === 0) {
icon = '';
}
return (
<ChannelUI
key={id}
app={app}
name={name}
icon={icon}
id={app.id}
favorite={false}
visibility={'public'}
unreadMessages={0}
notificationLevel={'all'}
mentions={0}
replies={0}
unread={0}
/>
);
}
return '';
})}
</div>
);
}
}
type PropsType = {
companyId: string;
};
export const CompanyApplications = ({ companyId }: PropsType) => {
const { applications: companyApplications } = useCompanyApplications(companyId);
useCompanyApplicationsRealtime();
const channelId = useRouterChannel();
return (
<div className="applications_channels" style={{ marginTop: 8 }}>
{companyApplications
.filter(app => app.display?.twake?.standalone)
.map(app => (
<ChannelUI
key={app.id}
id={app.id}
selected={app.id == channelId}
channelId={channelId}
app={app}
name={app.identity.name}
icon={app.identity.icon || <></>}
favorite={false}
visibility={'public'}
notificationLevel={'all'}
unreadMessages={0}
mentions={0}
replies={0}
unread={0}
/>
))}
</div>
);
};
@@ -1,119 +0,0 @@
.electron {
.channels_view {
padding-top: 24px;
}
}
.channels_view {
background: var(--grey-background);
width: 220px;
padding-top: 16px;
border-radius: var(--border-radius-base) 0 0 8px;
&.loading {
.applications_channels,
.workspace_channels,
.users_channels {
display: none;
}
}
& > div {
display: flex;
flex-direction: column;
}
.channel_small_text {
font-size: 12px;
margin: 0 16px;
color: var(--grey-dark);
font-weight: 500;
}
.current_group {
line-height: 24px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
color: var(--grey-dark);
font-size: 14px;
background: var(--primary-background);
border-radius: var(--border-radius-large);
display: flex;
padding: 8px;
margin: 0px 14px;
box-sizing: border-box;
height: 40px;
span {
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.group_image.invite {
margin-right: 4px;
width: auto;
background: var(--green);
border-radius: var(--border-radius-base);
color: var(--white);
padding-right: 4px;
white-space: nowrap;
display: flex;
font-weight: 600;
.group_switch {
margin-right: 4px;
}
}
.group_switch {
margin-right: 8px;
height: 24px;
.current_company_logo.has_image {
font-size: 0px !important;
}
.current_company_logo {
font-size: 16px;
line-height: 24px;
color: var(--secondary);
width: 24px;
height: 24px;
border-radius: var(--border-radius-base);
}
}
}
.scrollbar-container {
flex: 1;
}
}
.channels_view_loader {
background: var(--grey-background);
width: 220px;
border-radius: var(--border-radius-base) 0 0 8px;
.applications_channels_loader {
margin-top: 4px;
margin-bottom: 12px;
}
}
.loading_render {
.channel_small_text {
display: none;
}
.current_group {
display: none;
}
}
/* channel add */
.channelUserAdd .flexDiv {
display: flex;
.flex1 {
flex: 1;
margin-left: 8px;
}
}
@@ -1,138 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-use-before-define
import React, { Suspense, useEffect } from 'react';
import classNames from 'classnames';
import PerfectScrollbar from 'react-perfect-scrollbar';
import { Layout, Skeleton } from 'antd';
import CurrentUser from './Parts/CurrentUser/CurrentUser';
import { CompanyApplications } from './ChannelsApps/ChannelsApps';
import ChannelsWorkspace from './ChannelsWorkspace/ChannelsWorkspace';
import ChannelsUser from './ChannelsUser/ChannelsUser';
import Footer from './Parts/Footer.jsx';
import Shortcuts, {
defaultShortcutsMap,
ShortcutType,
} from 'app/features/global/services/shortcut-service';
import AddUserButton from 'components/add-user-button/add-user-button';
import Workspaces from 'app/deprecated/workspaces/workspaces';
import ScrollWithHiddenComponents from 'app/components/scroll-hidden-components/scroll-with-hidden-components';
import HiddenNotificationsButton from 'app/components/scroll-hidden-components/hidden-notifications-button';
import AccessRightsService from 'app/features/workspace-members/services/workspace-members-access-rights-service';
import useRouterCompany from 'app/features/router/hooks/use-router-company';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { useCurrentWorkspace } from 'app/features/workspaces/hooks/use-workspaces';
import useChannelWritingActivity from 'app/features/channels/hooks/use-channel-writing-activity';
import { useChannelsBarLoader } from 'app/features/channels/hooks/use-channels-bar-loader';
import { usePublicOrPrivateChannelsSetup } from 'app/features/channels/hooks/use-public-or-private-channels';
import { useDirectChannelsSetup } from 'app/features/channels/hooks/use-direct-channels';
import { useSetLastWorkspacePreference } from 'app/features/users/hooks/use-set-last-workspace-preferences';
import { useAutoSelectChannel } from 'app/features/channels/hooks/use-autoselect-channel';
import './ChannelsBar.scss';
import { useSearchModal } from 'app/features/search/hooks/use-search';
import { useSetRecoilState } from 'recoil';
import { SearchInputState } from 'app/features/search/state/search-input';
export default () => {
const companyId = useRouterCompany();
const workspaceId = useRouterWorkspace();
const { workspace } = useCurrentWorkspace();
useAutoSelectChannel();
useSetLastWorkspacePreference();
useChannelWritingActivity();
const { loading } = useChannelsBarLoader({ companyId, workspaceId });
usePublicOrPrivateChannelsSetup();
useDirectChannelsSetup();
const { setOpen: setSearchopen } = useSearchModal();
const setSearchInput = useSetRecoilState(SearchInputState);
useEffect(() => {
const openWorkspaceChannelList: ShortcutType = {
shortcut: defaultShortcutsMap.SEARCH_CHANNEL,
handler: event => {
event.preventDefault();
setSearchopen(true);
setSearchInput({ query: '' });
},
};
Shortcuts.addShortcut(openWorkspaceChannelList);
return () => {
Shortcuts.removeShortcut(openWorkspaceChannelList);
};
}, []);
if (loading || !companyId || !workspaceId || !workspace) {
return <LoadingChannelBar />;
}
return (
<Layout.Sider
theme="light"
width={220}
className={classNames('channels_view', {})}
style={{ height: '100%' }}
>
<CurrentUser />
<ScrollWithHiddenComponents
tag="channel_bar_component"
scrollTopComponent={<HiddenNotificationsButton position="top" type="important" />}
scrollBottomComponent={<HiddenNotificationsButton position="bottom" type="important" />}
>
<PerfectScrollbar options={{ suppressScrollX: true }}>
<Suspense fallback={<></>}>
<CompanyApplications companyId={companyId} />
</Suspense>
<ChannelsWorkspace key={`workspace_chans_${workspaceId}`} />
<ChannelsUser key={companyId} />
{AccessRightsService.hasLevel(workspaceId, 'moderator') &&
(Workspaces.getCurrentWorkspace()?.stats?.total_members || 0) <= 5 && <AddUserButton />}
</PerfectScrollbar>
</ScrollWithHiddenComponents>
<Footer />
</Layout.Sider>
);
};
export const LoadingChannelBar = () => {
return (
<Layout.Sider
theme="light"
width={220}
className={classNames('channels_view_loading channels_view')}
style={{ height: '100%', width: '90%', alignItems: 'center' }}
>
<ChannelLoading />
</Layout.Sider>
);
};
export const ChannelLoading = () => {
return (
<div className="channels_view_loader ">
<div className="small-x-margin">
<Skeleton
className="mt-8"
title={{ width: '50%' }}
paragraph={{ rows: 3, width: '100%' }}
/>
<Skeleton
className="mt-8"
title={{ width: '50%' }}
paragraph={{ rows: 4, width: '100%' }}
/>
<Skeleton
className="mt-8"
title={{ width: '50%' }}
paragraph={{ rows: 4, width: '100%' }}
/>
</div>
</div>
);
};
@@ -1,87 +0,0 @@
import React, { useEffect, useState } from 'react';
import Languages from 'app/features/global/services/languages-service';
import RouterServices from 'app/features/router/services/router-service';
import NewDirectMessagesPopup from 'app/views/client/channels-bar/Modals/new-direct-channel-popup';
import ChannelCategory from 'app/views/client/channels-bar/Parts/Channel/ChannelCategory';
import ChannelIntermediate from '../Parts/Channel/ChannelIntermediate';
import AccessRightsService from 'app/features/workspace-members/services/workspace-members-access-rights-service';
import { useDirectChannels } from 'app/features/channels/hooks/use-direct-channels';
import { Modal } from 'app/atoms/modal';
import { PencilAltIcon } from '@heroicons/react/outline';
export default () => {
const { companyId } = RouterServices.getStateFromRoute();
const { directChannels } = useDirectChannels();
const [max, setMax] = useState(20);
const [openDirect, setOpenDirect] = useState(false);
const openConv = () => {
console.log(openDirect);
setOpenDirect(true);
};
const [delayed, setDelayed] = useState(true);
//This delay make the app faster: could be removed with an implementation of virtualized list
useEffect(() => {
setTimeout(() => setDelayed(false), 50);
}, []);
if (delayed) {
return <></>;
}
const nonfavoriteDirectChannels = directChannels.filter(
channel => !channel.user_member?.favorite,
);
return (
<div className="users_channels">
<Modal open={openDirect} onClose={() => setOpenDirect(false)}>
<NewDirectMessagesPopup onClose={() => setOpenDirect(false)} />
</Modal>
<ChannelCategory
refAdd={(node) => {
// eslint-disable-next-line no-self-assign, @typescript-eslint/no-unused-vars
node = node;
}}
text={Languages.t(
'scenes.app.channelsbar.channelsuser.private_messages',
[],
'Direct messages',
)}
addIcon={<PencilAltIcon className="h-5 w-5" />}
onAdd={() => (AccessRightsService.hasCompanyLevel(companyId, 'member') ? openConv() : null)}
/>
{nonfavoriteDirectChannels
.sort(
(a, b) =>
(parseInt(b.last_activity?.toString() || '') || 0) -
(parseInt(a.last_activity?.toString() || '') || 0),
)
.slice(0, max)
.map(channel => {
return <ChannelIntermediate key={channel.id} channel={channel} />;
})}
{max < nonfavoriteDirectChannels.length && (
<div style={{ textAlign: 'center' }}>
<a href="#" onClick={() => setMax(max + 20)}>
Load more...
</a>
</div>
)}
{directChannels.length === 0 && (
<div className="channel_small_text">
{Languages.t(
'scenes.app.channelsbar.channelsuser.no_private_message_invite_collaboraters',
)}
</div>
)}
</div>
);
};
@@ -1,106 +0,0 @@
import React from 'react';
import { ChannelType } from 'app/features/channels/types/channel';
import WorkspaceChannels from './WorkspaceChannel';
import Languages from 'app/features/global/services/languages-service';
import { useDirectChannels } from 'app/features/channels/hooks/use-direct-channels';
import { usePublicOrPrivateChannels } from 'app/features/channels/hooks/use-public-or-private-channels';
type channelCategoryType = {
favorite: ChannelType[];
workspace: ChannelType[];
inGroup: ChannelType[];
direct: ChannelType[];
};
export default () => {
const channelCategory: channelCategoryType = {
favorite: [],
workspace: [],
inGroup: [],
direct: [],
};
const { privateChannels, publicChannels } = usePublicOrPrivateChannels();
const { directChannels } = useDirectChannels();
const channels: ChannelType[] = [...privateChannels, ...publicChannels];
channels
.concat(directChannels)
.filter(a => a.user_member?.user_id)
.sort((a, b) => (a.name || '').localeCompare(b.name || ''))
.forEach(channel => {
switch (true) {
case channel.user_member?.favorite:
channelCategory.favorite.push(channel);
break;
case channel.visibility === 'direct':
channelCategory.direct.push(channel);
break;
case channel.channel_group && channel.channel_group.length > 1:
channelCategory.inGroup.push(channel);
break;
default:
channelCategory.workspace.push(channel);
}
});
const groupsName: string[] = [];
const groups: { name: string; channels: ChannelType[] }[] = [];
const hasNonGroupWorkspaceChannels = !(
channelCategory.workspace.length === 0 && channelCategory.inGroup.length !== 0
);
channelCategory.inGroup.forEach(channel => {
if (channel.channel_group && channel.channel_group.length > 1) {
if (groups.length === 0) {
groupsName.push(channel.channel_group);
groups.push({
name: channel.channel_group,
channels: [channel],
});
} else {
if (groupsName.includes(channel.channel_group)) {
const groupIndex = groups.findIndex(group => group.name === channel.channel_group);
groups[groupIndex].channels.push(channel);
} else {
groupsName.push(channel.channel_group);
groups.push({
name: channel.channel_group,
channels: [channel],
});
}
}
}
});
return (
<div className="workspace_channels">
{channelCategory.favorite.length !== 0 && (
<WorkspaceChannels
key={'favoriteChannels'}
sectionTitle={Languages.t(
'scenes.app.channelsbar.channelsworkspace.channel_title.favorite',
)}
channels={channelCategory.favorite}
favorite
/>
)}
{hasNonGroupWorkspaceChannels && (
<WorkspaceChannels
key={'channels'}
sectionTitle={Languages.t('scenes.app.channelsbar.channelsworkspace.channel_title')}
channels={channelCategory.workspace}
/>
)}
{groups.map((group, index) => (
<WorkspaceChannels
key={index}
sectionTitle={group.name}
channels={group.channels}
subgroup={hasNonGroupWorkspaceChannels}
/>
))}
</div>
);
};
@@ -1,85 +0,0 @@
import React from 'react';
import { ChannelType } from 'app/features/channels/types/channel';
import Languages from 'app/features/global/services/languages-service';
import ChannelCategory from '../Parts/Channel/ChannelCategory';
import ChannelIntermediate from '../Parts/Channel/ChannelIntermediate';
import Menu from 'components/menus/menu.jsx';
import Icon from 'app/components/icon/icon';
import AccessRightsService from 'app/features/workspace-members/services/workspace-members-access-rights-service';
import RouterServices from 'app/features/router/services/router-service';
import { useSearchModal } from 'app/features/search/hooks/use-search';
import { SearchInputState, SearchTabsState } from 'app/features/search/state/search-input';
import { useSetRecoilState } from 'recoil';
import { useOpenChannelModal } from 'app/components/edit-channel';
type Props = {
sectionTitle: string;
channels: ChannelType[];
favorite?: boolean;
subgroup?: boolean;
};
export default (props: Props) => {
const { workspaceId, companyId } = RouterServices.getStateFromRoute();
const openChannelModal = useOpenChannelModal();
const addChannel = () => {
openChannelModal('');
};
const { setOpen: setSearchopen } = useSearchModal();
const setSearchInput = useSetRecoilState(SearchInputState);
const setSearchTab = useSetRecoilState(SearchTabsState);
const joinChannel = () => {
setSearchopen(true);
setSearchInput({ query: '' });
setSearchTab('channels');
};
return (
<>
<ChannelCategory
text={props.sectionTitle}
suffix={
!props.favorite &&
!props.subgroup &&
AccessRightsService.hasLevel(workspaceId, 'member') &&
AccessRightsService.getCompanyLevel(companyId) !== 'guest' && (
<Menu
className="add channel-menu"
menu={[
{
type: 'menu',
text: Languages.t('components.leftbar.channel.workspaceschannels.menu.option_1'),
// Don't remove this className, we need it for integration tests
className: 'add-channel',
onClick: () => addChannel(),
},
{
type: 'menu',
// Don't remove this className, we need it for integration tests
className: 'join-channel',
text: Languages.t('components.leftbar.channel.workspaceschannels.menu.option_2'),
onClick: () => joinChannel(),
},
]}
>
<Icon type="plus" className="m-icon-small" />
</Menu>
)
}
/>
{props.channels.length === 0 && (
<div className="channel_small_text">
{Languages.t('scenes.app.channelsbar.channelsworkspace.no_channel')}
</div>
)}
{props.channels.length > 0 &&
props.channels.map((data, key) => {
return <ChannelIntermediate key={key} channel={data || ''} />;
})}
</>
);
};
@@ -1,65 +0,0 @@
import { useState } from 'react';
import { Button } from '@atoms/button/button';
import { Typography } from 'antd';
import { ModalContent } from 'app/atoms/modal';
import { useOpenChannelModal } from 'app/components/edit-channel';
import { useDirectChannels } from 'app/features/channels/hooks/use-direct-channels';
import Languages from 'app/features/global/services/languages-service';
import SelectUsers from './select-users';
import './style.scss';
export default (props: { onClose: () => void }) => {
const [newUserDiscussion, setNewUserDiscussion] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const { openDiscussion } = useDirectChannels();
const openChannelModal = useOpenChannelModal();
const upsertDirectMessage = async (): Promise<void> => {
setLoading(true);
await openDiscussion(newUserDiscussion);
props.onClose();
};
const onClickLink = () => {
props.onClose();
openChannelModal('');
};
const max = 10;
return (
<ModalContent title={Languages.t('scenes.app.channelsbar.channelsuser.new_private_discussion')}>
<SelectUsers
onChange={users => setNewUserDiscussion(users.map(u => u.id as string))}
initialUsers={[]}
/>
<>
{newUserDiscussion.length >= max && (
<Typography.Text>
<Typography.Link onClick={onClickLink}>
{Languages.t(
'scenes.app.channelsbar.channelsuser.new_private_discussion.limit_reached_link',
)}
</Typography.Link>
<Typography.Text>
{Languages.t(
'scenes.app.channelsbar.channelsuser.new_private_discussion.limit_reached_text',
)}
</Typography.Text>
</Typography.Text>
)}
</>
<Button
loading={loading}
theme="primary"
size="md"
className="mt-4 float-right"
disabled={newUserDiscussion.length === 0 || newUserDiscussion.length > max}
onClick={upsertDirectMessage}
>
{Languages.t('general.continue', [], 'Continue')}
</Button>
</ModalContent>
);
};
@@ -1,97 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { Input } from '@atoms/input/input-text';
import { useSearchUsers } from 'app/features/users/hooks/use-search-user-list';
import User from 'app/components/ui/user';
import { Button } from '@atoms/button/button';
import { UserType } from 'app/features/users/types/user';
import { InputDecorationIcon } from 'app/atoms/input/input-decoration-icon';
import { SearchIcon } from '@heroicons/react/solid';
import { Info } from 'app/atoms/text';
import Languages from 'app/features/global/services/languages-service';
export default (props: { onChange: (users: UserType[]) => void; initialUsers: UserType[] }) => {
const [users, setUsers] = useState<UserType[]>(props.initialUsers);
const { search, result } = useSearchUsers({ scope: 'company' });
const inputElement = useRef<HTMLInputElement>(null);
useEffect(() => {
if (inputElement.current) inputElement.current.focus();
}, []);
useEffect(() => {
props.onChange(users);
}, [users]);
const addUser = (user: UserType) => {
setUsers([...users, user].filter(a => a) as UserType[]);
};
const removeUser = (user: UserType) => {
setUsers(users.filter(a => a.id !== user.id) as UserType[]);
};
return (
<div style={{ width: '100vw', maxWidth: '500px' }}>
<InputDecorationIcon
prefix={SearchIcon}
input={({ className }) => (
<Input
placeholder="Search users"
className={className + ' mt-2 mb-4 w-full'}
theme="plain"
inputRef={inputElement}
onChange={e => search(e.target.value)}
/>
)}
/>
<div style={{ minHeight: 200 }}>
{result.length === 0 && (
<div className="text-center pt-8">
<Info>{Languages.t('components.user_picker.modal_no_result')}</Info>
</div>
)}
{result
.filter(a => !users.map(a => a.id).includes(a.id))
.slice(0, 5)
.map(user => {
return (
<div key={user.id} className="new-direct-channel-proposed-user">
<div className="user-name">
<User data={user} />{' '}
<span className="email overflow-hidden text-ellipsis whitespace-nowrap">
({user.email})
</span>
</div>
<div>
<Button onClick={() => addUser(user)} size="sm">
Add
</Button>
</div>
</div>
);
})}
</div>
{users.length > 0 && (
<div className="border-t border-t-zinc-200 py-4">
{users.map(user => {
return (
<div key={user.id} className="new-direct-channel-added-user">
<div className="user-name">
<User data={user} />{' '}
<span className="email overflow-hidden text-ellipsis whitespace-nowrap">
({user.email})
</span>
</div>
<div>
<Button theme={'danger'} onClick={() => removeUser(user)} size="sm">
Remove
</Button>
</div>
</div>
);
})}
</div>
)}
</div>
);
};
@@ -1,39 +0,0 @@
.new-direct-channel-proposed-user, .new-direct-channel-added-user {
display: flex;
align-items: center;
padding: 4px;
margin: 0px -4px;
border-radius: 12px;
&.new-direct-channel-proposed-user{
.user-name {
opacity: 0.8;
}
}
.user-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
align-items: center;
.email {
font-size: 12px;
opacity: 0.8;
margin-left: 4px;
}
}
&:hover {
background-color: var(--primary-background);
}
.button.small {
height: 24px;
line-height: 24px;
margin-left: 8px;
}
}
@@ -1,407 +0,0 @@
.channel_category {
font-size: 12px;
font-weight: 500;
color: var(--black-alpha-70);
margin-top: 24px;
margin-left: 12px;
margin-right: 12px;
text-transform: uppercase;
display: flex;
&.sub {
margin-top: 8px;
text-transform: capitalize;
cursor: pointer;
.text {
flex: unset;
}
&:hover {
opacity: 0.8;
.edit {
opacity: 1;
}
}
.edit {
opacity: 0;
}
}
.text {
flex: 1;
line-height: 28px;
}
.add {
cursor: pointer;
float: right;
text-align: center;
font-size: 18px;
display: flex;
align-items: center;
margin-top: -2px;
&:hover {
color: var(--black);
}
}
}
.channel {
height: 28px;
width: auto;
margin: 0 8px;
padding: 0 8px;
border-radius: var(--border-radius-base);
max-width: 216px;
box-sizing: border-box;
font-size: 14px;
font-weight: 500;
font-style: normal;
font-stretch: normal;
line-height: 28px;
background: transparent;
color: var(--grey-dark);
display: flex;
cursor: pointer;
line-height: normal;
.writing_activity {
display: inline-block;
align-self: center;
color: var(--white);
}
&.unread {
font-weight: bold;
color: var(--black);
}
.user_list {
margin-top: 2px;
}
.more-icon {
display: none;
}
&:hover {
.more-icon {
display: inline-block;
&:hover {
color: var(--black);
}
}
.writing_activity {
display: none;
}
}
&.extra-margin {
padding-left: 6px;
}
&.has_new_content {
font-weight: 700;
}
&:hover,
&.menu-open {
background: var(--grey-light);
.user_head,
.user_head .status {
border-color: var(--grey-light);
}
}
&.selected {
background: var(--primary);
color: var(--white);
.user_head,
.user_head .status {
border-color: var(--primary);
}
.user_head .status.grey {
background-color: var(--grey-background);
}
.black-icon {
color: var(--white);
}
.grey-icon {
color: var(--grey-light) !important;
}
.more-icon {
&:hover {
color: var(--white);
}
}
.icon .icon-unicon {
color: var(--white);
}
}
&.muted {
.text,
.more {
opacity: 0.6;
}
}
.group_count {
background: var(--black-alpha-50);
height: 16px;
color: var(--white);
margin-right: 8px;
border-radius: var(--border-radius-base);
padding: 2px 4px;
box-sizing: border-box;
line-height: 12px;
font-size: 12px;
display: inline-block;
vertical-align: middle;
margin-top: 6px;
}
.app_indicator {
background: #92929c;
height: 16px;
color: #fff;
margin-right: 4px;
border-radius: var(--border-radius-base);
padding: 2px 3px;
font-weight: 700;
box-sizing: border-box;
line-height: 12px;
font-size: 12px;
display: inline-block;
vertical-align: middle;
margin-top: 8px;
}
.imported {
font-weight: 700;
font-size: 12px;
line-height: 32px;
margin-right: 4px;
}
.text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-left: 8px;
padding-right: 4px;
line-height: 28px;
}
.not_member {
margin-left: -6px;
font-size: 14px;
display: flex;
align-items: center;
width: auto;
.icon-unicon {
opacity: 0.5;
line-height: 32px;
}
}
.favorite {
margin-right: 4px;
font-size: 14px;
display: flex;
align-items: center;
width: auto;
opacity: 0.5;
line-height: 32px;
}
.more {
display: flex;
align-items: center;
font-size: 16px;
.merge-icon {
margin-left: -5px;
}
.grey-icon {
color: var(--grey-dark);
}
.green-icon {
color: var(--green);
}
}
.icon {
display: flex;
align-items: center;
font-size: 16px;
.icon-unicon {
color: var(--grey-dark);
margin-left: -4px;
margin-right: -2px;
}
img.emojione {
width: 16px;
height: 16px;
vertical-align: middle;
margin-top: -2px;
}
i.emojione.emoji-image {
margin-top: 0px;
border-radius: var(--border-radius-base);
}
.user_list {
& > div {
display: inline-block;
margin-left: -14px;
}
& > div:first-child {
margin-left: 0px;
}
}
}
.notification_dot {
min-width: 16px;
height: 16px;
color: var(--white);
background: var(--red);
font-size: 10px;
font-weight: 700;
text-align: center;
line-height: 16px;
border-radius: var(--border-radius-small);
margin: 8px 4px;
padding: 0 2px;
vertical-align: top;
box-sizing: border-box;
display: inline-block;
}
}
.drag_channel {
.channel {
pointer-events: none;
}
}
/** Channel drop zones **/
.channel_dropzone {
margin-left: 12px;
margin-right: 12px;
width: auto;
height: 0px;
background: var(--grey-light);
border: 0px;
border-radius: var(--border-radius-base);
box-sizing: border-box;
text-align: center;
line-height: 28px;
font-size: 12px;
color: var(--grey-dark);
font-weight: bold;
opacity: 0;
pointer-events: none;
transition: height 0.2s, margin 0.2s, opacity 0.2s, border 0.2s;
.icon {
width: 32px;
font-size: 16px;
height: 28px;
vertical-align: top;
line-height: 26px;
display: flex;
align-items: center;
padding-left: 10px;
box-sizing: border-box;
}
.text {
flex: 1;
padding-right: 32px;
}
}
.drag_channel {
.channel_dropzone {
pointer-events: all;
height: 32px;
opacity: 1;
margin-bottom: 16px;
display: flex;
border: 2px dashed transparent;
&:hover {
border: 2px dashed #979797;
}
}
}
/** loading and misc **/
.dragging_channel_view {
.channel {
pointer-events: none;
transition: background 0.2s, box-shadow 0.2s;
background: var(--white);
margin: 0px;
width: 188px;
opacity: 1;
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.05), 0 8px 20px 0 rgba(0, 0, 0, 0.15);
}
.channel.selected {
background: var(--primary);
}
}
.loading_render {
.channel {
pointer-events: none;
border-radius: var(--border-radius-large);
height: 16px;
margin-bottom: 16px;
animation-duration: 1s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: placeHolderShimmer;
animation-timing-function: linear;
background: var(--grey-light);
background: linear-gradient(to right, var(--grey-light) 8%, #f2f2f2 18%, var(--grey-light) 33%);
background-size: 800px 104px;
& > * {
opacity: 0 !important;
}
}
.channel_category {
pointer-events: none;
font-size: 0;
height: 16px;
background: #dfdfdf;
border-radius: var(--border-radius-large);
width: 60%;
margin-top: 30px;
margin-bottom: 16px;
& > * {
opacity: 0 !important;
}
}
}
.direct-channel-avatars {
padding-top: 4px;
margin-left: -2px;
margin-right: -2px;
.user_head {
border-radius: 50%;
height: 20px;
width: 20px;
}
}
@@ -1,154 +0,0 @@
import React from 'react';
import { Tooltip } from 'antd';
import { Calendar, CheckSquare, Folder, Hexagon, Star } from 'react-feather';
import WritingLoader from 'app/components/writing-loader/writing-loader';
import Icon from 'app/components/icon/icon';
import { Application } from 'app/features/applications/types/application';
import Emojione from 'components/emojione/emojione';
import Beacon from 'app/components/scroll-hidden-components/beacon';
import RouterServices from 'app/features/router/services/router-service';
import './Channel.scss';
import AvatarComponent from 'app/components/avatar/avatar';
import { AtSymbolIcon, BellIcon } from '@heroicons/react/outline';
import Avatar from 'app/atoms/avatar';
type Props = {
app?: Application;
name: string;
icon: string | JSX.Element;
id?: string;
channelId?: string;
notificationLevel: 'all' | 'none' | 'mentions' | 'me';
favorite: boolean;
unreadMessages: number;
mentions: number;
replies: number;
unread: number;
visibility: string; //"private" | "public" | "direct"
directMembers?: string[];
menu?: JSX.Element;
showTooltip?: boolean;
active?: boolean;
writingActivity?: boolean;
selected?: boolean;
};
export default (props: Props) => {
const selected = props.selected;
const writingActivity = props.writingActivity || false;
const onClick = () => {
const url = RouterServices.generateRouteFromState({
channelId: props.id,
});
RouterServices.push(url);
};
const getDefaultApplicationIcon = (app: Application) => {
switch (app.identity.code) {
case 'twake_tasks':
return <CheckSquare size={16} color={selected ? 'var(--white)' : 'var(--black)'} />;
case 'twake_calendar':
return <Calendar size={16} color={selected ? 'var(--white)' : 'var(--black)'} />;
case 'twake_drive':
return <Folder size={16} color={selected ? 'var(--white)' : 'var(--black)'} />;
default:
return props.app?.identity.icon ? (
<AvatarComponent url={props.app?.identity.icon} size={16} />
) : (
<Hexagon size={16} color={selected ? 'var(--white)' : 'var(--black)'} />
);
}
};
const grayBadgeClassName = ' bg-zinc-300 text-zinc-700 dark:bg-zing-800 dark:text-zinc-600';
const blueBadgeClassName = ' text-white bg-blue-500';
const counterValue = Math.max(1, Math.max(props.mentions + props.replies, props.unreadMessages));
return (
<Tooltip title={props.showTooltip ? props.name : false} placement="right" mouseEnterDelay={3}>
<div
className={`channel ${selected ? 'selected ' : ''} ${props.active ? 'menu-open' : ''}`}
onClick={onClick}
>
{!!props.favorite && (
<div className="icon small-right-margin">
<Star size={14} />
</div>
)}
{!props.app &&
(props.visibility === 'public' || props.visibility === 'private') &&
typeof props.icon === 'string' && (
<div className="icon">
<Avatar
size="xs"
nogradient={!!(props.icon && (props.icon?.length || 0) < 20)}
icon={
props.icon && (props.icon?.length || 0) < 20 ? (
<Emojione type={props.icon} />
) : (
false
)
}
avatar={(props.icon?.length || 0) > 20 ? props?.icon : ''}
title={props?.name || ''}
/>
</div>
)}
{!props.app && props.visibility === 'direct' && typeof props.icon === 'object' && (
<div className="direct-channel-avatars"> {props.icon}</div>
)}
{!!props.app && <div className="icon">{getDefaultApplicationIcon(props.app)}</div>}
<div className="text" style={{ textTransform: 'capitalize' }}>
{props.name + ' '}
{props.visibility === 'private' && <Icon type="lock merge-icon black-icon" />}
</div>
<div className="writing_activity">{!selected && writingActivity && <WritingLoader />}</div>
<div className="more">
{props.notificationLevel === 'none' && <Icon type="bell-slash merge-icon grey-icon" />}
{props.unread > 0 && props.mentions + props.replies === 0 && (
<div
className={
'text-xs font-medium h-5 w-5 flex items-center justify-center text-sm rounded-full ml-1' +
blueBadgeClassName
}
>
<BellIcon className="h-4 w-4" />
<Beacon tag="channel_bar_component" />
</div>
)}
{props.mentions + props.replies > 0 && (
<div
className={
'text-xs font-medium h-5 w-5 flex items-center justify-center text-sm rounded-full ml-1' +
(props.notificationLevel === 'none' ? grayBadgeClassName : blueBadgeClassName)
}
>
<AtSymbolIcon className="h-4 w-4" />
<Beacon tag="channel_bar_component" />
</div>
)}
{!(props.visibility === 'direct' && props.mentions + props.replies === 0) &&
((props.unreadMessages > 0 && props.mentions + props.replies === 0) ||
props.mentions + props.replies > 1) && (
<div
className={
'text-xs font-medium h-5 px-1.5 flex items-center justify-center text-sm rounded-full ml-1' +
(props.notificationLevel === 'all' ||
(props.mentions + props.replies > 0 && props.notificationLevel !== 'none')
? blueBadgeClassName
: grayBadgeClassName)
}
>
{counterValue > 99 ? '99+' : counterValue}
</div>
)}
{props.menu}
</div>
</div>
</Tooltip>
);
};
@@ -1,38 +0,0 @@
import React, { FC, ReactNode } from 'react';
import Icon from 'app/components/icon/icon';
import './Channel.scss';
type PropsType = {
text?: string;
editable?: boolean;
refAdd?: React.LegacyRef<HTMLDivElement> | undefined;
sub?: boolean;
onClick?: () => void;
onAdd?: () => void;
suffix?: string | ReactNode;
addIcon?: JSX.Element;
};
const ChannelCategory: FC<PropsType> = (props): JSX.Element => {
return (
<div
className={'channel_category ' + (props.sub ? 'sub ' : '')}
onClick={() => props.editable && props.onClick && props.onClick()}
>
<div className="text">{props.text}</div>
{props.sub && props.editable && (
<div className="edit">
<Icon type="edit-alt" />
</div>
)}
{props.onAdd && (
<div ref={props.refAdd} className="add" onClick={props.onAdd}>
{props.addIcon || <Icon type="plus" />}
</div>
)}
{props.suffix}
</div>
);
};
export default ChannelCategory;
@@ -1,76 +0,0 @@
import React, { useState } from 'react';
import ChannelUI from './Channel';
import ChannelMenu from './ChannelMenu';
import { ChannelType } from 'app/features/channels/types/channel';
import { getUserParts } from 'app/components/member/user-parts';
import { useChannelWritingActivityState } from 'app/features/channels/hooks/use-channel-writing-activity';
import useRouterChannelSelected from 'app/features/router/hooks/use-router-channel-selected';
import { useChannelNotifications } from 'app/features/users/hooks/use-notifications';
import { useChannel } from 'app/features/channels/hooks/use-channel';
type Props = {
channel: ChannelType;
};
export default (props: Props): JSX.Element => {
if (!props.channel || !props.channel.user_member?.user_id) return <></>;
const { channel } = useChannel(props.channel.id || '', {
companyId: props.channel.company_id || '',
workspaceId: props.channel.workspace_id || '',
});
const isDirectChannel = props.channel.visibility === 'direct';
const [isActive, setActive] = useState<boolean>(false);
const selected = useRouterChannelSelected(props.channel.id || '');
const writingActivity = useChannelWritingActivityState(props.channel.id || '');
const { badges: notifications } = useChannelNotifications(props.channel.id || '');
const { avatar, name } = isDirectChannel
? getUserParts({
usersIds: props.channel.members || [],
displayOnline: true,
})
: { avatar: '', name: '' };
const unreadMessages = Math.max(
0,
(channel.stats?.messages || 0) - (channel?.user_member?.last_increment || 0),
);
const channelIcon = isDirectChannel ? avatar : channel.icon || '';
const channelName = isDirectChannel ? name : channel.name || '';
return (
<ChannelUI
name={channelName}
icon={channelIcon}
favorite={channel.user_member?.favorite || false}
writingActivity={writingActivity.length > 0}
visibility={channel.visibility || 'public'}
notificationLevel={channel.user_member?.notification_level || 'mentions'}
unreadMessages={unreadMessages}
replies={notifications.filter(n => n.mention_type === 'reply').length || 0}
unread={notifications.filter(n => n.mention_type === 'unread').length || 0}
mentions={
notifications.filter(
n =>
!n.mention_type ||
(n.mention_type === 'me' &&
['mentions', 'me', 'all'].includes(channel.user_member?.notification_level || '')) ||
(n.mention_type === 'global' &&
['mentions', 'all'].includes(channel.user_member?.notification_level || '')),
).length || 0
}
selected={selected}
menu={
<ChannelMenu
channel={channel}
onClick={() => setActive(true)}
onClose={() => setActive(false)}
/>
}
active={isActive}
id={channel.id}
/>
);
};
@@ -1,87 +0,0 @@
import React from 'react';
import { useOpenChannelModal } from 'app/components/edit-channel';
import Icon from 'app/components/icon/icon';
import ChannelAPIClient from 'app/features/channels/api/channel-api-client';
import { useRefreshPublicOrPrivateChannels } from 'app/features/channels/hooks/use-public-or-private-channels';
import { ChannelType } from 'app/features/channels/types/channel';
import Languages from 'app/features/global/services/languages-service';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { useChannelNotifications } from 'app/features/users/hooks/use-notifications';
import AccessRightsService from 'app/features/workspace-members/services/workspace-members-access-rights-service';
import Menu from 'components/menus/menu';
type PropsType = {
channel: ChannelType;
onClick: () => void;
onClose: () => void;
};
export default (props: PropsType): JSX.Element => {
const [showMenu, setShowMenu] = React.useState(false);
if (showMenu) {
return <FullMenu onClose={props.onClose} onClick={props.onClick} channel={props.channel} />;
}
return (
<>
<div className="more-icon ml-1" onMouseOver={() => setShowMenu(true)}>
<Icon type="ellipsis-h more-icon grey-icon" />
</div>
</>
);
};
const FullMenu = (props: PropsType): JSX.Element => {
const companyId = props.channel.company_id;
const workspaceId = useRouterWorkspace();
const { badges } = useChannelNotifications(props.channel.id || '');
const { refresh: refreshChannels } = useRefreshPublicOrPrivateChannels();
const openChannelModal = useOpenChannelModal();
Languages.useListener();
const editChannel = () => {
openChannelModal(props.channel.id || '');
};
const menu: object[] = [
{
type: 'menu',
text: Languages.t(
badges.length > 0
? 'scenes.app.channelsbar.read_sign'
: 'scenes.app.channelsbar.unread_sign',
),
onClick: () => {
ChannelAPIClient.read(
props.channel.company_id || '',
props.channel.workspace_id || '',
props.channel.id || '',
{ status: badges.length > 0, now: true },
);
refreshChannels();
},
},
{
type: 'menu',
hide: !(
AccessRightsService.hasLevel(workspaceId, 'member') &&
AccessRightsService.getCompanyLevel(companyId) !== 'guest'
),
text: Languages.t('scenes.app.channelsbar.modify_channel_menu'),
onClick: () => editChannel(),
},
];
return (
<>
<div className="more-icon">
<Menu menu={menu} className="options" onClose={props.onClose}>
<Icon type="ellipsis-h more-icon grey-icon" onClick={props.onClick} />
</Menu>
</div>
</>
);
};
@@ -4,7 +4,6 @@ import { Badge } from 'antd';
import UserService from 'app/features/users/services/current-user-service'; import UserService from 'app/features/users/services/current-user-service';
import Icon from 'app/components/icon/icon.jsx'; import Icon from 'app/components/icon/icon.jsx';
import Emojione from 'components/emojione/emojione'; import Emojione from 'components/emojione/emojione';
import NotificationDelay from '../Notifications/NotificationDelay';
import './CompanyHeader.scss'; import './CompanyHeader.scss';
import { useCurrentUser } from 'app/features/users/hooks/use-current-user'; import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
@@ -46,9 +45,6 @@ export default (props: PropsType): JSX.Element => {
</div> </div>
)} )}
</div> </div>
<div className="notifications">
<NotificationDelay />
</div>
</div> </div>
); );
}; };
@@ -27,7 +27,6 @@ import ModalManager from 'app/components/modal/modal-manager';
import CompanyMessagesCounter from 'components/company-messages-counter/company-messages-counter'; import CompanyMessagesCounter from 'components/company-messages-counter/company-messages-counter';
import ConsoleService from 'app/features/console/services/console-service'; import ConsoleService from 'app/features/console/services/console-service';
import MenuCompanyHeader from './MenuCompanyHeader'; import MenuCompanyHeader from './MenuCompanyHeader';
import SaveNewStatus from './SaveNewStatus';
export default class CurrentUser extends Component { export default class CurrentUser extends Component {
constructor() { constructor() {
@@ -207,29 +206,6 @@ export default class CurrentUser extends Component {
usermenu = usermenu.concat([ usermenu = usermenu.concat([
{ type: 'separator' }, { type: 'separator' },
{
type: 'menu',
text: Languages.t('scenes.app.channelsbar.currentuser.change_my_status'),
emoji: (current_user.status.split(' ') || {})[0] || ':smiley:',
submenu_replace: false,
submenu: [
{
type: 'title',
text: Languages.t(
'scenes.app.channelsbar.currentuser.change_my_status',
[],
'Changer mon statut',
),
},
{
type: 'react-element',
reactElement: level => {
return <SaveNewStatus level={level} />;
},
},
],
},
{ {
type: 'menu', type: 'menu',
icon: 'user', icon: 'user',
@@ -1,75 +0,0 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import React from 'react';
import moment from 'moment';
import Menu from 'components/menus/menu';
import Icon from 'app/components/icon/icon.jsx';
import Languages from 'app/features/global/services/languages-service';
import NotificationPreferences from 'app/deprecated/user/NotificationPreferences';
export default () => {
const status = NotificationPreferences.areNotificationsAllowed();
const tomorrowMorning = moment().add(1, 'd').hour(9).minute(0).second(0);
const hoursUntilTomorrowMorning = moment
.duration(tomorrowMorning.diff(moment(new Date())))
.asHours();
const notifications_menu = [
{
type: 'menu',
hide: true, //Disabled until refactor
text: Languages.t('scenes.app.channelsbar.currentuser.user_parameter'),
onClick: () => {},
},
{
type: 'menu',
text: Languages.t('scenes.app.channelsbar.currentuser.disabling_notifications'),
onClick: () => NotificationPreferences.deactivateNotificationsUntil(1 * 60 * 60 * 1000),
},
{
type: 'menu',
text: '2 hours', // Add translation
onClick: () => NotificationPreferences.deactivateNotificationsUntil(2 * 60 * 60 * 1000),
},
{
type: 'menu',
text: Languages.t('scenes.app.channelsbar.currentuser.disabling_notifications_until'),
onClick: () =>
NotificationPreferences.deactivateNotificationsUntil(
hoursUntilTomorrowMorning * 60 * 60 * 1000,
),
},
{
type: 'menu',
text: Languages.t(
status
? 'scenes.app.channelsbar.currentuser.disable_notifications'
: 'scenes.app.channelsbar.currentuser.reactivate_notifications',
),
onClick: () => {
status
? NotificationPreferences.deactivateNotificationsUntil(10 * 365 * 24 * 60 * 60 * 1000)
: NotificationPreferences.deactivateNotificationsUntil(0);
},
},
{
// TODO: Calculate the real value
hide: true,
type: 'text',
text: '35 minutes remaning', // Vos notifications sont désactivées jusqu'à hh:mm
className: 'remaning-time',
},
];
return (
<Menu menu={notifications_menu} position="bottom">
<div className="notifications">
<div className={`bell ${status ? '' : 'sleep'}`}>
<Icon type={status ? 'bell' : 'bell-slash'} className="bell-icon" />
</div>
</div>
</Menu>
);
};
@@ -1,10 +0,0 @@
import React from 'react';
export default () => {
return <></>;
//return isEmpty(notificationPreferences) ? (
// <></>
//) : (
// <NotificationBell preferences={notificationPreferences} />
//);
};
@@ -1,15 +0,0 @@
.save-new-status-container {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 8px;
.error-button {
color: var(--error);
border: 1px solid var(--error);
&:hover {
color: var(--error);
border: 1px solid var(--error);
}
}
}
@@ -1,74 +0,0 @@
import React, { useCallback, useState, useRef, useEffect } from 'react';
import { Button, InputRef } from 'antd';
import classNames from 'classnames';
import Languages from 'app/features/global/services/languages-service';
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
import MenusManager from 'app/components/menus/menus-manager';
import InputWithIcon from 'app/components/inputs/input-with-icon';
import './SaveNewStatus.scss';
type PropsType = {
level: number;
};
export default ({ level }: PropsType): JSX.Element => {
const { user, updateStatus } = useCurrentUser();
const icon = user?.status?.split(' ')[0] || '';
const text = user?.status?.split(' ').slice(1).join(' ') || '';
const [status, setStatus] = useState([icon, text]);
const statusRef = useRef(status);
const inputRef = useRef<InputRef>();
const save = useCallback(() => {
updateStatus(status);
MenusManager.closeMenu();
}, [status, updateStatus]);
const reset = useCallback(() => {
updateStatus(['', '']);
MenusManager.closeMenu();
}, [updateStatus]);
useEffect(() => {
inputRef.current?.focus();
}, []);
return (
<div
onKeyDown={evt => {
if (evt.key === 'Enter') save();
}}
>
<InputWithIcon
inputRef={(node: InputRef) => node && (inputRef.current = node)}
focusOnDidMount
menu_level={level}
placeholder={Languages.t(
'scenes.app.popup.appsparameters.pages.status_tilte',
[],
'Status',
)}
value={status}
onChange={(value: string[]) => {
setStatus(value);
inputRef.current?.focus();
}}
/>
<div className="save-new-status-container">
<Button
className={classNames({ 'error-button': statusRef.current === status })}
type={statusRef.current === status ? 'default' : 'ghost'}
onClick={statusRef.current === status ? reset : save}
>
{Languages.t(
statusRef.current === status
? 'scenes.app.channelsbar.currentuser.reset'
: 'scenes.app.channelsbar.currentuser.update',
)}
</Button>
</div>
</div>
);
};
@@ -1,184 +0,0 @@
/* eslint-disable react/jsx-key */
import React, { Component } from 'react';
import Languages from 'app/features/global/services/languages-service';
import WorkspaceUserRights from 'app/features/workspaces/services/workspace-user-rights-service';
import Emojione from 'components/emojione/emojione';
import UserService from 'app/features/users/services/current-user-service';
import CurrentUserService from 'app/deprecated/user/CurrentUser';
import ElectronService from 'app/features/global/framework/electron-service';
import AlertManager from 'app/features/global/services/alert-manager-service';
import Workspaces from 'app/deprecated/workspaces/workspaces.jsx';
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
import WorkspaceParameter from 'app/views/client/popup/WorkspaceParameter/WorkspaceParameter.js';
import WorkspacesUsers from 'app/features/workspace-members/services/workspace-members-service';
import UserParameter from 'app/views/client/popup/UserParameter/UserParameter.js';
import popupManager from 'app/deprecated/popupManager/popupManager.js';
import TutorialUI from 'app/views/client/channels-bar/Parts/Tutorial/Tutorial.js';
import Globals from 'app/features/global/services/globals-twake-app-service';
export default class Tutorial extends Component {
constructor() {
super();
this.state = {
i18n: Languages,
};
Languages.addListener(this);
Collections.get('users').addListener(this);
Collections.get('users').listenOnly(this, [UserService.getCurrentUser().front_id]);
}
componentWillUnmount() {
Languages.removeListener(this);
Collections.get('users').removeListener(this);
}
componentDidMount() {
if (ElectronService.isElectron()) {
CurrentUserService.updateTutorialStatus('has_desktop_app');
}
}
removeTuto(tuto, evt) {
evt.stopPropagation();
evt.preventDefault();
AlertManager.confirm(
() => {
CurrentUserService.updateTutorialStatus(tuto);
},
() => {},
{
text: Languages.t(
'scenes.app.channelsbar.tutorial_alert',
[],
'Ne plus jamais afficher ce cadre ?',
),
},
);
}
render() {
var tutorial_status = UserService.getCurrentUser().tutorial_status || {};
var group = Collections.get('groups').find(Workspaces.currentGroupId);
if (
WorkspaceUserRights.hasWorkspacePrivilege() &&
Object.keys(WorkspacesUsers.getUsersByWorkspace(Workspaces.currentWorkspaceId) || {}).length >
1
) {
CurrentUserService.updateTutorialStatus('did_invite_collaborators');
}
if (
['openid', 'cas'].indexOf(CurrentUserService.get().identity_provider) >= 0 &&
!tutorial_status.no_tuto
) {
CurrentUserService.updateTutorialStatus('no_tuto');
tutorial_status.no_tuto = true;
}
return (
<div className="left_tutorials">
{WorkspaceUserRights.isInvite() && !tutorial_status.no_invite && false && (
<TutorialUI
title={[
Languages.t(
'scenes.app.channelsbar.company_invitation_alert_title',
[],
"Vous êtes invité dans l'entreprise ",
),
<b>{group.name}</b>,
]}
subtitle={
// eslint-disable-next-line no-constant-condition
true
? ''
: Languages.t(
'scenes.app.channelsbar.company_invitation_alert_subtitle',
[],
'Vous pouvez importer ces chaînes dans votre entreprise pour rester organisé en cliquant sur ce cadre.',
)
}
onClickFrame={e => {
/*window.open("https://twakeapp.com/")*/
}}
onClose={e => this.removeTuto('no_invite', e)}
/>
)}
{!WorkspaceUserRights.isInvite() && !tutorial_status.no_tuto && (
<TutorialUI
title={[
Languages.t('scenes.app.channelsbar.welcome', [], 'Bienvenue sur Twake'),
<Emojione type=":stars:" />,
]}
subtitle={Languages.t(
'scenes.app.channelsbar.welcoming_message_subtitle',
[],
'Devenez un pro de Twake en seulement quelques clics !',
)}
blocks={[
{
text: Languages.t(
'scenes.app.channelsbar.initial_instructions_tutorial',
[],
'Choisissez un prénom, un nom et une image de profil.',
),
emoji: ':woman_juggling:',
done: tutorial_status.has_identity,
onClick: () => {
popupManager.open(<UserParameter />);
},
},
{
text: Languages.t(
'scenes.app.channelsbar.sending_message_instruction',
[],
'Envoyer un premier message dans une discussion',
),
emoji: ':love_letter:',
done: tutorial_status.first_message_sent,
},
/*{
text: "Installer l'application mobile",
emoji: ":iphone:",
done: tutorial_status.has_mobile_app,
onClick: ()=>{ Globals.window.open("https://twakeapp.com/download") }
},*/
{
text: Languages.t(
'scenes.app.channelsbar.installation_desktop_tutorial',
[],
"Installer l'application desktop",
),
emoji: ':desktop:',
done: tutorial_status.has_desktop_app,
onClick: () => {
Globals.window.open('https://twakeapp.com/download');
},
},
{
text: Languages.t(
'scenes.app.channelsbar.invitation_collaboraors_tutorial',
[],
'Invitez vos collaborateurs dans votre espace de travail.',
),
emoji: ':handshake:',
done: tutorial_status.did_invite_collaborators,
onClick: () => {
popupManager.open(
<WorkspaceParameter initial_page={2} />,
true,
'workspace_parameters',
);
},
},
]}
onClose={e => this.removeTuto('no_tuto', e)}
/>
)}
</div>
);
}
}
@@ -1,56 +0,0 @@
import React, { Component } from 'react';
import './Tutorial.scss';
import Icon from 'app/components/icon/icon.jsx';
import Emojione from 'components/emojione/emojione';
export default class Tutorial extends React.Component {
constructor(props) {
super();
}
render() {
return (
<div className="tutorial" onClick={this.props.onClickFrame}>
<div
className="close"
onClick={evt => {
evt.preventDefault();
evt.stopPropagation();
this.props.onClose && this.props.onClose(evt);
}}
>
<Icon type="multiply" />
</div>
{this.props.title && <div className="small_title">{this.props.title}</div>}
{this.props.subtitle && <div className="small_subtitle">{this.props.subtitle}</div>}
{this.props.blocks && this.props.blocks.length > 0 && (
<div className="body">
{(this.props.blocks || [])
.sort((a, b) => (a.done ? 1 : -1))
.map((item, i) => {
return (
<div
key={'bloc_' + i}
className={
'block ' + (item.done ? 'done ' : '') + (item.onClick ? 'hoverable ' : '')
}
onClick={item.onClick}
>
{(item.emoji || item.done) && (
<div className="icon">
<Emojione type={item.done ? ':white_check_mark:' : item.emoji} />
</div>
)}
<div className="text">{item.text}</div>
</div>
);
})}
</div>
)}
</div>
);
}
}
@@ -1,73 +0,0 @@
.tutorial {
z-index: 5;
box-sizing: border-box;
width: 300px;
height: auto;
position: absolute;
left: 16px;
bottom: 4px;
background: var(--secondary);
color: #fff;
border-radius: var(--border-radius-base);
padding: 16px;
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.05), 0 0 20px 0 rgba(0, 0, 0, 0.15);
position: relative;
.close {
background: var(--grey-dark);
color: #fff;
width: 16px;
height: 16px;
font-size: 10px;
padding: 2px 1px;
box-sizing: border-box;
border-radius: var(--border-radius-large);
line-height: 10px;
position: absolute;
right: 16px;
top: 16px;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
.title {
font-size: 16px;
}
.body {
font-size: 12px;
font-weight: 500;
color: var(--white);
.block {
margin-top: 16px;
display: flex;
.icon {
display: flex;
align-items: center;
padding-right: 8px;
.emojione {
width: 16px;
height: 16px;
}
}
&.done .text {
text-decoration: line-through;
}
&.hoverable:not(.done) {
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
}
}
}
.loading_render {
.tutorial {
display: none;
}
}
@@ -18,7 +18,7 @@ import ModalComponent from 'app/components/modal/modal-component';
import ConnectionIndicator from 'components/connection-indicator/connection-indicator'; import ConnectionIndicator from 'components/connection-indicator/connection-indicator';
import SearchPopup from 'components/search-popup/search-popup'; import SearchPopup from 'components/search-popup/search-popup';
import NewVersionComponent from 'components/new-version/new-version-component'; import NewVersionComponent from 'components/new-version/new-version-component';
import SideBars, { LoadingSidebar } from './side-bars'; import SideBars from './side-bars';
import CompanyStatusComponent from 'app/components/on-boarding/company-status-component'; import CompanyStatusComponent from 'app/components/on-boarding/company-status-component';
import UserContext from 'app/features/users/state/integration/user-context'; import UserContext from 'app/features/users/state/integration/user-context';
import { useCurrentUser, useCurrentUserRealtime } from 'app/features/users/hooks/use-current-user'; import { useCurrentUser, useCurrentUserRealtime } from 'app/features/users/hooks/use-current-user';
@@ -60,13 +60,13 @@ export default React.memo((): JSX.Element => {
breakpoint="lg" breakpoint="lg"
collapsedWidth="0" collapsedWidth="0"
theme="light" theme="light"
width={290} width={70}
onCollapse={(collapsed, type) => { onCollapse={(collapsed, type) => {
if (type === 'responsive') return setTimeout(() => setMenuIsOpen(false), 0); if (type === 'responsive') return setTimeout(() => setMenuIsOpen(false), 0);
setTimeout(() => setMenuIsOpen(!collapsed), 0); setTimeout(() => setMenuIsOpen(!collapsed), 0);
}} }}
> >
<Suspense fallback={<LoadingSidebar />}> <Suspense fallback={<></>}>
<SideBars /> <SideBars />
</Suspense> </Suspense>
</Layout.Sider> </Layout.Sider>
@@ -1,11 +1,8 @@
import React, { FC } from 'react'; import useRouterCompany from 'app/features/router/hooks/use-router-company';
import Messages from 'app/views/applications/messages'; import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import Drive from 'app/views/applications/drive';
import Calendar from 'app/views/applications/calendar/calendar-content';
import Tasks from 'app/views/applications/tasks/tasks';
import NoApp from '../NoApp';
import AppViewService from 'app/features/router/services/app-view-service'; import AppViewService from 'app/features/router/services/app-view-service';
import { useChannel } from 'app/features/channels/hooks/use-channel'; import Drive from 'app/views/applications/drive';
import { FC } from 'react';
type PropsType = { type PropsType = {
viewService: AppViewService; viewService: AppViewService;
@@ -13,42 +10,16 @@ type PropsType = {
}; };
const AppView: FC<PropsType> = props => { const AppView: FC<PropsType> = props => {
//Listen context and app_id changes const companyId = useRouterCompany();
props.viewService.useWatcher(() => { const workspaceId = useRouterWorkspace();
return [
props.viewService.getConfiguration().app?.id,
props.viewService.getConfiguration().context,
];
});
const configuration = props.viewService.getConfiguration(); return (
<Drive
const { channel } = useChannel(props.id); context={{
companyId,
const app = props.viewService.getConfiguration().app; workspaceId,
}}
if (!channel) return <NoApp />; />
);
switch (app?.identity?.code) {
case 'twake_drive':
return (
<Drive
context={{
companyId: channel.company_id,
workspaceId: channel.workspace_id || '',
channelId: channel.id,
tabId: configuration.context.tabId,
}}
/>
);
case 'twake_calendar':
return <Calendar options={configuration} />;
case 'twake_tasks':
return <Tasks channel={channel} options={configuration} />;
case 'messages':
return <Messages channel={channel} options={configuration} />;
default:
return <NoApp />;
}
}; };
export default AppView; export default AppView;
@@ -1,30 +0,0 @@
import React, { FC } from 'react';
import AppViewService from 'app/features/router/services/app-view-service';
import Languages from 'app/features/global/services/languages-service';
import { useWorkspace } from 'app/features/workspaces/hooks/use-workspaces';
import { useChannel } from 'app/features/channels/hooks/use-channel';
type PropsType = {
id: string;
viewService: AppViewService;
};
const ViewName: FC<PropsType> = props => {
//Listen context and app_id changes
props.viewService.useWatcher(() => [
props.viewService.getConfiguration().app?.id,
props.viewService.getConfiguration().context,
]);
const { channel } = useChannel(props.id);
const { workspace } = useWorkspace(channel?.workspace_id || '');
let text = '';
if (channel && workspace) {
text = (workspace ? workspace.name + ' • ' : '') + (channel.name || '');
}
return <span>{Languages.t('scenes.app.side_app.messages_thread_title', [text])}</span>;
};
export default ViewName;
@@ -1,12 +1,9 @@
import React, { FC, Suspense } from 'react'; import React, { FC, Suspense } from 'react';
import { Layout } from 'antd'; import { Layout } from 'antd';
import Tabs from './Tabs/Tabs';
import AppView from './AppView/AppView';
import SideViewService from 'app/features/router/services/side-view-service';
import MainViewService from 'app/features/router/services/main-view-service'; import MainViewService from 'app/features/router/services/main-view-service';
import { X } from 'react-feather'; import SideViewService from 'app/features/router/services/side-view-service';
import ViewName from './AppView/ViewName'; import AppView from './AppView/AppView';
export const ViewContext = React.createContext({ type: '', id: '' }); export const ViewContext = React.createContext({ type: '', id: '' });
@@ -15,11 +12,6 @@ const MainContent: FC<unknown> = () => {
MainViewService.getViewType(), MainViewService.getViewType(),
MainViewService.getId(), MainViewService.getId(),
]); ]);
const mainConfiguration = MainViewService.getConfiguration();
const [sideType, sideId] = SideViewService.useWatcher(() => [
SideViewService.getViewType(),
SideViewService.getId(),
]);
// Put the sideview in full screen if screen has not a big width // Put the sideview in full screen if screen has not a big width
const { innerWidth } = window; const { innerWidth } = window;
@@ -31,11 +23,6 @@ const MainContent: FC<unknown> = () => {
<ViewContext.Provider value={{ type: 'main', id: mainId }}> <ViewContext.Provider value={{ type: 'main', id: mainId }}>
<Layout.Content className={'global-view-content'}> <Layout.Content className={'global-view-content'}>
<Layout style={{ flex: '1' }}> <Layout style={{ flex: '1' }}>
{mainConfiguration.hasTabs && (
<Layout.Header className="main-view-tabs-header">
<Tabs key={mainId} />
</Layout.Header>
)}
<Layout style={{ height: '100%' }} hasSider> <Layout style={{ height: '100%' }} hasSider>
<Layout.Content> <Layout.Content>
<Layout className="main-view-layout"> <Layout className="main-view-layout">
@@ -46,35 +33,6 @@ const MainContent: FC<unknown> = () => {
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</Layout.Content> </Layout.Content>
<ViewContext.Provider value={{ type: 'side', id: sideId }}>
<Layout.Sider
trigger={null}
className="global-side-view"
breakpoint="lg"
collapsedWidth="0"
theme="light"
width={sideViewWidth}
collapsed={sideType === ''}
>
{!!sideType && (
<Layout style={{ height: '100%' }}>
<Layout.Header className="side-header">
<ViewName
key={sideId + '-side-header'}
id={sideId}
viewService={SideViewService}
/>
<X onClick={() => SideViewService.select('', { context: {} })} />
</Layout.Header>
<Layout.Content style={{ flex: 1 }}>
<Suspense fallback={<></>}>
<AppView key={sideId + '-side'} id={sideId} viewService={SideViewService} />
</Suspense>
</Layout.Content>
</Layout>
)}
</Layout.Sider>
</ViewContext.Provider>
</Layout> </Layout>
</Layout> </Layout>
</Layout.Content> </Layout.Content>
@@ -1,65 +0,0 @@
import React from 'react';
import { Col, Typography } from 'antd';
import { capitalize } from 'lodash';
import Languages from 'app/features/global/services/languages-service';
import SearchInput from '../Search';
import MainViewService from 'app/features/router/services/main-view-service';
import { Calendar, CheckSquare, Folder } from 'react-feather';
import AvatarComponent from 'app/components/avatar/avatar';
import { Application } from 'app/features/applications/types/application';
export default (): JSX.Element => {
const application = MainViewService.getConfiguration().app;
if (!application) {
return <></>;
}
const channel = {
name: Languages.t(
'app.identity?.name.' + application?.identity?.code,
[],
application.identity?.name,
),
};
const getDefaultApplicationIcon = (app: Partial<Application>) => {
switch (app?.identity?.code) {
case 'twake_tasks':
return <CheckSquare size={16} color={'var(--black)'} />;
case 'twake_calendar':
return <Calendar size={16} color={'var(--black)'} />;
case 'twake_drive':
return <Folder size={16} color={'var(--black)'} />;
default:
return (
<AvatarComponent
url={app?.identity?.icon}
fallback={`${process.env.PUBLIC_URL}/public/img/hexagon.png`}
size={16}
/>
);
}
};
return (
<div
className="flex flex-nowrap items-center px-2 h-10"
style={{ lineHeight: '47px', flexWrap: 'nowrap' }}
>
<Col flex="auto">
<span className="ml-2 app-name" style={{ display: 'flex', alignItems: 'center' }}>
<div className="small-right-margin" style={{ lineHeight: 0, width: 16 }}>
{getDefaultApplicationIcon(application)}
</div>
<Typography.Text className="small-right-margin" strong>
{capitalize(channel.name)}
</Typography.Text>
<Typography.Text> </Typography.Text>
</span>
</Col>
<SearchInput />
</div>
);
};
@@ -1,20 +0,0 @@
import { getUserParts } from 'app/components/member/user-parts';
import { useUsersListener } from 'app/features/users/hooks/use-users-listener';
import useRouterCompany from 'app/features/router/hooks/use-router-company';
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
import { useChannelMembers } from 'app/features/channel-members-search/hooks/members-hook';
export default ({ workspaceId }: { workspaceId: string }): JSX.Element => {
const companyId = useRouterCompany();
const channelId = useRouterChannel();
const { channelMembers } = useChannelMembers({ companyId, workspaceId, channelId });
const members = channelMembers.filter((_m, i) => i < 10).map(m => m.user_id || '');
useUsersListener(members);
const { avatar } = getUserParts({ usersIds: members, keepMyself: true, max: 7 });
return avatar;
};
@@ -1,149 +0,0 @@
import { Button, Col, Row, Typography } from 'antd';
import { File, Info, Lock, Users } from 'react-feather';
import { useOpenChannelModal } from 'app/components/edit-channel';
import Emojione from 'app/components/emojione/emojione';
import { useUsersSearchModal } from 'app/features/channel-members-search/state/search-channel-member';
import { useChannel } from 'app/features/channels/hooks/use-channel';
import ChannelsBarService from 'app/features/channels/services/channels-bar-service';
import { channelAttachmentListState } from 'app/features/channels/state/channel-attachment-list';
import PseudoMarkdownCompiler from 'app/features/global/services/pseudo-markdown-compiler-service';
import RouterServices from 'app/features/router/services/router-service';
import { useUsersListener } from 'app/features/users/hooks/use-users-listener';
import AccessRightsService from 'app/features/workspace-members/services/workspace-members-access-rights-service';
import { useRecoilState } from 'recoil';
import SearchInput from '../Search';
import ChannelAvatars from './ChannelAvatars';
import ChannelUsersHeader from './ChannelUsersHeader';
import Avatar from 'app/atoms/avatar';
export default (): JSX.Element => {
const { companyId, workspaceId, channelId } = RouterServices.getStateFromRoute();
const { channel } = useChannel(channelId || '');
const members = channel?.members || [];
const [, setChannelAttachmentState] = useRecoilState(channelAttachmentListState);
const canAccessChannelParameters =
AccessRightsService.hasLevel(workspaceId, 'member') &&
AccessRightsService.getCompanyLevel(companyId) !== 'guest';
const openChannelModal = useOpenChannelModal();
const { setOpen: setParticipantsOpen } = useUsersSearchModal();
useUsersListener(members);
if (!channel) {
return <Col></Col>;
}
if (!channel.user_member?.user_id) {
ChannelsBarService.updateCurrentChannelId(companyId, workspaceId, '');
}
ChannelsBarService.updateCurrentChannelId(companyId, workspaceId, channelId);
return (
<div
className="flex flex-nowrap items-center px-2 h-10"
style={{ lineHeight: '47px', flexWrap: 'nowrap' }}
>
{
// Temporary, it's for spacing when the hamburger menu is displayed
<Col xs={1} sm={1} md={1} lg={0} xl={0} xxl={0}></Col>
}
{channel.visibility === 'direct' && (
<Col className="grow overflow-hidden text-ellipsis whitespace-nowrap">
<ChannelUsersHeader channel={channel} />
</Col>
)}
{channel.visibility !== 'direct' && (
<Col className="grow overflow-hidden text-ellipsis whitespace-nowrap">
<span
className="ml-2 text-overflow channel-name"
style={{ display: 'flex', alignItems: 'center' }}
>
<Avatar
size="xs"
className="mr-2"
nogradient={!!(channel.icon && (channel.icon?.length || 0) < 20)}
icon={
channel.icon && (channel.icon?.length || 0) < 20 ? (
<Emojione type={channel.icon} />
) : (
false
)
}
avatar={(channel?.icon?.length || 0) > 20 ? channel?.icon : ''}
title={channel?.name || ''}
/>
<Typography.Text className="small-right-margin" strong>
{channel.name}
</Typography.Text>
{channel.visibility === 'private' && <Lock size={16} className="small-right-margin" />}
<Typography.Text ellipsis={true} className="markdown" style={{ lineHeight: '16px' }}>
{PseudoMarkdownCompiler.compileToHTML(
PseudoMarkdownCompiler.compileToJSON(
(channel.description || '').replace(/\n/g, ' '),
),
)}
</Typography.Text>
</span>
</Col>
)}
<Col xs={0} sm={0} md={0} lg={6} xl={5} xxl={4}>
{!!channel.user_member?.user_id && (
<Row
align="middle"
justify="end"
gutter={[8, 0]}
style={{ padding: 0, flexWrap: 'nowrap' }}
>
{channel.visibility !== 'direct' && channel.workspace_id && (
<div className="small-right-margin" style={{ display: 'inline', lineHeight: 0 }}>
<ChannelAvatars workspaceId={channel.workspace_id} />
</div>
)}
{channel.visibility !== 'direct' && (
<>
{canAccessChannelParameters && (
<Button
size="small"
type="text"
className="px-1"
onClick={() => {
openChannelModal(channel.id || '');
}}
>
<Info className="h-5" />
</Button>
)}
<Button
size="small"
type="text"
className="px-1"
onClick={() => setParticipantsOpen(true)}
>
<Users className="h-5" />
</Button>
</>
)}
<Button
size="small"
type="text"
className="w-auto px-1 mr-2"
onClick={() => {
setChannelAttachmentState(true);
}}
>
<File className="h-5" />
</Button>
</Row>
)}
</Col>
<div className="w-2"></div>
<SearchInput />
</div>
);
};
@@ -1,66 +0,0 @@
import { Col, Typography } from 'antd';
import * as Text from 'app/atoms/text';
import { getUserParts } from 'app/components/member/user-parts';
import { ChannelType } from 'app/features/channels/types/channel';
import Languages from 'app/features/global/services/languages-service';
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
import { useOnlineUser } from 'app/features/users/hooks/use-online-user';
import { useUser } from 'app/features/users/hooks/use-user';
import { useUsersListener } from 'app/features/users/hooks/use-users-listener';
import moment from 'moment';
type Props = {
channel: ChannelType;
};
export default (props: Props) => {
useUsersListener(props.channel.members);
const { avatar, name } = getUserParts({
usersIds: props.channel.members || [],
});
const { user: currentUser } = useCurrentUser();
const otherMembers = (props.channel.members || []).filter(u => u !== currentUser?.id) || [];
const user = useUser(otherMembers[0] || '');
const userOnlineStatus = useOnlineUser(user?.id as string);
const lastSeen = userOnlineStatus?.lastSeen || user?.last_seen || 0;
return (
<Col>
<span
className="left-margin text-overflow overflow-hidden whitespace-nowrap text-ellipsis"
style={{ display: 'flex', alignItems: 'center' }}
>
<div className="small-right-margin" style={{ lineHeight: 0 }}>
{avatar}
</div>
<Typography.Text
className="small-right-margin"
style={{ textTransform: 'capitalize' }}
strong
>
{name}
</Typography.Text>
{otherMembers.length === 1 &&
lastSeen > 0 &&
lastSeen > Date.now() - 1000 * 60 * 60 * 24 * 70 &&
currentUser?.id !== user?.id && (
<>
{!userOnlineStatus.connected && (
<Text.Info className="small-right-margin">
{Languages.t('general.user.connected')} {moment(lastSeen).fromNow()}
</Text.Info>
)}
{!!userOnlineStatus.connected && (
<Text.Info className="small-right-margin">
{Languages.t('general.user.connected')}
</Text.Info>
)}
</>
)}
</span>
</Col>
);
};
@@ -1,20 +0,0 @@
import React, { FC } from 'react';
import { Layout } from 'antd';
import ChannelBar from './ChannelHeader/ChannelHeader';
import MainViewService from 'app/features/router/services/main-view-service';
import ApplicationBar from './ApplicationHeader/ApplicationHeader';
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
const MainHeader: FC<unknown> = () => {
const channelId = useRouterChannel();
const channelType = MainViewService.useWatcher(() => MainViewService.getViewType());
return (
<Layout.Header className={'global-view-header'}>
{channelType === 'channel' && <ChannelBar key={channelId} />}
{channelType === 'application' && <ApplicationBar key={channelId} />}
</Layout.Header>
);
};
export default MainHeader;
@@ -1,23 +1,18 @@
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
import React, { FC, useEffect, useState } from 'react';
import { Layout } from 'antd'; import { Layout } from 'antd';
import { FC, useEffect, useState } from 'react';
import MainHeader from './MainHeader/MainHeader';
import MainContent from './MainContent';
import NoApp from './NoApp';
import ChannelsBarService from 'app/features/channels/services/channels-bar-service';
import { useWatcher } from 'app/deprecated/Observable/Observable';
import AccountStatusComponent from 'app/components/on-boarding/account-status-component'; import AccountStatusComponent from 'app/components/on-boarding/account-status-component';
import CompanyBillingBanner from 'app/components/on-boarding/company-billing-banner'; import CompanyBillingBanner from 'app/components/on-boarding/company-billing-banner';
import './MainView.scss'; import useRouterChannel from 'app/features/router/hooks/use-router-channel';
import useRouterCompany from 'app/features/router/hooks/use-router-company'; import useRouterCompany from 'app/features/router/hooks/use-router-company';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace'; import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import useRouterChannel from 'app/features/router/hooks/use-router-channel'; import MainContent from './MainContent';
import MainViewService from 'app/features/router/services/main-view-service'; import Search from './MainHeader/Search';
import WindowState from 'app/features/global/utils/window'; import './MainView.scss';
import { useCompanyApplications } from 'app/features/applications/hooks/use-company-applications'; import CompanyHeader from '../channels-bar/Parts/CurrentUser/CompanyHeader/CompanyHeader';
import { useChannel, useIsChannelMember } from 'app/features/channels/hooks/use-channel'; import CurrentUser from '../channels-bar/Parts/CurrentUser/CurrentUser';
import ContentRestricted from './ContentRestricted'; import Footer from '../channels-bar/Parts/Footer';
type PropsType = { type PropsType = {
className?: string; className?: string;
@@ -27,68 +22,17 @@ const MainView: FC<PropsType> = ({ className }) => {
const companyId = useRouterCompany(); const companyId = useRouterCompany();
const workspaceId = useRouterWorkspace(); const workspaceId = useRouterWorkspace();
const channelId = useRouterChannel(); const channelId = useRouterChannel();
const { applications } = useCompanyApplications();
const { channel, loading: channelLoading } = useChannel(channelId);
const isChannelMember = useIsChannelMember(channelId);
const loaded = useWatcher(ChannelsBarService, async () => {
return (
(await ChannelsBarService.isReady(companyId, workspaceId)) &&
ChannelsBarService.isReady(companyId, 'direct')
);
});
const ready = !(channelLoading && !channel) && loaded && !!companyId && !!workspaceId;
const updateView = () => {
if (channelId) {
const app = applications.find(a => a.id === channelId);
MainViewService.select(channelId, {
app: app || {
identity: {
code: 'messages',
name: '',
icon: '',
description: '',
website: '',
categories: [],
compatibility: [],
},
},
context: { type: app ? 'application' : 'channel' },
hasTabs: channel?.visibility !== 'direct' && !app,
});
WindowState.setSuffix(channel?.name || app?.identity?.name);
}
};
if (channelId && MainViewService.getId() !== channelId) updateView();
if (
ready &&
!isChannelMember &&
MainViewService.getViewType() === 'channel' &&
channel?.visibility === 'private'
) {
return (
<>
<Layout className={'global-view-layout ' + (className ? className : '')}>
<ContentRestricted />
</Layout>
</>
);
}
return ( return (
<Layout className={'global-view-layout ' + (className ? className : '')}> <Layout className={'global-view-layout ' + (className ? className : '')}>
{!!channelId && ready && ( <AccountStatusComponent />
<> {companyId && <CompanyBillingBanner companyId={companyId || ''} />}
<AccountStatusComponent /> <div className="bg-white flex space-between w-full">
{companyId && <CompanyBillingBanner companyId={companyId || ''} />} <CurrentUser />
<MainHeader /> <Search />
<MainContentWrapper key={companyId + workspaceId + channelId} /> <Footer />
</> </div>
)} <MainContentWrapper key={companyId + workspaceId + channelId} />
{!channelId && ready && <NoApp />}
</Layout> </Layout>
); );
}; };
@@ -110,12 +54,5 @@ export const MainContentWrapper = () => {
}; };
export default ({ className }: PropsType) => { export default ({ className }: PropsType) => {
//This is a hack because main view is displayed before we detect the current "channel" is in fact an application
const channelId = useRouterChannel();
const { applications } = useCompanyApplications();
const isChannelMember = useIsChannelMember(channelId);
if (applications.length === 0 && !isChannelMember) return <></>;
return <MainView className={className} />; return <MainView className={className} />;
}; };
@@ -1,47 +1,24 @@
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
import React from 'react';
import { Layout } from 'antd'; import { Layout } from 'antd';
import ChannelsBar, { LoadingChannelBar } from './channels-bar/ChannelsBar'; import WorkspacesBar from './workspaces-bar';
import WorkspacesBar, { LoadingWorkspaceBar } from './workspaces-bar';
import { useWorkspaceLoader } from 'app/features/workspaces/hooks/use-workspaces';
import useRouterCompany from 'app/features/router/hooks/use-router-company';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { useCurrentCompanyRealtime } from '../../features/companies/hooks/use-companies'; import { useCurrentCompanyRealtime } from '../../features/companies/hooks/use-companies';
import './workspaces-bar/styles.scss';
import { usePreloadSomeUsers } from 'app/features/users/hooks/use-user-list';
import { useNotifications } from 'app/features/users/hooks/use-notifications'; import { useNotifications } from 'app/features/users/hooks/use-notifications';
import { usePreloadSomeUsers } from 'app/features/users/hooks/use-user-list';
import './workspaces-bar/styles.scss';
export default () => { export default () => {
const companyId = useRouterCompany();
const workspaceId = useRouterWorkspace();
const { loading } = useWorkspaceLoader(companyId);
useCurrentCompanyRealtime(); useCurrentCompanyRealtime();
// We call this hook here to be able to preload some users in user list state // We call this hook here to be able to preload some users in user list state
usePreloadSomeUsers(); usePreloadSomeUsers();
useNotifications(); useNotifications();
if (loading) {
return <LoadingSidebar />;
}
return ( return (
<Layout style={{ height: '100%', backgroundColor: 'var(--secondary)' }}> <Layout style={{ height: '100%', backgroundColor: 'var(--secondary)' }}>
<WorkspacesBar /> <WorkspacesBar />
{!!workspaceId && <ChannelsBar />}
{!workspaceId && <LoadingChannelBar />}
</Layout>
);
};
export const LoadingSidebar = () => {
return (
<Layout style={{ height: '100%' }}>
<LoadingWorkspaceBar />
<LoadingChannelBar />
</Layout> </Layout>
); );
}; };