From fc19e1d34ac7707fccb11e05ac531ed0cc35862a Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Wed, 26 Feb 2025 10:26:44 +0000 Subject: [PATCH] Merge pull request #23669 from overleaf/mj-ide-chat-look [web] Update chat in editor redesign GitOrigin-RevId: 79c79eb9c774fbaa1a5a1e15386b629cc03239b3 --- .../web/frontend/extracted-translations.json | 3 + .../features/chat/components/message-list.tsx | 12 +- .../js/features/chat/components/message.tsx | 2 +- .../components/{ => chat}/chat.tsx | 80 ++++++---- .../ide-redesign/components/chat/message.tsx | 60 ++++++++ .../integrations-panel/integrations-panel.tsx | 14 +- .../features/ide-redesign/components/rail.tsx | 17 +- .../components/integrations-panel.scss | 13 -- .../bootstrap-5/pages/editor/chat.scss | 145 +++++++++++++++++- .../bootstrap-5/pages/editor/rail.scss | 14 ++ services/web/locales/en.json | 3 + 11 files changed, 296 insertions(+), 67 deletions(-) rename services/web/frontend/js/features/ide-redesign/components/{ => chat}/chat.tsx (53%) create mode 100644 services/web/frontend/js/features/ide-redesign/components/chat/message.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 9c9ba60192..b81cdc3ada 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -259,6 +259,7 @@ "code_editor_tooltip_message": "", "code_editor_tooltip_title": "", "collaborate_online_and_offline": "", + "collaborator_chat": "", "collabs_per_proj": "", "collabs_per_proj_single": "", "collapse": "", @@ -1044,6 +1045,7 @@ "no_libraries_selected": "", "no_members": "", "no_messages": "", + "no_messages_yet": "", "no_new_commits_in_github": "", "no_one_has_commented_or_left_any_suggestions_yet": "", "no_other_projects_found": "", @@ -1585,6 +1587,7 @@ "start_by_fixing_the_first_error_in_your_doc": "", "start_free_trial": "", "start_free_trial_without_exclamation": "", + "start_the_conversation_by_saying_hello_or_sharing_an_update": "", "start_typing_find_your_company": "", "start_typing_find_your_organization": "", "start_typing_find_your_university": "", diff --git a/services/web/frontend/js/features/chat/components/message-list.tsx b/services/web/frontend/js/features/chat/components/message-list.tsx index bf3515719d..a87618d3c0 100644 --- a/services/web/frontend/js/features/chat/components/message-list.tsx +++ b/services/web/frontend/js/features/chat/components/message-list.tsx @@ -1,6 +1,7 @@ import moment from 'moment' import Message from './message' import type { Message as MessageType } from '@/features/chat/context/chat-context' +import MessageRedesign from '@/features/ide-redesign/components/chat/message' import { useUserContext } from '@/shared/context/user-context' const FIVE_MINUTES = 5 * 60 * 1000 @@ -16,11 +17,16 @@ function formatTimestamp(date: moment.MomentInput) { interface MessageListProps { messages: MessageType[] resetUnreadMessages(...args: unknown[]): unknown + newDesign?: boolean } -function MessageList({ messages, resetUnreadMessages }: MessageListProps) { +function MessageList({ + messages, + resetUnreadMessages, + newDesign, +}: MessageListProps) { const user = useUserContext() - + const MessageComponent = newDesign ? MessageRedesign : Message function shouldRenderDate(messageIndex: number) { if (messageIndex === 0) { return true @@ -58,7 +64,7 @@ function MessageList({ messages, resetUnreadMessages }: MessageListProps) { )} - diff --git a/services/web/frontend/js/features/chat/components/message.tsx b/services/web/frontend/js/features/chat/components/message.tsx index a0cf769129..aa2779f1e8 100644 --- a/services/web/frontend/js/features/chat/components/message.tsx +++ b/services/web/frontend/js/features/chat/components/message.tsx @@ -3,7 +3,7 @@ import MessageContent from './message-content' import type { Message as MessageType } from '@/features/chat/context/chat-context' import { User } from '../../../../../types/user' -interface MessageProps { +export interface MessageProps { message: MessageType fromSelf: boolean } diff --git a/services/web/frontend/js/features/ide-redesign/components/chat.tsx b/services/web/frontend/js/features/ide-redesign/components/chat/chat.tsx similarity index 53% rename from services/web/frontend/js/features/ide-redesign/components/chat.tsx rename to services/web/frontend/js/features/ide-redesign/components/chat/chat.tsx index 41606806b4..d76d82cb22 100644 --- a/services/web/frontend/js/features/ide-redesign/components/chat.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/chat/chat.tsx @@ -9,8 +9,10 @@ import MaterialIcon from '@/shared/components/material-icon' import { useUserContext } from '@/shared/context/user-context' import { lazy, Suspense, useEffect } from 'react' import { useTranslation } from 'react-i18next' +import classNames from 'classnames' +import { RailPanelHeader } from '../rail' -const MessageList = lazy(() => import('../../chat/components/message-list')) +const MessageList = lazy(() => import('../../../chat/components/message-list')) export const ChatIndicator = () => { const { unreadMessageCount } = useChatContext() @@ -24,7 +26,6 @@ const Loading = () => export const ChatPane = () => { const { t } = useTranslation() - const user = useUserContext() const { status, @@ -65,44 +66,55 @@ export const ChatPane = () => { } return ( - +
+ +
+ +
+
) } function Placeholder() { const { t } = useTranslation() return ( - <> -
{t('no_messages')}
-
- {t('send_first_message')} -
- +
+
+ + +
- +
+
{t('no_messages_yet')}
+
+ {t('start_the_conversation_by_saying_hello_or_sharing_an_update')} +
+
+
) } diff --git a/services/web/frontend/js/features/ide-redesign/components/chat/message.tsx b/services/web/frontend/js/features/ide-redesign/components/chat/message.tsx new file mode 100644 index 0000000000..a3676e2216 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/chat/message.tsx @@ -0,0 +1,60 @@ +import { MessageProps } from '@/features/chat/components/message' +import { User } from '../../../../../../types/user' +import { getHueForUserId } from '@/shared/utils/colors' +import MessageContent from '@/features/chat/components/message-content' +import classNames from 'classnames' + +function hue(user?: User) { + return user ? getHueForUserId(user.id) : 0 +} + +function getAvatarStyle(user?: User) { + return { + borderColor: `hsl(${hue(user)}, 85%, 40%)`, + backgroundColor: `hsl(${hue(user)}, 85%, 40%`, + } +} + +function Message({ message, fromSelf }: MessageProps) { + return ( +
+
+
+ {!fromSelf && ( +
+ {message.user.first_name || message.user.email} +
+ )} +
+ {message.contents.map((content, index) => ( +
+ <> + {!fromSelf && index === message.contents.length - 1 ? ( +
+
+ {message.user.first_name?.charAt(0) || + message.user.email.charAt(0)} +
+
+ ) : ( +
+ )} +
+
+ +
+
+ +
+ ))} +
+ ) +} + +export default Message diff --git a/services/web/frontend/js/features/ide-redesign/components/integrations-panel/integrations-panel.tsx b/services/web/frontend/js/features/ide-redesign/components/integrations-panel/integrations-panel.tsx index bf25593387..d1e4358907 100644 --- a/services/web/frontend/js/features/ide-redesign/components/integrations-panel/integrations-panel.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/integrations-panel/integrations-panel.tsx @@ -1,24 +1,18 @@ import { ElementType } from 'react' import importOverleafModules from '../../../../../macros/import-overleaf-module.macro' -import MaterialIcon from '@/shared/components/material-icon' -import OlButton from '@/features/ui/components/ol/ol-button' -import { useRailContext } from '../../contexts/rail-context' +import { RailPanelHeader } from '../rail' +import { useTranslation } from 'react-i18next' const integrationPanelComponents = importOverleafModules( 'integrationPanelComponents' ) as { import: { default: ElementType }; path: string }[] export default function IntegrationsPanel() { - const { handlePaneCollapse } = useRailContext() + const { t } = useTranslation() return (
-
-

Integrations

- - - -
+ {integrationPanelComponents.map( ({ import: { default: Component }, path }) => ( diff --git a/services/web/frontend/js/features/ide-redesign/components/rail.tsx b/services/web/frontend/js/features/ide-redesign/components/rail.tsx index 98c1bc78c3..8da9faf374 100644 --- a/services/web/frontend/js/features/ide-redesign/components/rail.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/rail.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useCallback, useMemo } from 'react' +import { FC, ReactElement, useCallback, useMemo } from 'react' import { Nav, NavLink, Tab, TabContainer } from 'react-bootstrap-5' import MaterialIcon, { AvailableUnfilledIcon, @@ -8,13 +8,14 @@ import { useLayoutContext } from '@/shared/context/layout-context' import { ErrorIndicator, ErrorPane } from './errors' import { RailTabKey, useRailContext } from '../contexts/rail-context' import FileTreeOutlinePanel from './file-tree-outline-panel' -import { ChatIndicator, ChatPane } from './chat' +import { ChatIndicator, ChatPane } from './chat/chat' import getMeta from '@/utils/meta' import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle' import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler' import { useTranslation } from 'react-i18next' import classNames from 'classnames' import IntegrationsPanel from './integrations-panel/integrations-panel' +import OLButton from '@/features/ui/components/ol/ol-button' type RailElement = { icon: AvailableUnfilledIcon @@ -234,3 +235,15 @@ const RailActionElement = ({ action }: { action: RailAction }) => { ) } } + +export const RailPanelHeader: FC<{ title: string }> = ({ title }) => { + const { handlePaneCollapse } = useRailContext() + return ( +
+

{title}

+ + + +
+ ) +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/integrations-panel.scss b/services/web/frontend/stylesheets/bootstrap-5/components/integrations-panel.scss index 17dc42d6ab..7312053d05 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/integrations-panel.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/integrations-panel.scss @@ -3,19 +3,6 @@ height: 100%; } -.integrations-panel-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--spacing-03) var(--spacing-04); -} - -.integrations-panel-title { - font-size: var(--font-size-02); - color: var(--content-primary); - margin-bottom: 0; -} - .integrations-panel-card-button { all: unset; background-color: var(--white); diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/chat.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/chat.scss index e0bcf91c83..f04fd1a905 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/chat.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/chat.scss @@ -5,6 +5,8 @@ --chat-new-message-bg: var(--neutral-70); --chat-new-message-textarea-color: var(--neutral-90); --chat-new-message-textarea-bg: var(--neutral-20); + --chat-new-message-textarea-border: var(--editor-border-color); + --chat-new-message-border: var(--editor-border-color); --chat-message-date-color: var(--neutral-40); --chat-message-name-color: var(--white); } @@ -16,6 +18,8 @@ --chat-new-message-bg: var(--neutral-10); --chat-new-message-textarea-color: var(--neutral-90); --chat-new-message-textarea-bg: var(--white); + --chat-new-message-textarea-border: var(--editor-border-color); + --chat-new-message-border: var(--editor-border-color); --chat-message-date-color: var(--neutral-70); --chat-message-name-color: var(--neutral-70); } @@ -24,11 +28,14 @@ --chat-bg: var(--white); --chat-color: var(--neutral-70); --chat-instructions-color: var(--neutral-70); - --chat-new-message-bg: var(--neutral-10); + --chat-new-message-bg: var(--white); --chat-new-message-textarea-color: var(--neutral-90); --chat-new-message-textarea-bg: var(--white); + --chat-new-message-textarea-border: var(--editor-border-color); + --chat-new-message-border: var(--white); --chat-message-date-color: var(--neutral-70); --chat-message-name-color: var(--neutral-70); + --chat-date-align: center; } .chat { @@ -72,7 +79,7 @@ font-size: var(--font-size-01); color: var(--chat-message-date-color); margin-bottom: calc(var(--line-height-03) / 2); - text-align: right; + text-align: var(--chat-date-align, right); } .message-wrapper { @@ -158,13 +165,13 @@ height: $new-message-height; background-color: var(--chat-new-message-bg); padding: calc(var(--line-height-03) / 4); - border-top: 1px solid var(--editor-border-color); + border-top: 1px solid var(--chat-new-message-border); textarea { overflow: auto; resize: none; border-radius: var(--border-radius-base); - border: 1px solid var(--editor-border-color); + border: 1px solid var(--chat-new-message-textarea-border); height: 100%; width: 100%; color: var(--chat-new-message-textarea-color); @@ -174,3 +181,133 @@ } } } + +.chat-empty-state-placeholder { + display: flex; + justify-content: center; + flex-direction: column; + text-align: center; + height: 100%; + gap: var(--spacing-06); + padding: var(--spacing-02); + + .chat-empty-state-icon { + padding: var(--spacing-08); + font-size: var(--font-size-08); + height: 80px; + width: 80px; + border-radius: 50%; + display: inline-block; + line-height: 32px; + background-color: var(--bg-light-secondary); + + .material-symbols { + font-size: 32px; + } + } + + .chat-empty-state-title { + font-size: var(--font-size-02); + line-height: var(--line-height-02); + font-weight: bold; + } + + .chat-empty-state-body { + font-size: var(--font-size-02); + line-height: var(--line-height-02); + color: var(--content-secondary); + } +} + +.chat-message-redesign { + display: flex; + flex-direction: column; + gap: var(--spacing-01); + + .message-row { + display: flex; + align-items: flex-end; + gap: var(--spacing-03); + } + + .message-avatar, + .message-avatar-placeholder { + flex: 0 0 24px; + } + + .message-avatar .avatar { + width: 24px; + height: 24px; + line-height: 24px; + border-radius: 50%; + text-align: center; + color: var(--white); + text-transform: uppercase; + } + + .message-author, + .message-container { + flex: 1 1 auto; + max-width: calc(100% - 24px - var(--spacing-03)); + } + + .message-container { + display: flex; + justify-content: flex-start; + } + + .message-author { + font-size: var(--font-size-01); + line-height: var(--line-height-01); + } + + .message-content { + background-color: var(--bg-light-secondary); + border-radius: var(--border-radius-large); + padding: var(--spacing-03) var(--spacing-04); + width: fit-content; + max-width: 100%; + overflow-x: auto; + + p { + margin: 0; + } + } + + .message-container.message-from-self { + justify-content: flex-end; + + .message-content { + background-color: var(--bg-accent-03); + } + + &:not(.first-row-in-message) .message-content { + border-top-right-radius: var(--border-radius-base); + } + + &:not(.last-row-in-message) .message-content { + border-bottom-right-radius: var(--border-radius-base); + } + } + + .message-container:not(.message-from-self) { + &:not(.first-row-in-message) .message-content { + border-top-left-radius: var(--border-radius-base); + } + + &:not(.last-row-in-message) .message-content { + border-bottom-left-radius: var(--border-radius-base); + } + } +} + +.chat-panel { + display: flex; + flex-direction: column; + height: 100%; + + .chat-wrapper { + flex: 1 1 auto; + position: relative; + } +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/rail.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/rail.scss index aa1b3ab324..e4a0cf0729 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/rail.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/rail.scss @@ -6,6 +6,20 @@ --ide-rail-link-active-indicator-background: var(--neutral-90); } +.rail-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-03) var(--spacing-04); + background-color: var(--ide-rail-background); +} + +.rail-panel-title { + font-size: var(--font-size-02); + color: var(--content-primary); + margin-bottom: 0; +} + .ide-rail-tab-button { border: 0; background: none; diff --git a/services/web/locales/en.json b/services/web/locales/en.json index d8259e1cad..9f4a77b4c1 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -337,6 +337,7 @@ "collaborate_online_and_offline": "Collaborate online and offline, using your own workflow", "collaboration": "Collaboration", "collaborator": "Collaborator", + "collaborator_chat": "Collaborator chat", "collabratec_account_not_registered": "IEEE Collabratecâ„¢ account not registered. Please connect to Overleaf from IEEE Collabratecâ„¢ or log in with a different account.", "collabs_per_proj": "__collabcount__ collaborators per project", "collabs_per_proj_single": "__collabcount__ collaborator per project", @@ -1381,6 +1382,7 @@ "no_libraries_selected": "No libraries selected", "no_members": "No members", "no_messages": "No messages", + "no_messages_yet": "No messages yet", "no_new_commits_in_github": "No new commits in GitHub since last merge.", "no_one_has_commented_or_left_any_suggestions_yet": "No one has commented or left any suggestions yet.", "no_other_projects_found": "No other projects found, please create another project first", @@ -2067,6 +2069,7 @@ "start_by_fixing_the_first_error_in_your_doc": "Start by fixing the first error in your doc to avoid problems later on.", "start_free_trial": "Start Free Trial!", "start_free_trial_without_exclamation": "Start Free Trial", + "start_the_conversation_by_saying_hello_or_sharing_an_update": "Start the conversation by saying hello or sharing an update", "start_typing_find_your_company": " Start typing to find your company", "start_typing_find_your_organization": "Start typing to find your organization", "start_typing_find_your_university": "Start typing to find your university",