mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-21 18:38:17 +00:00
Merge pull request #23669 from overleaf/mj-ide-chat-look
[web] Update chat in editor redesign GitOrigin-RevId: 79c79eb9c774fbaa1a5a1e15386b629cc03239b3
This commit is contained in:
parent
a6d71eb555
commit
fc19e1d34a
11 changed files with 296 additions and 67 deletions
|
@ -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": "",
|
||||
|
|
|
@ -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) {
|
|||
</time>
|
||||
</div>
|
||||
)}
|
||||
<Message
|
||||
<MessageComponent
|
||||
message={message}
|
||||
fromSelf={message.user ? message.user.id === user.id : false}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 = () => <FullSizeLoadingSpinner delay={500} className="pt-4" />
|
|||
|
||||
export const ChatPane = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const user = useUserContext()
|
||||
const {
|
||||
status,
|
||||
|
@ -65,44 +66,55 @@ export const ChatPane = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<aside className="chat">
|
||||
<InfiniteScroll
|
||||
atEnd={atEnd}
|
||||
className="messages"
|
||||
fetchData={loadMoreMessages}
|
||||
isLoading={status === 'pending'}
|
||||
itemCount={messageContentCount}
|
||||
>
|
||||
<div>
|
||||
<h2 className="visually-hidden">{t('chat')}</h2>
|
||||
<Suspense fallback={<Loading />}>
|
||||
{status === 'pending' && <Loading />}
|
||||
{shouldDisplayPlaceholder && <Placeholder />}
|
||||
<MessageList
|
||||
messages={messages}
|
||||
resetUnreadMessages={markMessagesAsRead}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
<MessageInput
|
||||
resetUnreadMessages={markMessagesAsRead}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
</aside>
|
||||
<div className="chat-panel">
|
||||
<RailPanelHeader title={t('collaborator_chat')} />
|
||||
<div className="chat-wrapper">
|
||||
<aside className="chat">
|
||||
<InfiniteScroll
|
||||
atEnd={atEnd}
|
||||
className="messages"
|
||||
fetchData={loadMoreMessages}
|
||||
isLoading={status === 'pending'}
|
||||
itemCount={messageContentCount}
|
||||
>
|
||||
<div className={classNames({ 'h-100': shouldDisplayPlaceholder })}>
|
||||
<h2 className="visually-hidden">{t('chat')}</h2>
|
||||
<Suspense fallback={<Loading />}>
|
||||
{status === 'pending' && <Loading />}
|
||||
{shouldDisplayPlaceholder && <Placeholder />}
|
||||
<MessageList
|
||||
messages={messages}
|
||||
resetUnreadMessages={markMessagesAsRead}
|
||||
newDesign
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
<MessageInput
|
||||
resetUnreadMessages={markMessagesAsRead}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Placeholder() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<div className="no-messages text-center small">{t('no_messages')}</div>
|
||||
<div className="first-message text-center">
|
||||
{t('send_first_message')}
|
||||
<br />
|
||||
<MaterialIcon type="arrow_downward" />
|
||||
<div className="chat-empty-state-placeholder">
|
||||
<div>
|
||||
<span className="chat-empty-state-icon">
|
||||
<MaterialIcon type="forum" />
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
<div>
|
||||
<div className="chat-empty-state-title">{t('no_messages_yet')}</div>
|
||||
<div className="chat-empty-state-body">
|
||||
{t('start_the_conversation_by_saying_hello_or_sharing_an_update')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -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 (
|
||||
<div className="chat-message-redesign">
|
||||
<div className="message-row">
|
||||
<div className="message-avatar-placeholder" />
|
||||
{!fromSelf && (
|
||||
<div className="message-author">
|
||||
<span>{message.user.first_name || message.user.email}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{message.contents.map((content, index) => (
|
||||
<div key={index} className="message-row">
|
||||
<>
|
||||
{!fromSelf && index === message.contents.length - 1 ? (
|
||||
<div className="message-avatar">
|
||||
<div className="avatar" style={getAvatarStyle(message.user)}>
|
||||
{message.user.first_name?.charAt(0) ||
|
||||
message.user.email.charAt(0)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="message-avatar-placeholder" />
|
||||
)}
|
||||
<div
|
||||
className={classNames('message-container', {
|
||||
'message-from-self': fromSelf,
|
||||
'first-row-in-message': index === 0,
|
||||
'last-row-in-message': index === message.contents.length - 1,
|
||||
})}
|
||||
>
|
||||
<div className="message-content">
|
||||
<MessageContent content={content} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Message
|
|
@ -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 (
|
||||
<div className="integrations-panel">
|
||||
<header className="integrations-panel-header">
|
||||
<h4 className="integrations-panel-title">Integrations</h4>
|
||||
<OlButton onClick={handlePaneCollapse} variant="ghost" size="sm">
|
||||
<MaterialIcon type="close" />
|
||||
</OlButton>
|
||||
</header>
|
||||
<RailPanelHeader title={t('integrations')} />
|
||||
{integrationPanelComponents.map(
|
||||
({ import: { default: Component }, path }) => (
|
||||
<Component key={path} />
|
||||
|
|
|
@ -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 (
|
||||
<header className="rail-panel-header">
|
||||
<h4 className="rail-panel-title">{title}</h4>
|
||||
<OLButton onClick={handlePaneCollapse} variant="ghost" size="sm">
|
||||
<MaterialIcon type="close" />
|
||||
</OLButton>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue