New Drive UI initial version

This commit is contained in:
Romaric Mourgues
2023-03-27 18:30:36 +02:00
parent fcb0f993fe
commit 42420df02d
261 changed files with 1496 additions and 16604 deletions
+34 -37
View File
@@ -4,8 +4,7 @@ import { Router } from 'react-router';
import { Switch, Route } from 'react-router-dom';
import { RecoilRoot } from 'recoil';
import MobileRedirect from './views/mobile-redirect';
import Integration from 'app/views/integration';
import MobileRedirect from './components/mobile-redirect';
import RouterServices, { RouteType } from './features/router/services/router-service';
import ErrorBoundary from 'app/views/error/error-boundary';
import InitService from './features/global/services/init-service';
@@ -56,41 +55,39 @@ export default () => {
<RecoilRoot>
<DebugState />
<MobileRedirect>
<Integration>
<Router history={RouterServices.history}>
<Switch>
{RouterServices.routes.map((route: RouteType, index: number) => (
<Route
key={`${route.key}_${index}`}
exact={route.exact ? route.exact : false}
path={route.path}
component={() =>
route.options?.withErrorBoundary ? (
<ErrorBoundary key={route.key}>
<route.component />
</ErrorBoundary>
) : (
<route.component key={route.key} />
)
}
/>
))}
{
<Route
path="/"
component={() => {
RouterServices.replace(
`${
RouterServices.pathnames.LOGIN
}?auto&${RouterServices.history.location.search.substr(1)}`,
);
return <div />;
}}
/>
}
</Switch>
</Router>
</Integration>
<Router history={RouterServices.history}>
<Switch>
{RouterServices.routes.map((route: RouteType, index: number) => (
<Route
key={`${route.key}_${index}`}
exact={route.exact ? route.exact : false}
path={route.path}
component={() =>
route.options?.withErrorBoundary ? (
<ErrorBoundary key={route.key}>
<route.component />
</ErrorBoundary>
) : (
<route.component key={route.key} />
)
}
/>
))}
{
<Route
path="/"
component={() => {
RouterServices.replace(
`${
RouterServices.pathnames.LOGIN
}?auto&${RouterServices.history.location.search.substr(1)}`,
);
return <div />;
}}
/>
}
</Switch>
</Router>
</MobileRedirect>
</RecoilRoot>
);
@@ -15,7 +15,7 @@ interface AvatarProps extends React.InputHTMLAttributes<HTMLInputElement> {
nogradient?: boolean;
}
const sizes = { xl: 24, lg: 14, md: 11, sm: 9, xs: 6 };
const sizes = { xl: 24, lg: 14, md: 10, sm: 9, xs: 6 };
const fontSizes = { xl: '2xl', lg: '2xl', md: 'lg', sm: 'md', xs: 'sm' };
export const getGradient = (name: string) => {
+10 -6
View File
@@ -3,7 +3,7 @@ import React from 'react';
import _ from 'lodash';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
theme?: 'primary' | 'secondary' | 'danger' | 'default' | 'outline' | 'dark';
theme?: 'primary' | 'secondary' | 'danger' | 'default' | 'outline' | 'dark' | 'white';
size?: 'md' | 'lg' | 'sm';
icon?: (props: any) => JSX.Element;
iconSize?: 'md' | 'lg';
@@ -19,18 +19,22 @@ export const Button = (props: ButtonProps) => {
if (props.theme === 'secondary')
className =
'text-blue-500 bg-blue-100 hover:bg-blue-200 active:bg-blue-300 border-transparent ';
'text-blue-500 bg-blue-500 hover:bg-opacity-30 active:bg-opacity-50 bg-opacity-25 border-transparent ';
if (props.theme === 'danger')
className = 'text-white bg-rose-500 hover:bg-rose-600 active:bg-rose-700 border-transparent ';
if (props.theme === 'default')
className =
'text-black dark:text-white bg-white dark:bg-zinc-800 dark:hover:bg-zinc-700 dark:active:bg-zinc-900 hover:bg-gray-50 active:bg-gray-200 border-gray-300';
'text-black dark:text-white bg-white dark:bg-zinc-800 dark:hover:bg-zinc-700 dark:active:bg-zinc-900 hover:bg-zinc-50 active:bg-zinc-200 border-zinc-300';
if (props.theme === 'white')
className =
'text-black dark:text-white dark:hover:bg-zinc-700 dark:active:bg-zinc-900 hover:bg-zinc-50 active:bg-zinc-200 border-transparent';
if (props.theme === 'outline')
className =
'text-blue-500 bg-white dark:bg-zinc-800 dark:hover:bg-zinc-700 dark:active:bg-zinc-900 hover:bg-gray-50 active:bg-gray-200 border-blue-500';
'text-blue-500 bg-white dark:bg-zinc-800 dark:hover:bg-zinc-700 dark:active:bg-zinc-900 hover:bg-zinc-50 active:bg-zinc-200 border-blue-500';
if (props.theme === 'dark')
className =
@@ -38,12 +42,12 @@ export const Button = (props: ButtonProps) => {
if (disabled) className += ' opacity-50 pointer-events-none';
if (props.size === 'lg') className = className + ' text-lg h-11';
if (props.size === 'lg') className = className + ' text-lg h-10';
else if (props.size === 'sm') className = className + ' text-sm h-7 px-3';
else className = className + ' text-base h-9';
if (!props.children) {
if (props.size === 'lg') className = className + ' w-11 !p-0 justify-center';
if (props.size === 'lg') className = className + ' w-10 !p-0 justify-center';
else if (props.size === 'sm') className = className + ' w-7 !p-0 justify-center';
else className = className + ' w-9 !p-0 justify-center';
}
@@ -20,13 +20,13 @@ interface InputProps
}
const baseInputClassName =
'tw-input block w-full rounded-md focus:ring-1 focus:ring-blue-500 z-0 focus:z-10 dark:text-white text-black ';
'tw-input block w-full rounded-md focus:ring-1 focus:ring-blue-500 z-0 focus:z-10 dark:text-white text-black text-base ';
export const defaultInputClassName = (theme: 'plain' | 'outline' = 'plain') => {
return (
baseInputClassName +
(theme === 'plain'
? 'bg-zinc-200 border-zinc-200 dark:bg-zinc-800 dark:border-zinc-800'
? 'bg-zinc-100 border-zinc-100 dark:bg-zinc-800 dark:border-zinc-800'
: 'bg-zinc-50 border-zinc-300 dark:bg-zinc-800 dark:border-zinc-700')
);
};
@@ -47,7 +47,7 @@ export const Input = (props: InputProps) => {
inputClassName = inputClassName + (props.disabled ? ' opacity-75' : '');
if (!props.multiline) {
if (props.size === 'lg') inputClassName = inputClassName + ' h-11';
if (props.size === 'lg') inputClassName = inputClassName + ' h-10 pl-10';
else if (props.size === 'sm') inputClassName = inputClassName + ' h-7';
else inputClassName = inputClassName + ' h-9';
}
+2 -2
View File
@@ -52,13 +52,13 @@ const Text = (props: TextProps) => {
defaultClassName =
'text-sm font-normal' +
' ' +
(props.noColor ? '' : 'text-zinc-400 dark:text-white dark:opacity-50');
(props.noColor ? '' : 'text-zinc-500 dark:text-white dark:opacity-50');
break;
case 'info-small':
defaultClassName =
'text-xs font-normal' +
' ' +
(props.noColor ? '' : 'text-zinc-400 dark:text-white dark:opacity-50');
(props.noColor ? '' : 'text-zinc-500 dark:text-white dark:opacity-50');
break;
}
@@ -1,119 +0,0 @@
import React from 'react';
import ConfiguratorsManager from 'app/deprecated/Configurators/ConfiguratorsManager.js';
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
import CloseIcon from '@material-ui/icons/CloseOutlined';
import './configurators.scss';
import Blocks from '../../views/applications/messages/message/parts/Blocks';
/*
Where the configurators will be displayed, this component should be in app.js (menus should be over all elements of the page)
*/
export default class ConfigBodyLayer extends React.Component {
constructor() {
super();
this.state = {};
ConfiguratorsManager.addListener(this);
}
componentWillUnmount() {
ConfiguratorsManager.removeListener(this);
clearTimeout(this.loading_interaction_timeout);
}
UNSAFE_componentWillMount() {
this.generateData();
}
shouldComponentUpdate(nextProps, nextState) {
this.generateData();
var string = JSON.stringify(this.configurator);
if (string != this.saved) {
nextState.loading_interaction = false;
clearTimeout(this.loading_interaction_timeout);
this.saved = string;
return true;
}
return nextState.loading_interaction ? true : false;
}
generateData() {
if (ConfiguratorsManager.configurator_order.length == 0) {
this.configurator = null;
}
this.configurator =
ConfiguratorsManager.currentConfigurators[
ConfiguratorsManager.configurator_order[ConfiguratorsManager.configurator_order.length - 1]
];
}
onAction(type, id, context, passives) {
if (type == 'interactive_action') {
this.setState({ loading_interaction: true });
clearTimeout(this.loading_interaction_timeout);
this.loading_interaction_timeout = setTimeout(() => {
this.setState({ loading_interaction: false });
}, 5000);
var app_id = this.configurator.app.id;
var ntype = 'interactive_configuration_action';
var event = id;
var data = {
interactive_context: context,
form: passives,
hidden_data: this.configurator.hidden_data,
configurator_id: this.configurator.id,
};
WorkspacesApps.notifyApp(app_id, ntype, event, data);
}
}
render() {
if (!this.configurator) {
return '';
}
return (
<div>
<div
className={
'config_modal fade_in ' + (this.state.loading_interaction ? 'loading_interaction ' : '')
}
>
<div className="modal">
<div
className="component"
style={{
height: this.configurator.hidden_data.height || 'auto',
width: this.configurator.hidden_data.width || '500px',
maxWidth: '90vw',
maxHeight: '90vh',
}}
>
<div className="header">
<div
className="app_logo"
style={{
backgroundImage: 'url(' + this.configurator.app.identity?.icon + ')',
}}
/>
<div className="app_name">{this.configurator.app.identity?.name}</div>
<CloseIcon
className="m-icon-medium close"
onClick={() => {
ConfiguratorsManager.closeConfigurator(this.configurator.app);
}}
/>
</div>
<div className="content">
<Blocks
className="allow_selection"
blocks={this.configurator.form}
fallback={''}
onAction={(type, id, context, passives, evt) =>
this.onAction(type, id, context, passives, evt)
}
allowAdvancedBlocks={true}
/>
</div>
</div>
</div>
</div>
</div>
);
}
}
@@ -1,60 +0,0 @@
.config_modal {
height: 100%;
width: 100%;
background: rgba(255, 255, 255, 0.5);
position: absolute;
z-index: 200;
top: 0;
.modal {
width: 0px;
height: 0px;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
.component {
background: white;
border-radius: var(--border-radius-base);
box-shadow: var(--box-shadow-base);
position: relative;
display: inline-flex;
flex-direction: column;
transform: translateX(-50%) translateY(-50%);
.header {
padding: 15px;
border-bottom: 1px solid #ddd;
display: flex;
.app_logo {
height: 30px;
width: 30px;
background-size: contain;
background-position: center;
border-radius: var(--border-radius-base);
margin-right: 5px;
}
.app_name {
line-height: 30px;
flex: auto;
}
.close {
margin: 2px;
opacity: 0.5;
cursor: pointer;
&:hover {
opacity: 1;
}
}
}
.content {
flex: 1;
overflow: auto;
padding: 15px;
box-sizing: border-box;
}
}
}
}
@@ -29,7 +29,7 @@ import RouterServices from 'app/features/router/services/router-service';
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
import workspaceUserRightsService from 'app/features/workspaces/services/workspace-user-rights-service';
import Block from 'app/molecules/grouped-rows/base';
import { addUrlTryDesktop } from 'app/views/desktop-redirect';
import { addUrlTryDesktop } from 'app/components/desktop-redirect';
import { useState } from 'react';
export const ChannelSettingsMenu = (props: {
@@ -64,7 +64,7 @@ export default (): React.ReactElement => {
style={{ maxHeight: 'calc(42vh - 98px)', minHeight: 'calc(42vh - 98px)' }}
options={{ suppressScrollX: true, suppressScrollY: false }}
>
{ members_limit_reached && <ReachedLimit />}
{members_limit_reached && <ReachedLimit />}
<ChipInput
value={emails}
disableUnderline={true}
@@ -12,17 +12,17 @@ import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
const AccountStatusComponent = (): JSX.Element => {
const { user } = useCurrentUser();
const maxUnverifiedDays =
InitService.server_infos?.configuration?.accounts?.console?.max_unverified_days || 7;
InitService.server_infos?.configuration?.accounts?.remote?.max_unverified_days || 7;
const oneDay = 1000 * 60 * 60 * 24;
const periodLimit = (user?.created_at || 0) + maxUnverifiedDays * oneDay;
const daysLeft = Math.ceil((periodLimit - Date.now()) / oneDay);
if (!user || InitService.server_infos?.configuration?.accounts?.type !== 'console') {
if (!user || InitService.server_infos?.configuration?.accounts?.type !== 'remote') {
return <></>;
}
const showBlockedModal = () => {
if (InitService.server_infos?.configuration?.accounts?.type === 'console')
if (InitService.server_infos?.configuration?.accounts?.type === 'remote')
return ModalManager.open(
<BlockedAccount email={user?.email} />,
{
@@ -34,7 +34,7 @@ const AccountStatusComponent = (): JSX.Element => {
};
const showUnverifiedModal = () => {
if (InitService.server_infos?.configuration?.accounts?.type === 'console')
if (InitService.server_infos?.configuration?.accounts?.type === 'remote')
return ModalManager.open(
<UnverifiedAccount daysLeft={daysLeft} limit={maxUnverifiedDays} email={user.email} />,
{
@@ -18,7 +18,7 @@ const CompanyStatusComponent = (): JSX.Element => {
const workspace = DepreciatedCollections.get('workspaces').find(workspaceId);
useEffect(() => {
if (InitService.server_infos?.configuration?.accounts?.type === 'console') {
if (InitService.server_infos?.configuration?.accounts?.type === 'remote') {
isNewAccount();
}
@@ -1,54 +0,0 @@
import React from 'react';
import { useMessage } from 'app/features/messages/hooks/use-message';
import useRouterCompany from 'app/features/router/hooks/use-router-company';
import MessageQuote from 'app/molecules/message-quote';
import { useUser } from 'app/features/users/hooks/use-user';
import { useMessageQuoteReply } from 'app/features/messages/hooks/use-message-quote-reply';
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
import User from 'app/features/users/services/current-user-service';
import { gotoMessage } from 'src/utils/messages';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import QuotedContent from 'app/molecules/quoted-content';
import { NodeMessage } from 'app/features/messages/types/message';
type PropsType = {
closable?: boolean;
onClose?: () => void;
};
export default ({ closable = true, onClose }: PropsType): React.ReactElement => {
const companyId = useRouterCompany();
const channelId = useRouterChannel();
const workspaceId = useRouterWorkspace();
const { message: quotedMessageId } = useMessageQuoteReply(channelId);
const quotedMessage = useMessage({
companyId,
threadId: quotedMessageId,
id: quotedMessageId,
});
if (!quotedMessage.message) {
return <></>;
}
const { message } = quotedMessage;
const author = useUser(message.user_id);
const authorName = author ? User.getFullName(author) : 'Anonymous';
const deleted = message.subtype === 'deleted';
const quotedContent = (
<QuotedContent message={message as unknown as NodeMessage['quote_message']} />
);
return (
<MessageQuote
className="mx-1"
message={quotedContent}
author={authorName}
closable={closable}
onClose={onClose}
deleted={deleted}
goToMessage={() => gotoMessage(message, companyId, channelId, workspaceId)}
/>
);
};
@@ -25,7 +25,7 @@ import { useRecoilValue, useRecoilState } from 'recoil';
import { openDriveItem, onDriveItemDownloadClick } from '../common';
import ResultContext from './result-context';
import { useCompanyApplications } from 'app/features/applications/hooks/use-company-applications';
import { DriveCurrentFolderAtom } from 'app/views/applications/drive/browser';
import { DriveCurrentFolderAtom } from 'app/views/client/body/drive/browser';
import { FolderIcon } from '@heroicons/react/solid';
export default (props: { driveItem: DriveItem & { user?: UserType } }) => {
@@ -49,6 +49,8 @@ export default class UploadZone extends React.Component<PropsType, StateType> {
this.file_input.style.left = '-10000px';
this.file_input.style.width = '100px';
this.file_input.multiple = this.props.multiple ? true : false;
this.file_input.directory = this.props.multiple ? true : false;
this.file_input.webkitdirectory = this.props.multiple ? true : false;
this.setCallback();
@@ -1,26 +1,21 @@
import React from 'react';
import $ from 'jquery';
import Observable from 'app/deprecated/CollectionsV1/observable.js';
import popupManager from 'app/deprecated/popupManager/popupManager.js';
import PopupManager from 'app/deprecated/popupManager/popupManager.js';
import Api from 'app/features/global/framework/api-service';
import ws from 'app/deprecated/websocket/websocket.js';
import DepreciatedCollections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
import Observable from 'app/deprecated/CollectionsV1/observable.js';
import { default as popupManager, default as PopupManager } from 'app/deprecated/popupManager/popupManager.js';
import ws from 'app/deprecated/websocket/websocket.js';
import Groups from 'app/deprecated/workspaces/groups.js';
import LocalStorage from 'app/features/global/framework/local-storage-service';
import workspacesUsers from 'app/features/workspace-members/services/workspace-members-service';
import WindowService from 'app/features/global/utils/window';
import workspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
import RouterServices from 'app/features/router/services/router-service';
import NoWorkspaces from 'app/views/client/workspaces-bar/components/NoWorkspaces/NoWorkspaces';
import NoCompanies from 'app/views/client/workspaces-bar/components/NoWorkspaces/NoCompanies';
import loginService from 'app/features/auth/login-service';
import Globals from 'app/features/global/services/globals-twake-app-service';
import JWTStorage from 'app/features/auth/jwt-storage-service';
import loginService from 'app/features/auth/login-service';
import ConsoleService from 'app/features/console/services/console-service';
import WorkspaceAPIClient from '../../features/workspaces/api/workspace-api-client';
import Api from 'app/features/global/framework/api-service';
import LocalStorage from 'app/features/global/framework/local-storage-service';
import Logger from 'app/features/global/framework/logger-service';
import UserAPIClient from 'app/features/users/api/user-api-client';
import Globals from 'app/features/global/services/globals-twake-app-service';
import WindowService from 'app/features/global/utils/window';
import RouterServices from 'app/features/router/services/router-service';
import workspacesUsers from 'app/features/workspace-members/services/workspace-members-service';
import $ from 'jquery';
import WorkspaceAPIClient from '../../features/workspaces/api/workspace-api-client';
class Workspaces extends Observable {
constructor() {
@@ -90,7 +85,6 @@ class Workspaces extends Observable {
openNoWorkspacesPage() {
this.showNoWorkspacesPage = true;
this.notify();
popupManager.open(<NoWorkspaces />, false, 'no_workspace_parameters');
}
closeNoWorkspacesPage() {
@@ -102,7 +96,6 @@ class Workspaces extends Observable {
openNoCompaniesPage() {
this.showNoCompaniesPage = true;
this.notify();
popupManager.open(<NoCompanies />, false, 'no_companies_parameters');
}
closeNoCompaniesPage() {
+1 -1
View File
@@ -1,6 +1,6 @@
export type EnvironmentType = {
env_dev: boolean;
env_dev_auth?: 'internal' | 'console';
env_dev_auth?: 'internal' | 'remote';
api_root_url: string;
front_root_url: string;
@@ -20,7 +20,7 @@ import Application from 'app/features/applications/services/application-service'
import LocalStorage from 'app/features/global/framework/local-storage-service';
import Globals from 'app/features/global/services/globals-twake-app-service';
type AccountType = 'console' | 'internal';
type AccountType = 'remote' | 'internal';
export type LoginState =
| ''
| 'app'
@@ -60,7 +60,7 @@ class AuthService {
if (Globals.environment.env_dev_auth) accountType = Globals.environment.env_dev_auth;
if (accountType === 'console') {
if (accountType === 'remote') {
this.provider = new OIDCAuthProviderService(config as ConsoleConfiguration);
} else if (accountType === 'internal') {
this.provider = new InternalAuthProviderService(config as InternalConfiguration);
@@ -14,7 +14,6 @@ import LocalStorage from 'app/features/global/framework/local-storage-service';
const OIDC_CALLBACK_URL = '/oidccallback';
const OIDC_SIGNOUT_URL = '/signout';
const OIDC_CLIENT_ID = 'twake';
@TwakeService('OIDCAuthProvider')
export default class OIDCAuthProviderService
@@ -45,13 +44,15 @@ export default class OIDCAuthProviderService
Oidc.Log.logger = Logger.getLogger('OIDCClient');
Oidc.Log.level = EnvironmentService.isProduction() ? Oidc.Log.WARN : Oidc.Log.DEBUG;
const audience = ['https://dev-mxb6x0f2.eu.auth0.com/api/v2/'];
this.userManager = new Oidc.UserManager({
userStore: new Oidc.WebStorageStateStore({ store: window.localStorage }),
authority: this.configuration?.authority || environment.api_root_url,
client_id: this.configuration?.client_id || OIDC_CLIENT_ID,
client_id: this.configuration?.client_id,
redirect_uri: getAsFrontUrl(OIDC_CALLBACK_URL),
response_type: 'code',
scope: 'openid profile email address phone offline_access',
scope: 'openid profile email address phone offline_access ' + audience.join(' '),
post_logout_redirect_uri: getAsFrontUrl(OIDC_SIGNOUT_URL),
//silent_redirect_uri: getAsFrontUrl(OIDC_SILENT_URL),
automaticSilentRenew: true,
@@ -236,7 +237,10 @@ export default class OIDCAuthProviderService
this.logger.info('getJWTFromOidcToken, user expired');
}
ConsoleService.getNewAccessToken({ access_token: user.access_token }, callback);
ConsoleService.getNewAccessToken(
{ id_token: user.id_token, access_token: user.access_token },
callback,
);
}
signinRedirect() {
@@ -109,7 +109,7 @@ class ConsoleService {
* @param callback
*/
public getNewAccessToken(
currentToken: { access_token: string },
currentToken: { access_token: string; id_token: string },
callback: (err?: Error, access_token?: JWTDataType) => void,
): void {
this.logger.debug(
@@ -117,7 +117,7 @@ class ConsoleService {
);
Api.post(
'/internal/services/console/v1/login',
{ remote_access_token: currentToken.access_token },
{ oidc_id_token: currentToken.id_token },
(response: {
access_token: JWTDataType;
message: string;
@@ -13,7 +13,7 @@ prefix.apply(log, {
return level.toUpperCase();
},
nameFormatter(name) {
return name || 'Twake';
return name || 'Tdrive';
},
timestampFormatter(date) {
return date.toISOString();
@@ -38,14 +38,15 @@ export type ServerInfoType = null | {
help_url: string | null;
pricing_plan_url: string | null;
app_download_url: string | null;
app_grid: { logo: string; name: string; url: string }[];
mobile: {
mobile_redirect: string;
mobile_appstore: string;
mobile_googleplay: string;
};
accounts: {
type: 'console' | 'internal';
console?: ConsoleConfiguration;
type: 'remote' | 'internal';
remote?: ConsoleConfiguration;
internal?: InternalConfiguration;
};
};
@@ -68,7 +69,7 @@ class InitService extends Observable {
companyId = companyId || WorkspaceService.currentGroupId;
const identity_provider_id =
getCompany(companyId || '')?.identity_provider_id || getCompany(companyId || '')?.id;
return (this.server_infos?.configuration?.accounts?.console?.[link] || '').replace(
return (this.server_infos?.configuration?.accounts?.remote?.[link] || '').replace(
/\{company_id\}/gm,
identity_provider_id,
);
@@ -3,7 +3,7 @@ import { capitalize } from 'lodash';
import Globals from 'app/features/global/services/globals-twake-app-service';
class WindowState {
public readonly app_name: string = 'Twake';
public readonly app_name: string = 'Tdrive';
public prefix = '';
public suffix = '';
@@ -8,7 +8,7 @@ import Login from 'app/views/login/login';
import Logout from 'app/views/login/logout';
import Error from 'app/views/error';
import Join from 'app/views/join';
import PublicMainView from 'app/views/applications/drive/shared';
import PublicMainView from 'app/views/client/body/drive/shared';
import Observable from '../../../deprecated/Observable/Observable';
import { getWorkspacesByCompany } from 'app/features/workspaces/state/workspace-list';
@@ -30,8 +30,8 @@ export default function Block(props: BlockProps) {
</div>
</div>
<div className="flex">
<div className="grow truncate leading-normal text-slate-500 mr-1">
<Text.Base className="text-slate-500 dark:text-slate-400">{props.subtitle}</Text.Base>
<div className="grow truncate leading-normal text-zinc-500 mr-1">
<Text.Base className="text-zinc-500 dark:text-zinc-400">{props.subtitle}</Text.Base>
</div>
<div className="whitespace-nowrap">{props.subtitle_suffix}</div>
</div>
@@ -1,119 +0,0 @@
import {
FileTypeArchiveIcon,
FileTypeDocumentIcon,
FileTypePdfIcon,
FileTypeSlidesIcon,
FileTypeSpreadsheetIcon,
FileTypeUnknownIcon,
} from 'app/atoms/icons-colored';
import { Base } from 'app/atoms/text';
import fileUploadApiClient from 'app/features/files/api/file-upload-api-client';
import Languages from 'app/features/global/services/languages-service';
import { useMessage, useSetMessage } from 'app/features/messages/hooks/use-message';
import { NodeMessage } from 'app/features/messages/types/message';
import { MessageContext } from 'app/views/applications/messages/message/message-with-replies';
import MessageContent from 'app/views/applications/messages/message/parts/MessageContent';
import React, { useContext, useEffect } from 'react';
type PropsType = {
message: NodeMessage['quote_message'];
};
export const useQuotedMessage = (
message: NodeMessage,
context: {
companyId: string;
workspaceId: string;
channelId: string;
threadId: string;
id: string;
},
) => {
const quotedMessageFromStore = useMessage({
...context,
threadId: message.quote_message?.thread_id as string,
id: message.quote_message?.id as string,
}).message;
const quotedMessage = {
...message.quote_message,
...(quotedMessageFromStore || message.quote_message),
} as NodeMessage['quote_message'];
const setMessage = useSetMessage(quotedMessage?.company_id || context.companyId);
useEffect(() => {
if (!quotedMessageFromStore?.id && quotedMessage?.id) {
setMessage(quotedMessage);
}
}, []);
return quotedMessage;
};
export default ({ message }: PropsType): React.ReactElement => {
const context = useContext(MessageContext);
if (!message) {
return <></>;
}
const attachmentType = fileUploadApiClient.mimeToType(message.files?.[0]?.metadata?.mime || '');
if (context.channelId && message.channel_id !== context.channelId) {
return (
<>
<MessageContext.Provider
value={{
companyId: message.company_id,
workspaceId: message.workspace_id,
channelId: message.channel_id,
threadId: message.thread_id,
id: message.id || message.thread_id,
}}
>
<MessageContent />
</MessageContext.Provider>
</>
);
}
return (
<>
{message.text && message.text.length ? (
<Base className="!text-sm overflow-hidden whitespace-nowrap text-ellipsis">
{message.text}
</Base>
) : (
<div className="mb-1"></div>
)}
{!!message.files?.length &&
(message.files?.length === 1 ? (
<div className="flex flex-row align-items items-center mb-1">
<>
{attachmentType === 'archive' ? (
<FileTypeArchiveIcon className="w-5 h-5 mr-1" />
) : attachmentType === 'pdf' ? (
<FileTypePdfIcon className="w-5 h-5 mr-1" />
) : attachmentType === 'document' ? (
<FileTypeDocumentIcon className="w-5 h-5 mr-1" />
) : attachmentType === 'spreadsheet' ? (
<FileTypeSpreadsheetIcon className="w-5 h-5 mr-1" />
) : attachmentType === 'slides' ? (
<FileTypeSlidesIcon className={'h-5 w-5 mr-1'} />
) : (
<FileTypeUnknownIcon className="w-5 h-5 mr-1" />
)}
</>
<Base className="!text-sm">{message.files?.[0].metadata?.name}</Base>
</div>
) : (
<div className="flex flex-row align-items items-center mb-1">
<FileTypeUnknownIcon className="w-5 h-5 mr-1" />
<Base className="!text-sm">
{Languages.t('molecules.quoted_content.attachements', [message.files?.length])}
</Base>
</div>
))}
</>
);
};
@@ -21,12 +21,14 @@ export default function Tab(props: TabsProps) {
return (
<>
<div className={`overflow-auto flex w-100 border-b border-zinc-200 dark:border-zinc-800 transition-all select-none ${props.className}`}>
<div
className={`overflow-auto flex w-100 border-b border-zinc-200 dark:border-zinc-800 transition-all select-none ${props.className}`}
>
{props.tabs.map((tab, idx) => {
const cl =
defaultTabClassName +
(idx === props.selected ? activeTabClassName : inactiveTabClassName)
+ props.parentClassName;
(idx === props.selected ? activeTabClassName : inactiveTabClassName) +
props.parentClassName;
return (
<div key={idx} className={cl} onClick={() => props.onClick(idx)}>
{tab}
+1 -1
View File
@@ -6,7 +6,7 @@ html {
color: var(--black);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--secondary) !important;
background-color: #FFF;
}
#root {
@@ -1,850 +0,0 @@
import React, { Component, useState } from 'react';
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
import Languages from 'app/features/global/services/languages-service';
import UserService from 'app/features/users/services/current-user-service';
import CalendarService from 'app/deprecated/Apps/Calendar/Calendar.js';
import LocalStorage from 'app/features/global/framework/local-storage-service';
import ModalManager from 'app/components/modal/modal-manager';
import Icon from 'app/components/icon/icon.jsx';
import moment from 'moment';
import Groups from 'app/deprecated/workspaces/groups.js';
import FullCalendar from './full-calendar/full-calendar.jsx';
import EventDetails from './modals/EventDetails.jsx';
import EventCreation from './modals/EventCreation.jsx';
import EventModification from './modals/EventModification.jsx';
import CalendarEditor from './modals/CalendarEditor.jsx';
import Menu from 'components/menus/menu.jsx';
import DayPicker from 'components/calendar/day-picker/day-picker.jsx';
import AlertManager from 'app/features/global/services/alert-manager-service';
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
import WorkspacesService from 'app/deprecated/workspaces/workspaces.jsx';
import popupManager from 'app/deprecated/popupManager/popupManager.js';
import ConnectorsListManager from 'components/connectors-list-manager/connectors-list-manager.jsx';
import WorkspaceUserRights from 'app/features/workspaces/services/workspace-user-rights-service';
import Checkbox from 'app/components/inputs/deprecated_checkbox.jsx';
import InputWithClipBoard from 'components/input-with-clip-board/input-with-clip-board.jsx';
import Select from 'components/select/select.jsx';
import WorkspaceParameter from 'app/views/client/popup/WorkspaceParameter/WorkspaceParameter.jsx';
import UnconfiguredTab from './unconfigured-tab.jsx';
import RouterService from 'app/features/router/services/router-service';
import MainPlus from 'components/main-plus/main-plus.jsx';
import {
getCompanyApplication as getApplication,
getCompanyApplications,
} from 'app/features/applications/state/company-applications';
import './calendar.scss';
const ExportView = props => {
const [export_my_calendar, set_export_my_calendar] = useState(props.values.export_my_calendar);
const [export_workspace_calendar, set_export_workspace_calendar] = useState(
props.values.export_workspace_calendar,
);
return (
<div style={{ marginTop: -8 }}>
<Checkbox
label={Languages.t('scenes.apps.calendar.my_calendar_label', [], 'Mon calendrier')}
small
className=""
value={export_my_calendar}
onChange={value => {
set_export_my_calendar(value);
props.onChange(export_my_calendar, export_workspace_calendar);
}}
/>
<Checkbox
label={Languages.t('scenes.apps.calendar.workspace_label', [], 'Cet espace de travail')}
small
className=""
value={export_workspace_calendar}
onChange={value => {
set_export_workspace_calendar(value);
props.onChange(export_my_calendar, export_workspace_calendar);
}}
/>
</div>
);
};
export default class Calendar extends Component {
constructor(props) {
super(props);
this.state = {
i18n: Languages,
view: 'timeGridWeek',
filter: 'workspace',
preview: null,
export_my_calendar: true,
export_workspace_calendar: true,
new_calendar: {},
};
CalendarService.filter_mode = this.state.filter;
this.props = props;
const { workspaceId, channelId } = RouterService.getStateFromRoute();
this.loaded_date_range = {};
this.calendar_collection_key = 'calendar_' + workspaceId;
this.setLoadedRange(
'both',
moment(CalendarService.date).startOf('month').toDate().getTime() / 1000 - 60 * 60 * 24 * 31,
moment(CalendarService.date).startOf('month').toDate().getTime() / 1000 +
2 * 60 * 60 * 24 * 31,
);
Collections.get('calendars').addListener(this);
Collections.get('calendars').addSource(
{
http_base_url: 'calendar/calendar',
http_options: {
channel_id: channelId,
workspace_id: workspaceId,
},
websockets: [
{
uri: 'calendars/' + workspaceId,
options: { type: 'calendar' },
},
],
},
this.calendar_collection_key,
() => {
this.onFirstLoad();
},
);
if (Collections.get('calendars').did_load_first_time[this.calendar_collection_key]) {
this.onFirstLoad();
}
Collections.get('events').addListener(this);
Languages.addListener(this);
CalendarService.addListener(this);
}
onFirstLoad() {
const { workspaceId, channelId } = RouterService.getStateFromRoute();
var calendar_list = Collections.get('calendars')
.findBy({ workspace_id: workspaceId })
.map(cal => {
return {
calendar_id: cal.id,
workspace_id: cal.workspace_id,
};
});
if (this.props.tab != null && this.props.tab.configuration.calendars) {
this.allowed_ids = this.props.tab.configuration.calendars.map(c => c.calendar_id);
calendar_list = calendar_list.filter(c => this.allowed_ids.indexOf(c.calendar_id) >= 0);
}
Collections.get('events').addSource(
{
http_base_url: 'calendar/event',
http_options: {
channel_id: channelId,
after_ts:
this.loaded_date_range['both'].min || new Date().getTime() / 1000 - 24 * 60 * 60 * 60,
before_ts:
this.loaded_date_range['both'].max || new Date().getTime() / 1000 + 24 * 60 * 60 * 60,
calendar_list: calendar_list,
mode: 'both',
},
websockets: [
{
uri: 'calendar_events/' + workspaceId,
options: { type: 'event' },
},
{
uri: 'calendar_events/user/' + UserService.getCurrentUserId(),
options: { type: 'event' },
},
],
},
this.calendar_collection_key,
);
}
setLoadedRange(key, min, max) {
if (!this.loaded_date_range[key]) {
this.loaded_date_range[key] = {};
}
this.loaded_date_range[key].min = min;
this.loaded_date_range[key].max = max;
this.loaded_date_range[key].last_updated = new Date();
}
componentDidMount() {
const view = LocalStorage.getItem('calendar_view');
if (view && this.calendar) {
this.setState({ view });
this.calendar.view(view);
}
}
componentWillUnmount() {
Collections.get('events').removeListener(this);
Collections.get('events').removeSource(this.calendar_collection_key);
Collections.get('calendars').removeListener(this);
Collections.get('calendars').removeSource(this.calendar_collection_key);
if (CalendarService.preview) {
CalendarService.closePopups();
}
Languages.removeListener(this);
CalendarService.removeListener(this);
}
UNSAFE_componentWillUpdate(nextProps, nextState) {
if (nextProps.tab != null && nextProps.tab.configuration.calendars) {
this.allowed_ids = nextProps.tab.configuration.calendars.map(c => c.calendar_id);
}
// eslint-disable-next-line no-unused-vars
var filter = nextState.filter;
if (!this.loaded_date_range['both']) {
this.loaded_date_range['both'] = {};
}
var range = this.loaded_date_range['both'];
var requested_min =
moment(CalendarService.date).startOf('month').toDate().getTime() / 1000 - 60 * 60 * 24 * 31;
var requested_max =
moment(CalendarService.date).startOf('month').toDate().getTime() / 1000 +
2 * 60 * 60 * 24 * 31;
if (
!range.min ||
range.min > requested_min ||
range.max < requested_max ||
new Date().getTime() - range.last_updated.getTime() > 60000
) {
this.setLoadedRange('both', requested_min, requested_max);
var calendar_list = Collections.get('calendars')
.findBy({ workspace_id: RouterService.getStateFromRoute().workspaceId })
.map(cal => {
return {
calendar_id: cal.id,
workspace_id: cal.workspace_id,
};
});
if (this.props.tab != null) {
calendar_list = calendar_list.filter(c => this.allowed_ids.indexOf(c.calendar_id) >= 0);
}
Collections.get('events').sourceLoad(
this.calendar_collection_key,
{
after_ts: requested_min,
before_ts: requested_max,
calendar_list: calendar_list,
mode: 'both',
},
() => {},
);
}
}
componentDidUpdate() {
if (this.date !== CalendarService.date && this.calendar) {
this.calendar.setDate(CalendarService.date, true);
this.date = CalendarService.date;
}
}
configureCalendarConnector(app, cal) {
var data = {
calendar: cal,
};
WorkspacesApps.notifyApp(app.id, 'configuration', 'calendar', data);
}
renderCalendarList() {
const { workspaceId } = RouterService.getStateFromRoute();
var calendars = Collections.get('calendars').findBy({
workspace_id: workspaceId,
});
if (this.props.tab != null) {
calendars = calendars.filter(c => this.allowed_ids.indexOf(c.id) >= 0);
}
if (calendars && calendars.length > 0) {
var list = [];
calendars.forEach(cal => {
var el = (
<div className="">
<div className="calendar_color " style={{ backgroundColor: cal.color }} /> {cal.title}
</div>
);
if (WorkspaceUserRights.hasWorkspacePrivilege()) {
list.push({
type: 'menu',
text: el,
submenu: [
{
type: 'menu',
text: Languages.t(
'scenes.apps.calendar.modify_calendar_menu',
[],
'Modifier le calendrier',
),
submenu_replace: true,
submenu: [
{
type: 'title',
text: Languages.t(
'scenes.apps.calendar.edit_calendar_title',
[],
'Éditer le calendrier',
),
},
{
type: 'react-element',
reactElement: level => (
<CalendarEditor
calendar={Collections.get('calendars').edit(cal)}
level={level}
collectionKey={this.calendar_collection_key}
/>
),
},
],
},
{
type: 'menu',
className: 'error',
text: Languages.t('general.delete', [], 'Supprimer'),
onClick: () => {
AlertManager.confirm(
() => {
Collections.get('calendars').remove(cal, this.calendar_collection_key);
},
() => {},
{
text: Languages.t(
'scenes.apps.calendar.remove_calendar_confirmation',
[],
'Supprimer le calendrier et ses événements définitivement ?',
),
},
);
},
},
{ type: 'separator' },
{
type: 'menu',
text: Languages.t('scenes.apps.calendar.connectors_menu', [], 'Connecteurs...'),
submenu: [
{
type: 'react-element',
reactElement: level => {
var apps = getCompanyApplications(Groups.currentGroupId).filter(app => false);
if (apps.length > 0) {
return (
<ConnectorsListManager
list={apps}
current={(cal.connectors || [])
.map(id => getApplication(id))
.filter(item => item)}
configurable={item =>
((item.display || {}).configuration || {}).can_configure_in_calendar
}
onChange={ids => {
cal.connectors = ids;
Collections.get('calendars').save(cal, this.calendar_collection_key);
}}
onConfig={app => {
this.configureCalendarConnector(app, cal);
}}
/>
);
}
return (
<div className="menu-text" style={{ margin: 0, padding: 0 }}>
{Languages.t(
'scenes.apps.calendar.no_connectors_menu_text',
[],
"Vous n'avez aucun connecteur capable de se connecter à un calendrier.",
)}
</div>
);
},
},
{ type: 'separator' },
{
type: 'menu',
text: Languages.t(
'scenes.apps.calendar.connectors_search_menu',
[],
'Chercher des connecteurs...',
),
onClick: () => {
popupManager.open(
<WorkspaceParameter initial_page={3} options={'open_search_apps'} />,
true,
'workspace_parameters',
);
},
},
],
},
],
});
} else {
list.push({ type: 'menu', text: el });
}
});
return list;
} else {
return [
{
type: 'text',
text: Languages.t(
'scenes.apps.calendar.no_calendar_text',
[],
"Vous n'avez défini aucun calendrier pour cet espace de travail.",
),
},
];
}
}
export(download) {
if (!this.state.export_my_calendar && !this.state.export_workspace_calendar) {
return;
}
var mode = 'mine';
var calendar_list = [];
if (this.state.export_my_calendar && this.state.export_workspace_calendar) {
mode = 'both';
} else if (this.state.export_workspace_calendar) {
mode = 'workspace';
}
if (mode === 'both' || mode === 'workspace') {
calendar_list = Collections.get('calendars')
.findBy({ workspace_id: RouterService.getStateFromRoute().workspaceId })
.map(cal => {
return {
calendar_id: cal.id,
workspace_id: cal.workspace_id,
};
});
if (this.props.tab != null) {
calendar_list = calendar_list.filter(c => this.allowed_ids.indexOf(c.calendar_id) >= 0);
}
}
CalendarService.export(
WorkspacesService.currentWorkspaceId,
{ mode: mode, calendar_list: calendar_list },
download,
token => {
if (!download) {
AlertManager.alert(() => {}, { text: <InputWithClipBoard value={token} /> });
}
},
);
}
completeRect(rect) {
rect.x = rect.x || rect.left;
rect.y = rect.y || rect.top;
return rect;
}
render() {
const { workspaceId } = RouterService.getStateFromRoute();
if (
this.props.tab != null &&
(!this.props.tab.configuration || this.props.tab.configuration.calendars === undefined)
) {
return (
<UnconfiguredTab
saveTab={this.props.saveTab}
channel={this.props.channel}
tab={this.props.tab}
/>
);
}
var calendars = Collections.get('calendars')
.findBy({ workspace_id: workspaceId })
.map(cal => cal.id);
if (this.props.tab != null) {
calendars = calendars.filter(c => this.allowed_ids.indexOf(c) >= 0);
}
var events = Collections.get('events')
.findBy({})
.filter(event => {
if (!event.id) {
return true;
}
event._user_transparent = false;
var not_mine =
(event.participants || []).filter(
part => part.user_id_or_mail === UserService.getCurrentUserId(),
).length === 0;
if (this.state.filter === 'mine') {
if (not_mine) {
event._user_transparent = true;
}
return true;
}
if (this.state.filter === 'workspace' || this.state.filter === 'custom') {
//Not in this workspace
if (
event.workspaces_calendars.filter(part => calendars.indexOf(part.calendar_id) >= 0)
.length === 0
) {
if (!not_mine) {
//Set transparent event
event._user_transparent = true;
return true;
} else {
return false;
}
}
}
return true;
});
if (
CalendarService.edited &&
CalendarService.edited.from &&
CalendarService.edited.from !== this.lastEditedFrom
) {
this.lastEditedFrom = CalendarService.edited.from;
this.calendar.setDate(new Date(CalendarService.edited.from * 1000), true);
if (!CalendarService.edited.id) {
setTimeout(() => {
var htmlEl = this.calendar.getDomElement(CalendarService.edited);
if (htmlEl) {
ModalManager.updateHighlight(this.completeRect(window.getBoundingClientRect(htmlEl)));
}
}, 200);
}
}
var calendar_menu = [
{
type: 'menu',
text: Languages.t('scenes.apps.calendar.my_calendar_menu', [], 'Mon calendrier'),
onClick: () => {
this.setState({ filter: 'mine' });
CalendarService.filter_mode = 'mine';
Menu.closeAll();
},
},
{
type: 'menu',
text: Languages.t('scenes.apps.calendar.workspace_menu', [], 'Espace de travail'),
onClick: () => {
this.setState({ filter: 'workspace' });
CalendarService.filter_mode = 'workspace';
Menu.closeAll();
},
},
/*{type:"menu", text:"Personnaliser...", onClick: ()=>{
}},*/
{ type: 'separator' },
{
type: 'menu',
text: Languages.t('scenes.apps.calendar.export_view_menu', [], 'Exporter la vue'),
submenu: [
{ type: 'title', text: Languages.t('scenes.apps.calendar.export_title', [], 'Exporter') },
{
type: 'react-element',
reactElement: () => {
return (
<ExportView
values={this.state}
onChange={(my, ws) => {
this.setState({
export_my_calendar: my,
export_workspace_calendar: ws,
});
}}
/>
);
},
},
{
type: 'menu',
text: Languages.t(
'scenes.apps.calendar.ics_subscription_menu',
[],
"Obtenir un lien d'abonnement ICS",
),
onClick: () => {
this.export(false);
},
},
{
type: 'menu',
text: Languages.t(
'scenes.apps.calendar.ics_download_menu',
[],
'Télécharger un fichier ICS',
),
onClick: () => {
this.export(true);
},
},
],
},
{ type: 'separator' },
];
calendar_menu = calendar_menu.concat(this.renderCalendarList());
if (this.props.tab === null && WorkspaceUserRights.hasWorkspacePrivilege()) {
calendar_menu = calendar_menu.concat([
{
type: 'menu',
text: Languages.t(
'scenes.apps.calendar.add_calendar_menu',
[],
'Ajouter un calendrier...',
),
submenu_replace: true,
submenu: [
{
type: 'title',
text: Languages.t(
'scenes.apps.calendar.create_calendar_title',
[],
'Créer un calendrier',
),
},
{
type: 'react-element',
reactElement: level => {
return (
<CalendarEditor level={level} collectionKey={this.calendar_collection_key} />
);
},
},
],
},
]);
}
var list = [];
list.push(
<div className="calendar_app">
<div className="calendar_header">
<div className="left">
<Menu
className="select medium"
style={{ width: 'auto' }}
position="bottom"
menu={calendar_menu}
>
{
{
mine: Languages.t('scenes.apps.calendar.my_calendar', [], 'Mon calendrier'),
workspace: Languages.t('scenes.apps.calendar.workspace', [], 'Espace de travail'),
}[this.state.filter]
}
</Menu>
</div>
<div className="right">
{this.state.view !== 'dayGridMonth' && (
<div className="week_number">
{Languages.t('scenes.apps.calendar.calendar.week_btn', [], 'Semaine')}{' '}
{moment(CalendarService.date).week()}
</div>
)}
<Menu
className={'current_date ' + moment(CalendarService.date).format('DD-MM-YYYY')}
position="bottom"
menu={[
{
type: 'menu',
text: Languages.t('scenes.apps.calendar.today_menu', [], "Aujourd'hui"),
onClick: () => this.calendar.today(),
},
{ type: 'separator' },
{
type: 'react-element',
reactElement: () => (
<div style={{ padding: '4px 8px' }}>
<DayPicker
value={moment(CalendarService.date)}
onChange={value => {
this.calendar.setDate(value);
Menu.closeAll();
}}
/>
</div>
),
},
]}
>
{CalendarService.date &&
(this.state.view === 'dayGridMonth' ||
this.state.view === 'timeGridWeek' ||
this.state.view === 'listYear') &&
moment(CalendarService.date).format('MMMM YYYY')}
{CalendarService.date &&
this.state.view === 'timeGridDay' &&
moment(CalendarService.date).format('LL')}
</Menu>
<div className="move">
<Icon
className="m-icon-small left"
type="arrow-left"
onClick={() => this.calendar.previous()}
/>
<Icon
className="m-icon-small right"
type="arrow-right"
onClick={() => this.calendar.next()}
/>
</div>
<div className="view_selector">
<Select
medium
style={{ width: 'auto' }}
value={this.state.view}
onChange={value => {
this.calendar.view(value);
LocalStorage.setItem('calendar_view', value);
Menu.closeAll();
}}
options={[
{
value: 'dayGridMonth',
text: Languages.t('scenes.apps.calendar.month_option', [], 'Mois'),
},
{
value: 'timeGridWeek',
text: Languages.t('scenes.apps.calendar.week_option', [], 'Semaine'),
},
{
value: 'timeGridDay',
text: Languages.t('scenes.apps.calendar.day_option', [], 'Jour'),
},
]}
/>
</div>
</div>
</div>
<FullCalendar
i18n={Languages.language}
ref={node => (this.calendar = node)}
onViewChange={view => this.setState({ view: view })}
onDateChange={date => {
CalendarService.date = date;
CalendarService.notify();
}}
date={CalendarService.date}
onCreate={_event => {
CalendarService.edit(_event);
CalendarService.fullSizeModal = false;
setTimeout(() => {
var htmlEl = this.calendar.getDomElement(CalendarService.edited);
ModalManager.open(
<EventCreation
event={CalendarService.edited}
collectionKey={this.calendar_collection_key}
/>,
{
highlight: htmlEl
? this.completeRect(window.getBoundingClientRect(htmlEl))
: null,
position: 'left',
margin: 5,
no_background: true,
size: { width: 440 },
},
);
}, 100);
}}
onUpdate={(event, htmlEl) => {
//Collections.get("events").updateObject(event);
if (event.id) {
CalendarService.save(event, this.calendar_collection_key);
}
if (
!CalendarService.fullSizeModal &&
CalendarService.preview &&
CalendarService.preview.front_id === event.front_id
) {
var updated = CalendarService.preview;
setTimeout(() => {
var e = this.calendar.getDomElement(updated);
e &&
ModalManager.updateHighlight(this.completeRect(window.getBoundingClientRect(e)));
}, 100);
}
}}
onClickOut={() => {
CalendarService.closePopups();
}}
onClickEvent={(event, jsEvent) => {
jsEvent.stopPropagation();
jsEvent.preventDefault();
if (
!ModalManager.isOpen() ||
!CalendarService.preview ||
CalendarService.preview.front_id !== event.front_id
) {
CalendarService.fullSizeModal = false;
CalendarService.startPreview(event);
setTimeout(() => {
var htmlEl = this.calendar.getDomElement(CalendarService.preview);
ModalManager.open(
<EventDetails event={event} collectionKey={this.calendar_collection_key} />,
{
highlight: htmlEl
? this.completeRect(window.getBoundingClientRect(htmlEl))
: null,
position: 'left',
margin: 5,
no_background: true,
size: { width: 440 },
},
);
}, 100);
}
}}
events={events}
getCalendar={id => Collections.get('calendars').find(id)}
/>
<MainPlus
onClick={() => {
CalendarService.fullSizeModal = true;
CalendarService.edit({
from: new Date().getTime() / 1000,
to: new Date().getTime() / 1000 + 60 * 60,
});
setTimeout(() => {
ModalManager.open(
<EventModification
event={CalendarService.edited}
collectionKey={this.calendar_collection_key}
/>,
{ size: { width: 600 } },
);
}, 100);
}}
/>
</div>,
);
return list;
}
}
@@ -1,76 +0,0 @@
.calendar_app {
height: 100%;
width: 100%;
left: 0;
right: 0;
display: flex;
flex-direction: column;
padding: 0px;
box-sizing: border-box;
}
.menu {
.calendar_color {
width: 12px;
height: 12px;
border-radius: 6px;
margin: 10px 0;
margin-right: 5px;
display: inline-block;
vertical-align: top;
}
}
.calendar_header {
display: flex;
padding: 16px;
.left {
flex: 1;
}
.right {
& > div {
display: inline-block;
}
.current_date {
height: 38px;
line-height: 38px;
text-transform: capitalize;
}
.move {
margin-left: 20px;
margin-right: 20px;
.m-icon-small {
font-size: 24px !important;
vertical-align: middle;
margin-top: -4px;
cursor: pointer;
&:hover {
opacity: 0.5;
}
}
}
.week_number {
color: var(--white);
background: var(--grey-dark);
border-radius: var(--border-radius-base);
padding: 1px 4px;
font-size: 12px;
vertical-align: middle;
margin-right: 20px;
}
}
}
.unconfigured_tab {
width: 100%;
max-width: 400px;
text-align: center;
margin: auto;
margin-top: 10vh;
}
@@ -1,24 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import { ViewConfiguration } from '../../../features/router/services/app-view-service';
import { useTab } from '../../../features/tabs/hooks/use-tabs';
import CalendarContent from './calendar-content';
type Props = {
options: ViewConfiguration;
};
export default (props: Props) => {
const tabId = props.options?.context?.tabId;
const { tab, save } = useTab(tabId);
return (
<CalendarContent
options={props.options}
tab={tab}
saveTab={(configuration: any) => {
save({ ...tab, configuration });
}}
/>
);
};
@@ -1,11 +0,0 @@
import React, { Component } from 'react';
export default class Event extends Component {
constructor(props) {
super(props);
}
render() {
return '';
}
}
@@ -1,377 +0,0 @@
import React, { Component } from 'react';
import ReactDOMServer from 'react-dom/server';
import '@fullcalendar/core';
import '@fullcalendar/core/main.css';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import momentPlugin from '@fullcalendar/moment';
import momentTimezonePlugin from '@fullcalendar/moment-timezone';
import interactionPlugin from '@fullcalendar/interaction';
import listPlugin from '@fullcalendar/list';
import FullCalendarPlugin from '@fullcalendar/react';
import '@fullcalendar/daygrid/main.css';
import '@fullcalendar/timegrid/main.css';
import '@fullcalendar/list/main.css';
import CalendarService from 'app/deprecated/Apps/Calendar/Calendar.js';
import EventUI from 'components/calendar/event/event.jsx';
import WorkspaceService from 'app/deprecated/workspaces/workspaces.jsx';
import Languages from 'app/features/global/services/languages-service';
import moment from 'moment';
import './full-calendar.scss';
export default class FullCalendar extends Component {
constructor() {
super();
this.calendarRef = null;
window.react_calendar = this;
this.event_dom_elements = {};
this.eventsById = {};
this.clickOut = this.clickOut.bind(this);
}
getDomElement(event) {
return this.event_dom_elements[event.front_id];
}
UNSAFE_componentWillMount() {
var that = this;
var scrollTime = moment().format('HH') + ':00:00';
this.options = {
defaultDate: this.props.date || new Date(),
header: false,
timezone: 'local',
height: 'parent',
locale: this.props.i18n,
nowIndicator: true,
scrollTime: scrollTime,
firstHour: new Date().getUTCHours() - 5,
allDaySlot: true,
editable: true,
selectable: true,
selectHelper: true,
firstDay: moment().startOf('week').isoWeekday(),
slotDuration: '00:30:00',
snapDuration: '00:15:00',
columnHeaderHtml: mom => {
return ReactDOMServer.renderToString(
<div className="">
<div className="number">{moment(mom).format('D')}</div>
<div className="day">{moment(mom).format('ddd')}</div>
</div>,
);
},
eventAllow: function (dropLocation, draggedEvent) {
// if(!that.state.calendar.calendars[draggedEvent.calendar]){
// return false;
// }
return true;
},
select: function (event) {
that.cancelClickOut = true;
//Create event
if (moment(event.end).diff(event.start) === 15 * 60 * 1000) {
that.api.unselect();
that.props.onClickOut && that.props.onClickOut();
return false;
}
if (that.props.onCreate) {
// eslint-disable-next-line no-unused-vars
var created_event = that.props.onCreate(
that.fcToCollection(event),
that.event_dom_elements[event.id],
);
}
if (that.props.onUpdate) {
that.props.onUpdate(that.fcToCollection(event), that.event_dom_elements[event.id]);
}
},
eventResize: function (event) {
event = event.event;
if (that.props.onUpdate) {
that.props.onUpdate(that.fcToCollection(event), that.event_dom_elements[event.id]);
}
},
eventDrop: function (event) {
event = event.event;
if (that.props.onUpdate) {
that.props.onUpdate(that.fcToCollection(event), that.event_dom_elements[event.id]);
}
},
eventClick: function (_event) {
if (this.disableUpdate) {
this.disableUpdate = false;
this.refreshEvent();
}
var event = _event.event;
if (that.props.onClickEvent) {
that.props.onClickEvent(that.fcToCollection(event), _event.jsEvent, _event.el);
}
//if(!event.private_content){ViewsServiceImpl.open();that.state.calendar.selectEvent(event);}
},
eventRender: function (event) {
if (event.event && event.event.id && event.isStart && !event.isMirror) {
that.event_dom_elements[event.event.id] = event.el;
}
var col_event = that.eventsById[event.event.id];
if (col_event._user_transparent) {
event.el.classList.add('transparent');
} else {
event.el.classList.remove('transparent');
}
var color = '';
if (col_event.workspaces_calendars && col_event.workspaces_calendars[0]) {
var calendar = null;
(col_event.workspaces_calendars || []).some(cal => {
var calendar_id = cal.calendar_id;
if (calendar_id && that.props.getCalendar) {
var tmp = that.props.getCalendar(calendar_id);
if (tmp && tmp.workspace_id === WorkspaceService.currentWorkspaceId) {
calendar = tmp;
return true;
}
}
return false;
});
color = calendar ? calendar.color : '#92929C';
}
if (color) {
event.el.style.backgroundColor = color;
}
if (col_event.type === 'deadline' || col_event.type === 'remind') {
event.el.classList.add('not_resizable');
}
event.el.firstChild.innerHTML = ReactDOMServer.renderToString(
<EventUI event={col_event} getColor={cal_id => color || 'var(--primary)'} />,
);
},
eventResizeStart: () => {
that.cancelClickOut = true;
this.disableUpdate = true;
},
eventDragStart: () => {
that.cancelClickOut = true;
this.disableUpdate = true;
},
eventResizeStop: () => {
that.cancelClickOut = false;
this.disableUpdate = false;
if (this.missedRefresh) {
this.refreshEvent();
}
},
eventDragStop: () => {
that.cancelClickOut = false;
this.disableUpdate = false;
if (this.missedRefresh) {
this.refreshEvent();
}
},
};
}
componentWillUnmount() {
var page = window.document.getElementsByClassName('appPage')[0];
if (page) {
page.removeEventListener('click', this.clickOut);
}
}
componentDidMount() {
var page = window.document.getElementsByClassName('appPage')[0];
if (page) {
page.addEventListener('click', this.clickOut);
}
if (this.calendarRef) {
this.api = this.calendarRef.getApi();
}
this.refreshEvent();
window.dispatchEvent(new Event('resize'));
}
componentDidUpdate() {
this.refreshEvent();
}
clickOut() {
if (this.cancelClickOut) {
return;
}
this.props.onClickOut && this.props.onClickOut();
}
refreshEvent() {
if (this.disableUpdate) {
this.missedRefresh = true;
return;
}
this.missedRefresh = false;
var events = this.props.events.map(event => this.collectionToFc(event));
//Monkey fix for events not appearing randomly
this.api.removeAllEvents();
this.api.addEventSource(events);
this.api.rerenderEvents();
}
previous() {
this.willChangeView(false);
this.api.prev();
this.props.onDateChange && this.props.onDateChange(this.api.getDate());
}
next() {
this.willChangeView(true);
this.api.next();
this.props.onDateChange && this.props.onDateChange(this.api.getDate());
}
today() {
this.willChangeView();
this.api.today();
this.props.onDateChange && this.props.onDateChange(this.api.getDate());
}
setDate(date, disableAnimation) {
if (!disableAnimation) {
this.willChangeView();
}
this.api.gotoDate(moment(date).toDate());
this.props.onDateChange && this.props.onDateChange(moment(date).toDate());
}
view(view) {
if (view !== this.api.type) {
this.willChangeView();
this.api.changeView(view);
this.props.onViewChange && this.props.onViewChange(view);
this.props.onDateChange && this.props.onDateChange(this.api.getDate());
}
}
willChangeView(forward) {
var old_effect =
this.calendarRef.elRef.current.parentElement.getElementsByClassName('false_calendar')[0];
if (old_effect) {
this.calendarRef.elRef.current.parentElement.removeChild(old_effect);
}
var calendar = this.calendarRef.elRef.current;
calendar.classList = 'fc fc-ltr fc-unthemed';
var scroll_state = calendar.getElementsByClassName('fc-scroller')[0];
if (scroll_state) {
scroll_state = scroll_state.scrollTop;
} else {
scroll_state = 0;
}
var clone = calendar.cloneNode(true);
calendar.classList.add('calendar_invisible');
setTimeout(() => {
this.calendarRef.elRef.current.classList.remove('calendar_invisible');
if (forward === undefined) {
this.calendarRef.elRef.current.classList.add('calendar_appear');
} else if (forward) {
this.calendarRef.elRef.current.classList.add('calendar_appear_right');
} else {
this.calendarRef.elRef.current.classList.add('calendar_appear_left');
}
}, 20);
clone.classList.add('calendar_disappear');
clone.classList.add('false_calendar');
calendar.parentElement.append(clone);
var scroller = clone.getElementsByClassName('fc-scroller')[0];
if (scroll_state !== scroller.scrollTop) {
scroller.scrollTop = scroll_state;
}
}
fcToCollection(event) {
return {
id: event.extendedProps ? event.extendedProps.real_id : '',
front_id: event.id,
from:
(event.allDay
? moment(moment(event.start).format('YYYY-MM-DDT00:00:00+00:00')).valueOf()
: moment(event.start).utc().valueOf()) / 1000,
to:
(event.allDay
? moment(moment(event.end).format('YYYY-MM-DDT00:00:00+00:00')).valueOf()
: moment(event.end).utc().valueOf()) /
1000 -
(event.allDay ? 24 * 60 * 60 : 0),
all_day: event.allDay,
//repetition_definition: { string: event.rrule, duration: event.duration },
};
}
collectionToFc(event) {
var force_allday = false;
if (Math.abs(event.from - event.to) > 60 * 60 * 24 * 2) {
force_allday = true;
}
this.eventsById[event.front_id] = event;
var from = event.from;
var to = event.to;
if (event.type === 'remind' || event.type === 'deadline') {
to = parseInt(from) + 15 * 60;
}
// eslint-disable-next-line no-redeclare
var event = {
id: event.front_id,
real_id: event.id,
start: moment(from * 1000).toDate(),
end: moment(to * 1000 + (event.all_day ? 24 * 60 * 60 * 1000 : 0)).toDate(),
allDay: event.all_day || force_allday,
title: event.title || Languages.t('scenes.apps.drive.navigators.new_file.untitled'),
//rrule: (event.repetition_definition || {}).string, //'DTSTART:20190201T103000Z\nRRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20190601;BYDAY=MO,FR',
duration: (event.repetition_definition || {}).duration,
editable: !CalendarService.getIsReadonly(event),
};
return event;
}
render() {
return (
<div className="twake_fullcalendar show_day_line">
<FullCalendarPlugin
defaultView="timeGridWeek"
ref={node => (this.calendarRef = node)}
plugins={[
dayGridPlugin,
timeGridPlugin,
momentPlugin,
momentTimezonePlugin,
interactionPlugin,
listPlugin,
]}
{...this.options}
/>
</div>
);
}
}
@@ -1,473 +0,0 @@
.twake_fullcalendar {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
flex: 1;
& > div {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.false_calendar {
pointer-events: none;
}
/* Annimations */
.calendar_invisible {
opacity: 0;
}
.calendar_disappear {
animation: calendar_disappear 0.2s;
opacity: 0;
}
.calendar_disappear_left {
animation: calendar_disappear_left 0.2s;
opacity: 0;
}
.calendar_disappear_right {
animation: calendar_disappear_right 0.2s;
opacity: 0;
}
.calendar_appear {
animation: calendar_disappear 0.2s reverse;
opacity: 1;
}
.calendar_appear_left {
animation: calendar_disappear_left 0.2s reverse;
opacity: 1;
}
.calendar_appear_right {
animation: calendar_disappear_right 0.2s reverse;
opacity: 1;
}
@keyframes calendar_disappear {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes calendar_disappear_left {
0% {
transform: translateX(0);
opacity: 1;
}
100% {
transform: translateX(-50px);
opacity: 0;
}
}
@keyframes calendar_disappear_right {
0% {
transform: translateX(0);
opacity: 1;
}
100% {
transform: translateX(50px);
opacity: 0;
}
}
.fc.fc-unthemed {
}
.fc-divider.fc-widget-header {
display: none;
}
.fc-widget-content {
border-top: 0px;
}
&.show_day_line {
.fc-timeGridWeek-view {
.fc-day-grid {
min-height: 2.5em;
height: auto;
}
}
}
.fc-timeGridWeek-view {
.fc-day-grid {
transition: height 0.2s;
height: 0;
overflow: hidden;
}
}
.fc-dayGridMonth-view {
.fc-week:last-child {
.fc-day {
border-bottom: 0px;
}
}
}
.fc-day-grid .fc-bg table {
border: unset;
border-bottom: none;
.fc-day {
border-top: 0px;
border-right: 0px;
}
.fc-today {
background: var(--white) !important;
}
.fc-axis.fc-widget-content {
span {
display: none;
}
}
}
.fc-scroller {
border-top: 1px solid var(--grey-background);
}
.fc-head .fc-head-container {
border: none;
}
.fc-day-header,
.fc-widget-header.fc-axis {
border: none !important;
}
.fc-time-grid .fc-slats .fc-minor td {
border-top: none;
}
.fc-timeGrid-view .fc-day-grid .fc-row {
min-height: 2em;
}
.fc-time-grid .fc-slats td {
border-top: none;
height: 2.5em;
span {
margin-top: -18px;
display: inline-block;
}
}
.fc-now-indicator.fc-now-indicator-arrow {
display: none;
}
.fc-time-grid .fc-now-indicator-line {
border-top-width: 2px;
border-color: var(--primary);
&:before {
content: '';
width: 8px;
height: 8px;
background: var(--primary);
position: absolute;
left: -4px;
top: -5px;
border-radius: 50%;
}
}
tr .fc-day-header .number {
margin-top: 0px;
text-align: center;
font-size: 22px;
font-weight: normal;
}
tr .fc-day-header .day {
padding-bottom: 10px;
text-align: center;
font-weight: 400;
font-size: 12px;
}
th.fc-today.fc-day-header {
color: var(--primary);
}
.fc-month-view {
tr .fc-day-header .number {
display: none;
height: 0px !important;
}
.fc-day,
.fc-unthemed td.fc-today {
border-top: 1px solid var(--grey-background) !important;
}
.fc-day-top {
background: transparent !important;
}
.fc-day-top .fc-day-number {
float: left;
margin: 5px;
}
.fc-day-header .day {
padding-bottom: 0;
padding-left: 6px;
padding-top: 6px;
text-align: left;
font-weight: 400;
border-bottom: 0 !important;
}
.fc-unthemed td.fc-today {
background: #fff;
border-right: 0;
border-top: 1px;
border-color: #eee;
}
}
.fc td {
border-left: 1px solid var(--grey-background);
}
.fc td:first-child {
border-left: 0px;
font-size: 12px;
border-top: 0px;
}
.fc-unthemed th,
.fc-unthemed td,
.fc-unthemed thead,
.fc-unthemed tbody,
.fc-unthemed .fc-divider,
.fc-unthemed .fc-row,
.fc-unthemed .fc-content,
.fc-unthemed .fc-popover,
.fc-unthemed .fc-list-view,
.fc-unthemed .fc-list-heading td {
border-color: var(--grey-background);
color: var(--grey-dark);
}
tr:nth-child(even) {
.fc-widget-content {
border-bottom: 1px solid var(--grey-background);
}
.fc-time {
border-bottom: 0;
}
}
.fc-day-top.fc-today {
color: var(--primary);
background: var(--grey-background);
}
.fc-unthemed td.fc-today {
background: var(--grey-background);
border-right: 0;
border-top: 0;
}
.fc-axis {
width: 40px !important;
padding-right: 10px;
}
.fc-time-grid-event .fc-time span {
color: var(--white);
font-size: 12px;
}
.fc-event {
min-height: 18px;
background-color: var(--primary);
border: 0;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
overflow: visible !important;
transition: z-index 0.5s;
transition-delay: 0s;
border-radius: var(--border-radius-base);
margin-bottom: 0;
background: #ffffff00 !important;
display: flex;
&.not_resizable {
.fc-resizer {
display: none;
}
}
&.transparent {
opacity: 0.5;
}
&.selected {
box-shadow: 0 0 4px 1px #549ae6;
border-bottom: 0px !important;
}
&:not(.selected):hover {
transition-delay: 0.5s;
z-index: 100 !important;
}
.fc-content {
background: transparent;
transition: max-height 0.5s, background 0.5s, transform 0.5s, border-radius 0.5s,
box-shadow 0.5s, min-width 0.5s;
min-height: calc(100% - 10px);
min-width: 100%;
transition-delay: 0s;
border-radius: 0px;
color: var(--white);
.event_container {
height: 100%;
}
.fc-title {
font-weight: bold;
font-size: 12px;
.fc-time {
opacity: 0.9;
font-weight: normal;
}
}
.fc-title,
.fc-time,
.fc-time span {
transition: color 0.5s;
transition-delay: 0s;
color: var(--white);
}
}
.fc-bg {
opacity: 0 !important;
}
.fc-time {
vertical-align: middle;
}
&.event-deadline {
max-height: 23px;
border-radius: 0 0 10px 10px;
white-space: nowrap;
text-overflow: ellipsis;
left: 0% !important;
right: 0% !important;
padding-top: 3px;
border-top: 3px solid #ff000a !important;
.fc-resizer {
display: none !important;
}
}
&.event-move {
border-radius: 0 10px 10px 10px;
}
&.event-reminder {
max-height: 23px;
border-radius: 16px;
white-space: nowrap;
text-overflow: ellipsis;
min-height: 23px;
.fc-resizer {
display: none !important;
}
}
&.private_content {
background-color: var(--black) !important;
border-radius: 0 !important;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAO0lEQVQImWNMS0tjgIF79+7B2UxYRZWUlJiwikJ1YIoyMDAwCwoKYoqi2IEseu/ePSasolAdmKIMDAwAGk0Wdkl75tAAAAAASUVORK5CYII=') !important;
background-repeat: repeat;
cursor: default;
border: none !important;
box-shadow: none !important;
opacity: 0.3 !important;
& > div {
display: none;
}
}
}
.fc-ltr .fc-time-grid .fc-event-container {
margin: 0;
}
.fc-content .fc-title {
font-size: 14px;
color: var(--white);
.icon.event-type {
margin-right: 5px;
opacity: 0.8;
}
.participant {
display: inline-block;
margin: 0 2px 2px 0;
width: 16px;
height: 16px;
background-size: cover;
background-repeat: no-repeat;
border-radius: 50%;
background-color: var(--white);
background-position: center;
}
.icon {
display: inline-block;
width: 16px;
height: 16px;
position: relative;
margin: 0;
padding: 0px;
vertical-align: middle;
}
.icon:before {
top: 30px;
position: absolute;
}
}
.fc-day-grid-event .fc-time {
font-weight: normal;
color: var(--white);
}
.fc-list-heading td {
font-weight: normal;
}
.fc-list-item-marker {
width: 10px;
padding: 10px !important;
}
.fc-list-item-title {
border-left: none !important;
padding-left: 0;
}
.fc-list-item-title > a {
cursor: pointer;
}
.fc-time-grid .fc-slats td {
height: 2em;
}
}
@@ -1,60 +0,0 @@
/* eslint-disable react/no-direct-mutation-state */
import React, { Component } from 'react';
import InputWithColor from 'components/inputs/input-with-color.jsx';
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
import WorkspaceService from 'app/deprecated/workspaces/workspaces.jsx';
import Button from 'components/buttons/button.jsx';
import Menu from 'components/menus/menu.jsx';
import Languages from 'app/features/global/services/languages-service';
export default class CalendarEditor extends Component {
constructor(props) {
super(props);
this.state = {
calendar: Collections.get('calendars').editCopy(props.calendar || {}),
};
}
save() {
this.state.calendar.workspace_id = WorkspaceService.currentWorkspaceId;
Collections.get('calendars').save(this.state.calendar, this.props.collectionKey);
Menu.closeAll();
}
setKey(obj) {
Object.keys(obj || {}).forEach(k => {
this.state.calendar[k] = obj[k];
});
this.setState({});
}
render() {
var calendar = this.state.calendar;
return (
<div className="">
<InputWithColor
className="medium bottom-margin full_width"
focusOnDidMount
menu_level={this.props.level}
placeholder={Languages.t('scenes.apps.calendar.calendar_modal.placeholder', [], 'Name')}
value={[this.state.calendar.color, this.state.calendar.title]}
onEnter={() => this.save()}
onChange={value => {
this.setKey({ color: value[0], title: value[1] });
}}
/>
<div className="menu-buttons">
<Button
disabled={(this.state.calendar.title || '').length <= 0}
type="button"
value={
this.state.calendar.id
? Languages.t('general.save', [], 'Enregistrer')
: Languages.t('general.add', [], 'Ajouter')
}
onClick={() => this.save()}
/>
</div>
</div>
);
}
}
@@ -1,199 +0,0 @@
import React, { Component } from 'react';
import Languages from 'app/features/global/services/languages-service';
import MediumPopupManager from 'app/components/modal/modal-manager';
import EventModification from './EventModification.jsx';
import CalendarService from 'app/deprecated/Apps/Calendar/Calendar.js';
import Input from 'components/inputs/input.jsx';
import InputIcon from 'components/inputs/input-icon.jsx';
import Participants from './Part/Participants.jsx';
import DateSelector from './Part/DateSelector.jsx';
import CalendarSelector from 'components/calendar/calendar-selector/calendar-selector.jsx';
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
import Button from 'components/buttons/button.jsx';
import Select from 'components/select/select.jsx';
import WorkspaceService from 'app/deprecated/workspaces/workspaces.jsx';
import Icon from 'app/components/icon/icon.jsx';
export default class EventCreation extends Component {
constructor(props) {
super(props);
Collections.get('events').addListener(this);
Collections.get('events').listenOnly([props.event.front_id], this);
}
UNSAFE_componentWillMount() {
CalendarService.fullSizeModal = false;
}
componentDidMount() {
MediumPopupManager.mountedComponent = this;
this.doNotCancelEdit = false;
}
componentWillUnmount() {
Collections.get('events').removeListener(this);
}
onMediumPopupClose() {
if (!this.doNotCancelEdit) {
CalendarService.closePopups();
}
}
save() {
CalendarService.saveEdit(this.props.collectionKey);
}
change(key, value, notify) {
this.props.event[key] = value;
if (notify || notify === undefined) {
this.update_timeout && clearTimeout(this.update_timeout);
this.update_timeout = setTimeout(() => {
Collections.get('events').notify();
}, 1000);
this.setState({});
}
}
render() {
var event = this.props.event;
var calendar_list = Collections.get('calendars').findBy({
workspace_id: WorkspaceService.currentWorkspaceId,
});
return (
<div className="eventModal event_creation" style={{ padding: '16px' }}>
<Input
autoFocus
value={event.title || ''}
placeholder={Languages.t('scenes.apps.calendar.modals.title_placeholder', [], 'Titre')}
onChange={evt => {
this.change('title', evt.target.value);
}}
className="full_width bottom-margin"
big
/>
<div className="bottom-margin date_and_type">
<Select
medium
value={event.type || 'event'}
onChange={value => {
this.change('type', value);
}}
options={CalendarService.event_types}
className="right-margin"
/>
<DateSelector
event={event}
onChange={(from, to, all_day, repetition_definition) => {
this.change('from', from, false);
this.change('to', to, false);
this.change('all_day', all_day, false);
this.change('repetition_definition', repetition_definition);
}}
/>
</div>
<div style={{ display: 'flex' }} className="full_width bottom-margin">
<div style={{ flex: 1, display: 'flex' }} className="right-margin">
<InputIcon
icon={(event.location || '').slice(0, 4) == 'http' ? 'link' : 'location-point'}
medium
value={event.location || ''}
placeholder={Languages.t(
'scenes.apps.calendar.modals.event_adresse_placeholder',
[],
'Adresse',
)}
onChange={evt => {
this.change('location', evt.target.value);
}}
className="full_width"
/>
</div>
<Button
className="button medium secondary"
onClick={() => {
this.change(
'location',
window.location.protocol +
'//' +
window.location.host +
'/bundle/connectors/jitsi/call/twake_event_' +
(event.front_id || '').replace(/-/g, '_') +
'__' +
(event.front_id || '').replace(/-/g, '_'),
);
}}
>
<Icon type="video" />
</Button>
</div>
<InputIcon
autoHeight
icon="align-left-justify"
medium
value={event.description || ''}
placeholder={Languages.t(
'scenes.apps.calendar.modals.description_placeholder',
[],
'Description',
)}
onChange={evt => {
this.change('description', evt.target.value);
}}
className="full_width bottom-margin"
/>
<CalendarSelector
medium
value={event.workspaces_calendars || []}
onChange={workspaces_calendars => {
this.change('workspaces_calendars', workspaces_calendars);
}}
calendarList={calendar_list || []}
className=""
/>
<div className="separator" />
<div className="small-bottom-margin">
<Participants
participants={event.participants}
owner={event.owner}
onChange={user_id_or_mail => this.change('participants', user_id_or_mail)}
/>
</div>
<div className="separator" />
<Button
className="button medium secondary-light"
style={{ width: 'auto' }}
onClick={() => {
this.doNotCancelEdit = true;
MediumPopupManager.open(
<EventModification
event={CalendarService.edited}
collectionKey={this.props.collectionKey}
/>,
{ size: { width: 600 } },
);
}}
>
{Languages.t('scenes.apps.calendar.modals.advanced_options', [], 'Options avancées')}
</Button>
<Button
className="button medium btn-primary medium"
style={{ width: 'auto', marginLeft: 10, float: 'right' }}
onClick={() => {
this.save();
}}
>
{Languages.t('general.save', [], 'Enregistrer')}
</Button>
</div>
);
}
}
@@ -1,251 +0,0 @@
import React, { Component } from 'react';
import Languages from 'app/features/global/services/languages-service';
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
import moment from 'moment';
import MediumPopupManager from 'app/components/modal/modal-manager';
import EventModification from './EventModification.jsx';
import CalendarService from 'app/deprecated/Apps/Calendar/Calendar.js';
import Participants from './Part/Participants.jsx';
import CalendarSelector from 'components/calendar/calendar-selector/calendar-selector.jsx';
import AlertManager from 'app/features/global/services/alert-manager-service';
import DateTimeUtils from 'app/features/global/utils/datetime.js';
import Button from 'components/buttons/button.jsx';
import Icon from 'app/components/icon/icon.jsx';
import Tabs from 'components/tabs/tabs.jsx';
import SearchService from 'app/deprecated/search/search.js';
import WorkspacesService from 'app/deprecated/workspaces/workspaces.jsx';
import UserService from 'app/features/users/services/current-user-service';
export default class EventDetails extends Component {
constructor(props) {
super(props);
Collections.get('events').addListener(this);
if (props.event.front_id) {
Collections.get('events').listenOnly([props.event.front_id], this);
}
}
UNSAFE_componentWillMount() {
CalendarService.fullSizeModal = false;
}
componentDidMount() {
MediumPopupManager.mountedComponent = this;
}
componentWillUnmount() {
Collections.get('events').removeListener(this);
}
remove() {
var event = Collections.get('events').findByFrontId(this.props.event.front_id);
AlertManager.confirm(
() => {
CalendarService.remove(event, this.props.collectionKey);
},
() => {},
{
text: Languages.t(
'scenes.apps.calendar.modals.remove_event_text',
[],
Languages.t(
'scenes.apps.calendar.modals.remove_event_alert_confirmation',
[],
"Supprimer l'événement ?",
),
),
},
);
}
render() {
var event = Collections.get('events').findByFrontId(this.props.event.front_id);
if (!event) {
return '';
}
var from = new Date(event.from * 1000);
var to = new Date(event.to * 1000);
var next = Math.min(from || to, to || from);
var from_formatted = moment(from).format(
'ddd Do MMMM YYYY' + (event.all_day ? '' : ', ' + DateTimeUtils.getDefaultTimeFormat()),
);
var to_formatted =
(!event.type || event.type === 'event' || event.type === 'move') &&
!isNaN(event.to) &&
moment(to).format(
(moment(to).format('D_M_YYYY') !== moment(from).format('D_M_YYYY')
? 'ddd Do ' +
(moment(to).format('M_YYYY') !== moment(from).format('M_YYYY')
? 'MMMM ' + (moment(to).format('YYYY') !== moment(from).format('YYYY') ? 'YYYY' : '')
: '') +
(event.all_day ? '' : ', ')
: '') + (event.all_day ? '' : DateTimeUtils.getDefaultTimeFormat()) || '[]',
);
from_formatted = (from_formatted || '').replace(' ' + new Date().getFullYear(), '');
to_formatted = (to_formatted || '').replace(' ' + new Date().getFullYear(), '');
var event_type =
CalendarService.event_types_by_value[event.type] ||
CalendarService.event_types_by_value['event'];
var readonly = CalendarService.getIsReadonly(this.props.event);
return (
<div className="eventModal event_details" style={{ padding: '16px' }}>
<div className="title">
{event.title || Languages.t('scenes.apps.drive.navigators.new_file.untitled')}
</div>
<div className="subtitle date">
<Icon type="clock" />
{from_formatted}
{to_formatted && ' - ' + to_formatted}
{' (' + moment(next).fromNow() + ')'}
</div>
<div style={{ margin: '0 -16px' }}>
<Tabs
tabs={[
{
title: Languages.t('scenes.apps.calendar.modals.details_title', [], 'Détails'),
render: (
<div>
<div className="bottom-margin">
<div className="event_type">
<Icon type={event_type.icon} />
{event_type.text}
</div>
<CalendarSelector
readonly
value={event.workspaces_calendars}
openEventInWorkspace={workspace => {
SearchService.select({
type: 'event',
event: this.props.event,
workspace: workspace,
});
}}
/>
</div>
{event.description && (
<div className="subtitle description bottom-margin">
<Icon type="align-left-justify" style={{ marginRight: 4 }} />
{event.description}
</div>
)}
{event.location && (event.location || '').slice(0, 4) !== 'http' && (
<div className="subtitle location bottom-margin">
<Icon type="location-point" style={{ marginRight: 4 }} />
{event.location}
</div>
)}
{event.location && (event.location || '').slice(0, 4) === 'http' && (
<div
onClick={() => {
var separator = '?';
if (event.location.indexOf('?') > 0) {
separator = '&';
}
window.open(
event.location +
separator +
'twake_user=' +
UserService.getCurrentUser().id +
'&twake_group=' +
WorkspacesService.currentGroupId,
);
}}
className="subtitle location bottom-margin"
style={{
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
<Icon type="link" style={{ marginRight: 4 }} />
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a>
{Languages.t('scenes.apps.calendar.video_link', [], 'Click to open link')}{' '}
- {event.location}
</a>
</div>
)}
{(event.notifications || []).length > 0 && (
<div className="subtitle reminders bottom-margin">
<Icon type="bell" style={{ marginRight: 4 }} />
{(event.notifications || []).length}{' '}
{Languages.t('scenes.apps.calendar.reminders', [], 'rappel(s)')}
</div>
)}
{/*
<div className="text">
{(event.notifications || []).length} rappel(s), événement {event.private?"privé":"public"} et marqué comme {event.available?"disponible":"occupé"}.
</div>*/}
<div style={{ marginTop: '-16px' }} />
</div>
),
},
{
title: Languages.t(
'scenes.apps.calendar.modals.participants_event',
[],
'Participants',
),
render: (
<div className="small-x-margin small-bottom-margin">
<Participants readOnly participants={event.participants} owner={event.owner} />
</div>
),
},
]}
/>
</div>
{!readonly && (
<div style={{ marginTop: '-16px' }}>
<div className="separator" />
<Button
className="button medium danger medium"
style={{ width: 'auto' }}
onClick={() => {
this.remove();
}}
>
{Languages.t('scenes.apps.calendar.modals.remove_button', [], 'Supprimer')}
</Button>
<Button
className="button medium secondary-light medium"
style={{ width: 'auto', float: 'right' }}
onClick={() => {
CalendarService.edit(this.props.event);
MediumPopupManager.open(
<EventModification
event={CalendarService.edited}
collectionKey={this.props.collectionKey}
/>,
{ size: { width: 600 } },
);
}}
>
{Languages.t(
'scenes.apps.calendar.modals.modify_event_button',
[],
"Modifier l'évènement",
)}
</Button>
</div>
)}
{readonly && <div style={{ marginTop: '-16px' }} />}
</div>
);
}
}
@@ -1,234 +0,0 @@
import React, { Component } from 'react';
import Languages from 'app/features/global/services/languages-service';
import CalendarService from 'app/deprecated/Apps/Calendar/Calendar.js';
import Input from 'components/inputs/input.jsx';
import InputIcon from 'components/inputs/input-icon.jsx';
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
import ReminderSelector from 'components/reminder-selector/reminder-selector.jsx';
import Participants from './Part/Participants.jsx';
import AlertManager from 'app/features/global/services/alert-manager-service';
import DateSelector from './Part/DateSelector.jsx';
import AttachmentPicker from 'components/attachment-picker/attachment-picker.jsx';
import CalendarSelector from 'components/calendar/calendar-selector/calendar-selector.jsx';
import Button from 'components/buttons/button.jsx';
import Select from 'components/select/select.jsx';
import WorkspaceService from 'app/deprecated/workspaces/workspaces.jsx';
import MediumPopupManager from 'app/components/modal/modal-manager';
import Icon from 'app/components/icon/icon.jsx';
import PerfectScrollbar from 'react-perfect-scrollbar';
import './Modals.scss';
export default class EventModification extends Component {
constructor(props) {
super(props);
}
UNSAFE_componentWillMount() {
CalendarService.fullSizeModal = true;
}
componentDidMount() {
MediumPopupManager.mountedComponent = this;
}
save() {
CalendarService.saveEdit(this.props.collectionKey);
}
remove() {
AlertManager.confirm(
() => {
CalendarService.remove(CalendarService.edited, this.props.collectionKey);
},
() => {},
{
text: Languages.t(
'scenes.apps.calendar.modals.remove_event_text',
[],
"Supprimer l'événement ?",
),
},
);
}
onMediumPopupClose() {
CalendarService.closePopups();
}
change(key, value, notify) {
this.props.event[key] = value;
if (notify || notify === undefined) {
this.update_timeout && clearTimeout(this.update_timeout);
this.update_timeout = setTimeout(() => {
Collections.get('events').notify();
}, 1000);
this.setState({});
}
}
render() {
var event = this.props.event;
var calendar_list = Collections.get('calendars').findBy({
workspace_id: WorkspaceService.currentWorkspaceId,
});
return (
<PerfectScrollbar options={{ suppressScrollX: true }} style={{ padding: '16px' }}>
<div className="eventModal event_modification">
<Input
autoFocus
value={event.title || ''}
placeholder={Languages.t('scenes.apps.calendar.modals.event_title_placeholder')}
onChange={evt => {
this.change('title', evt.target.value);
}}
className="full_width bottom-margin"
big
/>
<div className="bottom-margin date_and_type">
<Select
medium
value={event.type || 'event'}
onChange={value => {
this.change('type', value);
}}
options={CalendarService.event_types}
className="right-margin"
/>
<DateSelector
event={event}
onChange={(from, to, all_day, repetition_definition) => {
this.change('from', from, false);
this.change('to', to, false);
this.change('all_day', all_day, false);
this.change('repetition_definition', repetition_definition);
}}
/>
</div>
<div style={{ display: 'flex' }} className="full_width">
<div style={{ flex: 1, display: 'flex' }}>
<InputIcon
icon={(event.location || '').slice(0, 4) == 'http' ? 'link' : 'location-point'}
medium
value={event.location || ''}
placeholder={Languages.t('scenes.apps.calendar.modals.event_adresse_placeholder')}
onChange={evt => {
this.change('location', evt.target.value);
}}
className="full_width bottom-margin right-margin"
/>
</div>
<Button
className="button medium default bottom-margin"
onClick={() => {
this.change(
'location',
window.location.protocol +
'//' +
window.location.host +
'/bundle/connectors/jitsi/call/twake_event_' +
(event.front_id || '').replace(/-/g, '_') +
'__' +
(event.front_id || '').replace(/-/g, '_'),
);
}}
>
<Icon type="video" />
</Button>
</div>
<InputIcon
autoHeight
icon="align-left-justify"
medium
value={event.description || ''}
placeholder={Languages.t(
'scenes.apps.calendar.modals.event_description_placeholder',
[],
'Description',
)}
onChange={evt => {
this.change('description', evt.target.value);
}}
className="full_width bottom-margin"
/>
{/*
<span className="right-margin">
<Checkbox small value={event.available} onChange={(value)=>{this.change("available", value)}} label="Afficher comme disponible"/>
</span>
<Checkbox small value={event.private} onChange={(value)=>{this.change("private", value)}} label="Événement privé" />
<br/>
*/}
<CalendarSelector
medium
value={event.workspaces_calendars || []}
onChange={workspaces_calendars => {
this.change('workspaces_calendars', workspaces_calendars);
}}
calendarList={calendar_list}
className=""
/>
<div className="separator" />
<Participants
style={{ margin: 0 }}
participants={event.participants}
owner={event.owner}
onChange={user_id_or_mail => this.change('participants', user_id_or_mail)}
/>
<div className="separator" />
<div className="bottom-margin">
<b>{Languages.t('scenes.apps.tasks.modals.attachments')}</b>
</div>
<AttachmentPicker
attachments={event.attachments}
onChange={attachments => this.change('attachments', attachments)}
/>
<div className="separator" />
<div className="bottom-margin">
<b>{Languages.t('scenes.apps.calendar.modals.reminders')}</b>
</div>
<ReminderSelector
reminders={event.notifications || []}
onChange={reminders => this.change('notifications', reminders)}
/>
<div className="separator" />
<Button
className="button medium danger medium"
style={{ width: 'auto' }}
onClick={() => {
this.remove();
}}
>
{Languages.t('scenes.apps.calendar.modals.remove_event_button')}
</Button>
<Button
className="button medium btn-primary medium"
style={{ width: 'auto', float: 'right' }}
onClick={() => {
this.save();
}}
>
{Languages.t('general.save')}
</Button>
</div>
</PerfectScrollbar>
);
}
}
@@ -1,50 +0,0 @@
.eventModal {
.title {
font-size: 22px;
margin-bottom: 8px;
}
.date.subtitle,
.location.subtitle,
.reminders.subtitle {
color: var(--grey-dark);
}
.date_and_type {
display: flex;
.select {
min-width: 25%;
flex: 1;
max-width: 140px;
margin-right: 8px;
}
.date_selector_full {
& > div {
vertical-align: top;
}
}
}
.participants > .menu-list {
padding: 0;
}
.event_type {
display: inline-block;
height: 32px;
padding: 0 8px;
margin-right: 8px;
margin-bottom: 8px;
line-height: 32px;
vertical-align: middle;
background: var(--white);
border-radius: var(--border-radius-base);
border: 1px solid var(--grey-background);
box-sizing: border-box;
font-size: 14px;
padding-left: 4px;
.icon-unicon {
color: var(--grey-dark);
}
}
}
@@ -1,145 +0,0 @@
/* eslint-disable react/no-direct-mutation-state */
import React, { Component } from 'react';
import DateSelectorInput from 'components/calendar/date-picker.jsx';
import TimeSelector from 'components/calendar/time-selector.jsx';
import Checkbox from 'app/components/inputs/deprecated_checkbox.jsx';
import Icon from 'app/components/icon/icon.jsx';
import './DateSelector.scss';
import Languages from 'app/features/global/services/languages-service';
export default class DateSelector extends Component {
constructor(props) {
super(props);
this.state = {
data: {},
};
this.oldProps = JSON.stringify(props.event);
this.updateFromProps(props);
}
shouldComponentUpdate(nextProps) {
if (this.oldProps !== JSON.stringify(nextProps.event)) {
this.oldProps = JSON.stringify(nextProps.event);
this.updateFromProps(nextProps);
}
return true;
}
change(key, value) {
// eslint-disable-next-line react/no-direct-mutation-state
this.state.data[key] = value;
if (key === 'from') {
if (
this.state.data.to < this.state.data.from ||
(this.state.data.to - this.state.data.from > 60 * 60 * 24 * 2 && !this.state.data.all_day)
) {
this.state.data.to = this.state.data.from + (this.duration || 60 * 60);
}
}
if (key === 'to') {
if (
this.state.data.to < this.state.data.from ||
(this.state.data.to - this.state.data.from > 60 * 60 * 24 * 2 && !this.state.data.all_day)
) {
this.state.data.from = this.state.data.to - (this.duration || 60 * 60);
}
}
if (key === 'all_day') {
if (
this.state.data.to - this.state.data.from > 60 * 60 * 24 * 2 &&
!this.state.data.all_day
) {
this.state.data.to = this.state.data.from + Math.min(this.duration || 60 * 60, 60 * 60);
}
}
if (this.state.data.to > this.state.data.from) {
this.duration = this.state.data.to - this.state.data.from;
}
this.setState({});
this.update();
}
update() {
var from = this.state.data.from;
var to = this.state.data.to;
if (this.props.event.type === 'deadline' || this.props.event.type === 'remind') {
to = from + 15 * 60;
}
if (to < from) {
to = from + 60 * 60;
}
var all_day = this.state.data.all_day;
this.props.onChange && this.props.onChange(from, to, all_day, null);
}
updateFromProps(props) {
this.state.data.from = props.event.from;
this.state.data.to = props.event.to;
this.state.data.all_day = props.event.all_day;
if (this.state.data.to > this.state.data.from) {
this.duration = this.state.data.to - this.state.data.from;
}
}
render() {
var event = this.props.event;
return (
<div className="calendar_selector">
<div className="date_selector_full">
<span className="clock_part" style={{ verticalAlign: 'top', display: 'inline-block' }}>
<Icon type="clock" className="icon_clock" />
</span>
<div className="start">
<DateSelectorInput
ts={this.state.data.from}
onChangeBlur={value => this.change('from', value)}
className=""
/>
{!event.all_day && (
<TimeSelector
ts={this.state.data.from}
onChangeBlur={value => this.change('from', value)}
/>
)}
</div>
{(!event.type || event.type === 'event' || event.type === 'move') && [
<span className="middle" key={`title-${event}`}>
{Languages.t('scenes.apps.calendar.event_view.article_until')}
</span>,
<div className="end" key={`selector-${event}`}>
{!event.all_day && (
<TimeSelector
ts={this.state.data.to}
onChangeBlur={value => this.change('to', value)}
className=""
/>
)}
<DateSelectorInput
ts={this.state.data.to}
onChangeBlur={value => this.change('to', value)}
/>
</div>,
]}
</div>
<div>
<Checkbox
small
value={event.all_day}
onChange={value => {
this.change('all_day', value);
}}
label={Languages.t(
'scenes.apps.calendar.event_edition.checkbox_all_day',
[],
'All day',
)}
/>
</div>
</div>
);
}
}
@@ -1,57 +0,0 @@
.date_selector_full {
display: inline-flex;
padding-left: 4px;
padding-right: 4px;
border-radius: var(--border-radius-base);
background: var(--grey-background);
max-width: 360px;
width: 100%;
box-sizing: border-box;
.clock_part {
text-align: center;
width: 6%;
max-width: 32px;
min-width: 20px;
}
& > .middle {
width: 10%;
max-width: 32px;
text-align: center;
line-height: 40px;
vertical-align: top;
display: inline-block;
padding: 0px 4px;
opacity: 0.5;
}
& > .start,
& > .end {
flex: 1;
display: inline-flex;
max-width: 150px;
input {
width: 100%;
max-width: none !important;
}
& > .time_selector {
flex: 1;
}
& > .date_selector {
flex: 2;
}
}
.icon_clock {
color: var(--grey-dark);
margin-top: 12px;
display: inline-block;
margin-right: -4px;
}
.input.medium {
text-align: center;
padding: 0px;
}
}
@@ -1,44 +0,0 @@
import React, { Component } from 'react';
import Languages from 'app/features/global/services/languages-service';
import UserListManager from 'components/user-list-manager-depreciated/user-list-manager';
import Menu from 'components/menus/menu.jsx';
export default class Participants extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="participants">
<div className="bottom-margin">
<b>{Languages.t('scenes.apps.calendar.modals.part.participants', [], 'Participants')}</b>
</div>
<div className="menu-list">
<UserListManager
showAddMe
showAddAll
readOnly={this.props.readOnly}
canRemoveMyself
noPlaceholder
users={(this.props.participants || []).map(participant => {
return { id: participant.user_id_or_mail };
})}
scope="workspace"
allowMails
onUpdate={ids_mails => {
this.props.onChange &&
this.props.onChange(
ids_mails.map(id => {
return { user_id_or_mail: id };
}),
);
Menu.closeAll();
}}
/>
</div>
</div>
);
}
}
@@ -1,81 +0,0 @@
import React, { Component } from 'react';
import Languages from 'app/features/global/services/languages-service';
import Menu from 'components/menus/menu.jsx';
import Button from 'components/buttons/button.jsx';
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
import RouterService from 'app/features/router/services/router-service';
import CalendarSelector from 'components/calendar/calendar-selector/calendar-selector.jsx';
import AccessRightsService from 'app/features/workspace-members/services/workspace-members-access-rights-service';
import WorkspaceService from 'app/deprecated/workspaces/workspaces';
import './calendar.scss';
export default class UnconfiguredTab extends Component {
constructor() {
super();
this.state = {
i18n: Languages,
selected: [],
};
Languages.addListener(this);
}
componentWillUnmount() {
Languages.removeListener(this);
}
initInCalendars() {
if (this.props.saveTab) this.props.saveTab({ calendars: this.state.selected });
Menu.closeAll();
}
render() {
var calendar_list = Collections.get('calendars').findBy({
workspace_id: RouterService.getStateFromRoute().workspaceId,
});
return (
<div>
<div className="unconfigured_tab">
<div className="title">{this.props.tab.name}</div>
<div className="text" style={{ opacity: 0.5 }}>
{Languages.t(
'scenes.apps.calendar.unconfigured_tab',
[],
"Cet onglet n'est pas encore configuré.",
)}
</div>
{AccessRightsService.getCompanyLevel(WorkspaceService.currentGroupId) !== 'guest' && (
<>
<br />
<CalendarSelector
allowMultiple
medium
value={this.state.selected}
onChange={workspaces_calendars => {
this.setState({ selected: workspaces_calendars });
}}
calendarList={calendar_list || []}
className=""
/>
<br />
{this.state.selected.length > 0 && (
<Button
className="button medium"
onClick={() => this.initInCalendars()}
style={{ width: 'auto' }}
>
{Languages.t('general.continue', [], 'Continuer')}
</Button>
)}
</>
)}
</div>
</div>
);
}
}
@@ -1,47 +0,0 @@
import Browser from './browser';
import { SelectorModal } from './modals/selector';
import TwakeTabConfiguration from './twake-tab-configuration';
export type EmbedContext = {
companyId?: string;
workspaceId?: string;
channelId?: string;
tabId?: string;
};
export default ({
initialParentId,
context,
inPublicSharing,
}: {
initialParentId?: string;
context?: EmbedContext;
inPublicSharing?: boolean;
}) => {
return (
<>
<SelectorModal />
<Drive
initialParentId={initialParentId}
context={context}
inPublicSharing={inPublicSharing}
/>
</>
);
};
const Drive = ({
initialParentId,
context,
inPublicSharing,
}: {
initialParentId?: string;
context?: EmbedContext;
inPublicSharing?: boolean;
}) => {
if (context?.tabId) {
return <TwakeTabConfiguration context={context} />;
}
return <Browser initialParentId={initialParentId} inPublicSharing={inPublicSharing} />;
};
@@ -1,77 +0,0 @@
import { Modal } from 'app/atoms/modal';
import { Info } from 'app/atoms/text';
import Button from 'app/components/buttons/button';
import { useDriveTwakeTab } from 'app/features/drive-twake/hooks/use-drive-twake-tab';
import { useDriveItem } from 'app/features/drive/hooks/use-drive-item';
import { useEffect, useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { EmbedContext } from '.';
import Browser from './browser';
import { SelectorModalAtom } from './modals/selector';
export default ({ context }: { context?: EmbedContext }) => {
const { tab, setTab, loading } = useDriveTwakeTab(context?.channelId || '', context?.tabId || '');
const { item, loading: itemLoading, refresh } = useDriveItem(tab?.item_id || '');
const setSelectorModalState = useSetRecoilState(SelectorModalAtom);
useEffect(() => {
if (tab?.item_id) refresh(tab!.item_id);
}, [tab?.item_id, refresh]);
// If nothing is configured, then show the selector and when selected the folder will give access to the whole channel
const modalOpen = (!tab || !item) && !loading && !itemLoading;
const isConfigured = tab && item;
useEffect(() => {
if (modalOpen) {
setSelectorModalState({
open: true,
parent_id: 'root',
mode: 'move',
title: `Select what folder this tab should display`,
onSelected: async ids => {
await setTab(ids[0], 'write');
},
});
}
}, [modalOpen]);
if (!item && !tab) return <></>;
// If configured then show the content of the tab and forward the fact that the access is done through a specific channel
return (
<div>
{isConfigured && (
<Browser
initialParentId={item.id}
twakeTabContextToken={context?.channelId + '+' + context?.tabId}
/>
)}
{!isConfigured && !loading && !(tab?.item_id && itemLoading) && (
<div className="w-full h-full flex items-center justify-center">
<div className="text-center">
<Info>This Documents tabs is not configured yet.</Info>
<br />
<Button
theme="outlined"
className="mt-4"
onClick={() =>
setSelectorModalState({
open: true,
parent_id: 'root',
mode: 'move',
title: `Select what folder this tab should display`,
onSelected: async ids => {
await setTab(ids[0], 'write');
},
})
}
>
Configure
</Button>
</div>
</div>
)}
</div>
);
};
@@ -1,29 +0,0 @@
import React from 'react';
import { ChannelType } from 'app/features/channels/types/channel';
import Messages from './messages';
import { ViewConfiguration } from 'app/features/router/services/app-view-service';
import './messages.scss';
type Props = {
channel: ChannelType;
options: ViewConfiguration;
};
/**
* Instanciate a Messages component with a good unicity key
*/
export default (props: Props) => {
if (!props.channel) {
return <></>;
}
return (
<Messages
channel={props.channel}
options={props.options}
key={`${props.options?.context?.threadId}${props.channel?.id}`}
/>
);
};
@@ -1,216 +0,0 @@
.new-thread-button {
.message {
align-items: center;
color: var(--secondary);
.plus-icon {
margin-right: 8px;
}
}
.thread.with-block {
cursor: pointer;
width: auto;
display: inline-block;
&:hover {
opacity: 0.8;
}
&:active {
.thread.with-block {
box-shadow: none;
}
}
}
}
.new-thread {
.message-input {
.upload-zone-centerer {
margin-top: 8px;
}
}
}
.message-input {
flex: 1;
background: #fff;
&.loading {
opacity: 0.5;
pointer-events: none;
}
&.unfocused {
opacity: 0.5;
.input-options {
opacity: 0.5;
}
}
.autocomplete {
div.input {
background: #fff;
textarea.input {
background: #fff;
}
}
.input.small {
margin-top: -2px;
padding: 0;
}
.input.autoheight_container.small textarea {
padding-left: 0px;
}
}
.editorview-submit {
display: flex;
justify-content: space-between;
flex-direction: row;
align-items: flex-start;
.editor {
display: block;
width: 100%;
}
.submit-button {
display: flex;
align-self: flex-start;
color: var(--primary);
opacity: 50%;
cursor: not-allowed;
&.disabled{
pointer-events: none;
}
&:not(.disabled):hover {
color: var(--primary-hover);
}
&:not(.disabled) {
cursor: pointer;
color: var(--primary);
opacity: unset;
}
}
}
.counter-right {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
height: 0;
}
.input-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
flex-direction: row;
margin-top: 8px;
button {
width: 28px;
height: 28px;
padding: 4px;
display: inline-flex;
justify-content: center;
align-items: center;
&:not(:last-child) {
margin-right: 2px;
}
&:hover {
background-color: var(--grey-background);
}
}
}
.input-options {
height: 28px;
display: flex;
flex-direction: row;
flex: 1;
.option {
color: var(--grey-light);
&:first-child {
margin-left: -4px;
}
&:not(.disabled):hover {
color: var(--black);
}
&:not(.disabled) {
cursor: pointer;
color: var(--grey-dark);
}
.messages-input-app-icon {
width: 16px;
height: 16px;
background-position: center;
background-size: contain;
border-radius: var(--border-radius-base);
}
}
.richtext {
&.selected {
background: var(--primary-background);
.option {
color: var(--primary);
}
}
}
}
.upload-zone-centerer {
margin: 0 auto;
min-width: 0;
width: 100%;
position: relative;
}
.ephemerals {
text-align: left;
.thread-section {
width: 100%;
padding-bottom: 8px;
.message {
padding: 0px;
}
}
.ephemerals_text {
font-size: 12px;
color: var(--grey-dark);
padding-bottom: 12px;
max-width: 1362px;
width: calc(100% - 48px);
display: inline-flex;
text-align: left;
}
}
}
.small-section.alinea {
.ephemerals .thread-container .sender-space {
width: 16px;
}
}
.attached-files-container {
width: 100%;
.attached-file {
margin-right: 8px;
}
}
@@ -1,471 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState, useRef, useEffect } from 'react';
import classNames from 'classnames';
import { EditorState } from 'draft-js';
import { Tooltip } from 'antd';
import FileUploadAPIClient from 'app/features/files/api/file-upload-api-client';
import InputOptions from './parts/InputOptions';
import EphemeralMessages from './parts/EphemeralMessages';
import MessageEditorsManager from 'app/features/messages/services/message-editor-service-factory';
import MenusManager from 'app/components/menus/menus-manager.jsx';
import PendingAttachments from './parts/PendingAttachments';
import RichTextEditorStateService from 'app/components/rich-text-editor/editor-state-service';
import { EditorView } from 'app/components/rich-text-editor';
import Languages from 'app/features/global/services/languages-service';
import { TextCount, TextCountService } from 'app/components/rich-text-editor/text-count/';
import UploadZone from 'app/components/uploads/upload-zone';
import Workspaces from 'app/deprecated/workspaces/workspaces';
import { FileType } from 'app/features/files/types/file';
import { useUploadZones } from 'app/features/files/hooks/use-upload-zones';
import { useMessageEditor } from 'app/features/messages/hooks/use-message-editor';
import useRouterCompany from 'app/features/router/hooks/use-router-company';
import { delayRequest } from 'app/features/global/utils/managedSearchRequest';
import { useChannel } from 'app/features/channels/hooks/use-channel';
import {
useChannelWritingActivityEmit,
useWritingDetector,
} from 'app/features/channels/hooks/use-channel-writing-activity';
import {
getCompanyApplication,
getCompanyApplications,
} from 'app/features/applications/state/company-applications';
import AlertManager from 'app/features/global/services/alert-manager-service';
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
import { useMessage } from 'app/features/messages/hooks/use-message';
import './input.scss';
import { Application } from 'app/features/applications/types/application';
import MessageExternalFilePicker from './parts/MessageExternalFilePicker';
import FilePicker from 'app/components/drive/file-picker/file-picker';
import { MessageFileType } from 'app/features/messages/types/message';
import { ChannelType } from 'app/features/channels/types/channel';
import { useMessageQuoteReply } from 'app/features/messages/hooks/use-message-quote-reply';
import QuotedMessage from 'app/components/quoted-message/quoted-message';
import { UpIcon, PlusIcon } from 'app/atoms/icons-agnostic';
type Props = {
messageId?: string;
channelId?: string;
threadId: string;
collectionKey?: string;
onResize?: (evt: any) => void;
onEscape?: (evt: any) => void;
onFocus?: () => void;
ref?: (node: any) => void;
onSend?: (text: string) => void;
onChange?: (editorState: EditorState) => void;
triggerApp?: (app: any, from_icon: any, evt: any) => void;
localStorageIdentifier?: string;
disableApps?: boolean;
context?: string; //Main input or response input (empty string)
format?: 'markdown' | 'raw';
editorPlugins?: Array<string>;
editorState?: EditorState;
};
export default (props: Props) => {
const { channel } = useChannel(props.channelId || '');
const companyId = useRouterCompany();
const {
editor,
setValue,
setFiles,
send,
key: editorId,
} = useMessageEditor({
companyId,
workspaceId: channel?.workspace_id || '',
channelId: props.channelId,
threadId: props.threadId,
messageId: props.messageId,
});
const { message: parentMessage } = useMessage({
companyId,
threadId: props.threadId,
id: props.threadId,
});
const {
isActive: isBeingQuoted,
close,
message: messageBeingQuoted,
} = useMessageQuoteReply(props.channelId || '');
const { upload, clear: clearUploads } = useUploadZones(editorId);
const format = props.format || 'markdown';
const editorRef = useRef<EditorView>(null);
const submitRef = useRef<HTMLDivElement>(null);
const [hasEphemeralMessage, setHasEphemeralMessage] = useState(false);
const messageEditorService = MessageEditorsManager.get(props.channelId || '');
const editorPlugins = props.editorPlugins || ['emoji', 'mention', 'channel', 'command'];
const [editorState, setEditorState] = useState(() =>
RichTextEditorStateService.get(editorId, { plugins: editorPlugins }),
);
const [isTooLong, setTooLong] = useState(false);
const [inPlus, setInPlus] = useState(false);
const { iAmWriting } = useChannelWritingActivityEmit(props.channelId || '', props.threadId);
const { onKeydown: onKeydownRealtimeListener } = useWritingDetector();
useEffect(() => {
setTooLong(TextCountService.getStats(editorState).isTooLong);
}, [editorState]);
useEffect(() => {
focusEditor();
(async () => {
if (editor.value && editor.value.length) {
setEditorState(
RichTextEditorStateService.get(editorId, {
plugins: editorPlugins,
clearIfExists: true,
initialContent: RichTextEditorStateService.getDataParser(editorPlugins).fromString(
editor.value,
format,
),
}),
);
}
})();
}, []);
const disable_app: any = {};
useEffect(() => {
if (props.editorState && props.editorState !== editorState) {
setEditorState(props.editorState);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.editorState]);
const getContentOutput = (editorState: EditorState) => {
return RichTextEditorStateService.getDataParser(editorPlugins).toString(editorState, format);
};
const onSend = async () => {
const content = getContentOutput(editorState);
setValue(content);
if (props.onSend) {
props.onSend(content);
return;
}
//Sending commands
if (content.indexOf('/') === 0 && !props.messageId) {
let app: any = null;
const app_name = content.split(' ')[0].slice(1);
// eslint-disable-next-line array-callback-return
getCompanyApplications(companyId).map((_app: any) => {
if (_app?.identity?.code === app_name) {
app = _app;
}
});
if (!app) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
AlertManager.alert(() => {}, {
text: Languages.t('services.apps.messages.no_command_possible', [content, app_name]),
title: Languages.t('services.apps.messages.no_app'),
});
return;
}
const data = {
command: content.split(' ').slice(1).join(' '),
channel: channel,
thread: parentMessage?.id ? parentMessage : null,
};
WorkspacesApps.notifyApp(app.id, 'action', 'command', data);
setEditorState(RichTextEditorStateService.clear(editorId).get(editorId));
return;
}
if (content || editor.files.length > 0) {
setEditorState(RichTextEditorStateService.clear(editorId).get(editorId));
clearUploads();
await send();
}
};
const triggerApp = (app: any, from_icon: any, evt: any) => {
if (disable_app[app.id] && new Date().getTime() - disable_app[app.id] < 1000) {
return;
}
disable_app[app.id] = new Date().getTime();
const threadId = parentMessage.id;
if (app?.identity?.code === 'twake_drive') {
const menu = [];
const has_drive_app = getCompanyApplication(app.id);
if (has_drive_app) {
menu.push({
type: 'react-element',
reactElement: () => {
let fileHandler: (file: MessageFileType) => void;
return (
<MessageExternalFilePicker
channel={channel as ChannelType}
threadId={threadId}
setHandler={handler => (fileHandler = handler)}
>
<FilePicker
mode="select_file"
onChoose={(file: any) => {
if (fileHandler)
fileHandler({
metadata: {
external_id: {
id: file.id,
workspace_id: file.workspace_id,
parent_id: file.parent_id,
company_id: channel?.company_id || '',
},
source: 'drive',
name: file.name,
size: parseInt(file.size),
mime: FileUploadAPIClient.extensionToMime(file.extension),
thumbnails: file.preview_has_been_generated
? [
{
mime: 'image/png',
url: file.preview_link,
},
]
: [],
},
});
}}
/>
</MessageExternalFilePicker>
);
},
});
}
MenusManager.openMenu(menu, { x: evt.clientX, y: evt.clientY }, 'center', {});
return;
}
if ((app as Application).display?.twake?.chat?.input) {
WorkspacesApps.openAppPopup(app.id);
}
const data = {
channel,
parent_message: parentMessage?.id ? parentMessage : null,
from_icon: from_icon,
};
WorkspacesApps.notifyApp(app.id, 'action', 'open', data);
};
const focus = () => {
messageEditorService.openEditor(props.threadId || '', props.messageId || '', props.context);
};
const focusEditor = () => {
requestAnimationFrame(() => editorRef.current?.focus());
};
const setRichTextEditorState = (editorState: EditorState): void => {
setEditorState(editorState);
RichTextEditorStateService.set(editorId, editorState);
};
const isEmpty = (): boolean => {
return (
editorState.getCurrentContent().getPlainText().trim().length === 0 && !editor.files.length
);
};
const onUpArrow = (): void => {
if (isEmpty()) {
//TODO
}
};
const onChange = async (newEditorState: EditorState) => {
const statsAfter = TextCountService.getStats(newEditorState);
const statsBefore = TextCountService.getStats(editorState);
if (statsAfter.length > statsBefore.length)
onKeydownRealtimeListener(state => iAmWriting(state));
//Delay request make the input faster (getContentOutput is a heavy call)
delayRequest(`editor-${editorId}`, async () => {
setValue(getContentOutput(newEditorState));
});
if (props.onChange) {
props.onChange(newEditorState);
return;
}
setRichTextEditorState(newEditorState);
};
const setUploadZoneRef = (node: UploadZone): void =>
messageEditorService.setUploadZone(props.messageId || props.threadId || '', node);
const onUploaded = (file: FileType) =>
messageEditorService.onAddAttachment(props.messageId || props.threadId, file);
const onDragEnter = (): void => {
messageEditorService.getUploadZone(props.threadId);
};
const getFilesLimit = () => {
const attachements = messageEditorService.getAttachements(editorId) || [];
const limit = messageEditorService.ATTACHEMENTS_LIMIT;
return attachements.length ? limit - attachements.length : limit;
};
const onAddFiles = async (files: File[]) => {
await upload(files);
};
const onFilePaste = (blobs: Blob[]) => {
if (blobs.length > 0) {
const file = new File(
[blobs[0]],
'pasted_' +
new Date()
.toISOString()
.replaceAll(/(Z|\.[0-9]+)/gm, '')
.replace(/T/, '_') +
'.png',
{
type: 'image/png',
},
);
upload([file]);
}
};
useEffect(() => {
focusEditor();
}, [messageBeingQuoted]);
const disabled = isEmpty() || isTooLong;
return (
<div className={'message-input w-full'} ref={props.ref} onClick={() => focus()}>
{isBeingQuoted && <QuotedMessage onClose={() => close()} />}
<UploadZone
className="upload-zone-centerer"
ref={setUploadZoneRef}
disableClick
parent={''}
driveCollectionKey={props.collectionKey}
uploadOptions={{ workspace_id: Workspaces.currentWorkspaceId, detached: true }}
onUploaded={onUploaded}
onDragEnter={onDragEnter}
multiple={true}
allowPaste={true}
filesLimit={getFilesLimit()}
onAddFiles={onAddFiles}
>
<EphemeralMessages
channelId={props.channelId || ''}
workspaceId={channel?.workspace_id || ''}
threadId={props.threadId}
onHasEphemeralMessage={() => {
if (!hasEphemeralMessage) {
setHasEphemeralMessage(true);
}
}}
onNotEphemeralMessage={() => {
if (hasEphemeralMessage) {
setHasEphemeralMessage(false);
}
}}
/>
{!hasEphemeralMessage && (
<div className="editorview-submit flex flex-row items-center px-1 relative">
<div className="absolute -bottom-2 right-8">
<TextCount editorState={editorState} displayOnlyAfterThresold={true} />
</div>
{!props.messageId && (
<div className="mr-2 self-start mt-[6px]">
<PlusIcon
onClick={() => {
setInPlus(!inPlus);
}}
className={
'cursor-pointer text-blue-500 hover:text-blue-600 w-5 h-5 transition-transform ' +
(inPlus ? ' rotate-45 ' : '')
}
/>
</div>
)}
<EditorView
ref={editorRef}
onChange={editorState => {
onChange(editorState);
}}
clearOnSubmit={true}
outputFormat={format}
plugins={editorPlugins}
editorState={editorState}
onSubmit={() => onSend()}
onUpArrow={() => onUpArrow()}
onFilePaste={onFilePaste}
placeholder={Languages.t('scenes.apps.messages.input.placeholder')}
/>
{!props.messageId && (
<Tooltip
title={Languages.t('scenes.apps.messages.input.send_message')}
placement="top"
>
<div
ref={submitRef}
className={classNames('ml-2 submit-button self-start mt-0.5', {
disabled: disabled,
scale: !disabled,
})}
onClick={() => {
if (!isEmpty() && !isTooLong) {
onSend();
}
}}
>
<UpIcon className="text-blue-500 hover:text-blue-600 w-7 h-7" />
</div>
</Tooltip>
)}
</div>
)}
{!hasEphemeralMessage && !props.messageId && (
<div className={'transition-all ' + (inPlus ? 'h-8 opacity-1' : 'h-0 opacity-0')}>
<InputOptions
isEmpty={isEmpty()}
channelId={props.channelId || ''}
threadId={props.threadId}
onSend={() => onSend()}
triggerApp={(app, fromIcon, evt) => triggerApp(app, fromIcon, evt)}
onAddEmoji={emoji => editorRef.current?.insertCommand('EMOJI', emoji)}
richTextEditorState={editorState}
onRichTextChange={editorState => setRichTextEditorState(editorState)}
/>
</div>
)}
<PendingAttachments
zoneId={editorId}
initialValue={editor.files || []}
onChange={list => setFiles(list)}
/>
</UploadZone>
</div>
);
};
@@ -1,51 +0,0 @@
import React, { useContext } from 'react';
import { PlusCircle } from 'react-feather';
import Thread from '../parts/thread';
import ThreadSection from '../parts/thread-section';
import Input from './input';
import Languages from 'app/features/global/services/languages-service';
import { ViewContext } from 'app/views/client/main-view/MainContent';
import { useVisibleMessagesEditorLocation } from 'app/features/messages/hooks/use-message-editor';
import './input.scss';
type Props = {
useButton?: boolean;
collectionKey: string;
channelId: string;
threadId: string;
};
export default (props: Props) => {
const location = `new-thread-${props.threadId || props.channelId}`;
const subLocation = useContext(ViewContext).type;
const { active: editorIsActive, set: setVisibleEditor } = useVisibleMessagesEditorLocation(
location,
subLocation,
);
if (!editorIsActive && props.useButton) {
return (
<Thread withBlock className="new-thread-button">
<ThreadSection
noSenderSpace
onClick={() => {
setVisibleEditor({ location, subLocation });
}}
>
<PlusCircle size={16} className="plus-icon" />{' '}
{Languages.t('scenes.apps.messages.new_thread', [], 'Start a new discussion')}
</ThreadSection>
</Thread>
);
} else {
return (
<Thread withBlock className="new-thread">
<ThreadSection noSenderSpace>
<Input channelId={props.channelId} threadId={props.threadId} />
</ThreadSection>
</Thread>
);
}
};
@@ -1,27 +0,0 @@
import { Card, Col, Row, Tooltip, Typography } from 'antd';
import { FileType } from 'app/features/files/types/file';
import '../input.scss';
import React from 'react';
type PropsType = {
files: FileType[];
};
const { Text } = Typography;
export default ({ files }: PropsType) => {
return (
<Row className="attached-files-container" justify="start">
{files.map(f => (
<Col key={f.id} className="attached-file">
<Card size="small">
<Tooltip placement="top" title={f?.metadata?.name || ''}>
<Text ellipsis style={{ width: 80, verticalAlign: 'middle' }}>
{f?.metadata?.name || ''}
</Text>
</Tooltip>
</Card>
</Col>
))}
</Row>
);
};
@@ -1,66 +0,0 @@
import Languages from 'app/features/global/services/languages-service';
import { useEphemeralMessages } from 'app/features/messages/hooks/use-ephemeral-messages';
import useRouterCompany from 'app/features/router/hooks/use-router-company';
import { useEffect } from 'react';
import { MessageContext } from '../../message/message-with-replies';
import MessageContent from '../../message/parts/MessageContent';
import ThreadSection from '../../parts/thread-section';
type Props = {
channelId: string;
workspaceId: string;
threadId: string;
onHasEphemeralMessage: () => void;
onNotEphemeralMessage: () => void;
};
export default (props: Props) => {
const companyId = useRouterCompany();
const workspaceId = props.workspaceId;
const channelId = props.channelId;
const { lastEphemeral } = useEphemeralMessages({
companyId,
channelId: props.channelId,
});
const messageKey = {
id: lastEphemeral?.id || '',
threadId: lastEphemeral?.thread_id || '',
companyId,
};
const hasEphemeral =
lastEphemeral &&
(lastEphemeral.thread_id === props.threadId ||
(!props.threadId &&
(!lastEphemeral.thread_id || lastEphemeral.thread_id === lastEphemeral.id)));
useEffect(() => {
if (lastEphemeral && hasEphemeral) {
props.onHasEphemeralMessage();
} else {
props.onNotEphemeralMessage();
}
}, [lastEphemeral, hasEphemeral]);
if (!hasEphemeral) {
return <div />;
}
const updatedKey =
lastEphemeral.id + lastEphemeral.ephemeral?.version + lastEphemeral.ephemeral?.id;
return (
<div className="ephemerals" key={updatedKey}>
<div className="ephemerals_text">
{Languages.t('scenes.apps.messages.just_you', [], 'Visible uniquement par vous')}
</div>
<MessageContext.Provider value={{ ...messageKey, companyId, workspaceId, channelId }}>
<ThreadSection withAvatar head>
<MessageContent key={updatedKey} />
</ThreadSection>
</MessageContext.Provider>
</div>
);
};
@@ -1,276 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState } from 'react';
import { EditorState } from 'draft-js';
import { Smile, Video, MoreHorizontal, Paperclip, Type } from 'react-feather';
import { Button, Tooltip } from 'antd';
import EmojiPicker from 'components/emoji-picker/emoji-picker';
import Menu from 'components/menus/menu';
import MenusManager from 'app/components/menus/menus-manager';
import Languages from 'app/features/global/services/languages-service';
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps';
import MessageEditorsManager from 'app/features/messages/services/message-editor-service-factory';
import EditorToolbar from 'app/components/rich-text-editor/editor-toolbar';
import { Application } from 'app/features/applications/types/application';
import { useCompanyApplications } from 'app/features/applications/hooks/use-company-applications';
type Props = {
channelId: string;
threadId: string;
onAddEmoji?: (emoji: any) => void;
onSend?: () => void;
triggerApp?: (app: any, fromIcon: any, evt: any) => void;
isEmpty: boolean;
onRichTextChange: (editorState: EditorState) => void;
richTextEditorState: EditorState;
};
type MenuItem = {
type: string;
emoji?: any;
icon?: any;
text?: string;
onClick?: (event: Event) => void;
};
export default (props: Props) => {
const [displayRichTextOptions, setDisplayRichTextOptions] = useState(false);
const [, setDisplayFileMenu] = useState(false);
const [, setDisplayEmojiMenu] = useState(false);
const addon_menu: MenuItem[] = [];
const addon_right_icon: any[] = [];
const addon_files: any[] = [];
const addon_calls: any[] = [];
const apps = useCompanyApplications().applications.filter(
(app: Application) => app.display?.twake?.chat?.input,
);
if (props.triggerApp) {
if (apps.length > 0) {
// eslint-disable-next-line array-callback-return
apps.map((app: Application) => {
if (app) {
let icon = WorkspacesApps.getAppIcon(app);
let emoji = '';
if ((icon || '').indexOf('http') === 0) {
emoji = icon;
icon = '';
}
const menu_item: MenuItem = {
type: 'menu',
emoji,
icon,
text: app.identity?.name,
onClick: (evt: unknown) => {
props.triggerApp && props.triggerApp(app, undefined, evt);
},
};
if (
app.display?.twake?.chat?.input?.type === 'file' &&
app?.identity?.code !== 'twake_drive'
) {
addon_files.push(menu_item);
} else if (
app?.identity?.code === 'jitsi' ||
app.display?.twake?.chat?.input?.type === 'call'
) {
addon_calls.push(menu_item);
} else if (app?.identity?.code !== 'twake_drive') {
addon_menu.push(menu_item);
}
}
});
}
}
const RichTextToolbar = () => (
<EditorToolbar
editorState={props.richTextEditorState}
onChange={editorState => props.onRichTextChange(editorState)}
/>
);
const displayToolbar = () => {
return displayRichTextOptions;
};
return (
<div className="input-toolbar">
<div className="input-options">
{addon_files.length > 0 && (
<Menu
className="option"
position="top"
toggle={true}
onOpen={() => setDisplayFileMenu(true)}
onClose={() => setDisplayFileMenu(false)}
menu={[
{
type: 'menu',
icon: 'desktop',
text: Languages.t('scenes.apps.messages.input.attach_file.from_computer'),
onClick: () => {
MessageEditorsManager.get(props.channelId).openFileSelector(props.threadId);
},
},
...addon_files,
]}
>
<Tooltip
placement="top"
title={Languages.t('scenes.apps.messages.input.attach_file', [], 'Attach file(s)')}
>
<Button type="text" size="small" className="ant-btn-icon-only">
<Paperclip size={16} />
</Button>
</Tooltip>
</Menu>
)}
{addon_files.length === 0 && (
<Tooltip
placement="top"
title={Languages.t('scenes.apps.messages.input.attach_file', [], 'Attach file(s)')}
>
<Button
type="text"
size="small"
className="ant-btn-icon-only"
onClick={() =>
MessageEditorsManager.get(props.channelId).openFileSelector(props.threadId)
}
>
<Paperclip size={16} />
</Button>
</Tooltip>
)}
{props.onAddEmoji && (
<Menu
className="option"
position="top"
toggle={true}
onOpen={() => setDisplayEmojiMenu(true)}
onClose={() => setDisplayEmojiMenu(false)}
menu={[
{
type: 'react-element',
className: 'menu-cancel-margin',
reactElement: () => {
return (
<EmojiPicker
onChange={(emoji: unknown) => {
MenusManager.closeMenu();
props.onAddEmoji && props.onAddEmoji(emoji);
}}
/>
);
},
},
]}
>
<Tooltip
placement="top"
title={Languages.t('scenes.apps.messages.input.emoji', [], 'Emoji')}
>
<Button type="text" size="small">
<Smile size={16} />
</Button>
</Tooltip>
</Menu>
)}
{addon_calls.length > 1 && (
<Menu className="option" position="top" menu={addon_calls}>
<Tooltip
placement="top"
title={Languages.t('scenes.apps.messages.input.start_call', [], 'Start a call')}
>
<Button type="text" size="small" className="ant-btn-icon-only">
<Video size={16} />
</Button>
</Tooltip>
</Menu>
)}
{addon_calls.length === 1 && (
<Tooltip
placement="top"
title={Languages.t('scenes.apps.messages.input.start_call', [], 'Start a call')}
>
<Button
type="text"
size="small"
className="ant-btn-icon-only option"
onClick={evt => addon_calls[0].onClick(evt)}
>
<Video size={16} />
</Button>
</Tooltip>
)}
<Tooltip
placement="top"
title={
displayRichTextOptions
? Languages.t('scenes.apps.messages.input.hide_formatting', [], 'Hide formatting')
: Languages.t('scenes.apps.messages.input.show_formatting', [], 'Show formatting')
}
>
<Button
type="text"
size="small"
className={`option ant-btn-icon-only richtext ${
displayRichTextOptions ? 'selected' : ''
}`}
onMouseDown={e => {
e.preventDefault();
setDisplayRichTextOptions(!displayRichTextOptions);
}}
>
<Type size={16} />
</Button>
</Tooltip>
{addon_right_icon.map((app: Application) => {
return (
<Button
key={app.id}
type="text"
size="small"
className="option"
onClick={(evt: unknown) => {
props.triggerApp && props.triggerApp(app, true, evt);
}}
>
<div
className="messages-input-app-icon"
style={{
backgroundImage:
'url(' + (app.display?.twake?.chat?.input?.icon || app.identity?.icon) + ')',
}}
/>
</Button>
);
})}
{addon_menu.length > 0 && (
<Menu className="option" position="top" menu={addon_menu}>
<Button type="text" size="small">
<MoreHorizontal size={16} />
</Button>
</Menu>
)}
</div>
{displayToolbar() && (
<div className="input-options-toolbar">
<div className="richtext-toolbar fade_in">
<RichTextToolbar />
</div>
</div>
)}
</div>
);
};
@@ -1,60 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useChannelWritingActivityState } from 'app/features/channels/hooks/use-channel-writing-activity';
import Languages from 'app/features/global/services/languages-service';
import WritingLoader from 'app/components/writing-loader/writing-loader';
import { ChannelWritingActivityType } from 'app/features/channels/state/channel-writing-activity';
type PropsType = {
channelId: string;
threadId: string;
};
const getChannelActivityMessage = (channelActivityUsers: ChannelWritingActivityType[]) => {
const channelActivityUsersCount = channelActivityUsers.length;
let channelActivityMessage = '';
if (channelActivityUsersCount === 1) {
channelActivityMessage = Languages.t(
'scenes.app.messages.input.parts.is_writing.message.user_is_writing',
[channelActivityUsers[0].name],
);
}
if (channelActivityUsersCount === 2) {
channelActivityMessage = Languages.t(
'scenes.app.messages.input.parts.is_writing.message.users_are_writing',
[channelActivityUsers[0].name, channelActivityUsers[1].name],
);
}
if (channelActivityUsersCount > 2) {
channelActivityMessage = Languages.t(
'scenes.app.messages.input.parts.is_writing.message.users_and_more_are_writing',
[channelActivityUsers[0].name, channelActivityUsers[1].name, channelActivityUsersCount - 2],
);
}
return channelActivityMessage;
};
export default ({ channelId, threadId }: PropsType): JSX.Element => {
const channelActivityUsers = useChannelWritingActivityState(channelId, threadId);
const [writtingInfo, setWritingInfo] = useState('');
useEffect(() => {
const message = getChannelActivityMessage(channelActivityUsers);
setWritingInfo(message);
}, [channelActivityUsers]);
return writtingInfo.length > 0 ? (
<div className="user-writing-info-message-view">
<div className="user-writing-info-message-view-inner">
<WritingLoader /> <div className="small-left-margin">{writtingInfo}</div>
</div>
</div>
) : (
<></>
);
};
@@ -1,28 +0,0 @@
import React, { ReactNode, useEffect } from 'react';
import { MessageFileType } from 'app/features/messages/types/message';
import { useUploadZones } from 'app/features/files/hooks/use-upload-zones';
import { ChannelType } from 'app/features/channels/types/channel';
import { useMessageEditor } from 'app/features/messages/hooks/use-message-editor';
export default (props: {
children: ReactNode;
setHandler: (handler: (file: MessageFileType) => void) => void;
channel: ChannelType;
threadId?: string;
}) => {
const { key: editorId } = useMessageEditor({
companyId: props.channel.company_id || '',
workspaceId: props.channel.workspace_id || '',
channelId: props.channel.id,
threadId: props.threadId,
});
const { files, setFiles } = useUploadZones(editorId);
useEffect(() => {
props.setHandler((file: MessageFileType) => {
setFiles([...files, file]);
});
}, [props.setHandler]);
return <>{props.children}</>;
};
@@ -1,49 +0,0 @@
import React, { useEffect } from 'react';
import { Col, Row } from 'antd';
import { useUploadZones } from 'app/features/files/hooks/use-upload-zones';
import '../input.scss';
import { MessageFileType } from 'app/features/messages/types/message';
import _ from 'lodash';
import PossiblyPendingAttachment from '../../message/parts/PossiblyPendingAttachment';
type PropsType = {
zoneId: string;
initialValue?: MessageFileType[];
onChange?: (list: MessageFileType[]) => void;
};
export default ({ zoneId, onChange, initialValue }: PropsType) => {
const { files, setFiles } = useUploadZones(zoneId);
useEffect(() => {
if (initialValue) setFiles(initialValue.filter(f => f?.metadata?.source !== 'pending'));
}, []);
useEffect(() => {
if (onChange) onChange(files);
}, [files]);
return files.length > 0 ? (
<Row className="attached-files-container mt-2 -mb-1" justify="start">
{files.map((file, index) => {
return (
<Col key={index}>
<PossiblyPendingAttachment
file={file}
type={'input'}
onRemove={() =>
setFiles(
files.filter(
f => !_.isEqual(f.metadata?.external_id, file.metadata?.external_id),
),
)
}
/>
</Col>
);
})}
</Row>
) : (
<></>
);
};
@@ -1,150 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/prop-types */
import React, {
forwardRef,
ReactNode,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import { ItemContent, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import _ from 'lodash';
export type ListBuilderHandle = VirtuosoHandle & unknown;
type Props = {
items: any[];
followOutput: false | 'smooth' | 'auto';
loadMore: (direction: 'history' | 'future', limit: number, offset?: any) => Promise<unknown[]>;
itemContent: ItemContent<any, any>;
itemId: (item: any) => string;
emptyListComponent: ReactNode;
onScroll: (e: React.UIEvent<'div', UIEvent>) => void;
style?: React.CSSProperties;
atBottomStateChange?: (atBottom: boolean) => void;
//Will be called just before to finish append messages for a final filtering
filterOnAppend?: (item: any[]) => any[];
};
let prependMoreLock = false;
let appendMoreLock = false;
export default React.memo(
forwardRef(
(
{
emptyListComponent,
filterOnAppend,
followOutput,
itemId,
loadMore,
onScroll,
items: _items,
itemContent,
atBottomStateChange,
style,
}: Props,
ref,
) => {
const START_INDEX = 10000000;
const INITIAL_ITEM_COUNT = (_items || []).length;
const [firstItemIndex, setFirstItemIndex] = useState(START_INDEX);
const [items, setItems] = useState(_items || []);
const refVirtuoso = useRef<VirtuosoHandle>(null);
useImperativeHandle(ref, () => ({
...refVirtuoso.current,
}));
useEffect(() => {
// Detect append or prepend or full replace items
const ids = items.map(i => itemId(i));
//Find the first index in props _items that is already in displayed items
const first = _items.findIndex(i => ids.includes(itemId(i)));
//Find the last index in props _items that is already in displayed items
const lastIndex = _items
.slice()
.reverse()
.findIndex(i => ids.includes(itemId(i)));
const last = lastIndex >= 0 ? _items.length - 1 - lastIndex : lastIndex;
if (first == -1) {
//Replacement
setFirstItemIndex(START_INDEX);
setItems(_items);
} else if (first === 0 && last !== _items.length - 1) {
//Append
let newList = [...items, ..._items.slice(last + 1)];
if (filterOnAppend) newList = filterOnAppend(newList);
setItems(newList);
} else if (last === _items.length - 1 && first !== 0) {
//Prepend
const newItems = _items.slice(0, first);
const nextFirstItemIndex = firstItemIndex - newItems.length;
setFirstItemIndex(() => nextFirstItemIndex);
setItems([...newItems, ...items]);
} else {
if (filterOnAppend) {
const newList = filterOnAppend([...items, ..._items]);
if (
_.difference(
items.map(i => itemId(i)),
newList.map(i => itemId(i)),
).length > 0
) {
setItems(newList);
}
}
}
}, [_items]);
const appendItems = useCallback(() => {
if (appendMoreLock) return;
appendMoreLock = true;
setTimeout(async () => {
await loadMore('future', 20, items[items.length - 1]);
appendMoreLock = false;
}, 10);
return false;
}, [items]);
const prependItems = useCallback(() => {
if (prependMoreLock) return;
prependMoreLock = true;
setTimeout(async () => {
await loadMore('history', 20, items[0]);
prependMoreLock = false;
}, 10);
return false;
}, [items]);
if (items.length === 0) {
return <div style={{ flex: 1 }}>{emptyListComponent}</div>;
}
return (
<Virtuoso
ref={refVirtuoso}
style={style}
followOutput={followOutput}
alignToBottom={true}
firstItemIndex={firstItemIndex}
initialTopMostItemIndex={INITIAL_ITEM_COUNT - 1}
data={items}
startReached={prependItems}
endReached={appendItems}
itemContent={itemContent}
onScroll={e => onScroll(e)}
atBottomStateChange={atBottomStateChange}
computeItemKey={(_index, item) => itemId(item)}
/>
);
},
),
);
@@ -1,92 +0,0 @@
import React, { useContext, useState } from 'react';
import Message from './message';
import Responses from './responses';
import ReplyBlock from './parts/ReplyBlock';
import LoadMoreReplies from './parts/LoadMoreReplies';
import { MessagesListContext } from '../messages-list';
import ThreadSection from '../parts/thread-section';
import Thread from '../parts/thread';
import { useMessage } from 'app/features/messages/hooks/use-message';
import ActivityMessage, { ActivityType } from './parts/ChannelActivity/ActivityMessage';
import { useHighlightMessage } from 'app/features/messages/hooks/use-highlight-message';
import { NodeMessage } from 'app/features/messages/types/message';
export const MessageContext = React.createContext({
companyId: '',
workspaceId: '',
channelId: '',
threadId: '',
id: '',
});
type Props = {
companyId: string;
workspaceId: string;
channelId: string;
threadId: string;
id?: string;
};
export default React.memo(({ threadId, workspaceId, channelId, companyId, id }: Props) => {
return (
<MessageContext.Provider
value={{ companyId, workspaceId, channelId, threadId, id: id || threadId }}
>
<MessageType />
</MessageContext.Provider>
);
});
const MessageType = () => {
const listContext = useContext(MessagesListContext);
const context = useContext(MessageContext);
const { message } = useMessage(context);
const { highlight } = useHighlightMessage();
const highlighted =
(highlight &&
((highlight.threadId === context.id && !listContext.hideReplies) ||
highlight.answerId === context.id)) ||
false;
const [firstMessageId, setFirstMessageId] = useState(
message.last_replies?.[0]?.id || message.thread_id,
);
if (message.subtype === 'system') {
const activity = message?.context?.activity as ActivityType;
return <ActivityMessage message={message} activity={activity} />;
}
return (
<Thread withBlock={listContext.withBlock} highlighted={highlighted}>
<HeadMessage />
{!listContext.hideReplies && (
<>
<LoadMoreReplies
firstMessageId={firstMessageId}
onFirstMessageChanged={(firstMessage: NodeMessage) => {
if (firstMessage) setFirstMessageId(firstMessage.id);
}}
/>
<Responses
companyId={context.companyId}
workspaceId={context.workspaceId}
channelId={context.channelId}
threadId={context.threadId}
firstMessageId={firstMessageId}
/>
{!listContext.readonly && <ReplyBlock />}
</>
)}
</Thread>
);
};
const HeadMessage = () => {
const context = useContext(MessageContext);
const { message } = useMessage(context);
return (
<ThreadSection withAvatar head pinned={!!message.pinned_info?.pinned_at}>
<Message />
</ThreadSection>
);
};
@@ -1,245 +0,0 @@
.message {
text-align: left;
.deleted-message {
opacity: 0.5;
}
.message_is_loading {
opacity: 0.5;
}
.message-not-sent.content {
opacity: 0.5;
}
.alert_failed_icon {
margin-left: 8px;
margin-top: 0px;
display: flex;
color: var(--red);
}
.sender-space {
width: 28px;
margin-right: 8px;
display: flex;
justify-content: flex-end;
.sender-head,
.app_icon {
width: 28px;
height: 28px;
border-radius: 50%;
background-color: var(--grey-background);
background-size: cover;
background-position: center;
.online_user_status {
position: relative;
bottom: -20px;
right: -20px;
}
}
.sender-head {
.user_online_stPPatus {
right: -20px;
}
}
}
.message-content {
flex: 1;
min-width: 0;
width: 0;
&.link-to-thread {
color: var(--grey-dark);
.content-parent {
&:before {
content: '';
position: absolute;
left: 0px;
height: 100%;
width: 3px;
border-radius: 2px;
background: var(--grey-light);
}
position: relative;
padding-left: 8px;
}
.sender-status {
display: none;
}
}
.message-content-header-container {
position: relative;
display: inline-block;
overflow: hidden;
white-space: nowrap;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
.message-content-header {
position: relative;
display: inline-block;
overflow: hidden;
white-space: nowrap;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: baseline;
.sender-name {
font-weight: 700;
margin-right: 4px;
cursor: pointer;
}
.sender-status {
font-size: 12px;
color: var(--grey-dark);
margin-right: 4px;
}
.reply-text {
margin-right: 4px;
a,
a * {
display: inline;
}
overflow: hidden;
text-overflow: ellipsis;
}
.date {
color: var(--grey-dark);
font-size: 12px;
&:hover {
text-decoration: underline;
}
}
}
.message_header_loader {
height: 16px;
width: 16px;
margin-left: 4px;
}
}
}
.message-options {
position: absolute;
background-color: var(--white);
height: 32px;
border-radius: 8px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
border: 1px solid var(--grey-light);
display: block;
top: -16px;
margin: auto;
right: 8px;
z-index: 1;
opacity: 0;
display: flex;
flex-direction: row;
overflow: hidden;
&.drag {
left: -8px;
right: unset;
display: none;
.option,
.option:hover {
min-width: 28px;
color: var(--primary);
cursor: grab;
}
}
.separator {
height: 100%;
border-right: 1px solid var(--grey-light);
}
.option {
min-width: 24px;
display: flex;
align-items: center;
justify-content: center;
color: var(--grey-dark);
cursor: pointer;
margin: 4px;
border-radius: 4px;
&:hover {
color: var(--black);
background-color: var(--primary-background);
}
&.active {
border: 1px solid var(--primary);
background-color: var(--primary-background);
}
}
}
.message-content.active {
.message-options.right {
opacity: 1;
}
}
&:hover {
.message-options {
opacity: 1;
}
}
.message-content.loading-interaction {
opacity: 0.5;
pointer-events: none;
}
.reactions {
padding-top: 8px;
.reaction_container {
width: auto;
display: inline-block;
}
.reaction {
display: inline-flex;
justify-content: center;
align-items: center;
margin-right: 4px;
border-radius: var(--border-radius-base);
box-sizing: border-box;
color: var(--black);
height: 24px;
min-width: 36px;
border: 0;
padding: 3px 4px;
font-size: 14px;
line-height: 16px;
border: 1px solid var(--grey-background);
font-weight: 500;
cursor: pointer;
.emoji-container {
margin-right: 4px;
line-height: 0px;
}
&.is_selected {
border: 1px solid var(--primary);
color: var(--primary);
background: var(--white);
}
&:hover {
background: var(--primary-background);
}
}
}
}
body:not(.drag_message) {
.draggable {
.message-options.drag {
display: flex;
}
}
}
@@ -1,6 +0,0 @@
import React from 'react';
import MessageContent from './parts/MessageContent';
export default () => {
return <MessageContent />;
};
@@ -1,113 +0,0 @@
import Twacode from 'app/components/twacode/twacode';
import React, { ReactNode, Suspense } from 'react';
import Markdown from 'markdown-to-jsx';
import HighlightedCode from 'app/components/highlighted-code/highlighted-code';
import { preparse, preunparse } from './Blocks.utils';
import User from 'components/twacode/blocks/user';
import Chan from 'components/twacode/blocks/chan';
import { blocksToTwacode, formatData } from 'app/components/twacode/blocksCompiler';
import environment from 'app/environment/environment';
import { Block } from 'app/components/twacode/types';
type Props = {
blocks: unknown[];
fallback: string | ReactNode;
onAction: (type: string, id: string, context: unknown, passives: unknown, evt: unknown) => void;
allowAdvancedBlocks?: boolean;
};
const Code = ({ className, children }: { className: string; children: string }) => {
children = preunparse(children);
if (children.split('\n').length === 1) {
return <code>{children}</code>;
}
return <HighlightedCode className={className + ' multiline-code'} code={children} />;
};
const Link = ({ href, children }: { href: string; children: string }) => {
let target = '_blank';
if (!href) {
return <a href="#">{children}</a>;
}
//If same domain, stay on the same tab
if (
href
?.replace(/https?:\/\//g, '')
?.split('/')[0]
?.toLocaleLowerCase() ===
environment.front_root_url
.replace(/https?:\/\//g, '')
.split('/')[0]
?.toLocaleLowerCase()
) {
target = '_self';
}
return (
<a target={target} rel="noreferrer" href={href?.replace(/^javascript:/, '')}>
{children}
</a>
);
};
export default React.memo((props: Props) => {
const flattedBlocks: Block[] = [];
formatData(props.blocks || [], 'content', flattedBlocks);
const blocks = blocksToTwacode(flattedBlocks);
if (!props.blocks?.length || !props.allowAdvancedBlocks) {
return typeof props.fallback === 'string' ? (
<div className="markdown">
<Markdown
options={{
forceBlock: true,
overrides: {
code: {
component: Code,
},
a: {
component: Link,
},
user: {
component: ({ id }) => (
<>
<User hideUserImage={false} username={id} />{' '}
</>
),
},
channel: {
component: ({ id }) => (
<>
<Chan id={id} name={id} />{' '}
</>
),
},
h1: ({ children }) => children,
h2: ({ children }) => children,
h3: ({ children }) => children,
h4: ({ children }) => children,
h5: ({ children }) => children,
h6: ({ children }) => children,
},
}}
>
{preparse(props.fallback || '')}
</Markdown>
</div>
) : (
<>{props.fallback}</>
);
}
return (
<Suspense fallback={<></>}>
<Twacode
content={blocks}
isApp={props.allowAdvancedBlocks}
onAction={(type: string, id: string, context: unknown, passives: unknown, evt: unknown) =>
props.onAction(type, id, context, passives, evt)
}
/>
</Suspense>
);
});
@@ -1,28 +0,0 @@
export const preparse = (str: string) => {
const ret = str
//Fix markdown simple line break
.replace(/\n/g, ' \n')
.replace(/^(([0-9]+\.)|[>`\-*].*\n) *$\n/gm, '$1\n\n')
//Prevent html security issues
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
//Prepare mentions
.replace(/([ \t]|^)@([\w]+)/g, '$1<user id="$2"/>')
.replace(/([ \t]|^)#([\w]+)/g, '$1<channel id="$2"/>')
//Prepare code
.replace(/[^\n]\r?\n{1}`{3}/, '\n\n```');
return ret;
};
export const preunparse = (str: string) => {
return (
str
//Prepare mentions
.replace(/([ \t]|^)<user id="([\w]+)"\/>/g, '$1@$2')
.replace(/([ \t]|^)<channel id="([\w]+)"\/>/g, '$1#$2')
//Prevent html security issues
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
);
};
@@ -1,275 +0,0 @@
import React from 'react';
import { Row, Typography } from 'antd';
import Languages from 'app/features/global/services/languages-service';
import Emojione from 'app/components/emojione/emojione';
import User from 'app/components/twacode/blocks/user';
import { ChannelType } from 'app/features/channels/types/channel';
import { TabType } from 'app/features/tabs/types/tab';
import { getCompanyApplications } from 'app/features/applications/state/company-applications';
import Groups from 'app/deprecated/workspaces/groups.js';
import { ChannelMemberType } from 'app/features/channel-members/types/channel-member-types';
import { MessageWithReplies } from 'app/features/messages/types/message';
import { Application } from 'app/features/applications/types/application';
enum ChannelActivityEnum {
CHANNEL_MEMBER_CREATED = 'channel:activity:member:created',
CHANNEL_MEMBER_DELETED = 'channel:activity:member:deleted',
CHANNEL_UPDATED = 'channel:activity:updated',
CHANNEL_TAB_CREATED = 'channel:activity:tab:created',
CHANNEL_TAB_DELETED = 'channel:activity:tab:deleted',
CHANNEL_CONNECTOR_CREATED = 'channel:activity:connector:created',
CHANNEL_CONNECTOR_DELETED = 'channel:activity:connector:deleted',
}
export type ActivityType = {
type: ChannelActivityEnum;
actor: {
type: 'user';
id: string;
};
context: {
type: 'add' | 'diff' | 'remove';
array?: {
type: string;
resource: ChannelMemberType | TabType;
}[];
previous?: {
type: string;
// should be a real type instead
resource: { id: string; name: string; description: string; icon: string };
};
next?: {
type: string;
// should be a real type instead
resource: { id: string; name: string; description: string; icon: string };
};
};
};
type PropsType = {
refDom?: React.Ref<HTMLDivElement>;
activity: ActivityType;
message: MessageWithReplies;
};
// i18n but with react nodes as replacements
// TODO: maybe there is betters ways to do it with lodash
const translateUsingReactNode = (key: string, replacements: JSX.Element[]): JSX.Element[] => {
let temp =
Languages.t(
key,
replacements.map((_, i) => `{${i}}`),
) || '';
const list: JSX.Element[] = [];
replacements.forEach((replacement, i) => {
const split = temp.split(`{${i}}`);
list.push(
<Typography.Text key={i} type="secondary">
{split[0]}
</Typography.Text>,
);
list.push(replacement);
temp = split[1];
});
list.push(
<Typography.Text key={key + 'b'} type="secondary">
{temp}
</Typography.Text>,
);
return list;
};
export default (props: PropsType): JSX.Element => {
const generateTypographyName = (id: string) => <User id={id} username="Unknown" hideUserImage />;
const message = props.message;
const memberJoinedOrInvited = (activity: ActivityType) => {
if (activity.context.array) {
const resource = activity.context.array[0]?.resource as ChannelMemberType;
if (activity.actor.id === resource.user_id) {
return [<></>]; //Do not show this information to not polute the chat
}
if (activity.actor.id !== resource.user_id) {
return translateUsingReactNode(
'scenes.apps.messages.message.activity_message.a_added_b_to_the_channel',
[
<span key={message.id + '-1'} style={{ marginRight: 5, lineHeight: 0 }}>
{generateTypographyName(activity.actor.id)}
</span>,
<span key={message.id + '-2'} style={{ margin: '0 5px', lineHeight: 0 }}>
{generateTypographyName(resource.user_id || '')}
</span>,
],
);
}
}
};
const memberLeftOrRemoved = (activity: ActivityType) => {
if (activity.context.array) {
const resource = activity.context.array[0]?.resource as ChannelMemberType;
if (activity.actor.id === resource.user_id) {
return []; //Do not show this information to not polute the chat
}
if (activity.actor.id !== resource.user_id) {
return translateUsingReactNode(
'scenes.apps.messages.message.activity_message.a_removed_b_from_the_channel',
[
<span key={message.id + '-3'} style={{ marginRight: 5, lineHeight: 0 }}>
{generateTypographyName(activity.actor.id)}
</span>,
<span key={message.id + '-4'} style={{ margin: '0 5px', lineHeight: 0 }}>
{generateTypographyName(resource.user_id || '')}
</span>,
],
);
}
}
};
const channelNameOrDescription = (activity: ActivityType) => {
const previous = activity.context.previous;
const next = activity.context.next;
if (
previous?.resource?.name !== next?.resource?.name ||
previous?.resource?.icon !== next?.resource?.icon
) {
const icon = <Emojione type={next?.resource.icon || ''} />;
return translateUsingReactNode(
'scenes.apps.messages.message.activity_message.a_updated_channel_name',
[
<span key={message.id + '-5'} style={{ marginRight: 5, lineHeight: 0 }}>
{generateTypographyName(activity.actor.id)}
</span>,
<Typography.Text key={message.id + '-6'} strong style={{ margin: '0 5px' }}>
{icon} {next?.resource?.name}
</Typography.Text>,
],
);
}
if (previous?.resource.description !== next?.resource.description) {
return translateUsingReactNode(
'scenes.apps.messages.message.activity_message.a_updated_channel_description',
[
<span key={message.id + '-7'} style={{ marginRight: 5, lineHeight: 0 }}>
{generateTypographyName(activity.actor.id)}
</span>,
],
);
}
};
const channelTabCreatedOrDeleted = (activity: ActivityType) => {
if (activity.context.array) {
const resource = activity.context.array[0].resource as TabType;
const connector = getCompanyApplications(Groups.currentGroupId).filter(
(app: Application) => app.id === resource.application_id,
);
if (activity.context.type === 'add') {
return translateUsingReactNode(
'scenes.apps.messages.message.activity_message.a_created_channel_tab',
[
<span key={message.id + '-8'} style={{ marginRight: 5, lineHeight: 0 }}>
{generateTypographyName(activity.actor.id)}
</span>,
<Typography.Text key={message.id + '-9'} strong style={{ margin: '0 5px' }}>
{connector[0]?.identity?.name}
</Typography.Text>,
<Typography.Text key={message.id + '-10'} strong style={{ marginLeft: 5 }}>
{resource?.name}
</Typography.Text>,
],
);
}
if (activity.context.type === 'remove') {
return translateUsingReactNode(
'scenes.apps.messages.message.activity_message.a_deleted_channel_tab',
[
<span key={message.id + '-11'} style={{ marginRight: 5, lineHeight: 0 }}>
{generateTypographyName(activity.actor.id)}
</span>,
<Typography.Text key={message.id + '-12'} strong style={{ margin: '0 5px' }}>
{connector[0]?.identity?.name}
</Typography.Text>,
<Typography.Text key={message.id + '-13'} strong style={{ marginLeft: 5 }}>
{resource?.name}
</Typography.Text>,
],
);
}
}
};
const channelConnectorCreatedOrDeleted = (activity: ActivityType) => {
if (activity.context.array) {
const resource = activity.context.array[0].resource as ChannelType;
const connector = getCompanyApplications(Groups.currentGroupId).filter((app: Application) =>
resource.connectors?.includes(app.id),
);
if (connector.length) {
if (activity.context.type === 'add') {
return translateUsingReactNode(
'scenes.apps.messages.message.activity_message.a_created_channel_connector',
[
<span key={message.id + '-14'} style={{ marginRight: 5, lineHeight: 0 }}>
{generateTypographyName(activity.actor.id)}
</span>,
<Typography.Text key={message.id + '-15'} strong style={{ marginLeft: 5 }}>
{connector[0]?.identity?.name}
</Typography.Text>,
],
);
}
if (activity.context.type === 'remove') {
return translateUsingReactNode(
'scenes.apps.messages.message.activity_message.a_deleted_channel_connector',
[
<span key={message.id + '-16'} style={{ marginRight: 5, lineHeight: 0 }}>
{generateTypographyName(activity.actor.id)}
</span>,
<Typography.Text key={message.id + '-17'} strong style={{ marginLeft: 5 }}>
{connector[0]?.identity?.name}
</Typography.Text>,
],
);
}
}
}
};
const compute = (): string | JSX.Element[] => {
const process = new Map<
ChannelActivityEnum,
(activity: ActivityType) => JSX.Element[] | undefined
>();
process
.set(ChannelActivityEnum.CHANNEL_MEMBER_CREATED, memberJoinedOrInvited)
.set(ChannelActivityEnum.CHANNEL_MEMBER_DELETED, memberLeftOrRemoved)
.set(ChannelActivityEnum.CHANNEL_UPDATED, channelNameOrDescription)
.set(ChannelActivityEnum.CHANNEL_TAB_CREATED, channelTabCreatedOrDeleted)
.set(ChannelActivityEnum.CHANNEL_TAB_DELETED, channelTabCreatedOrDeleted)
.set(ChannelActivityEnum.CHANNEL_CONNECTOR_CREATED, channelConnectorCreatedOrDeleted)
.set(ChannelActivityEnum.CHANNEL_CONNECTOR_DELETED, channelConnectorCreatedOrDeleted);
const method = process.get(props.activity?.type) || (() => undefined);
return method(props.activity) || '';
};
return (
<div style={{ height: 40, paddingTop: 8 }}>
<Row className="markdown" align="middle" justify="center" ref={props.refDom}>
{compute()}
</Row>
</div>
);
};
@@ -1,93 +0,0 @@
import React, { useEffect, useState } from 'react';
import UsersService from 'app/features/users/services/current-user-service';
import userAsyncGet from 'app/features/users/utils/async-get';
import MenuManager from 'app/components/menus/menus-manager';
import UserCard from 'app/components/user-card/user-card';
import Languages from 'app/features/global/services/languages-service';
import '../message.scss';
import { Typography } from 'antd';
import { useDirectChannels } from 'app/features/channels/hooks/use-direct-channels';
type PropsType = {
userId: string;
};
export default ({ userId }: PropsType) => {
const [fullName, setFullName] = useState<string>('');
const { openDiscussion } = useDirectChannels();
const _setFullName = async () => {
const user = await userAsyncGet(userId);
if (user) setFullName(UsersService.getFullName(user));
else setFullName('not found');
};
useEffect(() => {
_setFullName();
});
let user_name_node: HTMLSpanElement | null = null;
const displayUserCard = async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const box = (window as any).getBoundingClientRect(user_name_node);
const user = await userAsyncGet(userId);
MenuManager.openMenu(
[
user && {
type: 'react-element',
reactElement: () => (
<UserCard user={user} onClick={() => openDiscussion([user.id || ''])} />
),
},
],
box,
null,
{ margin: 8 },
);
};
const translateUsingReactNode = (key: string, replacements: JSX.Element[]): JSX.Element[] => {
let temp = Languages.t(
key,
replacements.map((_, i) => `{${i}}`),
);
const list: JSX.Element[] = [];
replacements.forEach((replacement, i) => {
const split = temp.split(`{${i}}`);
list.push(
<Typography.Text key={i} type="secondary">
{split[0]}
</Typography.Text>,
);
list.push(replacement);
temp = split[1];
});
list.push(<Typography.Text type="secondary">{temp}</Typography.Text>);
return list;
};
const clickableFullName = (
<span
ref={node => (user_name_node = node)}
style={{
fontWeight: 700,
marginRight: 4,
cursor: ' pointer',
}}
onClick={() => displayUserCard()}
>
{fullName}
</span>
);
const isCurrentUser = UsersService.getCurrentUserId() === userId;
return isCurrentUser ? (
<>{Languages.t('scenes.apps.messages.message.parts.deleted_content.text.current_user')}</>
) : (
<>
{translateUsingReactNode('scenes.apps.messages.message.parts.deleted_content.text', [
clickableFullName,
])}
</>
);
};
@@ -1,44 +0,0 @@
.first_message {
width: 100%;
text-align: center;
padding-bottom: 10vh;
padding-top: 10vh;
.emojione {
width: 88px;
height: 88px;
padding-bottom: 10px;
}
.icon {
display: inline-block;
font-size: 88px;
}
.channel_first_message_icon {
.ant-avatar-group .ant-avatar:not(:first-child) {
margin-left: -32px !important;
}
.user_image {
width: 78px;
height: 78px;
background-size: cover;
background-position: center;
background-color: var(--white);
border-radius: 50%;
display: inline-block;
padding-bottom: 20px;
border: 2px solid #fff;
margin-left: -40px;
&:first-child {
margin-left: 0px;
}
}
}
.text {
padding-top: 10px;
opacity: 0.5;
}
}
@@ -1,20 +0,0 @@
import React from 'react';
import Languages from 'app/features/global/services/languages-service';
import Emojione from 'components/emojione/emojione';
import './FirstMessage.scss';
export default React.memo(() => {
return (
<div className="first_message">
<div className="content">
<div className="icon">
<Emojione s128 type={'🥇'} />
</div>
<div className="text">
{Languages.t('scenes.apps.messages.message.types.first_channel_message_text')}
</div>
</div>
</div>
);
});
@@ -1,17 +0,0 @@
import React from 'react';
import Languages from 'app/features/global/services/languages-service';
import './FirstMessage.scss';
export default ({ noReplies }: { noReplies?: boolean }) => {
return (
<div className="first_message">
<div className="content">
<div className="text">
{noReplies
? Languages.t('scenes.apps.messages.message.types.no_message_in_thread')
: Languages.t('scenes.apps.messages.message.types.first_message_text')}
</div>
</div>
</div>
);
};
@@ -1,70 +0,0 @@
.link-preview {
border-radius: 0px !important;
border: 4px solid var(--primary) !important;
border-width: 0 0 0 4px !important;
margin-top: 15px !important;
padding-left: 15px !important;
.delete-link-preview {
display: none;
position: absolute;
left: -32px;
top: -8px;
color: var(--grey) !important;
opacity: 0.5;
cursor: pointer;
padding: 8px;
&:hover {
opacity: 1;
}
}
&:hover .delete-link-preview {
display: block;
}
&.ant-card .ant-card-body {
padding: 0;
}
&.ant-card .ant-card-meta-avatar {
padding-right: 0px;
width: 100%;
margin-bottom: 0px;
span > img {
width: 16px;
height: 16px;
display: inline-block;
vertical-align: text-top;
}
.ant-avatar {
text-align: start;
width: 24px;
}
}
.link-preview-domain {
font-weight: bold;
}
.preview-title > a {
font-size: 14px;
font-weight: bold;
float: left;
}
.ant-card-meta-description {
margin-bottom: 10px;
color: var(--grey);
}
.ant-card-cover {
cursor: pointer;
img {
border-radius: 8px 8px 8px 8px;
}
}
}
@@ -1,64 +0,0 @@
import React, { useContext } from 'react';
import { MessageLinkType } from 'app/features/messages/types/message';
import './LinkPreview.scss';
import { useMessage } from 'app/features/messages/hooks/use-message';
import User from 'app/features/users/services/current-user-service';
import { MessageContext } from '../message-with-replies';
import { X } from 'react-feather';
type PropsType = {
preview: MessageLinkType;
};
export default ({ preview }: PropsType): React.ReactElement => {
const context = useContext(MessageContext);
const { deleteLinkPreview, message } = useMessage(context);
return (
<div className="xs:max-w-xs max-w-md ant-card ant-card-bordered ant-card-small ant-card-type-inner link-preview">
{message.user_id === User.getCurrentUserId() ? (
<div className="delete-link-preview">
<X size={16} onClick={() => deleteLinkPreview(preview.url)} />
</div>
) : null}
<div className="ant-card-body">
<div className="ant-card-meta">
<div className="ant-card-meta-detail">
<div className="ant-card-meta-avatar">
{preview.favicon && (
<span className="ant-avatar ant-avatar-circle ant-avatar-image">
<img alt={preview.domain} src={preview.favicon} />
</span>
)}
<span className="link-preview-domain">{preview.domain}</span>
</div>
<div className="preview-title">
<a
href={preview.url?.replace(/^javascript:/, '')}
target="_blank"
rel="noreferrer"
className="truncate text-ellipsis w-full"
>
{preview.title}
</a>
</div>
<div className="ant-card-meta-description">{preview.description}</div>
</div>
</div>
{preview.img && (
<div className="ant-card-cover">
<img
alt={preview.title}
src={preview.img}
onClick={() => window.open(preview.url, '_blank')}
style={{
maxWidth: preview.img_width ?? '100%',
maxHeight: preview.img_height ?? '100%',
}}
/>
</div>
)}
</div>
</div>
);
};
@@ -1,38 +0,0 @@
import { useMessage } from 'app/features/messages/hooks/use-message';
import { useThreadMessages } from 'app/features/messages/hooks/use-thread-messages';
import React, { useContext } from 'react';
import Languages from 'app/features/global/services/languages-service';
import ThreadSection from '../../parts/thread-section';
import { MessageContext } from '../message-with-replies';
import { NodeMessage } from 'app/features/messages/types/message';
export default (props: { firstMessageId: string; onFirstMessageChanged: (item: NodeMessage) => void }) => {
const context = useContext(MessageContext);
const { message } = useMessage(context);
const { messages, window, loadMore } = useThreadMessages({
companyId: context.companyId,
threadId: message.thread_id,
});
const loadMoreMessages = async (direction: 'history' | 'future') => {
const messages = await loadMore(direction, 10, props.firstMessageId);
props.onFirstMessageChanged && props.onFirstMessageChanged(messages[0]);
};
return (
<>
{!(window.reachedStart && window.start === props.firstMessageId) &&
window.end &&
messages.length < message.stats.replies && (
<ThreadSection gradient>
<div className="message-content">
<span onClick={() => loadMoreMessages('history')} className="link">
{Languages.t('scenes.apps.messages.message.show_responses_button')} (
{Math.max(message.stats.replies, messages.length)})
</span>
</div>
</ThreadSection>
)}
</>
);
};
@@ -1,55 +0,0 @@
import { Row } from 'antd';
import { useUploadZones } from 'app/features/files/hooks/use-upload-zones';
import { useMessage } from 'app/features/messages/hooks/use-message';
import 'moment-timezone';
import { useContext, useEffect } from 'react';
import { MessageContext } from '../message-with-replies';
import PossiblyPendingAttachment from './PossiblyPendingAttachment';
export default () => {
const context = useContext(MessageContext);
const { message, save } = useMessage(context);
const { files, setFiles } = useUploadZones(`message-${message.id}`);
useEffect(() => {
setFiles(message.files || []);
}, [message.files]);
useEffect(() => {
const uploaded = files.filter(f => f.metadata?.source !== 'pending');
const inMessage = (message.files || []).filter(f => f.metadata?.source !== 'pending');
if (uploaded.length > inMessage.length) {
save({ ...message, files: uploaded });
}
}, [files]);
if (files.length === 0) {
return <></>;
}
return (
<Row justify="start" align="middle" className="small-top-margin" wrap>
{files
.filter(f => f.metadata)
.map(file => (
<PossiblyPendingAttachment
key={file.metadata?.external_id || file.id}
type={'message'}
file={file}
xlarge={files.length === 1 && (files[0].metadata?.thumbnails?.length || 0) > 0}
large={
//If all the documents are images
files.length <= 6 &&
files.filter(
file =>
file.metadata?.source === 'internal' &&
(file.metadata?.thumbnails?.length || 0) > 0,
).length === files.length
}
onRemove={() => setFiles(files.filter(f => f.id !== file.id))}
/>
))}
</Row>
);
};
@@ -1,250 +0,0 @@
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
import { useIsChannelMember } from 'app/features/channels/hooks/use-channel';
import PseudoMarkdownCompiler from 'app/features/global/services/pseudo-markdown-compiler-service';
import { useMessage } from 'app/features/messages/hooks/use-message';
import { useVisibleMessagesEditorLocation } from 'app/features/messages/hooks/use-message-editor';
import { MessageWithReplies } from 'app/features/messages/types/message';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { useUser } from 'app/features/users/hooks/use-user';
import User from 'app/features/users/services/current-user-service';
import MessageQuote from 'app/molecules/message-quote';
import MessageStatus from 'app/molecules/message-status';
import QuotedContent, { useQuotedMessage } from 'app/molecules/quoted-content';
import { ViewContext } from 'app/views/client/main-view/MainContent';
import classNames from 'classnames';
import 'moment-timezone';
import { ReactNode, useContext, useEffect, useState } from 'react';
import { gotoMessage } from 'src/utils/messages';
import { MessageContext } from '../message-with-replies';
import Blocks from './Blocks';
import DeletedContent from './DeletedContent';
import LinkPreview from './LinkPreview';
import MessageForward from './message-forward';
import MessageAttachments from './MessageAttachments';
import MessageEdition from './MessageEdition';
import MessageHeader from './MessageHeader';
import Options from './Options';
import Reactions from './Reactions';
import RetryButtons from './RetryButtons';
type Props = {
linkToThread?: boolean;
threadHeader?: string;
};
let loadingInteractionTimeout = 0;
export default (props: Props) => {
const [active, setActive] = useState(false);
const [loadingAction, setLoadingAction] = useState(false);
const [didMouseOver, setDidMouseOver] = useState(false);
const context = useContext(MessageContext);
const channelId = context.channelId;
const { message } = useMessage(context);
// Quoted message logic
const quotedMessage = useQuotedMessage(message, context);
const showQuotedMessage = quotedMessage && quotedMessage.thread_id;
let authorName = '';
const currentRouterWorkspace = useRouterWorkspace();
const workspaceId =
context.workspaceId === 'direct' ? currentRouterWorkspace : context.workspaceId;
const deletedQuotedMessage = quotedMessage && quotedMessage.subtype === 'deleted';
if (showQuotedMessage) {
const author = useUser(quotedMessage.user_id || '');
authorName = author ? User.getFullName(author) : 'Anonymous';
}
const onInteractiveMessageAction = (action_id: string, context: unknown, passives: unknown) => {
const app_id = message.application_id;
const type = 'interactive_message_action';
const event = action_id;
const data = {
interactive_context: context,
form: passives,
message: message,
};
WorkspacesApps.notifyApp(app_id, type, event, data);
};
const onAction = (type: string, id: string, context: unknown, passives: unknown) => {
if (type === 'interactive_action') {
setLoadingAction(true);
clearTimeout(loadingInteractionTimeout);
loadingInteractionTimeout = window.setTimeout(() => {
setLoadingAction(false);
}, 5000);
onInteractiveMessageAction(id, context, passives);
}
};
useEffect(() => {
setLoadingAction(false);
}, [JSON.stringify(message.blocks)]);
const deleted = message.subtype === 'deleted';
const location = `message-${message.id}`;
const { active: editorIsActive } = useVisibleMessagesEditorLocation(
location,
useContext(ViewContext).type,
);
const showEdition = !props.linkToThread && editorIsActive;
const messageIsLoading = message._status === 'sending';
const messageSaveFailed = message._status === 'failed';
const isChannelMember = useIsChannelMember(channelId);
const quotedContent = <QuotedContent message={quotedMessage} />;
const showMessageStatus = message.user_id === User.getCurrentUserId();
return (
<div
className={classNames('message-content', {
active,
'loading-interaction': loadingAction,
'link-to-thread': props.linkToThread,
})}
onMouseEnter={() => {
setDidMouseOver(true);
}}
onClick={() => setActive(false)}
key={`message_container_${message.id}`}
>
<MessageHeader linkToThread={props.linkToThread} />
{showQuotedMessage && !showEdition && quotedMessage.channel_id === context.channelId && (
<MessageQuote
className="mb-1"
author={authorName}
message={quotedContent}
closable={false}
deleted={deletedQuotedMessage}
goToMessage={() =>
gotoMessage(
quotedMessage,
quotedMessage.company_id || context.companyId,
quotedMessage.channel_id || context.channelId,
quotedMessage.workspace_id || workspaceId,
)
}
/>
)}
{showQuotedMessage && !showEdition && quotedMessage.channel_id !== context.channelId && (
<MessageForward
onAction={(type: string, id: string, context: unknown, passives: unknown) => {
if (isChannelMember) onAction(type, id, context, passives);
}}
className="mb-1"
author={authorName}
message={quotedMessage}
closable={false}
deleted={deletedQuotedMessage}
goToMessage={() =>
gotoMessage(
quotedMessage,
quotedMessage.company_id || context.companyId,
quotedMessage.channel_id || context.channelId,
quotedMessage.workspace_id || workspaceId,
)
}
/>
)}
{!!showEdition && !deleted && (
<div className="content-parent pt-1">
<MessageEdition />
</div>
)}
{!showEdition && (
<MessageBlockContent
onAction={(type: string, id: string, context: unknown, passives: unknown) => {
if (isChannelMember) onAction(type, id, context, passives);
}}
deleted={deleted}
linkToThread={props.linkToThread}
message={message}
className={classNames({
message_is_loading: messageIsLoading,
'message-not-sent': messageSaveFailed,
})}
suffix={
<>
{message?.files && (message?.files?.length || 0) > 0 && <MessageAttachments />}
{!messageSaveFailed && <Reactions />}
{messageSaveFailed && !messageIsLoading && <RetryButtons />}
</>
}
/>
)}
{isChannelMember &&
!showEdition &&
!deleted &&
!messageSaveFailed &&
didMouseOver &&
!messageIsLoading && (
<Options
onOpen={() => setActive(true)}
onClose={() => setActive(false)}
threadHeader={props.threadHeader}
key={`options_${message.id}`}
/>
)}
{showMessageStatus && !showEdition && (
<MessageStatus key={`message_status_${message.id}`} status={message.status} />
)}
</div>
);
};
export const MessageBlockContent = ({
deleted,
message,
linkToThread,
suffix,
className,
onAction,
}: {
deleted: boolean;
linkToThread?: boolean;
message: MessageWithReplies;
suffix?: ReactNode;
className?: string;
onAction: (type: string, id: string, context: unknown, passives: unknown) => void;
}) => {
return (
<div className="content-parent dont-break-out">
{deleted === true ? (
<div className="deleted-message">
<DeletedContent userId={message.user_id || ''} key={`deleted_${message.thread_id}`} />
</div>
) : (
<>
<div className={'content allow_selection' + (className || '')}>
{!!linkToThread && message.text}
{!linkToThread && (
<>
<Blocks
blocks={message.blocks}
fallback={PseudoMarkdownCompiler.transformBackChannelsUsers(message.text)}
onAction={onAction}
allowAdvancedBlocks={message.subtype === 'application'}
/>
</>
)}
</div>
{message?.links &&
(message?.links?.length || 0) > 0 &&
message.links
.filter(link => link && (link.title || link.description || link.img))
.map((preview, i) => (
<LinkPreview key={`${i}-${preview.url}-${message.thread_id}`} preview={preview} />
))}
{suffix}
</>
)}
</div>
);
};
@@ -1,18 +0,0 @@
.message-edition {
padding-top: 4px;
.message-edition-toolbar {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
.editor-toolbar {
padding-left: 0;
.button-group:first-child {
margin-left: -8px;
}
}
}
}
@@ -1,129 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import 'moment-timezone';
import MessageInput from '../../input/input';
import Button from 'components/buttons/button.jsx';
import AlertManager from 'app/features/global/services/alert-manager-service';
import Languages from 'app/features/global/services/languages-service';
import EditorToolbar from 'app/components/rich-text-editor/editor-toolbar';
import RichTextEditorStateService from 'app/components/rich-text-editor/editor-state-service';
import './MessageEdition.scss';
import { EditorState } from 'draft-js';
import { useMessage } from 'app/features/messages/hooks/use-message';
import { MessageContext } from '../message-with-replies';
import { ViewContext } from 'app/views/client/main-view/MainContent';
import { useVisibleMessagesEditorLocation } from 'app/features/messages/hooks/use-message-editor';
type Props = {
/**
* The editor plugins to enable during edition
*/
editorPlugins?: string[];
};
export default (props: Props) => {
const context = useContext(MessageContext);
const { message, remove, save: updateMessage } = useMessage(context);
const location = `message-${context.id}`;
const subLocation = useContext(ViewContext).type;
const editorId = `thread:${context.threadId}/message:${context.id}`;
const { close } = useVisibleMessagesEditorLocation(location, subLocation);
const editorPlugins = props.editorPlugins || ['emoji', 'mention', 'channel'];
const format = 'markdown';
const [editorState, setEditorState] = useState<EditorState>();
const [isReady, setReady] = useState(false);
useEffect(() => {
(async () => {
const dataParser = RichTextEditorStateService.getDataParser(editorPlugins);
const initialContent = message?.text || '';
setEditorState(() =>
RichTextEditorStateService.get(editorId, {
plugins: editorPlugins,
clearIfExists: true,
initialContent: dataParser.fromString(initialContent, format),
}),
);
setReady(true);
})();
}, []);
const getContentOutput = (editorState: EditorState) => {
return RichTextEditorStateService.getDataParser(editorPlugins).toString(editorState, format);
};
const save = () => {
let content = null;
if (editorState) content = getContentOutput(editorState);
if (!content) {
AlertManager.confirm(
() => {
remove();
},
() => undefined,
{
title: Languages.t('scenes.apps.messages.chatbox.chat.delete_message_btn', [], 'Delete'),
},
);
} else {
updateMessage({ ...message, text: content });
}
close();
};
const setRichTextEditorState = (editorState: EditorState): void => {
setEditorState(editorState);
RichTextEditorStateService.set(editorId, editorState);
};
return !isReady || !editorState ? (
<></>
) : (
<div className="message-edition">
<MessageInput
threadId={context.threadId}
messageId={context.id}
context={'edition'}
onSend={() => save()}
editorState={editorState}
onChange={editorState => {
setRichTextEditorState(editorState);
}}
/>
<div className="message-edition-toolbar message-input px-1 pl-2 mt-1">
<div className="input-options-toolbar">
<div className="richtext-toolbar input-toolbar fade_in">
<EditorToolbar
editorState={editorState}
onChange={editorState => {
setRichTextEditorState(editorState);
}}
/>
</div>
</div>
<div className="message-edition-buttons">
<Button
className="primary small-right-margin"
small
onClick={async () => {
save();
}}
value={Languages.t('scenes.apps.messages.message.save_button', [], 'Save')}
></Button>
<Button
className="secondary-light"
small
onClick={() => close()}
value={Languages.t('scenes.apps.messages.message.cancel_button', [], 'Cancel')}
></Button>
</div>
</div>
</div>
);
};
@@ -1,159 +0,0 @@
import React, { ReactNode, useContext, useState } from 'react';
import 'moment-timezone';
import Moment from 'react-moment';
import { Typography } from 'antd';
import classNames from 'classnames';
import { AlertTriangle } from 'react-feather';
import { useRecoilState } from 'recoil';
import User from 'app/features/users/services/current-user-service';
import MenusManager from 'app/components/menus/menus-manager.jsx';
import UserCard from 'app/components/user-card/user-card';
import Emojione from 'components/emojione/emojione';
import RouterServices from 'app/features/router/services/router-service';
import { NodeMessage } from 'app/features/messages/types/message';
import Languages from 'app/features/global/services/languages-service';
import Loader from 'components/loader/loader.jsx';
import { useMessage } from 'app/features/messages/hooks/use-message';
import { MessageContext } from '../message-with-replies';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
import { useUser } from 'app/features/users/hooks/use-user';
import { UserType } from 'app/features/users/types/user';
import { CompanyApplicationsStateFamily } from 'app/features/applications/state/company-applications';
import { useDirectChannels } from 'app/features/channels/hooks/use-direct-channels';
import { addUrlTryDesktop } from 'app/views/desktop-redirect';
type Props = {
linkToThread?: boolean;
};
const { Link } = Typography;
export default (props: Props) => {
const channelId = useRouterChannel();
const workspaceId = useRouterWorkspace();
const [messageLink, setMessageLink] = useState('');
const { openDiscussion } = useDirectChannels();
const context = useContext(MessageContext);
const { message } = useMessage(context);
const parentMessage: NodeMessage | null = useMessage({
...context,
id: message.thread_id,
}).message;
const user = useUser(message.user_id);
const companyApplications =
useRecoilState(CompanyApplicationsStateFamily(context.companyId))[0] || [];
const application = companyApplications.find(a => a.id === message.application_id);
const scrollToMessage = () => {
if (message.thread_id !== message.id) {
//TODO messageService.scrollTo({ id: message.thread_id });
}
};
const updateMessageLink = () => {
const url = RouterServices.generateRouteFromState({
workspaceId: workspaceId,
channelId: channelId,
messageId: message.thread_id || message.id,
});
setMessageLink(url);
};
let userNameRef: ReactNode = null;
const displayUserCard = () => {
if (user) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const box = (window as any).getBoundingClientRect(userNameRef);
MenusManager.openMenu(
[
{
type: 'react-element',
reactElement: () => (
<UserCard
user={user as UserType}
onClick={() => openDiscussion([(user as UserType).id || ''])}
/>
),
},
],
box,
null,
{ margin: 8 },
);
}
};
const icon = user?.status ? user.status.split(' ')[0] : undefined;
const status = user?.status ? user.status.split(' ').splice(1).join(' ') : undefined;
return (
<div
className={classNames('message-content-header-container', {
'message-not-sent': message._status === 'failed',
})}
>
<div className={'message-content-header '}>
<span
className="sender-name"
ref={node => (userNameRef = node as ReactNode)}
onClick={() => displayUserCard()}
>
{message.override?.title ||
(!!user && User.getFullName(user)) ||
(message.application_id && application?.identity?.name)}
</span>
{!!user && (
<div className="sender-status">
{!!icon && <Emojione size={12} type={icon} />} {!!status && status}
</div>
)}
{props.linkToThread && (
<span className="reply-text">
{Languages.t('scenes.apps.messages.input.replied_to')}
<Link onClick={() => scrollToMessage()}>{parentMessage?.text}</Link>
</span>
)}
{message.created_at && (
<a
className="date"
// eslint-disable-next-line react/jsx-no-target-blank
target="_BLANK"
href={(messageLink ? addUrlTryDesktop(messageLink) : '#')?.replace(/^javascript:/, '')}
onMouseEnter={() => updateMessageLink()}
rel="noreferrer"
>
<Moment
format={
new Date().getTime() - message.created_at > 12 * 60 * 60 * 1000 ? 'lll' : 'LT'
}
>
{message.created_at}
</Moment>
{message.edited?.edited_at && (
<span style={{ textTransform: 'lowercase' }}>
{' '}
- {Languages.t('scenes.apps.messages.input.edited', [], 'Edited')}
</span>
)}
</a>
)}
</div>
{message._status === 'sending' && (
<div className="loading">
<Loader color="#999" className="message_header_loader" />
</div>
)}
{message._status === 'failed' && (
<div className="alert_failed_icon">
<AlertTriangle size={16} />
</div>
)}
</div>
);
};
@@ -1,379 +0,0 @@
import React, { useContext } from 'react';
import 'moment-timezone';
import { MoreHorizontal, Smile, ArrowUpRight, Trash2, CornerDownLeft } from 'react-feather';
import EmojiPicker from 'components/emoji-picker/emoji-picker.jsx';
import Menu from 'components/menus/menu.jsx';
import MenusManager from 'app/components/menus/menus-manager.jsx';
import Languages from 'app/features/global/services/languages-service';
import AlertManager from 'app/features/global/services/alert-manager-service';
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
import WorkspaceUserRights from 'app/features/workspaces/services/workspace-user-rights-service';
import User from 'app/features/users/services/current-user-service';
import RouterServices from 'app/features/router/services/router-service';
import { Application } from 'app/features/applications/types/application';
import { getCompanyApplications } from 'app/features/applications/state/company-applications';
import Groups from 'app/deprecated/workspaces/groups.js';
import { MessageContext } from '../message-with-replies';
import { useMessage } from 'app/features/messages/hooks/use-message';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
import { useVisibleMessagesEditorLocation } from 'app/features/messages/hooks/use-message-editor';
import { ViewContext } from 'app/views/client/main-view/MainContent';
import SideViewService from 'app/features/router/services/side-view-service';
import Emojione from 'app/components/emojione/emojione';
import { useChannel } from 'app/features/channels/hooks/use-channel';
import { useEphemeralMessages } from 'app/features/messages/hooks/use-ephemeral-messages';
import { copyToClipboard } from 'app/features/global/utils/CopyClipboard';
import { addUrlTryDesktop } from 'app/views/desktop-redirect';
import { useMessageQuoteReply } from 'app/features/messages/hooks/use-message-quote-reply';
import { useMessageSeenBy } from 'app/features/messages/hooks/use-message-seen-by';
import { EmojiSuggestionType } from 'app/components/rich-text-editor/plugins/emoji';
import { MessagesListContext } from '../../messages-list';
import { useSetRecoilState } from 'recoil';
import { ForwardMessageAtom } from 'app/components/forward-message';
type Props = {
onOpen?: () => void;
onClose?: () => void;
threadHeader?: string;
};
export default (props: Props) => {
const channelId = useRouterChannel();
const workspaceId = useRouterWorkspace();
const context = useContext(MessageContext);
const listContext = useContext(MessagesListContext);
const { message, react, remove, pin } = useMessage(context);
const { channel } = useChannel(channelId);
const { message: thread } = useMessage({
companyId: channel.company_id || '',
threadId: message.thread_id,
id: message.thread_id,
});
const { remove: removeLastEphemeral } = useEphemeralMessages({
companyId: context.companyId,
channelId: channelId,
});
const location = `message-${message.id}`;
const subLocation = useContext(ViewContext).type;
const { set: setVisibleEditor } = useVisibleMessagesEditorLocation(location, subLocation);
const { set: setQuoteReply } = useMessageQuoteReply(channelId);
const setForwardMessage = useSetRecoilState(ForwardMessageAtom);
const { openSeenBy } = useMessageSeenBy();
const menu: Record<string, string | (() => void)>[] = [];
const triggerApp = (app: Application) => {
const data = {
channel: channel,
thread: thread.id && thread.id !== message.id ? thread : null,
message: message,
};
WorkspacesApps.notifyApp(app.id, 'action', 'action', data);
};
const onOpen = (evt: Event) => {
props.onOpen && props.onOpen();
if (evt) {
evt.preventDefault();
evt.stopPropagation();
}
};
if (message.ephemeral) {
menu.push({
type: 'menu',
icon: 'trash',
text: Languages.t('scenes.apps.messages.message.remove_button', [], 'Delete'),
className: 'error',
onClick: () => {
removeLastEphemeral();
},
});
} else {
if (channel && channel.visibility !== 'direct') {
menu.push({
type: 'menu',
icon: 'arrow-up-right',
text: Languages.t('scenes.apps.messages.message.show_button', [], 'Display'),
onClick: () => {
SideViewService.select(channel?.id || '', {
app: { identity: { code: 'messages' } } as Application,
context: {
viewType: 'channel_thread',
threadId: message.thread_id,
},
});
},
});
}
menu.push({
type: 'menu',
icon: 'link',
text: Languages.t('scenes.apps.messages.message.copy_link', [], 'Copy link to message'),
onClick: () => {
const url = addUrlTryDesktop(
`${document.location.origin}${RouterServices.generateRouteFromState({
workspaceId: workspaceId,
channelId: channelId,
threadId: message.thread_id,
messageId: message.id,
})}`,
);
copyToClipboard(url);
},
});
if (channel && channel.visibility === 'direct' && !listContext.readonly) {
menu.push({
type: 'menu',
icon: 'corner-down-left',
text: Languages.t('scenes.apps.messages.message.reply_button', [], 'Reply'),
onClick: () => {
setQuoteReply({ message: message.thread_id, channel: channelId });
},
});
}
menu.push({
type: 'menu',
icon: 'comment-info',
text: Languages.t('components.message_seen_by.btn', [], 'Information'),
onClick: () => {
openSeenBy({
message_id: message.id,
company_id: context.companyId,
thread_id: message.thread_id,
workspace_id: context.workspaceId,
});
},
});
if (!message.context?.disable_pin && !listContext.readonly)
menu.push({
type: 'menu',
icon: 'map-pin',
text: Languages.t(
!message.pinned_info?.pinned_at
? 'scenes.apps.messages.message.pin_button'
: 'scenes.apps.messages.message.unpin_button',
[],
'Pin message',
),
className: 'option_button',
onClick: () => {
pin(!message.pinned_info?.pinned_at);
},
});
menu.push({
type: 'menu',
icon: 'envelope-send',
text: Languages.t('scenes.apps.messages.message.forward'),
className: 'option_button',
onClick: () => {
setForwardMessage({
id: message.id,
thread_id: message.thread_id,
channel_id: channelId,
workspace_id: context.workspaceId,
company_id: context.companyId,
});
},
});
const apps =
getCompanyApplications(Groups.currentGroupId).filter(
(app: Application) => app.display?.twake?.chat?.actions?.length,
) || [];
if (apps.length > 0 && !listContext.readonly) {
menu.push({ type: 'separator' });
menu.push({
type: 'react-element',
reactElement: () => {
return apps.map((app: Application) => {
return (
<div
key={app.id}
className="menu"
onClick={() => {
triggerApp(app);
}}
>
<div className="text">
<div
className="menu-app-icon"
style={{ backgroundImage: 'url(' + app.identity?.icon + ')' }}
/>
{app?.display?.twake?.chat?.actions?.[0].description || app.identity?.name}
</div>
</div>
);
});
},
});
}
if (
(message.user_id === User.getCurrentUserId() ||
(message.application_id && message.context?.allow_delete === 'everyone') ||
(message.application_id &&
WorkspaceUserRights.hasWorkspacePrivilege() &&
message.context?.allow_delete === 'administrators')) &&
!listContext.readonly
) {
if (menu.length > 0 && (!message.application_id || !message?.stats?.replies)) {
menu.push({ type: 'separator' });
}
if (!message.application_id) {
menu.push({
type: 'menu',
icon: 'edit-alt',
text: Languages.t('scenes.apps.messages.message.modify_button', [], 'Edit'),
onClick: () => {
setVisibleEditor({ location, subLocation });
},
});
}
if (message?.stats?.replies <= 1) {
menu.push({
type: 'menu',
icon: 'trash-alt',
text: Languages.t('scenes.apps.messages.message.remove_button', [], 'Delete'),
className: 'error',
onClick: () => {
AlertManager.confirm(() => remove());
},
});
}
}
}
if (message.ephemeral) {
return (
<div className="message-options right">
<div
className="option"
onClick={() => {
removeLastEphemeral();
}}
>
<Trash2 size={16} />
</div>
</div>
);
}
const userReactions = (message.reactions || [])?.filter(r =>
r.users.includes(User.getCurrentUserId()),
);
return (
<div>
{/*!props.threadHeader && (
<div className="message-options drag" key="drag">
<div className="option js-drag-handler-message">
<DragIndicator style={{ width: '18px' }} />
</div>
</div>
)*/}
<div className="message-options right" key="options">
{!listContext.readonly && (
<>
{[':heart:', ':+1:', ':eyes:', ':tada:'].map(emoji => (
<>
<div
key={emoji}
className={
'option ' + (userReactions.map(m => m.name).includes(emoji) ? 'active' : '')
}
onClick={() => react([emoji], 'toggle')}
>
<Emojione type={emoji} />
</div>
<div className="separator"></div>
</>
))}
<Menu
className="option"
onOpen={(evt: Event) => onOpen(evt)}
menu={[
{
type: 'react-element',
className: 'menu-cancel-margin',
reactElement: () => {
return (
<EmojiPicker
selected={userReactions.map(e => e.name) || []}
onChange={(emoji: EmojiSuggestionType) => {
MenusManager.closeMenu();
props.onClose && props.onClose();
react([emoji.colons], 'toggle');
}}
/>
);
},
},
]}
position="top"
>
<Smile size={16} />
</Menu>
<div className="separator"></div>
</>
)}
{!props.threadHeader && channel && channel.visibility !== 'direct' && (
<>
<div
className="option"
onClick={() => {
SideViewService.select(channelId, {
app: { identity: { code: 'messages' } } as Application,
context: {
viewType: 'channel_thread',
threadId: message.thread_id || message.id,
},
});
}}
>
<ArrowUpRight size={16} />
</div>
<div className="separator"></div>
</>
)}
{channel && channel.visibility === 'direct' && !listContext.readonly && (
<>
<div
className="option"
onClick={() => {
setQuoteReply({ message: message.thread_id, channel: channelId });
}}
>
<CornerDownLeft size={16} />
</div>
<div className="separator"></div>
</>
)}
<Menu
className="option"
onOpen={(evt: Event) => onOpen(evt)}
onClose={() => props.onClose && props.onClose()}
menu={menu}
position={'left'}
>
<MoreHorizontal size={16} />
</Menu>
</div>
</div>
);
};
@@ -1,80 +0,0 @@
import React from 'react';
import { useUpload } from 'app/features/files/hooks/use-upload';
import FileComponent from 'app/components/file/file-component';
import { DataFileType } from 'app/components/file/types';
import { PendingFileRecoilType } from 'app/features/files/types/file';
import FileUploadAPIClient from 'app/features/files/api/file-upload-api-client';
import { MessageFileType } from 'app/features/messages/types/message';
type PropsType = {
file: MessageFileType;
onRemove?: () => void;
type: 'input' | 'message';
large?: boolean;
xlarge?: boolean;
};
export default ({ file, onRemove, type, large, xlarge }: PropsType) => {
const { getOnePendingFile } = useUpload();
const id =
(typeof file.metadata?.external_id === 'string'
? file.metadata?.external_id
: file.metadata?.external_id?.id) || '';
const companyId =
(typeof file.metadata?.external_id === 'string'
? file.company_id
: file.metadata?.external_id?.company_id) || '';
let status: PendingFileRecoilType['status'] | undefined = 'success';
let progress = 1;
let formatedFile: DataFileType = {
id: id,
company_id: companyId,
name: file.metadata?.name || '',
size: file.metadata?.size || 0,
thumbnail: FileUploadAPIClient.getFileThumbnailUrlFromMessageFile(file) || '',
thumbnail_ratio:
(file.metadata?.thumbnails?.[0]?.width || 1) / (file.metadata?.thumbnails?.[0]?.height || 1),
type: FileUploadAPIClient.mimeToType(file.metadata?.mime || ''),
};
if (file?.metadata?.source === 'pending') {
const pendingFile = getOnePendingFile(id);
if (!pendingFile) {
if (onRemove) onRemove();
return <></>;
}
formatedFile = {
id: pendingFile?.backendFile?.id || '',
company_id: pendingFile?.backendFile?.company_id || '',
name: pendingFile?.originalFile.name || '',
size: pendingFile?.originalFile.size || 0,
thumbnail: URL.createObjectURL(pendingFile.originalFile),
thumbnail_ratio: 1,
type: FileUploadAPIClient.mimeToType(pendingFile?.originalFile.type || ''),
};
status = pendingFile.status || undefined;
progress = pendingFile.progress;
}
return formatedFile ? (
<FileComponent
className="small-right-margin small-bottom-margin"
context={type}
source={file.metadata?.source || 'internal'}
externalId={file.metadata?.external_id}
file={formatedFile}
messageFile={file}
large={large}
xlarge={xlarge}
status={status}
progress={progress}
onRemove={onRemove}
/>
) : (
<></>
);
};
@@ -1,86 +0,0 @@
import React, { useContext } from 'react';
import User from 'app/features/users/services/current-user-service';
import 'moment-timezone';
import Emojione from 'components/emojione/emojione';
import { ReactionType } from 'app/features/messages/types/message';
import { Tooltip } from 'antd';
import classNames from 'classnames';
import { MessageContext } from '../message-with-replies';
import { useMessage } from 'app/features/messages/hooks/use-message';
import { getUser } from 'app/features/users/hooks/use-user-list';
import { MessagesListContext } from '../../messages-list';
export default () => {
const context = useContext(MessageContext);
const { message, react } = useMessage(context);
if (
!(!message?.context?.disable_reactions && message?.reactions && message?.reactions.length > 0)
)
return <></>;
return (
<div className="reactions">
{(message?.reactions || [])
.map(r => r) //To avoid modifing the original one with sort
.sort((a, b) => b.count || 0 - a.count)
.map((reaction, index) => (
<Reaction reaction={reaction} react={react} key={index} />
))}
</div>
);
};
const Reaction = ({
reaction,
react,
}: {
reaction: ReactionType;
react: (emojis: string[], mode?: 'add' | 'toggle' | 'remove' | 'replace') => Promise<void>;
}): JSX.Element => {
const listContext = useContext(MessagesListContext);
const noReactions: boolean = (reaction.count || 0) <= 0;
const users: ReactionType['users'] = reaction.users || [];
if (noReactions) return <></>;
const reactionClassName = classNames('reaction', {
is_selected: reaction.users.includes(User.getCurrentUserId()),
});
return (
<Tooltip
className="reaction_container"
placement="top"
title={<ReactionTooltip users={users} />}
>
<div
className={reactionClassName}
onClick={() => {
if (!listContext.readonly) react([reaction.name], 'toggle');
}}
>
<Emojione type={reaction.name} />
{reaction.count}
</div>
</Tooltip>
);
};
const ReactionTooltip = ({ users }: { users: ReactionType['users'] }): JSX.Element => (
<>
{users.map(id => {
const user = getUser(id);
if (!user) return <></>;
const name = User.getFullName(user);
return (
<div key={id} style={{ whiteSpace: 'nowrap' }}>
{name}
</div>
);
})}
</>
);
@@ -1,69 +0,0 @@
import React, { useContext } from 'react';
import { CornerDownRight } from 'react-feather';
import ThreadSection from '../../parts/thread-section';
import Languages from 'app/features/global/services/languages-service';
import { useMessage } from 'app/features/messages/hooks/use-message';
import { MessageContext } from '../message-with-replies';
import { useVisibleMessagesEditorLocation } from 'app/features/messages/hooks/use-message-editor';
import { ViewContext } from 'app/views/client/main-view/MainContent';
import Input from '../../input/input';
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
import { getChannel, useIsChannelMember } from 'app/features/channels/hooks/use-channel';
export default () => {
const context = useContext(MessageContext);
const channelId = useRouterChannel();
const { message } = useMessage(context);
const channel = getChannel(channelId);
const location = `thread-${message.thread_id}`;
const subLocation = useContext(ViewContext).type;
const { active: editorIsActive, set: setVisibleEditor } = useVisibleMessagesEditorLocation(
location,
subLocation,
);
const isChannelMember = useIsChannelMember(channelId);
if (!isChannelMember) {
return <></>;
}
if (message.subtype === 'deleted' || message.thread_id != message.id) {
return <></>;
}
if (channel?.visibility === 'direct') {
return <></>;
}
if (editorIsActive) {
return (
<ThreadSection small alinea>
<div className="message-content">
<Input threadId={message?.id || ''} channelId={channelId} />
</div>
</ThreadSection>
);
}
return (
<ThreadSection compact>
<div className="message-content">
<span
className="link"
onClick={() =>
setVisibleEditor({
location,
subLocation,
})
}
>
<CornerDownRight size={14} className="inline" />{' '}
{Languages.t('scenes.apps.messages.message.reply_button')}
</span>
</div>
</ThreadSection>
);
};
@@ -1,26 +0,0 @@
import { useMessage } from 'app/features/messages/hooks/use-message';
import { useMessageEditor } from 'app/features/messages/hooks/use-message-editor';
import React, { useContext } from 'react';
import Languages from 'app/features/global/services/languages-service';
import { MessageContext } from '../message-with-replies';
export default () => {
const context = useContext(MessageContext);
const { message } = useMessage(context);
const { retry, cancel } = useMessageEditor(context);
return (
<div>
<span
className="link"
style={{ fontWeight: 500, marginRight: 8 }}
onClick={() => retry(message)}
>
{Languages.t('general.retry')}
</span>
<span className="link red" style={{ fontWeight: 500 }} onClick={() => cancel(message)}>
{Languages.t('general.remove')}
</span>
</div>
);
};
@@ -1,19 +0,0 @@
import { Row } from 'antd';
import { MessageFileType } from 'app/features/messages/types/message';
import PossiblyPendingAttachment from '../PossiblyPendingAttachment';
export const ForwardedFiles = (props: { files: MessageFileType[] }) => {
return (
<Row justify="start" align="middle" className="small-top-margin" wrap>
{props.files
.filter(f => f.metadata)
.map(file => (
<PossiblyPendingAttachment
key={file.metadata?.external_id || file.id}
type={'message'}
file={file}
/>
))}
</Row>
);
};
@@ -1,79 +0,0 @@
import { Info } from 'app/atoms/text';
import Languages from 'app/features/global/services/languages-service';
import { XIcon } from '@atoms/icons-agnostic';
import { MessageWithReplies, NodeMessage } from 'app/features/messages/types/message';
import { UserType } from 'app/features/users/types/user';
import { MessageBlockContent } from '../MessageContent';
import { ForwardedFiles } from './files';
type PropsType = {
message: NodeMessage & {
users?: UserType[] | undefined;
company_id: string;
workspace_id: string;
channel_id: string;
thread_id: string;
id: string;
};
author: string;
closable?: boolean;
deleted?: boolean;
goToMessage?: () => void;
onClose?: () => void;
className?: string;
onAction: (type: string, id: string, context: unknown, passives: unknown) => void;
};
export default ({
author,
message,
closable = true,
onClose,
deleted = false,
goToMessage,
className = '',
onAction,
}: PropsType) => {
const clickable = !closable;
return (
<div
className={
'flex flex-row pl-3 pr-2 relative ' +
(className || '') +
' ' +
(clickable ? 'cursor-pointer hover:bg-blue-100 hover:bg-opacity-50' : '')
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onClick={!closable ? goToMessage : () => {}}
>
<div className="w-[3px] rounded-full bg-blue-500 absolute left-0 top-0 h-full"></div>
<div className="grow w-full max-w-full">
<h3 className="mt-0.5 -mb-0.5 text-xs text-blue-500">{author}</h3>
<div className="">
{deleted ? (
<Info className="italic text-xs">{Languages.t('molecules.message_quote.deleted')}</Info>
) : (
<MessageBlockContent
deleted={false}
message={message as unknown as MessageWithReplies}
onAction={onAction}
suffix={<>{message.files && <ForwardedFiles files={message.files} />}</>}
/>
)}
</div>
</div>
{closable && onClose && (
<div className="flex-none w-2">
<XIcon
className="cursor-pointer mt-1 float-right w-3 h-3 text-blue-500 hover:text-blue-600"
onClick={e => {
e.stopPropagation();
onClose();
}}
/>
</div>
)}
</div>
);
};
@@ -1,47 +0,0 @@
import React from 'react';
import { useThreadMessages } from 'app/features/messages/hooks/use-thread-messages';
import Message from './message';
import { MessageContext } from './message-with-replies';
import ThreadSection from '../parts/thread-section';
import { useMessage } from 'app/features/messages/hooks/use-message';
import Numbers from 'app/features/global/utils/Numbers';
type Props = {
companyId: string;
workspaceId: string;
channelId: string;
threadId: string;
firstMessageId: string;
};
export default ({ threadId, companyId, channelId, workspaceId, firstMessageId }: Props) => {
const { messages } = useThreadMessages({ companyId, threadId });
return (
<>
{messages
.filter(m => !firstMessageId || Numbers.compareTimeuuid(m.id, firstMessageId) >= 0)
.filter(m => m.threadId !== m.id)
.map(m => {
return (
<MessageContext.Provider
key={m.id}
value={{ ...m, id: m.id || '', channelId, workspaceId, companyId }}
>
<Reply />
</MessageContext.Provider>
);
})}
</>
);
};
const Reply = () => {
const context = React.useContext(MessageContext);
const { message } = useMessage(context);
return (
<ThreadSection withAvatar alinea small pinned={!!message.pinned_info?.pinned_by}>
<Message />
</ThreadSection>
);
};
@@ -1,26 +0,0 @@
import React from 'react';
import Moment from 'react-moment';
import 'moment-timezone';
import './message.scss';
type Props = {
date: number;
};
export default React.memo((props: Props) => {
return (
<div className="time_separator">
<div className="message_timeline">
<div className="time_container">
<div className="time">
{new Date().getTime() - props.date > 24 * 60 * 60 * 1000 ? (
<Moment date={props.date || 0} format="LL"></Moment>
) : (
<Moment date={props.date || 0} fromNow></Moment>
)}
</div>
</div>
</div>
</div>
);
});
@@ -1,272 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { useChannelMessages } from 'app/features/messages/hooks/use-channel-messages';
import ListBuilder, { ListBuilderHandle } from './list-builder';
import TimeSeparator from './message/time-separator';
import MessageWithReplies from './message/message-with-replies';
import FirstMessage from './message/parts/FirstMessage/FirstMessage';
import LockedHistoryBanner from 'app/components/locked-features-components/locked-history-banner/locked-history-banner';
import MessageHistoryService from 'app/features/messages/services/message-history-service';
import { useCurrentCompany } from 'app/features/companies/hooks/use-companies';
import ChannelAPIClient from 'app/features/channels/api/channel-api-client';
import {
MessagesAndComponentsType,
withNonMessagesComponents,
} from '../../../features/messages/hooks/with-non-messages-components';
import { useHighlightMessage } from 'app/features/messages/hooks/use-highlight-message';
import SideViewService from 'app/features/router/services/side-view-service';
import { Application } from 'app/features/applications/types/application';
import GoToBottom from './parts/go-to-bottom';
import { MessagesPlaceholder } from './placeholder';
import { cleanFrontMessagesFromListOfMessages } from 'app/features/messages/hooks/use-message-editor';
import { getMessage } from 'app/features/messages/hooks/use-message';
import messageApiClient from 'app/features/messages/api/message-api-client';
import User from 'app/features/users/services/current-user-service';
import { useChannelMembersReadSections } from 'app/features/channel-members/hooks/use-channel-members-read-sections';
import { delayRequest } from 'app/features/global/utils/managedSearchRequest';
import { useRefreshPublicOrPrivateChannels } from 'app/features/channels/hooks/use-public-or-private-channels';
import { usePageVisibility } from "react-page-visibility";
type Props = {
companyId: string;
workspaceId?: string;
channelId?: string;
threadId?: string;
readonly?: boolean;
};
export const MessagesListContext = React.createContext({
hideReplies: false,
withBlock: false,
readonly: false,
});
export default ({ channelId, companyId, workspaceId, readonly }: Props) => {
const listBuilderRef = useRef<ListBuilderHandle>(null);
const [atBottom, setAtBottom] = useState(true);
const { seen, refresh: loadReadSections } = useChannelMembersReadSections(
companyId,
workspaceId || 'direct',
channelId || '',
);
const {
messages: _messages,
loadMore,
window,
jumpTo,
convertToKeys,
} = useChannelMessages({
companyId,
workspaceId: workspaceId || '',
channelId: channelId || '',
});
let messages = _messages;
const { company } = useCurrentCompany();
const shouldLimit = MessageHistoryService.shouldLimitMessages(
company,
messages[0]?.id || '',
messages.length,
);
messages = withNonMessagesComponents(messages, window.reachedStart, shouldLimit);
const loadMoreMessages = async (
direction: 'history' | 'future',
limit?: number,
offsetItem?: MessagesAndComponentsType,
) => {
const messages = await loadMore(direction, limit, offsetItem?.threadId);
return withNonMessagesComponents(
convertToKeys(company.id, messages),
window.reachedStart && direction === 'history',
shouldLimit && direction === 'history',
);
};
const { refresh: refreshChannels } = useRefreshPublicOrPrivateChannels();
const isPageVisible = usePageVisibility();
useEffect(() => {
if (messages.length === 0) loadMore('history');
}, []);
useEffect(() => {
if (window.reachedEnd && atBottom && messages.length > 0 && isPageVisible) {
const seenMessages = messages.filter(message => {
const m = getMessage(message.id || message.threadId);
const currentUserId = User.getCurrentUserId();
if (m.user_id === currentUserId) {
return false;
}
return m.status === 'delivered' || (m.status === 'read' && !seen(currentUserId, m.id));
});
if (seenMessages.length > 0) {
delayRequest('message-list-read-request', async () => {
await messageApiClient.read(
companyId,
channelId || '',
workspaceId || 'direct',
seenMessages,
);
await loadReadSections();
});
}
}
}, [messages, messages.length, window.reachedEnd, isPageVisible]);
const { highlight, cancelHighlight, reachedHighlight } = useHighlightMessage();
useEffect(() => {
//Manage scroll to highlight
if (listBuilderRef.current && highlight && !highlight.reachedThread) {
if (highlight.answerId) {
SideViewService.select(channelId || '', {
app: { identity: { code: 'messages' } } as Application,
context: {
viewType: 'channel_thread',
threadId: highlight.threadId,
},
});
}
// Find the correct index of required message
const index = messages.findIndex(m => m.id === highlight.threadId);
if (index < 0) {
// Load the right portion of messages
jumpTo(highlight.threadId);
return;
}
setTimeout(() => {
if (listBuilderRef.current)
listBuilderRef.current.scrollToIndex({
align: 'start',
index: index,
});
setTimeout(() => {
//Need to wait a bit for the scroll to ends
reachedHighlight('thread');
}, 1000);
}, 1000);
}
}, [highlight, messages.length]);
useEffect(() => {
if (messages.length) {
ChannelAPIClient.read(companyId, workspaceId || '', channelId || '', { status: true }).then(
() => {
refreshChannels();
},
);
}
}, [messages.length > 0]);
useEffect(() => {
if (messages.length) {
delayRequest('message-list-load-read-sections', async () => {
loadReadSections();
});
}
}, [companyId, workspaceId, channelId, messages.length > 0]);
const row = React.useMemo(
() => (_: number, m: MessagesAndComponentsType) => {
if (m.type === 'timeseparator') {
return (
<div key={m.type + m.threadId}>
<TimeSeparator date={m.date || 0} />
</div>
);
}
if (m.type === 'header') {
return (
<div key={m.type + m.threadId}>
<FirstMessage />
</div>
);
}
if (m.type === 'locked') {
return (
<div key={m.type + m.threadId}>
<LockedHistoryBanner />
</div>
);
}
return (
<div key={m.type + m.threadId}>
<MessageWithReplies
companyId={m.companyId}
workspaceId={workspaceId || ''}
channelId={channelId || ''}
threadId={m.threadId}
/>
</div>
);
},
[],
);
const jumpToBottom = () => {
if (window.reachedEnd) {
listBuilderRef.current?.scrollTo({ top: 10000000, behavior: 'smooth' });
} else {
// Load the right portion of messages
jumpTo('');
}
};
//This hide virtuoso but it start to work in backend
const virtuosoLoading = highlight && !highlight?.reachedThread;
return (
<MessagesListContext.Provider
value={{ hideReplies: false, withBlock: true, readonly: !!readonly }}
>
{(!window.loaded || virtuosoLoading) && <MessagesPlaceholder />}
{!window.loaded && <div style={{ flex: 1 }}></div>}
{window.loaded && (
<ListBuilder
ref={listBuilderRef}
style={virtuosoLoading ? { opacity: 0 } : {}}
onScroll={(e: React.UIEvent<'div', UIEvent>) => {
const scrollBottom =
(e.target as HTMLElement).scrollHeight -
(e.target as HTMLElement).scrollTop -
(e.target as HTMLElement).clientHeight;
const closeToBottom = scrollBottom < 500;
if (closeToBottom !== atBottom) setAtBottom(closeToBottom);
cancelHighlight();
}}
items={messages}
filterOnAppend={messages => {
return cleanFrontMessagesFromListOfMessages(messages);
}}
itemId={m => m.type + (getMessage(m.id)?.context?._front_id || m.threadId) + m.id}
emptyListComponent={<FirstMessage />}
itemContent={row}
followOutput={!!window.reachedEnd && 'smooth'}
loadMore={loadMoreMessages}
atBottomStateChange={async (atBottom: boolean) => {
if (atBottom && window.reachedEnd) {
setAtBottom(true);
await ChannelAPIClient.read(companyId, workspaceId || '', channelId || '', {
status: true,
});
refreshChannels();
}
}}
/>
)}
{!(atBottom && window.reachedEnd) && window.loaded && messages.length > 0 && (
<GoToBottom
onClick={() => {
jumpToBottom();
}}
/>
)}
</MessagesListContext.Provider>
);
};
@@ -1,281 +0,0 @@
.messages-view {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
.loading-blink {
@keyframes loading-blink {
50% {
opacity: 0.5;
}
20%,
80% {
opacity: 1;
}
}
animation: loading-blink 1s infinite;
}
.go-to-bottom {
position: relative;
padding: 0 8px;
bottom: 46px;
margin-bottom: -32px;
height: 28px;
width: fit-content;
z-index: 4;
font-size: 14px;
color: var(--white);
align-items: center;
align-self: center;
display: flex;
flex-direction: row;
justify-content: center;
background-color: var(--primary);
border-radius: 14px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: opacity 0.2s;
opacity: 1;
}
.user-writing-info-message-view {
position: relative;
margin: 0 auto;
min-width: 0;
max-width: 1000px;
width: calc(100% - 32px);
position: relative;
.user-writing-info-message-view-inner {
position: absolute;
display: flex;
justify-content: flex-start;
align-items: center;
color: var(--grey-dark);
bottom: -8px;
left: 0px;
font-size: 11px;
pointer-events: none;
background: #ffffff88;
border-radius: 4px;
}
}
.loading-full {
position: absolute;
width: 100%;
height: 100%;
background: #fff;
z-index: 1;
.loading {
position: absolute;
margin: auto;
width: 50px;
height: 50px;
right: 0px;
bottom: 0px;
top: 0px;
left: 0px;
}
}
.messages-list {
display: flex;
flex-direction: column;
.header {
display: flex;
justify-content: center;
.loader {
margin: 10px;
}
}
.footer {
display: flex;
justify-content: center;
.loader {
margin: 10px;
}
}
&:not(.scrolled-up) {
.go-to-bottom {
pointer-events: none;
opacity: 0;
}
}
}
.messages-scroller-parent {
flex: 1;
min-height: 0;
position: relative;
&:before {
content: '';
position: absolute;
width: 100%;
height: 0px;
background: var(--white);
border-top: 1px solid transparent;
z-index: 4;
bottom: 0;
pointer-events: none;
transition: height 0.2s;
}
&:after {
content: '';
position: absolute;
width: 100%;
height: 100%;
box-shadow: 0 -16px 8px -8px rgba(255, 255, 255, 0) inset;
z-index: 4;
bottom: 8px;
pointer-events: none;
transition: box-shadow 0.2s, bottom 0.2s;
}
&.scrolled-up-100 {
&:after,
&:before {
opacity: 1;
}
&:before {
border-top: 1px solid var(--grey-background);
height: 16px;
}
&:after {
bottom: 16px;
box-shadow: 0 -32px 32px -32px rgba(0, 0, 0, 0.1) inset;
}
}
&.scrolled-up {
&:after {
content: '';
position: absolute;
width: 100%;
height: 100%;
box-shadow: 0 -16px 8px -8px rgba(255, 255, 255, 1) inset;
z-index: 4;
bottom: 0px;
pointer-events: none;
transition: box-shadow 0.2s, bottom 0.2s;
}
}
.message_header {
margin-top: 16px;
}
}
.thread .message_timeline {
text-align: left;
background: var(--white);
margin: 0;
width: 100%;
.time_container {
margin-left: 32px;
top: -12px;
}
&::before {
display: none;
}
}
.time_separator {
height: 24px;
display: flex;
align-self: center;
justify-content: center;
align-items: baseline;
}
.message_timeline {
text-align: center;
color: var(--grey-dark);
font-size: 12px;
position: relative;
height: 22px;
margin: 0;
padding-bottom: 0px;
padding-top: 0px;
min-width: 0;
max-width: 1000px;
width: calc(100% - 32px);
&::before {
content: '';
z-index: 0;
width: 100%;
border-bottom: 1px solid var(--grey-background);
position: relative;
left: 0;
margin: 0;
display: inline-block;
}
&.new_messages {
&::before {
border-color: var(--primary);
}
.time_container {
.time {
font-weight: 700;
background: var(--white);
color: var(--primary);
border-radius: 8px;
}
}
}
.time_container {
display: inline-block;
padding: 0px 11px;
background: var(--white);
top: -15px;
position: relative;
border-radius: 8px;
.time {
color: var(--grey-dark);
width: auto;
z-index: 1;
height: 22px;
line-height: 22px;
margin: auto;
font-size: 12px;
border-radius: 12px;
padding: 0 4px;
}
}
}
.writing_message {
font-size: 12px;
color: var(--grey-dark);
font-size: 12px;
padding-bottom: 8px;
position: absolute;
bottom: 0;
left: 0;
margin: auto;
right: 0;
width: calc(100% - 48px);
background: #fff;
transition: opacity 0.2s;
min-width: 0;
max-width: 1000px;
width: calc(100% - 32px);
&:hover {
opacity: 0.2;
}
}
}
@@ -1,148 +0,0 @@
import React, { Suspense, useState } from 'react';
import { ChannelType } from 'app/features/channels/types/channel';
import { ViewConfiguration } from 'app/features/router/services/app-view-service';
import NewThread from './input/new-thread';
import MessagesList from './messages-list';
import ThreadMessagesList from './thread-messages-list';
import IsWriting from './input/parts/IsWriting';
import {
useChannel,
useIsChannelMember,
useIsReadOnlyChannel,
} from 'app/features/channels/hooks/use-channel';
import { Button } from 'app/atoms/button/button';
import ChannelsReachableAPIClient from 'app/features/channels/api/channels-reachable-api-client';
import UserService from 'app/features/users/services/current-user-service';
import * as Text from '@atoms/text';
import Languages from 'app/features/global/services/languages-service';
import MessageSeenBy from 'app/components/message-seen-by/message-seen-by';
import { useUser } from 'app/features/users/hooks/use-user';
import { UserType } from 'app/features/users/types/user';
import { ForwardMessageModal } from 'app/components/forward-message';
import AccessRightsService from 'app/features/workspace-members/services/workspace-members-access-rights-service';
type Props = {
channel: ChannelType;
tab?: unknown;
options: ViewConfiguration;
};
export default (props: Props) => {
if (!props.channel) {
return <></>;
}
const companyId = props.channel.company_id || '';
const workspaceId = props.channel.workspace_id || '';
const channelId = props.channel.id || '';
const isDirectChannel = props.channel.visibility !== 'direct';
const threadId = props.options.context?.threadId || '';
const isChannelMember = useIsChannelMember(channelId);
const currentUser = UserService.getCurrentUser();
let userIsNotInCompany = false;
const otherChannelsMembersThanMe =
(props.channel.members || []).filter(id => id !== currentUser?.id) || [];
const otherUserThatIsNotMe = useUser(otherChannelsMembersThanMe[0] || '');
if (
otherUserThatIsNotMe &&
otherChannelsMembersThanMe.length === 1 &&
!UserService.isInCompany(otherUserThatIsNotMe as UserType, companyId)
) {
userIsNotInCompany = true;
}
const channelIsRestricted =
useIsReadOnlyChannel(channelId) &&
currentUser.id !== props.channel.owner &&
!AccessRightsService.hasLevel(workspaceId, 'moderator');
return (
<div className="messages-view">
<ForwardMessageModal />
<Suspense fallback={<></>}>
{!threadId ? (
<MessagesList
key={channelId + threadId}
companyId={companyId}
workspaceId={workspaceId}
channelId={channelId}
threadId={threadId}
readonly={userIsNotInCompany}
/>
) : (
<ThreadMessagesList
key={channelId + threadId}
companyId={companyId}
workspaceId={workspaceId}
channelId={channelId}
threadId={threadId}
readonly={userIsNotInCompany}
/>
)}{' '}
<MessageSeenBy />
</Suspense>
<IsWriting channelId={channelId} threadId={threadId} />
{isChannelMember && channelIsRestricted && <ChannelIsRestricted />}
{isChannelMember && !channelIsRestricted && !userIsNotInCompany && (
<NewThread
collectionKey=""
useButton={isDirectChannel && !threadId}
channelId={channelId}
threadId={threadId}
/>
)}
{isChannelMember && userIsNotInCompany && <UserIsNotInCompany />}
{!isChannelMember && <JoinChanneBlock channelId={channelId} />}
</div>
);
};
const JoinChanneBlock = ({ channelId }: { channelId: string }) => {
const [loading, setLoading] = useState(false);
const { channel, refresh } = useChannel(channelId);
if (!channel) {
return <></>;
}
return (
<div className="border-t border-zinc-200 dark:border-zinc-700 p-8 text-center">
<Button
loading={loading}
onClick={async () => {
setLoading(true);
await ChannelsReachableAPIClient.inviteUser(
channel.company_id || '',
channel.workspace_id || '',
channel.id || '',
UserService.getCurrentUserId(),
);
refresh();
setLoading(false);
}}
className="mb-4"
>
{Languages.t('scenes.client.join_public_channel')}
</Button>
<br />
<Text.Info>{Languages.t('scenes.client.join_public_channel.info')}</Text.Info>
</div>
);
};
const UserIsNotInCompany = () => {
return (
<div className="border-t border-zinc-200 dark:border-zinc-700 p-8 text-center">
<Text.Info>{Languages.t('scenes.apps.messages.message.user_deactivated')}</Text.Info>
</div>
);
};
const ChannelIsRestricted = () => {
return (
<div className="border-t border-zinc-200 dark:border-zinc-700 p-8 text-center">
<Text.Info>{Languages.t('scenes.client.readonly.info')}</Text.Info>
</div>
);
};
@@ -1,21 +0,0 @@
import React from 'react';
import Languages from 'app/features/global/services/languages-service';
import { ArrowDown } from 'react-feather';
type Props = {
onClick: () => void;
newMessages?: number;
};
export default (props: Props) => {
const msg = props.newMessages
? Languages.t('scenes.apps.messages.messageslist.go_last_message_button.new_messages')
: Languages.t('scenes.apps.messages.messageslist.go_last_message_button');
return (
<div className={'go-to-bottom'} key="go-to-bottom" onClick={() => props.onClick()}>
<ArrowDown size={16} />
<span>{msg}</span>
</div>
);
};
@@ -1,48 +0,0 @@
import React, { useContext } from 'react';
import 'moment-timezone';
import User from 'app/features/users/services/current-user-service';
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
import Icon from 'app/components/icon/icon.jsx';
import './threads.scss';
import { MessageContext } from '../message/message-with-replies';
import { useMessage } from 'app/features/messages/hooks/use-message';
import { useRecoilState } from 'recoil';
import { CompanyApplicationsStateFamily } from 'app/features/applications/state/company-applications';
import { useUser } from 'app/features/users/hooks/use-user';
type Props = {
small?: boolean;
};
export default (props: Props) => {
const context = useContext(MessageContext);
const { message } = useMessage(context);
const user =
useUser(message.user_id) || (message.users || []).find(u => u.id === message.user_id);
const companyApplications =
useRecoilState(CompanyApplicationsStateFamily(context.companyId))[0] || [];
const application = companyApplications.find(a => a.id === message.application_id);
return (
<>
{!!user && !message.application_id && (
<div
className="sender-head"
style={{
backgroundImage: "url('" + User.getThumbnail(user) + "')",
}}
></div>
)}
{!!message.application_id && (
<Icon
className="no-margin-left"
style={{ fontSize: props.small ? '16px' : '24px' }}
type={
message.override?.picture || WorkspacesApps.getAppIcon(application) || 'puzzle-piece'
}
/>
)}
</>
);
};
@@ -1,49 +0,0 @@
import React, { ReactNode, Suspense } from 'react';
import 'moment-timezone';
import './threads.scss';
import ThreadAvatar from './thread-avatar';
type Props = {
compact?: boolean;
gradient?: boolean;
small?: boolean;
head?: boolean;
alinea?: boolean;
children?: ReactNode;
noSenderSpace?: boolean;
withAvatar?: boolean;
pinned?: boolean;
className?: string;
onClick?: () => void;
};
export default (props: Props) => {
return (
<div
onClick={props.onClick}
className={
'thread-section ' +
(props.compact ? 'compact ' : '') +
(props.gradient ? 'gradient ' : '') +
(props.small ? 'small-section ' : '') +
(props.alinea ? 'alinea ' : '') +
(props.head ? 'head-section ' : '') +
(props.pinned ? 'pinned-section ' : '') +
(props.className ? props.className + ' ' : '')
}
>
<div className="message">
{!props.noSenderSpace && (
<div className="sender-space">
{props.withAvatar && (
<Suspense fallback={''}>
<ThreadAvatar small={props.small} />
</Suspense>
)}
</div>
)}
{props.children}
</div>
</div>
);
};
@@ -1,36 +0,0 @@
import React, { ReactNode } from 'react';
import './threads.scss';
import classNames from 'classnames';
export const LoadingThread = (
<div className="thread-section">
<div className="message">
<div className="sender-space">
<div className="sender-head" />
</div>
<div className="message-content">
<div className="message-content-header">
<span className="sender-name"></span>
</div>
<div className="content-parent"></div>
<div className="content-parent" style={{ width: '40%' }}></div>
</div>
</div>
</div>
);
type Props = {
hidden?: boolean;
highlighted?: boolean;
withBlock?: boolean;
className?: string;
children: ReactNode;
};
export default ({ hidden, highlighted, className, withBlock, children }: Props) => (
<div className={classNames('thread-container', { hidden, highlighted }, className)}>
<div className="thread-centerer">
<div className={classNames('thread', { 'with-block': withBlock })}>{children}</div>
</div>
</div>
);
@@ -1,207 +0,0 @@
.dragged {
.draggable_clone > .thread,
.draggable_clone > .thread-section {
opacity: 1;
border: 1px solid var(--grey-background);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
border-radius: var(--border-radius-base);
width: 100%;
font-size: 14px;
.message {
padding: 12px;
}
.sender-space {
width: 28px;
}
}
.draggable_clone > .thread-section,
.draggable_clone > .thread .head-section {
.message-options.drag {
display: flex;
opacity: 1;
box-shadow: none;
}
}
}
.drag_message {
.thread-container.has-droppable {
.thread:hover:not(.dragging_opacity) {
border: 1px solid var(--primary) !important;
box-shadow: 0 2px 4px 0 var(--primary-background) !important;
overflow: hidden;
}
}
.message-options {
display: none;
}
}
.thread-container,
.dragged {
.thread.dragging_opacity > *,
.thread-section.dragging_opacity > * {
opacity: 0.2;
}
padding-top: 8px;
padding-bottom: 8px;
&.highlighted .thread,
&.highlighted .thread.with-block {
border-radius: var(--border-radius-base);
box-shadow: 0 0 0 4px #fff892;
.thread-section .message {
border-radius: var(--border-radius-base);
}
}
opacity: 1;
transition: background-color 0.2s, box-shadow 0.2s;
min-width: 0;
&.hidden {
opacity: 0;
}
.thread-centerer {
margin: 0 auto;
min-width: 0;
max-width: 1000px;
width: calc(100% - 32px);
position: relative;
}
.thread {
font-size: 14px;
margin-bottom: 8px;
&.with-block {
border: 1px solid var(--grey-background);
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
border-radius: var(--border-radius-base);
// in infinite list, margin are bad to compute render size because they are hard to measure
// using padding solves this issue anbd allow to scroll correctly
// cf https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around
margin-bottom: 0px;
background: var(--white);
.message {
padding: 12px;
}
}
}
.thread-section {
position: relative;
&.gradient {
margin-bottom: -40px;
height: 64px;
background: linear-gradient(180deg, #fff 40%, #ffffff00);
z-index: 1;
position: relative;
border: none;
.message {
background: none;
}
}
&:not(:last-child):not(.gradient) {
border-bottom: 1px solid var(--grey-background);
}
&.compact {
.message {
padding: 4px 12px !important;
}
}
.message {
border-left: 4px solid transparent;
background-color: var(--white);
padding: 4px;
padding-left: 8px;
display: flex;
border-radius: var(--border-radius-base) 0 0 0;
}
&.small-section {
.message .sender-space {
.sender-head,
.app_icon {
width: 16px;
height: 16px;
margin-top: 2px;
.online_user_status {
position: relative;
bottom: -12px;
right: -12px;
}
}
}
}
&.alinea {
.message {
padding-left: 32px;
}
}
&.pinned-section {
.message {
border-left: 4px solid var(--primary);
box-shadow: -8px 0px 12px -8px var(--primary-background);
overflow: hidden;
}
}
}
&.loading {
.sender-head,
.app_icon,
.sender-name,
.content-parent {
animation-duration: 10s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: placeHolderShimmer;
animation-timing-function: linear;
background-repeat: repeat !important;
background-image: linear-gradient(
to right,
var(--grey-light) 0px,
#f2f2f2 120px,
var(--grey-light) 234px
) !important;
}
.sender-name {
width: 30%;
max-width: 200px;
}
.content-parent {
width: 100%;
}
.sender-name,
.content-parent {
height: 14px;
margin: 4px 0;
display: inline-block;
border-radius: var(--border-radius-base);
}
div,
span {
font-size: 0px !important;
}
.thread-section {
border: 0px !important;
}
.thread-section.compact {
display: none;
}
}
}
@@ -1,17 +0,0 @@
import { Skeleton } from 'antd';
import React from 'react';
export const MessagesPlaceholder = () => {
return (
<div
className="messages-placeholder thread-container loading-blink"
style={{ flex: 1, overflow: 'hidden', bottom: '0px', position: 'absolute', width: '100%' }}
>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => (
<div key={i} className="thread-centerer">
<Skeleton avatar></Skeleton>
</div>
))}{' '}
</div>
);
};
@@ -1,166 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import ListBuilder, { ListBuilderHandle } from './list-builder';
import TimeSeparator from './message/time-separator';
import MessageWithReplies from './message/message-with-replies';
import FirstThreadMessage from './message/parts/FirstMessage/FirstThreadMessage';
import { MessagesListContext } from './messages-list';
import { useThreadMessages } from 'app/features/messages/hooks/use-thread-messages';
import {
MessagesAndComponentsType,
withNonMessagesComponents,
} from '../../../features/messages/hooks/with-non-messages-components';
import { useHighlightMessage } from 'app/features/messages/hooks/use-highlight-message';
import GoToBottom from './parts/go-to-bottom';
import { MessagesPlaceholder } from './placeholder';
import { cleanFrontMessagesFromListOfMessages } from 'app/features/messages/hooks/use-message-editor';
import { getMessage } from 'app/features/messages/hooks/use-message';
type Props = {
companyId: string;
workspaceId: string;
channelId: string;
threadId: string;
readonly?: boolean;
};
export default ({ companyId, workspaceId, channelId, threadId, readonly }: Props) => {
const listBuilderRef = useRef<ListBuilderHandle>(null);
const [atBottom, setAtBottom] = useState(true);
const { highlight, cancelHighlight, reachedHighlight } = useHighlightMessage();
const {
messages: _messages,
loadMore,
window,
jumpTo,
convertToKeys,
} = useThreadMessages({
companyId,
threadId: threadId || '',
});
const messages = withNonMessagesComponents(_messages, window.reachedStart);
useEffect(() => {
loadMore('history');
}, []);
const loadMoreMessages = async (
direction: 'history' | 'future',
limit?: number,
offsetItem?: MessagesAndComponentsType,
) => {
const messages = await loadMore(direction, limit, offsetItem?.id);
return withNonMessagesComponents(
convertToKeys(companyId, messages),
window.reachedStart && direction === 'history',
);
};
useEffect(() => {
//Manage scroll to highlight
if (
listBuilderRef.current &&
highlight &&
highlight.answerId &&
highlight.reachedThread &&
!highlight.reachedAnswer
) {
// Find the correct index of required message
const index = messages.findIndex(m => m.id === highlight.answerId);
if (index < 0) {
// Load the right portion of messages
jumpTo(highlight.answerId);
return;
}
setTimeout(() => {
if (listBuilderRef.current)
listBuilderRef.current.scrollToIndex({
align: 'start',
index: index,
});
setTimeout(() => reachedHighlight('answer'), 1000);
}, 1000);
}
}, [highlight, messages.length]);
const jumpToBottom = () => {
if (window.reachedEnd) {
listBuilderRef.current?.scrollTo({ top: 10000000, behavior: 'smooth' });
} else {
// Load the right portion of messages
jumpTo('');
}
};
//This hide virtuoso but it start to work in backend
const virtuosoLoading = highlight && highlight.answerId && !highlight?.reachedAnswer;
return (
<MessagesListContext.Provider
value={{ hideReplies: true, withBlock: false, readonly: !!readonly }}
>
{(!window.loaded || virtuosoLoading) && <MessagesPlaceholder />}
{!window.loaded && <div style={{ flex: 1 }}></div>}
{window.loaded && (
<ListBuilder
key={threadId}
followOutput={!!window.reachedEnd && 'smooth'}
ref={listBuilderRef}
style={virtuosoLoading ? { opacity: 0 } : {}}
onScroll={(e: React.UIEvent<'div', UIEvent>) => {
const scrollBottom =
(e.target as HTMLElement).scrollHeight -
(e.target as HTMLElement).scrollTop -
(e.target as HTMLElement).clientHeight;
const closeToBottom = scrollBottom < 100;
if (closeToBottom !== atBottom) setAtBottom(closeToBottom);
cancelHighlight();
}}
items={messages}
itemId={m => m.type + (getMessage(m.id)?.context?._front_id || m.id) + m.id}
emptyListComponent={<FirstThreadMessage noReplies />}
filterOnAppend={messages => {
return cleanFrontMessagesFromListOfMessages(messages);
}}
itemContent={(_index, m) => {
if (m.type === 'timeseparator') {
return (
<div key={m.type + m.id}>
<TimeSeparator date={m.date || 0} />
</div>
);
}
if (m.type === 'header') {
return (
<div key={m.type + m.threadId}>
<FirstThreadMessage />
</div>
);
}
return (
<div key={m.type + m.id}>
<MessageWithReplies
companyId={m.companyId}
workspaceId={workspaceId || ''}
channelId={channelId || ''}
threadId={m.threadId}
id={m.id}
/>
</div>
);
}}
loadMore={loadMoreMessages}
/>
)}
{!(atBottom && window.reachedEnd) && window.loaded && messages.length > 0 && (
<GoToBottom
onClick={() => {
jumpToBottom();
}}
/>
)}
</MessagesListContext.Provider>
);
};
@@ -1,75 +0,0 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/jsx-key */
import React, { Component } from 'react';
import Languages from 'app/features/global/services/languages-service';
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
import Workspaces from 'app/deprecated/workspaces/workspaces.jsx';
import Emojione from 'components/emojione/emojione';
import Loader from 'components/loader/loader.jsx';
export default class BoardPicker extends Component {
constructor() {
super();
this.state = {
i18n: Languages,
};
Languages.addListener(this);
this.boards_collection_key = 'boards_picker_' + Workspaces.getCurrentUserId;
Collections.get('boards').addListener(this);
Collections.get('boards').addSource(
{
http_base_url: 'tasks/board',
http_options: {
workspace_id: Workspaces.getCurrentUserId,
},
websockets: [{ uri: 'boards/' + Workspaces.getCurrentUserId, options: { type: 'board' } }],
},
this.boards_collection_key,
// eslint-disable-next-line @typescript-eslint/no-empty-function
() => {},
);
}
componentWillUnmount() {
Languages.removeListener(this);
Collections.get('boards').removeSource(this.boards_collection_key);
}
render() {
var boards = Collections.get('boards').findBy({ workspace_id: Workspaces.currentWorkspaceId });
var loading =
!Collections.get('boards').did_load_first_time[this.boards_collection_key] &&
boards.length == 0;
return (
<div className="boardPicker">
{loading && (
<div className="loading">
<Loader color="#CCC" className="app_loader" />
</div>
)}
{!loading &&
boards.map(board => {
return (
<div
className="board_frame fade_in"
onClick={() => {
this.props.onChoose(board);
}}
>
<div className="board_name app_title">
{board.emoji && <Emojione type={board.emoji} s32 className="board_emoji" />}
{board.title}
</div>
<div className="board_info">
{board.active_tasks || '0'}{' '}
{Languages.t('scenes.apps.tasks.active_tasks', [], 'tâches actives')}
</div>
</div>
);
})}
</div>
);
}
}
@@ -1,501 +0,0 @@
/* eslint-disable react/prop-types */
import React from 'react';
import Languages from 'app/features/global/services/languages-service';
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import List from './list/list.jsx';
import PerfectScrollbar from 'react-perfect-scrollbar';
import Loader from 'components/loader/loader.jsx';
import './Board.scss';
import Emojione from 'components/emojione/emojione';
import Rounded from 'components/inputs/rounded.jsx';
import Menu from 'components/menus/menu.jsx';
import ListEditor from './list/list-editor.jsx';
import TasksService from 'app/deprecated/Apps/Tasks/Tasks.js';
import Tabs from 'components/tabs/tabs.jsx';
import ChevronDownIcon from '@material-ui/icons/KeyboardArrowDownOutlined';
import User from 'components/user/user.jsx';
import MenusManager from 'app/components/menus/menus-manager.jsx';
import RouterService from 'app/features/router/services/router-service';
export default class Board extends React.Component {
constructor(props) {
super(props);
this.props = props;
this.state = {
i18n: Languages,
archived: false,
};
Languages.addListener(this);
this.board_collection_key = 'board_' + this.props.board.id;
this.user_mode = this.props.board.id.split('_')[0] === 'user';
const { channelId } = RouterService.getStateFromRoute();
//Lists (only in board mode, not in user mode)
if (!this.user_mode) {
Collections.get('lists').addListener(this);
Collections.get('lists').addSource(
{
http_base_url: 'tasks/list',
http_options: {
channel_id: channelId,
board_id: this.props.board.id,
},
websockets: [{ uri: 'board_lists/' + this.props.board.id, options: { type: 'list' } }],
},
this.board_collection_key,
);
}
Collections.get('tasks').addListener(this);
Collections.get('tasks').addSource(
{
http_base_url: 'tasks/task',
http_options: {
channel_id: channelId,
board_id: this.props.board.id,
},
websockets: [{ uri: 'board_tasks/' + this.props.board.id, options: { type: 'task' } }],
},
this.board_collection_key,
);
}
componentWillUnmount() {
Languages.removeListener(this);
if (!this.user_mode) {
Collections.get('lists').removeListener(this);
Collections.get('lists').removeSource(this.board_collection_key);
}
Collections.get('tasks').removeListener(this);
Collections.get('tasks').removeSource(this.board_collection_key);
}
shouldComponentUpdate() {
if (this.retry_update) clearTimeout(this.retry_update);
if (TasksService.paused_notify[this.props.board.id]) {
this.retry_update = setTimeout(() => {
this.setState({});
}, 1000);
return false;
}
return true;
}
onDragStart = () => {
TasksService.paused_notify[this.props.board.id] = true;
};
onDragEnd = event => {
TasksService.paused_notify[this.props.board.id] = false;
if (!event.destination) {
return;
}
if (event.type === 'board') {
var element_front_id = event.draggableId.split('_')[1];
var new_index = event.destination.index;
var list = Collections.get('lists').findByFrontId(element_front_id);
if (list) {
Collections.get('lists').updateObject(
{
order: TasksService.newIndexAfter(
'lists_' + this.props.board.id,
new_index - (new_index < event.source.index ? 1 : 0),
),
},
list.front_id,
);
TasksService.setElementIndexPool(
'lists_' + this.props.board.id,
Collections.get('lists').findBy({ board_id: this.props.board.id }),
);
Collections.get('lists').save(list, this.board_collection_key);
}
}
if (event.type === 'list') {
// eslint-disable-next-line no-redeclare
var element_front_id = event.draggableId.split('_')[1];
var source_list_front_id = event.source.droppableId;
var destination_list_front_id = event.destination.droppableId;
var destination_index = event.destination.index;
var source_list = Collections.get('lists').findByFrontId(source_list_front_id);
// eslint-disable-next-line no-redeclare
var list = Collections.get('lists').findByFrontId(destination_list_front_id);
var task = Collections.get('tasks').findByFrontId(element_front_id);
if (task && list && list.id) {
Collections.get('tasks').updateObject(
{
list_id: list.id,
order: TasksService.newIndexAfter(
'tasks_' + list.id,
destination_index -
(destination_index < event.source.index ||
destination_list_front_id !== source_list_front_id
? 1
: 0),
),
},
task.front_id,
);
if (source_list && source_list.id) {
TasksService.setElementIndexPool(
'tasks_' + source_list.id,
Collections.get('tasks').findBy({
board_id: this.props.board.id,
list_id: source_list.id,
}),
);
}
TasksService.setElementIndexPool(
'tasks_' + list.id,
Collections.get('tasks').findBy({ board_id: this.props.board.id, list_id: list.id }),
);
Collections.get('tasks').save(task, this.board_collection_key);
}
}
};
render() {
var current_board = this.props.board;
this.isDragDisabled = false;
var lists = Collections.get('lists').findBy({ board_id: this.props.board.id });
if (this.user_mode) {
lists = [
{
id: 'allusertasks_' + this.props.board.id.split('_')[1],
title: Languages.t('components.workspace.list_manager.all', [], 'All'),
all: true,
},
];
var workspaces = [];
TasksService.getTasksInList(
this.props.board.id,
lists[0].id,
this.state.archived ? true : false,
).forEach(task => {
if (workspaces.indexOf(task.workspace_id) < 0) {
workspaces.push(task.workspace_id);
var workspace = Collections.get('workspaces').find(task.workspace_id);
if (workspace) {
lists.push({
id:
'workspaceusertasks_' + this.props.board.id.split('_')[1] + '_' + task.workspace_id,
title: workspace.name,
other_group: true,
});
}
}
});
lists.sort((a, b) => {
if (a.all) {
return -1;
}
if (a.other_group) {
return 1;
}
if (b.other_group) {
return -1;
}
return 0;
});
this.isDragDisabled = true;
}
var loading =
(!this.user_mode &&
!Collections.get('lists').did_load_first_time[this.board_collection_key]) ||
!Collections.get('tasks').did_load_first_time[this.board_collection_key];
if (loading) {
return (
<div className="loading">
<Loader color="#CCC" className="app_loader" />
</div>
);
}
TasksService.setElementIndexPool(
'lists_' + this.props.board.id,
Collections.get('lists').findBy({ board_id: this.props.board.id }),
);
// eslint-disable-next-line no-redeclare
var lists = lists.sort(
(a, b) =>
TasksService.getElementIndex(a, 'lists_' + a.board_id) -
TasksService.getElementIndex(b, 'lists_' + b.board_id),
);
if (!this.user_mode) {
lists.push({
id: 'add_list',
render: (
<div
style={{
display: 'inline-block',
paddingRight: '40px',
position: 'relative',
top: '-8px',
}}
>
<Menu
style={{ display: 'inline-block' }}
menu={[
{
type: 'title',
text: Languages.t('scenes.apps.tasks.list_modal.new_list', [], 'New list'),
},
{
type: 'react-element',
reactElement: level => {
return (
<ListEditor
menuLevel={level}
board={this.props.board}
collectionKey={this.board_collection_key}
/>
);
},
},
]}
>
<Rounded
text={Languages.t('scenes.apps.tasks.list_modal.new_list', [], 'New list')}
className="list_add"
/>
</Menu>
</div>
),
});
}
return (
<DragDropContext onDragStart={this.onDragStart} onDragEnd={this.onDragEnd}>
<div className="board_header">
{!this.props.noTitle && (
<div className="app_title">
{!((this.props.tab || {}).configuration || {}).board_id && (
<div
className="app_back_btn"
onClick={() => {
TasksService.openBoard(null);
}}
>
{Languages.t('scenes.apps.board.all_boards', [], 'All Boards')}
</div>
)}
{current_board.emoji && (
<Emojione type={current_board.emoji} s32 className="board_emoji" />
)}
{current_board.user_image && (
<User user={{ thumbnail: current_board.user_image }} medium />
)}
{current_board.title}
</div>
)}
<div className="nomobile info" />
{!this.props.hideMore && (
<div
className="nomobile options app_right_btn"
onClick={evt => {
MenusManager.openMenu(
[
{
type: 'title',
text: Languages.t('scenes.apps.board.display_as', [], 'Afficher en tant que'),
},
{
type: 'menu',
icon: current_board.view_mode === 'grid' ? 'check' : ' ',
className: current_board.view_mode === 'grid' ? 'primary' : ' ',
rightIcon: 'window-restore',
text: Languages.t('scenes.apps.board.kanban', [], 'Kanban'),
onClick: () => {
current_board.view_mode = 'grid';
Collections.get('boards').save(
current_board,
this.props.boardsCollectionKey,
);
},
},
{
type: 'menu',
icon: current_board.view_mode === 'list' ? 'check' : ' ',
className: current_board.view_mode === 'list' ? 'primary' : ' ',
rightIcon: 'list-ul',
text: Languages.t('scenes.apps.calendar.calendar.list_btn', [], 'Liste'),
onClick: () => {
current_board.view_mode = 'list';
Collections.get('boards').save(
current_board,
this.props.boardsCollectionKey,
);
},
},
{ type: 'separator' },
{
type: 'menu',
icon: this.state.archived === false ? 'check' : ' ',
className: this.state.archived === false ? 'primary' : ' ',
text: Languages.t('scenes.apps.board.active_tasks', [], 'Tâches actives'),
onClick: () => {
this.setState({ archived: false });
},
},
{
type: 'menu',
icon: this.state.archived ? 'check' : ' ',
className: this.state.archived ? 'primary' : ' ',
text: Languages.t(
'scenes.apps.board.archived_tasks',
[
Collections.get('tasks').findBy({
archived: true,
board_id: current_board.id,
}).length,
],
'Tâches archivées ($1)',
),
onClick: () => {
this.setState({ archived: true });
},
},
],
{ x: evt.clientX, y: evt.clientY },
'bottom',
);
}}
>
{Languages.t('general.more', [], 'Plus')}
<ChevronDownIcon className="m-icon-small" />
</div>
)}
</div>
<div
className={
'board ' +
(this.props.inline ? 'inline ' : '') +
(this.props.mode === 'list' ? 'mode_list ' : 'mode_grid ')
}
>
<div className="lists_before">
{this.props.mode === 'list' && (
<Tabs
tabs={lists
.map(item => {
return {
id: item.front_id || item.id,
titleClassName: item.id === 'add_list' ? 'no-selection-border' : '',
titleStyle: { borderBottomColor: item.color },
title: () => {
if (item.id === 'add_list') {
return item.render;
}
return (
(item.title || '-') +
' (' +
TasksService.getTasksInList(
item.board_id,
item.id,
this.state.archived ? true : false,
).length +
')'
);
},
render: () => {
if (item.id === 'add_list') {
return '';
}
return (
<List
showArchived={this.state.archived}
isDragDisabled={this.isDragDisabled}
canCreate={!this.user_mode}
list={item}
board={this.props.board}
collectionKey={this.board_collection_key}
key={item.front_id}
/>
);
},
};
})
.reduce((acc, cur) => {
acc[cur.id] = cur;
return acc;
}, {})}
/>
)}
{this.props.mode === 'grid' && (
<PerfectScrollbar className="lists_scrollable" options={{ suppressScrollX: false }}>
<Droppable
droppableId={'lists'}
direction="horizontal"
type="board"
className="droppable_list"
>
{provided => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
style={{
display: 'flex',
minWidth: 100 + 280 * (lists.length - 1),
height: '100%',
}}
>
{lists
.filter(a => a.id !== 'add_list')
.map(item => {
return (
<List
showArchived={this.state.archived}
isDragDisabled={this.isDragDisabled}
canCreate={!this.user_mode}
list={item}
board={this.props.board}
collectionKey={this.board_collection_key}
key={item.front_id}
/>
);
})}
{provided.placeholder}
{lists[lists.length - 1].render}
</div>
)}
</Droppable>
</PerfectScrollbar>
)}
</div>
</div>
{/*<MainPlus onClick={()=>{
MediumPopupManager.open(<TaskEditor collectionKey={this.board_collection_key} />, {size: {width: 600}});
}} />*/}
</DragDropContext>
);
}
}
@@ -1,233 +0,0 @@
.board {
display: flex;
flex-direction: column;
height: 100%;
flex: 1;
margin: 0 -32px;
margin-bottom: -15px;
margin-top: -8px;
&.inline {
height: auto;
.list,
.component_tabs,
.component_tabs > .body,
.lists_before {
display: block !important;
height: auto !important;
position: relative !important;
max-height: none !important;
}
.list .list_draggable .droppable {
max-height: none;
height: auto;
flex: none;
}
}
&.mode_list {
.lists_before {
padding-top: 16px;
}
.lists_before .component_tabs .body .scrollable_task_list {
padding: 0px 24px;
}
.lists_before .component_tabs .component_tabs_tabs,
.lists_before .component_tabs .body .add_task {
height: auto;
margin-right: 24px;
margin-left: 24px;
}
.component_tabs {
height: 100%;
.body {
display: flex;
flex-direction: column;
}
}
.component_tabs > .body {
padding-left: 0px;
padding-right: 0px;
& > .component_tabs_tab {
&.no-selection-border {
border-bottom: 0px;
margin-left: 0px;
padding-left: 0px;
}
}
}
.list .list_draggable .list_header {
display: none;
}
.list {
position: absolute;
height: 100%;
width: 100%;
}
.list,
.list_draggable {
margin-right: 0px;
width: 100%;
.task_draggable_parent {
margin-bottom: 0px;
.task {
border-left: 0px;
border-right: 0px;
border-top: 0px;
border-radius: 0px;
display: flex;
padding-right: 56px;
.task_options {
padding-top: 2px;
display: block;
background: transparent;
}
.task_line_1 {
padding-top: 2px;
flex: 1;
.task_title {
line-height: 22px;
font-size: 14px;
font-weight: bold;
}
.tags-in-name {
display: inline-block;
vertical-align: bottom;
}
}
.task_line_2,
.task_line_tags,
.task_users {
margin-top: 0px;
width: 20%;
max-width: 110px;
}
.task_line_tags {
display: none;
}
}
}
}
.task {
.attachmentPicker {
width: auto;
margin: 0px;
.attachments .attachment {
width: 40px;
height: 40px;
margin-top: -8px;
margin-bottom: -8px;
margin-right: 8px;
}
.attachments,
.attachments .attachment > .file,
.attachments .attachment > .file .preview {
width: 100%;
height: 100%;
margin: 0px;
}
.attachments .attachment > .file .data {
display: none;
}
}
}
}
.lists_before {
flex: 1;
position: relative;
overflow: hidden;
.lists_scrollable {
white-space: nowrap;
height: 100%;
position: absolute;
width: 100%;
& > div:first-child {
padding-left: 32px;
padding-bottom: 16px;
box-sizing: border-box;
padding-top: 16px;
}
}
}
.list_add {
font-size: 14px;
font-weight: 500;
width: auto;
padding: 4px;
height: 32px;
padding-right: 4px;
padding-left: 8px;
line-height: 24px;
vertical-align: top;
opacity: 0.8;
&:hover {
opacity: 1;
}
.m-icon-small {
vertical-align: middle;
margin-top: -2px;
font-size: 18px !important;
}
}
}
.board_header {
display: flex;
.app_title {
flex: 1;
}
.options {
font-size: 13px;
margin-left: 10px;
line-height: 18px;
display: flex;
align-items: center;
}
.view_title {
font-size: 24px;
margin-bottom: 0;
padding-bottom: 0;
line-height: 24px;
height: 50px;
display: flex;
.name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.filter_input {
flex: 1;
margin-left: 20px;
margin-top: -2px;
margin-right: 20px;
text-align: right;
}
.anticon {
margin-right: 5px;
line-height: 30px;
}
.board_parameters {
float: right;
cursor: pointer;
color: #666;
&:hover {
opacity: 0.5;
}
}
}
}
@@ -1,76 +0,0 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/no-direct-mutation-state */
import React, { Component } from 'react';
import Languages from 'app/features/global/services/languages-service';
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
import MenuManager from 'app/components/menus/menus-manager.jsx';
import Workspaces from 'app/deprecated/workspaces/workspaces.jsx';
import InputWithIcon from 'components/inputs/input-with-icon';
import Button from 'components/buttons/button.jsx';
export default class BoardEditor extends Component {
constructor() {
super();
this.state = {
i18n: Languages,
name: '',
emoji: '',
};
Languages.addListener(this);
}
UNSAFE_componentWillMount() {
if (this.props.id) {
var board = Collections.get('boards').find(this.props.id);
if (board) {
this.state.name = board.title;
this.state.emoji = board.emoji;
}
}
}
componentWillUnmount() {
Languages.removeListener(this);
}
save() {
var board = {};
if (this.props.id) {
board = Collections.get('boards').find(this.props.id);
} else {
board = Collections.get('boards').editCopy({});
board.workspace_id = Workspaces.currentWorkspaceId;
}
board.title = this.state.name;
board.emoji = this.state.emoji;
Collections.get('boards').save(board, this.props.collectionKey);
MenuManager.closeMenu();
}
render() {
return (
<div>
<div className="menu-buttons bottom-margin">
<InputWithIcon
className="full_width"
focusOnDidMount
menu_level={this.props.menuLevel}
placeholder={Languages.t('scenes.apps.tasks.board.place_holder', [], 'Board name')}
value={[this.state.emoji, this.state.name]}
onEnter={() => this.save()}
onChange={value => {
this.setState({ emoji: value[0], name: value[1] });
}}
/>
</div>
<div className="menu-buttons">
<Button
disabled={(this.state.name || '').length <= 0}
type="button"
value={Languages.t('general.save', [], 'Enregistrer')}
onClick={() => {
this.save();
}}
/>
</div>
</div>
);
}
}
@@ -1,77 +0,0 @@
/* eslint-disable react/prop-types */
import React, { Component } from 'react';
import Languages from 'app/features/global/services/languages-service';
import Input from 'components/inputs/input.jsx';
import Button from 'components/buttons/button.jsx';
import OutsideClickHandler from 'react-outside-click-handler';
export default class AddTask extends Component {
constructor(props) {
super(props);
this.state = {
i18n: Languages,
unselected: true,
};
Languages.addListener(this);
}
componentWillUnmount() {
Languages.removeListener(this);
}
newTask() {
this.setState({ new_task_title: '' });
if (!(this.state.new_task_title || '').trim()) {
return;
}
var task = {
title: this.state.new_task_title,
};
this.props.onSubmit(task);
}
render() {
if (this.state.unselected) {
return (
<div
className="add_task unselected"
onClick={() => {
this.setState({ unselected: false });
}}
>
{Languages.t('scenes.apps.board.new_task', [], '+ New task')}
</div>
);
}
return (
<OutsideClickHandler
onOutsideClick={() => {
this.setState({ unselected: true });
}}
>
<div className="add_task">
<Input
autoFocus
className="medium"
placeholder={Languages.t('general.add', [], 'Add')}
value={this.state.new_task_title}
onChange={evt => {
this.setState({ new_task_title: evt.target.value });
}}
onEnter={() => {
this.newTask();
}}
/>
<Button
className="small"
value={Languages.t('general.add', [], 'Add')}
onClick={() => {
this.newTask();
}}
/>
</div>
</OutsideClickHandler>
);
}
}

Some files were not shown because too many files have changed in this diff Show More