Skip to content
Merged
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"dependencies": {
"@internxt/css-config": "^1.1.0",
"@internxt/lib": "^1.4.1",
"@internxt/sdk": "^1.15.1",
"@internxt/ui": "^0.1.11",
"@phosphor-icons/react": "^2.1.10",
Expand Down
9 changes: 8 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { routes } from './routes';
import { NavigationService } from './services/navigation';
import { useEffect } from 'react';
import { Activity, useEffect } from 'react';
import { useAppDispatch } from './store/hooks';
import { initializeUserThunk } from './store/slices/user/thunks';
import { Toaster } from 'react-hot-toast';
import { ActionDialog, useActionDialog } from './context/dialog-manager';
import { ComposeMessageDialog } from './components/compose-message';

const router = createBrowserRouter(routes);
const navigation = router.navigate;
Expand All @@ -13,6 +15,8 @@ NavigationService.instance.init(navigation);

function App() {
const dispatch = useAppDispatch();
const { isDialogOpen } = useActionDialog();
const isComposeMessageDialogOpen = isDialogOpen(ActionDialog.ComposeMessage);

useEffect(() => {
dispatch(initializeUserThunk());
Expand All @@ -27,6 +31,9 @@ function App() {
}}
/>
<RouterProvider router={router} />
<Activity mode={isComposeMessageDialogOpen ? 'visible' : 'hidden'}>
<ComposeMessageDialog />
</Activity>
</>
);
}
Expand Down
17 changes: 14 additions & 3 deletions src/components/Sidenav/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react';

import { Sidenav as SidenavComponent } from '@internxt/ui';
import { Button, Sidenav as SidenavComponent } from '@internxt/ui';
import logo from '../../assets/logos/Internxt/small-logo.svg';
import { useTranslationContext } from '@/i18n';
import { NavigationService } from '@/services/navigation';
Expand All @@ -11,14 +11,16 @@ import { useSuiteLauncher } from '@/hooks/navigation/useSuiteLauncher';
import { useSidenavNavigation } from '@/hooks/navigation/useSidenavNavigation';
import { useGetStorageLimitQuery, useGetStorageUsageQuery } from '@/store/queries/storage/storage.query';
import { useAppSelector } from '@/store/hooks';
import { bytesToString } from '@/utils/bytesToString';
import { bytesToString } from '@/utils/bytes-to-string';
import { ActionDialog, useActionDialog } from '@/context/dialog-manager';

const Sidenav = () => {
const { translate } = useTranslationContext();
const { userSubscription: subscription } = useAppSelector((state: RootState) => state.user);
const { isLoading: isLoadingPlanLimit, data: planLimit = 1 } = useGetStorageLimitQuery();
const { isLoading: isLoadingPlanUsage, data: planUsage = 0 } = useGetStorageUsageQuery();
const storagePercentage = planLimit > 0 ? Math.min((planUsage / planLimit) * 100, 100) : 0;
const { openDialog } = useActionDialog();

const { itemsNavigation } = useSidenavNavigation();
const { suiteArray } = useSuiteLauncher();
Expand Down Expand Up @@ -48,6 +50,10 @@ const Sidenav = () => {
});
};

const onPrimaryActionClicked = () => {
openDialog(ActionDialog.ComposeMessage);
};

return (
<div className="flex flex-col h-screen z-50">
<SidenavComponent
Expand All @@ -57,6 +63,11 @@ const Sidenav = () => {
onClick: onLogoClicked,
className: '!pt-0 pb-3',
}}
primaryAction={
<Button className="w-full" variant="primary" onClick={onPrimaryActionClicked}>
{translate('actions.newMessage')}
</Button>
}
suiteLauncher={{
suiteArray: suiteArray,
soonText: translate('modals.upgradePlanDialog.soonBadge'),
Expand All @@ -69,7 +80,7 @@ const Sidenav = () => {
limit: bytesToString({ size: planLimit }),
percentage: storagePercentage,
onUpgradeClick: () => {},
upgradeLabel: isUpgradeAvailable() ? translate('preferences.account.plans.upgrade') : undefined,
upgradeLabel: isUpgradeAvailable() ? translate('actions.upgrade') : undefined,
isLoading: isLoadingPlanUsage || isLoadingPlanLimit,
}}
/>
Expand Down
12 changes: 7 additions & 5 deletions src/components/compose-message/components/RecipientInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useState, type KeyboardEvent } from 'react';
import type { Recipient } from '../types';
import UserChip from '@/components/user-chip';
import { DEFAULT_USER_NAME } from '@/constants';
import isValidEmail from '@internxt/lib/dist/src/auth/isValidEmail';

interface RecipientInputProps {
label: string;
Expand Down Expand Up @@ -37,7 +39,7 @@ export const RecipientInput = ({
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
const email = inputValue.trim().replace(/,$/, '');
if (email) {
if (email && isValidEmail(email)) {
onAddRecipient(email);
setInputValue('');
}
Expand All @@ -48,7 +50,7 @@ export const RecipientInput = ({

const handleBlur = () => {
const email = inputValue.trim();
if (email) {
if (email && isValidEmail(email)) {
onAddRecipient(email);
setInputValue('');
}
Expand All @@ -60,15 +62,15 @@ export const RecipientInput = ({
<div className="flex-1 flex items-center gap-1 flex-wrap rounded-lg border border-gray-10 bg-surface px-3 py-1.5 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary">
{recipients.map((recipient) => (
<UserChip
key={recipient.email}
key={recipient.id}
email={recipient.email}
name={recipient?.name || 'My Internxt'}
name={recipient?.name ?? DEFAULT_USER_NAME}
avatar={recipient.avatar}
onRemove={() => onRemoveRecipient(recipient.id)}
/>
))}
<input
type="text"
type="email"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
Expand Down
13 changes: 13 additions & 0 deletions src/components/compose-message/components/RichTextEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { EditorContent, Editor } from '@tiptap/react';

export interface RichTextEditorProps {
editor: Editor | null;
}

const RichTextEditor = ({ editor }: RichTextEditorProps) => (
<div className="h-75 overflow-y-auto">
<EditorContent editor={editor} className="h-full" />
</div>
);

export default RichTextEditor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface EditorBarButtonProps {
onClick: () => void;
isActive?: boolean;
disabled?: boolean;
children: React.ReactNode;
}

export const EditorBarButton = ({ onClick, isActive, disabled, children }: EditorBarButtonProps) => (
<button
onClick={onClick}
disabled={disabled}
aria-pressed={isActive}
className={`p-1 rounded transition-colors ${isActive ? 'bg-gray-10 text-primary' : 'hover:bg-gray-5 text-gray-60'} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{children}
</button>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { EditorBarItem } from '../../types';
import { EditorBarButton } from './EditorBarButton';

export interface EditorBarGroupProps {
items: EditorBarItem[];
disabled?: boolean;
}

export const EditorBarGroup = ({ items, disabled }: EditorBarGroupProps) => (
<div className="flex flex-row items-center gap-2">
{items.map((item) => (
<EditorBarButton key={item.id} onClick={item.onClick} isActive={item.isActive} disabled={disabled}>
<item.icon size={20} />
</EditorBarButton>
))}
</div>
);
Loading
Loading