diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index ba4a45f566..a9dc474bc1 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -1179,7 +1179,6 @@
"no_image_files_found": "",
"no_libraries_selected": "",
"no_members": "",
- "no_messages": "",
"no_messages_yet": "",
"no_new_commits_in_github": "",
"no_one_has_commented_or_left_any_suggestions_yet": "",
@@ -1643,7 +1642,6 @@
"selection_deleted": "",
"send": "",
"send_confirmation_code": "",
- "send_first_message": "",
"send_message": "",
"send_request": "",
"sending": "",
diff --git a/services/web/frontend/js/features/chat/components/chat-indicator.tsx b/services/web/frontend/js/features/chat/components/chat-indicator.tsx
new file mode 100644
index 0000000000..a65ec4675c
--- /dev/null
+++ b/services/web/frontend/js/features/chat/components/chat-indicator.tsx
@@ -0,0 +1,12 @@
+import { RailIndicator } from '@/features/ide-react/components/rail/rail-indicator'
+import { useChatContext } from '@/features/chat/context/chat-context'
+
+export const ChatIndicator = () => {
+ const { unreadMessageCount } = useChatContext()
+ if (unreadMessageCount === 0) {
+ return null
+ }
+ return
+}
+
+export default ChatIndicator
diff --git a/services/web/frontend/js/features/chat/components/chat-pane.tsx b/services/web/frontend/js/features/chat/components/chat-pane.tsx
index 3397e1a722..60d5d0a391 100644
--- a/services/web/frontend/js/features/chat/components/chat-pane.tsx
+++ b/services/web/frontend/js/features/chat/components/chat-pane.tsx
@@ -1,27 +1,23 @@
-import React, { lazy, Suspense, useEffect, useState } from 'react'
-import { useTranslation } from 'react-i18next'
-
-import MessageInput from './message-input'
-import InfiniteScroll from './infinite-scroll'
-import ChatFallbackError from './chat-fallback-error'
-import { useLayoutContext } from '../../../shared/context/layout-context'
-import { useUserContext } from '../../../shared/context/user-context'
-import withErrorBoundary from '../../../infrastructure/error-boundary'
-import { FetchError } from '../../../infrastructure/fetch-json'
-import { useChatContext } from '../context/chat-context'
-import { FullSizeLoadingSpinner } from '../../../shared/components/loading-spinner'
+import ChatFallbackError from '@/features/chat/components/chat-fallback-error'
+import InfiniteScroll from '@/features/chat/components/infinite-scroll'
+import MessageInput from '@/features/chat/components/message-input'
+import { useChatContext } from '@/features/chat/context/chat-context'
+import { FetchError } from '@/infrastructure/fetch-json'
+import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
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 '@/features/ide-react/components/rail/rail-panel-header'
const MessageList = lazy(() => import('./message-list'))
const Loading = () =>
-const ChatPane = React.memo(function ChatPane() {
+export const ChatPane = () => {
const { t } = useTranslation()
-
- const { chatIsOpen } = useLayoutContext()
const user = useUserContext()
-
const {
status,
messages,
@@ -36,23 +32,13 @@ const ChatPane = React.memo(function ChatPane() {
} = useChatContext()
useEffect(() => {
- if (chatIsOpen && !initialMessagesLoaded) {
+ if (!initialMessagesLoaded) {
loadInitialMessages()
}
- }, [chatIsOpen, loadInitialMessages, initialMessagesLoaded])
+ }, [loadInitialMessages, initialMessagesLoaded])
const shouldDisplayPlaceholder = status !== 'pending' && messages.length === 0
- const messageContentCount = messages.length
-
- // Keep the chat pane in the DOM to avoid resetting the form input and re-rendering MathJax content.
- const [chatOpenedOnce, setChatOpenedOnce] = useState(chatIsOpen)
- useEffect(() => {
- if (chatIsOpen) {
- setChatOpenedOnce(true)
- }
- }, [chatIsOpen])
-
if (error) {
// let user try recover from fetch errors
if (error instanceof FetchError) {
@@ -64,51 +50,58 @@ const ChatPane = React.memo(function ChatPane() {
if (!user) {
return null
}
- if (!chatOpenedOnce) {
- return null
- }
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')}
+
+
+
)
}
-export default withErrorBoundary(ChatPane, () =>
)
+export default ChatPane
diff --git a/services/web/frontend/js/features/chat/components/message-and-dropdown.tsx b/services/web/frontend/js/features/chat/components/message-and-dropdown.tsx
index 889a207bb5..0f06f12ae9 100644
--- a/services/web/frontend/js/features/chat/components/message-and-dropdown.tsx
+++ b/services/web/frontend/js/features/chat/components/message-and-dropdown.tsx
@@ -1,39 +1,89 @@
+import type { Message } from '@/features/chat/context/chat-context'
+import { User } from '../../../../../types/user'
import {
- Message as MessageType,
- useChatContext,
-} from '@/features/chat/context/chat-context'
-import classNames from 'classnames'
-import MessageDropdown from '@/features/chat/components/message-dropdown'
+ getBackgroundColorForUserId,
+ hslStringToLuminance,
+} from '@/shared/utils/colors'
import MessageContent from '@/features/chat/components/message-content'
+import classNames from 'classnames'
+import MaterialIcon from '@/shared/components/material-icon'
+import MessageDropdown from '@/features/chat/components/message-dropdown'
import { useFeatureFlag } from '@/shared/context/split-test-context'
+import firstCharacter from '@/shared/utils/first-character'
+
+function getAvatarStyle(user?: User) {
+ if (!user?.id) {
+ // Deleted user
+ return {
+ backgroundColor: 'var(--bg-light-disabled)',
+ borderColor: 'var(--bg-light-disabled)',
+ color: 'var(--content-disabled)',
+ }
+ }
+
+ const backgroundColor = getBackgroundColorForUserId(user.id)
+
+ return {
+ borderColor: backgroundColor,
+ backgroundColor,
+ color:
+ hslStringToLuminance(backgroundColor) < 0.5
+ ? 'var(--content-primary-dark)'
+ : 'var(--content-primary)',
+ }
+}
export function MessageAndDropdown({
message,
fromSelf,
+ isLast,
+ isFirst,
}: {
- message: MessageType
+ message: Message
fromSelf: boolean
+ isLast: boolean
+ isFirst: boolean
}) {
- const { idOfMessageBeingEdited } = useChatContext()
const hasChatEditDelete = useFeatureFlag('chat-edit-delete')
- const editing = idOfMessageBeingEdited === message.id
-
return (
-
- {hasChatEditDelete && fromSelf && !message.pending && !editing ? (
-
- ) : null}
-
-
+
+ {!fromSelf && isLast ? (
+
+
+ {message.user?.id && message.user.email ? (
+ firstCharacter(message.user.first_name || message.user.email)
+ ) : (
+
+ )}
+
+
+ ) : (
+
+ )}
+
+
+ {hasChatEditDelete && fromSelf ? (
+
+ ) : null}
+
+
+
+
)
diff --git a/services/web/frontend/js/features/chat/components/message-group.tsx b/services/web/frontend/js/features/chat/components/message-group.tsx
index a48acddcba..9c28871d82 100644
--- a/services/web/frontend/js/features/chat/components/message-group.tsx
+++ b/services/web/frontend/js/features/chat/components/message-group.tsx
@@ -1,9 +1,7 @@
-import { getHueForUserId } from '@/shared/utils/colors'
+import { MessageAndDropdown } from './message-and-dropdown'
+import { useTranslation } from 'react-i18next'
import type { Message as MessageType } from '@/features/chat/context/chat-context'
import { User } from '../../../../../types/user'
-import classNames from 'classnames'
-import { MessageAndDropdown } from '@/features/chat/components/message-and-dropdown'
-import { useTranslation } from 'react-i18next'
export interface MessageGroupProps {
messages: MessageType[]
@@ -11,50 +9,40 @@ export interface MessageGroupProps {
fromSelf: boolean
}
-function hue(user?: User) {
- return user ? getHueForUserId(user.id) : 0
-}
-
-function getMessageStyle(user?: User) {
- return {
- borderColor: `hsl(${hue(user)}, 85%, 40%)`,
- backgroundColor: `hsl(${hue(user)}, 85%, 40%`,
- }
-}
-
-function getArrowStyle(user?: User) {
- return {
- borderColor: `hsl(${hue(user)}, 85%, 40%)`,
- }
-}
-
function MessageGroup({ messages, user, fromSelf }: MessageGroupProps) {
const { t } = useTranslation()
return (
-
- {!fromSelf && (
-
-
- {user ? user.first_name || user.email : t('deleted_user')}
-
+
+
+
+
+ {!fromSelf && (
+
+
+ {user?.id && user.email
+ ? user.first_name || user.email
+ : t('deleted_user')}
+
+
+ )}
- )}
-
- {!fromSelf &&
}
-
- {messages.map(message => (
+
+ {messages.map(message => {
+ const nonDeletedMessages = messages.filter(m => !m.deleted)
+ const nonDeletedIndex = nonDeletedMessages.findIndex(
+ m => m.id === message.id
+ )
+ return (
- ))}
-
+ )
+ })}
)
}
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 d541a3c4f3..25d3901daa 100644
--- a/services/web/frontend/js/features/chat/components/message-list.tsx
+++ b/services/web/frontend/js/features/chat/components/message-list.tsx
@@ -3,7 +3,6 @@ import type { Message as MessageType } from '@/features/chat/context/chat-contex
import { useUserContext } from '@/shared/context/user-context'
import { User } from '../../../../../types/user'
import MessageGroup from '@/features/chat/components/message-group'
-import MessageGroupRedesign from '@/features/ide-redesign/components/chat/message-group'
const FIVE_MINUTES = 5 * 60 * 1000
const TIMESTAMP_GROUP_SIZE = FIVE_MINUTES
@@ -19,7 +18,6 @@ function formatTimestamp(date: moment.MomentInput) {
interface MessageListProps {
messages: MessageType[]
resetUnreadMessages(...args: unknown[]): unknown
- newDesign?: boolean
}
type MessageGroupType = {
@@ -63,15 +61,9 @@ function groupMessages(messages: MessageType[]) {
return groups
}
-function MessageList({
- messages,
- resetUnreadMessages,
- newDesign,
-}: MessageListProps) {
+function MessageList({ messages, resetUnreadMessages }: MessageListProps) {
const user = useUserContext()
- const MessageGroupComponent = newDesign ? MessageGroupRedesign : MessageGroup
-
function shouldRenderDate(messageIndex: number) {
if (messageIndex === 0) {
return true
@@ -110,7 +102,7 @@ function MessageList({
)}
-
import('../../../chat/components/message-list'))
-
-export const ChatIndicator = () => {
- const { unreadMessageCount } = useChatContext()
- if (unreadMessageCount === 0) {
- return null
- }
- return
-}
-
-const Loading = () =>
-
-export const ChatPane = () => {
- const { t } = useTranslation()
- const user = useUserContext()
- const {
- status,
- messages,
- initialMessagesLoaded,
- atEnd,
- loadInitialMessages,
- loadMoreMessages,
- reset,
- sendMessage,
- markMessagesAsRead,
- error,
- } = useChatContext()
-
- useEffect(() => {
- if (!initialMessagesLoaded) {
- loadInitialMessages()
- }
- }, [loadInitialMessages, initialMessagesLoaded])
-
- const shouldDisplayPlaceholder = status !== 'pending' && messages.length === 0
-
- if (error) {
- // let user try recover from fetch errors
- if (error instanceof FetchError) {
- return
- }
- throw error
- }
-
- if (!user) {
- return null
- }
-
- return (
-
-
-
-
-
-
- )
-}
-
-function Placeholder() {
- const { t } = useTranslation()
- return (
-
-
-
-
-
-
-
-
{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-and-dropdown.tsx b/services/web/frontend/js/features/ide-redesign/components/chat/message-and-dropdown.tsx
deleted file mode 100644
index 8f95e01ac3..0000000000
--- a/services/web/frontend/js/features/ide-redesign/components/chat/message-and-dropdown.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import type { Message } from '@/features/chat/context/chat-context'
-import { User } from '../../../../../../types/user'
-import {
- getBackgroundColorForUserId,
- hslStringToLuminance,
-} from '@/shared/utils/colors'
-import MessageContent from '@/features/chat/components/message-content'
-import classNames from 'classnames'
-import MaterialIcon from '@/shared/components/material-icon'
-import MessageDropdown from '@/features/chat/components/message-dropdown'
-import { useFeatureFlag } from '@/shared/context/split-test-context'
-
-function getAvatarStyle(user?: User) {
- if (!user?.id) {
- // Deleted user
- return {
- backgroundColor: 'var(--bg-light-disabled)',
- borderColor: 'var(--bg-light-disabled)',
- color: 'var(--content-disabled)',
- }
- }
-
- const backgroundColor = getBackgroundColorForUserId(user.id)
-
- return {
- borderColor: backgroundColor,
- backgroundColor,
- color:
- hslStringToLuminance(backgroundColor) < 0.5
- ? 'var(--content-primary-dark)'
- : 'var(--content-primary)',
- }
-}
-
-export function MessageAndDropdown({
- message,
- fromSelf,
- isLast,
- isFirst,
-}: {
- message: Message
- fromSelf: boolean
- isLast: boolean
- isFirst: boolean
-}) {
- const hasChatEditDelete = useFeatureFlag('chat-edit-delete')
-
- return (
-
- <>
- {!fromSelf && isLast ? (
-
-
- {message.user?.id && message.user.email ? (
- message.user.first_name?.charAt(0) ||
- message.user.email.charAt(0)
- ) : (
-
- )}
-
-
- ) : (
-
- )}
-
-
- {hasChatEditDelete && fromSelf ? (
-
- ) : null}
-
-
-
-
-
- >
-
- )
-}
diff --git a/services/web/frontend/js/features/ide-redesign/components/chat/message-group.tsx b/services/web/frontend/js/features/ide-redesign/components/chat/message-group.tsx
deleted file mode 100644
index 6fd71f3ffb..0000000000
--- a/services/web/frontend/js/features/ide-redesign/components/chat/message-group.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { MessageGroupProps } from '@/features/chat/components/message-group'
-import { MessageAndDropdown } from './message-and-dropdown'
-import { useTranslation } from 'react-i18next'
-
-function MessageGroup({ messages, user, fromSelf }: MessageGroupProps) {
- const { t } = useTranslation()
-
- return (
-
-
-
-
- {!fromSelf && (
-
-
- {user?.id && user.email
- ? user.first_name || user.email
- : t('deleted_user')}
-
-
- )}
-
-
- {messages.map((message, index) => {
- const nonDeletedMessages = messages.filter(m => !m.deleted)
- const nonDeletedIndex = nonDeletedMessages.findIndex(
- m => m.id === message.id
- )
- return (
-
- )
- })}
-
- )
-}
-
-export default MessageGroup
diff --git a/services/web/frontend/js/shared/utils/first-character.ts b/services/web/frontend/js/shared/utils/first-character.ts
new file mode 100644
index 0000000000..5444a1e93a
--- /dev/null
+++ b/services/web/frontend/js/shared/utils/first-character.ts
@@ -0,0 +1,26 @@
+export default function firstCharacter(str: string): string {
+ if (!str) {
+ return ''
+ }
+
+ if (Intl?.Segmenter) {
+ try {
+ const segmenter = new Intl.Segmenter(undefined, {
+ granularity: 'grapheme',
+ })
+ // eslint-disable-next-line no-unreachable-loop
+ for (const { segment } of segmenter.segment(str)) {
+ return segment
+ }
+ } catch {
+ // Fall back to the code point approach below.
+ }
+ }
+
+ // NOTE: .charAt(0), [0], and .substring(0, 1) will all split multi-byte
+ // characters (e.g. emojis) into multiple characters, but the spread operator
+ // will keep them mostly intact. This still isn't perfect, so the grapheme
+ // segmenter above is preferred when available.
+ const [first] = [...str]
+ return first ?? ''
+}
diff --git a/services/web/frontend/stylesheets/pages/editor/chat.scss b/services/web/frontend/stylesheets/pages/editor/chat.scss
index 97daccae70..ce2e2a7aac 100644
--- a/services/web/frontend/stylesheets/pages/editor/chat.scss
+++ b/services/web/frontend/stylesheets/pages/editor/chat.scss
@@ -1,17 +1,4 @@
:root {
- --chat-bg: var(--neutral-80);
- --chat-color: var(--white);
- --chat-instructions-color: var(--neutral-20);
- --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);
-}
-
-.ide-redesign-main {
--chat-bg: var(--bg-dark-primary);
--chat-color: var(--content-primary);
--chat-instructions-color: var(--content-primary-dark);
@@ -20,34 +7,21 @@
--chat-new-message-textarea-bg: var(--bg-dark-primary);
--chat-message-date-color: var(--content-secondary-dark);
--chat-message-name-color: var(--content-secondary-dark);
+ --chat-new-message-border: var(--border-divider-dark);
--chat-date-align: center;
}
@include theme('light') {
--chat-bg: var(--white);
- --chat-color: var(--neutral-70);
+ --chat-color: var(--content-primary);
--chat-instructions-color: var(--neutral-70);
--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-new-message-border: var(--white);
--chat-message-date-color: var(--neutral-70);
--chat-message-name-color: var(--neutral-70);
-
- .ide-redesign-main {
- --chat-bg: var(--white);
- --chat-color: var(--content-primary);
- --chat-instructions-color: var(--neutral-70);
- --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(--white);
- --chat-message-date-color: var(--neutral-70);
- --chat-message-name-color: var(--neutral-70);
- --chat-date-align: center;
- }
+ --chat-date-align: center;
}
.chat {
@@ -55,19 +29,6 @@
color: var(--chat-color);
- .no-messages {
- padding: calc(var(--line-height-03) / 2);
- color: var(--chat-instructions-color);
- }
-
- .first-message {
- position: absolute;
- bottom: 0;
- width: 100%;
- padding: calc(var(--line-height-03) / 2);
- color: var(--chat-instructions-color);
- }
-
.chat-error {
position: absolute;
top: 0;
@@ -94,97 +55,6 @@
text-align: var(--chat-date-align, right);
}
- .message-wrapper {
- .name {
- font-size: var(--font-size-01);
- color: var(--chat-message-name-color);
- margin-bottom: var(--spacing-02);
- min-height: 16px;
- }
-
- .message {
- border-left: 3px solid transparent;
- font-size: var(--font-size-02);
- box-shadow: none;
- border-radius: var(--border-radius-base);
- position: relative;
-
- .message-and-dropdown {
- clear: both;
-
- &.pending-message {
- opacity: 0.6;
- }
- }
-
- .message-dropdown:not(.show) {
- visibility: hidden;
- }
-
- .message-content {
- padding: var(--spacing-03) var(--spacing-05);
- overflow-x: auto;
- color: var(--white);
- font-weight: bold;
-
- a {
- color: var(--white);
- }
-
- .message-edited {
- @include body-xs;
-
- font-weight: normal;
- }
- }
-
- .arrow {
- transform: rotate(90deg);
- right: 90%;
- top: -15px;
- border: solid;
- content: ' ';
- height: 0;
- width: 0;
- position: absolute;
- pointer-events: none;
- border-top-color: transparent !important;
- border-bottom-color: transparent !important;
- border-width: 10px;
- }
-
- .message-dropdown-menu {
- min-width: var(--bs-dropdown-min-width);
- }
-
- .message-dropdown-menu-btn {
- @include reset-button;
- @include action-button;
-
- color: var(--white);
- padding: 0;
- width: 30px;
- height: 30px;
- }
- }
-
- &.own-message-wrapper .message-and-dropdown:hover {
- background-color: rgb($neutral-90, 0.08);
-
- .message-dropdown {
- visibility: visible;
- }
- }
-
- p {
- margin-bottom: calc(var(--line-height-03) / 4);
-
- &:last-child {
- margin-bottom: 0;
- }
- }
- }
-
&:not(.self) {
.message {
.arrow {
@@ -195,17 +65,6 @@
&.self {
margin-top: var(--line-height-03);
-
- .message-wrapper .message {
- border-left: none;
- border-right: 3px solid transparent;
-
- .arrow {
- left: 100%;
- right: auto;
- border-right-color: transparent !important;
- }
- }
}
}
}
@@ -223,7 +82,7 @@
overflow: auto;
resize: none;
border-radius: var(--border-radius-base);
- border: 1px solid var(--chat-new-message-textarea-border);
+ border: 1px solid var(--border-divider-themed);
height: 100%;
width: 100%;
color: var(--chat-new-message-textarea-color);
@@ -273,7 +132,7 @@
}
}
-.chat-message-redesign {
+.chat-message {
display: flex;
flex-direction: column;
gap: var(--spacing-01);
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 5bd55a2914..8dfeaf3c10 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -1519,7 +1519,6 @@
"no_image_files_found": "No image files found",
"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.",
@@ -2110,7 +2109,6 @@
"selection_deleted": "Selection deleted",
"send": "Send",
"send_confirmation_code": "Send confirmation code",
- "send_first_message": "Send your first message to your collaborators",
"send_message": "Send message",
"send_request": "Send request",
"send_reset_link": "Send reset link",
diff --git a/services/web/test/frontend/features/chat/components/chat-pane.test.tsx b/services/web/test/frontend/features/chat/components/chat.test.tsx
similarity index 90%
rename from services/web/test/frontend/features/chat/components/chat-pane.test.tsx
rename to services/web/test/frontend/features/chat/components/chat.test.tsx
index f990a8c6ce..bac3f4f856 100644
--- a/services/web/test/frontend/features/chat/components/chat-pane.test.tsx
+++ b/services/web/test/frontend/features/chat/components/chat.test.tsx
@@ -78,7 +78,9 @@ describe('', function () {
// should now reconnect with placeholder message
fireEvent.click(reconnectButton)
- await screen.findByText('Send your first message to your collaborators')
+ await screen.findByText(
+ 'Start the conversation by saying hello or sharing an update'
+ )
})
it('a loading spinner is rendered while the messages are loading, then disappears', async function () {
@@ -102,7 +104,9 @@ describe('', function () {
renderWithEditorContext(, { user })
- await screen.findByText('Send your first message to your collaborators')
+ await screen.findByText(
+ 'Start the conversation by saying hello or sharing an update'
+ )
})
it('is not rendered when messages are displayed', function () {
@@ -111,7 +115,9 @@ describe('', function () {
renderWithEditorContext(, { user })
expect(
- screen.queryByText('Send your first message to your collaborators')
+ screen.queryByText(
+ 'Start the conversation by saying hello or sharing an update'
+ )
).to.not.exist
})
})
diff --git a/services/web/test/frontend/shared/utils/first-character.test.ts b/services/web/test/frontend/shared/utils/first-character.test.ts
new file mode 100644
index 0000000000..252b1063b2
--- /dev/null
+++ b/services/web/test/frontend/shared/utils/first-character.test.ts
@@ -0,0 +1,51 @@
+import firstCharacter from '@/shared/utils/first-character'
+import { expect } from 'chai'
+import sinon from 'sinon'
+
+const CASES_ALL = [
+ // Regular ASCII characters
+ ['Hello', 'H'],
+ // Multi-byte characters (e.g. Chinese)
+ ['ไฝ ๅฅฝ', 'ไฝ '],
+ // Multi-byte characters (e.g. emojis)
+ ['๐ Smile', '๐'],
+ // Empty string
+ ['', ''],
+]
+
+const CASES_SEGMENTER = [
+ // Regional indicator symbols (e.g. flag emojis)
+ ['๐ฉ๐ฐ Denmark', '๐ฉ๐ฐ'],
+ // ZWJ sequences (e.g. family emoji)
+ ['๐ฉโ๐ฉโ๐งโ๐ฆ Family', '๐ฉโ๐ฉโ๐งโ๐ฆ'],
+ // Combining characters (e.g. accented characters)
+ ['e\u0301 Accented', 'e\u0301'],
+ ['eฬ Accented', 'eฬ'],
+]
+
+describe('firstCharacter', function () {
+ it('works for different types of strings', function () {
+ for (const [input, expected] of CASES_ALL) {
+ expect(firstCharacter(input)).to.equal(expected)
+ }
+ for (const [input, expected] of CASES_SEGMENTER) {
+ expect(firstCharacter(input)).to.equal(expected)
+ }
+ })
+
+ describe('when Intl.Segmenter is unavailable', function () {
+ before(function () {
+ this.segmenterStub = sinon.stub(Intl, 'Segmenter').value(undefined)
+ })
+
+ after(function () {
+ this.segmenterStub.restore()
+ })
+
+ it('falls back to the code point approach', function () {
+ for (const [input, expected] of CASES_ALL) {
+ expect(firstCharacter(input)).to.equal(expected)
+ }
+ })
+ })
+})