Cleaning
This commit is contained in:
@@ -62,6 +62,11 @@
|
||||
"flushInterval": "SEARCH_ES_FLUSHINTERVAL"
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"local": {
|
||||
"path": "STORAGE_LOCAL_PATH"
|
||||
}
|
||||
},
|
||||
"message-queue": {
|
||||
"type": "PUBSUB_TYPE",
|
||||
"amqp": {
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
},
|
||||
"webserver": {
|
||||
"port": 3000,
|
||||
"port": 4000,
|
||||
"logger": {
|
||||
"level": "info"
|
||||
},
|
||||
@@ -154,6 +154,7 @@
|
||||
"api_key": "secret",
|
||||
"sender": "noreply@twake.app"
|
||||
},
|
||||
"applications": [],
|
||||
"services": [
|
||||
"auth",
|
||||
"push",
|
||||
|
||||
@@ -34,7 +34,8 @@ export const SearchInput = () => {
|
||||
const mediasLoading = useSearchMessagesMediasLoading();
|
||||
const driveItemsLoading = useSearchDriveItemsLoading();
|
||||
|
||||
const loading = channelsLoading || messagesLoading || filesLoading || mediasLoading || driveItemsLoading;
|
||||
const loading =
|
||||
channelsLoading || messagesLoading || filesLoading || mediasLoading || driveItemsLoading;
|
||||
|
||||
return (
|
||||
<div className="relative flex mt-2 w-full">
|
||||
@@ -54,32 +55,11 @@ export const SearchInput = () => {
|
||||
inputRef={inputElement}
|
||||
onChange={e => setInput({ ...input, query: e.target.value })}
|
||||
value={input.query}
|
||||
className={className + ' rounded-tr-none rounded-br-none'}
|
||||
className={className}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,106 +1,24 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import Tabs from '@molecules/tabs';
|
||||
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 { SearchInputState } from 'app/features/search/state/search-input';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import { useRecoilState, 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 { useRecoilValue } from 'recoil';
|
||||
import SearchResulsDriveItems from './tabs/drive';
|
||||
|
||||
const orderedTabs = ['all', 'messages', 'medias', 'files', 'channels', 'drive'];
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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
|
||||
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"
|
||||
>
|
||||
{tab === 'all' && <SearchResultsAll />}
|
||||
{tab === 'messages' && <SearchResultsMessages />}
|
||||
{tab === 'medias' && <SearchResultsMedias />}
|
||||
{tab === 'files' && <SearchResultsFiles />}
|
||||
{tab === 'channels' && <SearchResultsChannels />}
|
||||
{tab === 'drive' && <SearchResulsDriveItems />}
|
||||
<SearchResulsDriveItems />
|
||||
</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 count = props.count < 100 ? props.count : '99+';
|
||||
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>
|
||||
);
|
||||
};
|
||||
-106
@@ -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>
|
||||
);
|
||||
};
|
||||
-85
@@ -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 || ''} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-65
@@ -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>
|
||||
);
|
||||
};
|
||||
-97
@@ -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>
|
||||
);
|
||||
};
|
||||
-39
@@ -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;
|
||||
-76
@@ -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
@@ -4,7 +4,6 @@ import { Badge } from 'antd';
|
||||
import UserService from 'app/features/users/services/current-user-service';
|
||||
import Icon from 'app/components/icon/icon.jsx';
|
||||
import Emojione from 'components/emojione/emojione';
|
||||
import NotificationDelay from '../Notifications/NotificationDelay';
|
||||
|
||||
import './CompanyHeader.scss';
|
||||
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
|
||||
@@ -46,9 +45,6 @@ export default (props: PropsType): JSX.Element => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="notifications">
|
||||
<NotificationDelay />
|
||||
</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 ConsoleService from 'app/features/console/services/console-service';
|
||||
import MenuCompanyHeader from './MenuCompanyHeader';
|
||||
import SaveNewStatus from './SaveNewStatus';
|
||||
|
||||
export default class CurrentUser extends Component {
|
||||
constructor() {
|
||||
@@ -207,29 +206,6 @@ export default class CurrentUser extends Component {
|
||||
usermenu = usermenu.concat([
|
||||
{ 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',
|
||||
icon: 'user',
|
||||
|
||||
-75
@@ -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>
|
||||
);
|
||||
};
|
||||
-10
@@ -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 SearchPopup from 'components/search-popup/search-popup';
|
||||
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 UserContext from 'app/features/users/state/integration/user-context';
|
||||
import { useCurrentUser, useCurrentUserRealtime } from 'app/features/users/hooks/use-current-user';
|
||||
@@ -60,13 +60,13 @@ export default React.memo((): JSX.Element => {
|
||||
breakpoint="lg"
|
||||
collapsedWidth="0"
|
||||
theme="light"
|
||||
width={290}
|
||||
width={70}
|
||||
onCollapse={(collapsed, type) => {
|
||||
if (type === 'responsive') return setTimeout(() => setMenuIsOpen(false), 0);
|
||||
setTimeout(() => setMenuIsOpen(!collapsed), 0);
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<LoadingSidebar />}>
|
||||
<Suspense fallback={<></>}>
|
||||
<SideBars />
|
||||
</Suspense>
|
||||
</Layout.Sider>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import React, { FC } from 'react';
|
||||
import Messages from 'app/views/applications/messages';
|
||||
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 useRouterCompany from 'app/features/router/hooks/use-router-company';
|
||||
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
|
||||
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 = {
|
||||
viewService: AppViewService;
|
||||
@@ -13,42 +10,16 @@ type PropsType = {
|
||||
};
|
||||
|
||||
const AppView: FC<PropsType> = props => {
|
||||
//Listen context and app_id changes
|
||||
props.viewService.useWatcher(() => {
|
||||
return [
|
||||
props.viewService.getConfiguration().app?.id,
|
||||
props.viewService.getConfiguration().context,
|
||||
];
|
||||
});
|
||||
const companyId = useRouterCompany();
|
||||
const workspaceId = useRouterWorkspace();
|
||||
|
||||
const configuration = props.viewService.getConfiguration();
|
||||
|
||||
const { channel } = useChannel(props.id);
|
||||
|
||||
const app = props.viewService.getConfiguration().app;
|
||||
|
||||
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 />;
|
||||
}
|
||||
return (
|
||||
<Drive
|
||||
context={{
|
||||
companyId,
|
||||
workspaceId,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
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 { 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 { X } from 'react-feather';
|
||||
import ViewName from './AppView/ViewName';
|
||||
import SideViewService from 'app/features/router/services/side-view-service';
|
||||
import AppView from './AppView/AppView';
|
||||
|
||||
export const ViewContext = React.createContext({ type: '', id: '' });
|
||||
|
||||
@@ -15,11 +12,6 @@ const MainContent: FC<unknown> = () => {
|
||||
MainViewService.getViewType(),
|
||||
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
|
||||
const { innerWidth } = window;
|
||||
@@ -31,11 +23,6 @@ const MainContent: FC<unknown> = () => {
|
||||
<ViewContext.Provider value={{ type: 'main', id: mainId }}>
|
||||
<Layout.Content className={'global-view-content'}>
|
||||
<Layout style={{ flex: '1' }}>
|
||||
{mainConfiguration.hasTabs && (
|
||||
<Layout.Header className="main-view-tabs-header">
|
||||
<Tabs key={mainId} />
|
||||
</Layout.Header>
|
||||
)}
|
||||
<Layout style={{ height: '100%' }} hasSider>
|
||||
<Layout.Content>
|
||||
<Layout className="main-view-layout">
|
||||
@@ -46,35 +33,6 @@ const MainContent: FC<unknown> = () => {
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</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.Content>
|
||||
|
||||
-65
@@ -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>
|
||||
);
|
||||
};
|
||||
-20
@@ -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;
|
||||
};
|
||||
-149
@@ -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>
|
||||
);
|
||||
};
|
||||
-66
@@ -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
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
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 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 useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
|
||||
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
|
||||
import MainViewService from 'app/features/router/services/main-view-service';
|
||||
import WindowState from 'app/features/global/utils/window';
|
||||
import { useCompanyApplications } from 'app/features/applications/hooks/use-company-applications';
|
||||
import { useChannel, useIsChannelMember } from 'app/features/channels/hooks/use-channel';
|
||||
import ContentRestricted from './ContentRestricted';
|
||||
import MainContent from './MainContent';
|
||||
import Search from './MainHeader/Search';
|
||||
import './MainView.scss';
|
||||
import CompanyHeader from '../channels-bar/Parts/CurrentUser/CompanyHeader/CompanyHeader';
|
||||
import CurrentUser from '../channels-bar/Parts/CurrentUser/CurrentUser';
|
||||
import Footer from '../channels-bar/Parts/Footer';
|
||||
|
||||
type PropsType = {
|
||||
className?: string;
|
||||
@@ -27,68 +22,17 @@ const MainView: FC<PropsType> = ({ className }) => {
|
||||
const companyId = useRouterCompany();
|
||||
const workspaceId = useRouterWorkspace();
|
||||
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 (
|
||||
<Layout className={'global-view-layout ' + (className ? className : '')}>
|
||||
{!!channelId && ready && (
|
||||
<>
|
||||
<AccountStatusComponent />
|
||||
{companyId && <CompanyBillingBanner companyId={companyId || ''} />}
|
||||
<MainHeader />
|
||||
<MainContentWrapper key={companyId + workspaceId + channelId} />
|
||||
</>
|
||||
)}
|
||||
{!channelId && ready && <NoApp />}
|
||||
<AccountStatusComponent />
|
||||
{companyId && <CompanyBillingBanner companyId={companyId || ''} />}
|
||||
<div className="bg-white flex space-between w-full">
|
||||
<CurrentUser />
|
||||
<Search />
|
||||
<Footer />
|
||||
</div>
|
||||
<MainContentWrapper key={companyId + workspaceId + channelId} />
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
@@ -110,12 +54,5 @@ export const MainContentWrapper = () => {
|
||||
};
|
||||
|
||||
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} />;
|
||||
};
|
||||
|
||||
@@ -1,47 +1,24 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
import React from 'react';
|
||||
import { Layout } from 'antd';
|
||||
|
||||
import ChannelsBar, { LoadingChannelBar } from './channels-bar/ChannelsBar';
|
||||
import WorkspacesBar, { LoadingWorkspaceBar } from './workspaces-bar';
|
||||
import WorkspacesBar 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 './workspaces-bar/styles.scss';
|
||||
import { usePreloadSomeUsers } from 'app/features/users/hooks/use-user-list';
|
||||
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 () => {
|
||||
const companyId = useRouterCompany();
|
||||
const workspaceId = useRouterWorkspace();
|
||||
const { loading } = useWorkspaceLoader(companyId);
|
||||
useCurrentCompanyRealtime();
|
||||
|
||||
// We call this hook here to be able to preload some users in user list state
|
||||
usePreloadSomeUsers();
|
||||
useNotifications();
|
||||
|
||||
if (loading) {
|
||||
return <LoadingSidebar />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100%', backgroundColor: 'var(--secondary)' }}>
|
||||
<WorkspacesBar />
|
||||
{!!workspaceId && <ChannelsBar />}
|
||||
{!workspaceId && <LoadingChannelBar />}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export const LoadingSidebar = () => {
|
||||
return (
|
||||
<Layout style={{ height: '100%' }}>
|
||||
<LoadingWorkspaceBar />
|
||||
<LoadingChannelBar />
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user