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) + } + }) + }) +})