-
-
-
-
-
-
- {joinedTypingUsers}
+ {displayInfo.length > 0 && (
+
0 ? overflowCount : undefined}
+ size='md'
+ />
+ )}
+
);
diff --git a/src/components/TypingIndicator/TypingIndicatorDots.tsx b/src/components/TypingIndicator/TypingIndicatorDots.tsx
new file mode 100644
index 0000000000..c88d04e4cc
--- /dev/null
+++ b/src/components/TypingIndicator/TypingIndicatorDots.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+/** Three dots for typing indicator; fill and opacity animation come from TypingIndicator.scss (--chat-text-typing-indicator). */
+export const TypingIndicatorDots = () => (
+
+);
diff --git a/src/components/TypingIndicator/TypingIndicatorHeader.tsx b/src/components/TypingIndicator/TypingIndicatorHeader.tsx
new file mode 100644
index 0000000000..751fa0cdeb
--- /dev/null
+++ b/src/components/TypingIndicator/TypingIndicatorHeader.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import clsx from 'clsx';
+
+import { TypingIndicatorDots } from './TypingIndicatorDots';
+import { useChannelStateContext } from '../../context/ChannelStateContext';
+import { useChatContext } from '../../context/ChatContext';
+import { useTranslationContext } from '../../context/TranslationContext';
+import { useTypingContext } from '../../context/TypingContext';
+import { useThreadContext } from '../Threads';
+
+import { useDebouncedTypingActive } from './hooks/useDebouncedTypingActive';
+
+export type TypingIndicatorHeaderProps = {
+ /** When true, show typing in the current thread only; when false, show typing in the channel. */
+ threadList?: boolean;
+};
+
+/**
+ * Inline typing indicator for ChannelHeader or ThreadHeader: text (1/2/3+ people) followed by animated dots.
+ * Only shows other participants; respects channelConfig.typing_events.
+ */
+export const TypingIndicatorHeader = (props: TypingIndicatorHeaderProps) => {
+ const { threadList = false } = props;
+
+ const { t } = useTranslationContext();
+ const { channelConfig, thread } = useChannelStateContext('TypingIndicatorHeader');
+ const threadInstance = useThreadContext();
+ const parentId = threadInstance?.id ?? thread?.id;
+ const { client } = useChatContext('TypingIndicatorHeader');
+ const { typing = {} } = useTypingContext('TypingIndicatorHeader');
+
+ const typingInChannel = !threadList
+ ? Object.values(typing).filter(
+ ({ parent_id, user }) => user?.id !== client.user?.id && !parent_id,
+ )
+ : [];
+
+ const typingInThread = threadList
+ ? Object.values(typing).filter(
+ ({ parent_id, user }) => user?.id !== client.user?.id && parent_id === parentId,
+ )
+ : [];
+
+ const typingUsers = threadList ? typingInThread : typingInChannel;
+ const { displayUsers } = useDebouncedTypingActive(typingUsers);
+
+ if (channelConfig?.typing_events === false || displayUsers.length === 0) {
+ return null;
+ }
+
+ const names = displayUsers.map(({ user }) => user?.name ?? '');
+ let label: string;
+ if (names.length === 1) {
+ label = t('{{ typing }} is typing', { typing: names[0] });
+ } else if (names.length === 2) {
+ label = t('{{ typing }} are typing', {
+ typing: `${names[0]} and ${names[1]}`,
+ });
+ } else {
+ label = t('{{ count }} people are typing', { count: names.length });
+ }
+
+ return (
+
+ {label}
+
+
+
+
+ );
+};
diff --git a/src/components/TypingIndicator/__tests__/TypingIndicator.test.js b/src/components/TypingIndicator/__tests__/TypingIndicator.test.js
index 283da34ba3..8e96c6b82a 100644
--- a/src/components/TypingIndicator/__tests__/TypingIndicator.test.js
+++ b/src/components/TypingIndicator/__tests__/TypingIndicator.test.js
@@ -23,7 +23,12 @@ expect.extend(toHaveNoViolations);
const me = generateUser();
-async function renderComponent(typing = {}, threadList, value = {}) {
+async function renderComponent(
+ typing = {},
+ threadList,
+ value = {},
+ typingIndicatorProps = {},
+) {
const client = await getTestClientWithUser(me);
return render(
@@ -31,7 +36,7 @@ async function renderComponent(typing = {}, threadList, value = {}) {
-
+
@@ -84,7 +89,7 @@ describe('TypingIndicator', () => {
});
expect(container.firstChild).toHaveClass('str-chat__typing-indicator--typing');
- expect(screen.getByText('{{ user }} is typing...')).toBeInTheDocument();
+ expect(screen.getByTestId('typing-indicator')).toBeInTheDocument();
const results = await axe(container);
expect(results).toHaveNoViolations();
});
@@ -97,7 +102,7 @@ describe('TypingIndicator', () => {
});
expect(container.firstChild).toHaveClass('str-chat__typing-indicator--typing');
- expect(screen.getByText('{{ user }} is typing...')).toBeInTheDocument();
+ expect(screen.getByTestId('typing-indicator')).toBeInTheDocument();
const results = await axe(container);
expect(results).toHaveNoViolations();
});
@@ -109,9 +114,7 @@ describe('TypingIndicator', () => {
joris: { user: { id: 'joris', image: 'joris.jpg' } },
margriet: { user: { id: 'margriet', image: 'margriet.jpg' } },
});
- expect(
- screen.getByText('{{ users }} and {{ user }} are typing...'),
- ).toBeInTheDocument();
+ expect(screen.getByTestId('typing-indicator')).toBeInTheDocument();
const results = await axe(container);
expect(results).toHaveNoViolations();
});
@@ -124,11 +127,21 @@ describe('TypingIndicator', () => {
joris: { user: { id: 'joris', image: 'joris.jpg' } },
margriet: { user: { id: 'margriet', image: 'margriet.jpg' } },
});
- expect(screen.getByText('{{ users }} and more are typing...')).toBeInTheDocument();
+ expect(screen.getByTestId('typing-indicator')).toBeInTheDocument();
const results = await axe(container);
expect(results).toHaveNoViolations();
});
+ it('should render null when isMessageListScrolledToBottom is false', async () => {
+ const { container } = await renderComponent(
+ { jessica: { user: { id: 'jessica', image: 'jessica.jpg' } } },
+ false,
+ {},
+ { isMessageListScrolledToBottom: false },
+ );
+ expect(container).toBeEmptyDOMElement();
+ });
+
it('should render null if typing_events is disabled', async () => {
const client = await getTestClientWithUser();
const ch = generateChannel({ config: { typing_events: false } });
diff --git a/src/components/TypingIndicator/hooks/__tests__/useDebouncedTypingActive.test.js b/src/components/TypingIndicator/hooks/__tests__/useDebouncedTypingActive.test.js
new file mode 100644
index 0000000000..e41d381a35
--- /dev/null
+++ b/src/components/TypingIndicator/hooks/__tests__/useDebouncedTypingActive.test.js
@@ -0,0 +1,61 @@
+import { act, renderHook } from '@testing-library/react';
+import { useDebouncedTypingActive } from '../useDebouncedTypingActive';
+
+jest.useFakeTimers();
+
+const entry = (id, name = `User ${id}`) => ({
+ user: { id, image: undefined, name },
+});
+
+describe('useDebouncedTypingActive', () => {
+ it('returns empty displayUsers when typingUsers is empty initially', () => {
+ const { result } = renderHook(() => useDebouncedTypingActive([]));
+ expect(result.current.displayUsers).toEqual([]);
+ });
+
+ it('returns displayUsers when typingUsers is non-empty', () => {
+ const users = [entry('1'), entry('2')];
+ const { result } = renderHook(() => useDebouncedTypingActive(users));
+ expect(result.current.displayUsers).toHaveLength(2);
+ expect(result.current.displayUsers[0].user?.id).toBe('1');
+ });
+
+ it('keeps last displayUsers for delayMs after typingUsers becomes empty', () => {
+ const users = [entry('1')];
+ const { rerender, result } = renderHook(
+ ({ typingUsers }) => useDebouncedTypingActive(typingUsers, 2000),
+ { initialProps: { typingUsers: users } },
+ );
+ expect(result.current.displayUsers).toHaveLength(1);
+
+ rerender({ typingUsers: [] });
+ expect(result.current.displayUsers).toHaveLength(1);
+
+ act(() => {
+ jest.advanceTimersByTime(1999);
+ });
+ expect(result.current.displayUsers).toHaveLength(1);
+
+ act(() => {
+ jest.advanceTimersByTime(1);
+ });
+ expect(result.current.displayUsers).toEqual([]);
+ });
+
+ it('clears hide timer when typingUsers becomes non-empty again before delay', () => {
+ const { rerender, result } = renderHook(
+ ({ typingUsers }) => useDebouncedTypingActive(typingUsers, 2000),
+ { initialProps: { typingUsers: [entry('1')] } },
+ );
+ rerender({ typingUsers: [] });
+ act(() => {
+ jest.advanceTimersByTime(500);
+ });
+ rerender({ typingUsers: [entry('1'), entry('2')] });
+ expect(result.current.displayUsers).toHaveLength(2);
+ act(() => {
+ jest.advanceTimersByTime(2000);
+ });
+ expect(result.current.displayUsers).toHaveLength(2);
+ });
+});
diff --git a/src/components/TypingIndicator/hooks/index.ts b/src/components/TypingIndicator/hooks/index.ts
new file mode 100644
index 0000000000..65f3d81b83
--- /dev/null
+++ b/src/components/TypingIndicator/hooks/index.ts
@@ -0,0 +1,2 @@
+export { useDebouncedTypingActive } from './useDebouncedTypingActive';
+export type { TypingEntry } from './useDebouncedTypingActive';
diff --git a/src/components/TypingIndicator/hooks/useDebouncedTypingActive.ts b/src/components/TypingIndicator/hooks/useDebouncedTypingActive.ts
new file mode 100644
index 0000000000..f2aa09f1f2
--- /dev/null
+++ b/src/components/TypingIndicator/hooks/useDebouncedTypingActive.ts
@@ -0,0 +1,52 @@
+import { useEffect, useRef, useState } from 'react';
+
+const DEFAULT_HIDE_DELAY_MS = 2000;
+
+export type TypingEntry = {
+ user?: { id?: string; name?: string; image?: string };
+ parent_id?: string;
+};
+
+/**
+ * Debounces hiding the typing indicator: when typing users go to zero, keep showing for delayMs
+ * to avoid flicker when typing stops and starts again quickly. Returns the list to display
+ * (current typers or last non-empty list during the debounce period). Show the indicator
+ * when displayUsers.length > 0.
+ */
+export function useDebouncedTypingActive(
+ typingUsers: readonly TypingEntry[],
+ delayMs: number = DEFAULT_HIDE_DELAY_MS,
+): { displayUsers: TypingEntry[] } {
+ const [displayUsers, setDisplayUsers] = useState
([]);
+ const timerRef = useRef | null>(null);
+ const hadTypersRef = useRef(false);
+
+ useEffect(() => {
+ if (typingUsers.length > 0) {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ timerRef.current = null;
+ }
+ hadTypersRef.current = true;
+ setDisplayUsers([...typingUsers]);
+ return;
+ }
+
+ if (typingUsers.length === 0 && hadTypersRef.current && !timerRef.current) {
+ timerRef.current = setTimeout(() => {
+ timerRef.current = null;
+ hadTypersRef.current = false;
+ setDisplayUsers([]);
+ }, delayMs);
+ }
+ }, [typingUsers, delayMs]);
+
+ useEffect(
+ () => () => {
+ if (timerRef.current) clearTimeout(timerRef.current);
+ },
+ [],
+ );
+
+ return { displayUsers };
+}
diff --git a/src/components/TypingIndicator/index.ts b/src/components/TypingIndicator/index.ts
index 07efe0caac..61c59a58f9 100644
--- a/src/components/TypingIndicator/index.ts
+++ b/src/components/TypingIndicator/index.ts
@@ -1 +1,2 @@
export * from './TypingIndicator';
+export * from './TypingIndicatorHeader';
diff --git a/src/components/TypingIndicator/styling/TypingIndicator.scss b/src/components/TypingIndicator/styling/TypingIndicator.scss
new file mode 100644
index 0000000000..436d71c961
--- /dev/null
+++ b/src/components/TypingIndicator/styling/TypingIndicator.scss
@@ -0,0 +1,128 @@
+/* Shared transition: fade-in when subtitle or typing indicator content appears */
+@keyframes str-chat__typing-indicator-fade-in {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.str-chat__subtitle-content-transition {
+ animation: str-chat__typing-indicator-fade-in 0.2s ease-out;
+ display: inline-block;
+}
+
+/* Inline typing indicator: avatar stack + bubble with animated dots (message-row layout) */
+.str-chat__typing-indicator {
+ display: flex;
+ align-items: flex-end;
+ width: 100%;
+ max-width: calc(var(--str-chat__message-composer-max-width) + var(--str-chat__message-composer-padding));
+ margin: auto;
+ gap: var(--spacing-xs);
+ padding-block: var(--spacing-xs);
+ padding-inline: var(--str-chat__message-composer-padding);
+
+ &.str-chat__typing-indicator--with-transition {
+ animation: str-chat__typing-indicator-fade-in 0.25s ease-out;
+ }
+}
+
+.str-chat__typing-indicator__bubble {
+ display: inline-flex;
+ align-items: center;
+ min-height: 36px;
+ max-height: 36px;
+ padding-block: var(--spacing-xs);
+ padding-inline: var(--spacing-sm);
+ border-radius: var(--message-bubble-radius-group-bottom)
+ var(--message-bubble-radius-group-bottom) var(--message-bubble-radius-group-bottom)
+ var(--message-bubble-radius-tail);
+ background: var(--chat-bg-incoming);
+
+ // variable --chat-bg-typing-indicator has incorrect color value?
+ //background: var(--chat-bg-typing-indicator);
+ border: 1px solid var(--chat-bg-incoming);
+ background: var(--chat-bg-incoming, #ebeef1);
+}
+
+.str-chat__typing-indicator__dots {
+ display: flex;
+ align-items: center;
+ column-gap: var(--spacing-xxs);
+
+ .str-chat__typing-indicator__dot {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background-color: var(--chat-text-typing-indicator);
+ animation: str-chat__typing-indicator-dot 1.2s ease-in-out infinite both;
+
+ &:nth-child(1) {
+ animation-delay: 0s;
+ }
+
+ &:nth-child(2) {
+ animation-delay: 0.15s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.3s;
+ }
+ }
+}
+
+/* SVG dots (TypingIndicatorDots component) */
+.str-chat__typing-indicator__dots svg {
+ display: block;
+
+ circle:nth-child(1) {
+ fill: var(--chat-text-typing-indicator);
+ animation: str-chat__typing-indicator-dot 1.2s ease-in-out infinite both;
+ animation-delay: 0s;
+ }
+
+ circle:nth-child(2) {
+ fill: var(--chat-text-typing-indicator);
+ animation: str-chat__typing-indicator-dot 1.2s ease-in-out infinite both;
+ animation-delay: 0.15s;
+ }
+
+ circle:nth-child(3) {
+ fill: var(--chat-text-typing-indicator);
+ animation: str-chat__typing-indicator-dot 1.2s ease-in-out infinite both;
+ animation-delay: 0.3s;
+ }
+}
+
+@keyframes str-chat__typing-indicator-dot {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+
+ 33% {
+ opacity: 0.75;
+ }
+
+ 66% {
+ opacity: 0.5;
+ }
+}
+
+/* Header variant: text + inline dots (ChannelHeader / ThreadHeader) */
+.str-chat__typing-indicator-header {
+ display: inline-flex;
+ align-items: baseline;
+ gap: var(--spacing-xs, 4px);
+ white-space: nowrap;
+ color: var(--text-secondary);
+ font: var(--str-chat__caption-default-text);
+}
+
+.str-chat__typing-indicator-header__dots {
+ display: inline-flex;
+ align-items: center;
+ vertical-align: middle;
+}
diff --git a/src/components/TypingIndicator/styling/index.scss b/src/components/TypingIndicator/styling/index.scss
new file mode 100644
index 0000000000..265362a69b
--- /dev/null
+++ b/src/components/TypingIndicator/styling/index.scss
@@ -0,0 +1 @@
+@use 'TypingIndicator';
diff --git a/src/i18n/de.json b/src/i18n/de.json
index c3a5476c3d..ae52d4eea0 100644
--- a/src/i18n/de.json
+++ b/src/i18n/de.json
@@ -3,6 +3,9 @@
"{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} und {{ lastUser }}",
"{{ count }} files_one": "{{ count }} Datei",
"{{ count }} files_other": "{{ count }} Dateien",
+ "{{ count }} people are typing_one": "{{ count }} Person tippt",
+ "{{ count }} people are typing_many": "{{ count }} Personen tippen",
+ "{{ count }} people are typing_other": "{{ count }} Personen tippen",
"{{ count }} photos_one": "{{ count }} Foto",
"{{ count }} photos_other": "{{ count }} Fotos",
"{{ count }} reactions_one": "{{ count }} Reaktion",
@@ -12,6 +15,8 @@
"{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} und {{ secondUser }}",
"{{ imageCount }} more": "{{ imageCount }} mehr",
"{{ memberCount }} members": "{{ memberCount }} Mitglieder",
+ "{{ typing }} are typing": "{{ typing }} tippen",
+ "{{ typing }} is typing": "{{ typing }} tippt",
"{{ user }} has been muted": "{{ user }} wurde stummgeschaltet",
"{{ user }} has been unmuted": "Die Stummschaltung von {{ user }} wurde aufgehoben",
"{{ user }} is typing...": "{{ user }} tippt...",
diff --git a/src/i18n/en.json b/src/i18n/en.json
index a9d090101c..28e5e0b19f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -3,6 +3,9 @@
"{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }}, and {{ lastUser }}",
"{{ count }} files_one": "{{ count }} file",
"{{ count }} files_other": "{{ count }} files",
+ "{{ count }} people are typing_one": "{{ count }} person is typing",
+ "{{ count }} people are typing_many": "{{ count }} people are typing",
+ "{{ count }} people are typing_other": "{{ count }} people are typing",
"{{ count }} photos_one": "{{ count }} photo",
"{{ count }} photos_other": "{{ count }} photos",
"{{ count }} reactions_one": "{{ count }} reaction",
@@ -12,6 +15,8 @@
"{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} and {{ secondUser }}",
"{{ imageCount }} more": "{{ imageCount }} more",
"{{ memberCount }} members": "{{ memberCount }} members",
+ "{{ typing }} are typing": "{{ typing }} are typing",
+ "{{ typing }} is typing": "{{ typing }} is typing",
"{{ user }} has been muted": "{{ user }} has been muted",
"{{ user }} has been unmuted": "{{ user }} has been unmuted",
"{{ user }} is typing...": "{{ user }} is typing...",
diff --git a/src/i18n/es.json b/src/i18n/es.json
index 0de68dc8b3..70629cbb81 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -4,6 +4,9 @@
"{{ count }} files_one": "{{ count }} archivo",
"{{ count }} files_many": "{{ count }} archivos",
"{{ count }} files_other": "{{ count }} archivos",
+ "{{ count }} people are typing_one": "{{ count }} persona está escribiendo",
+ "{{ count }} people are typing_many": "{{ count }} personas están escribiendo",
+ "{{ count }} people are typing_other": "{{ count }} personas están escribiendo",
"{{ count }} photos_one": "{{ count }} foto",
"{{ count }} photos_many": "{{ count }} fotos",
"{{ count }} photos_other": "{{ count }} fotos",
@@ -16,6 +19,8 @@
"{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} y {{ secondUser }}",
"{{ imageCount }} more": "{{ imageCount }} más",
"{{ memberCount }} members": "{{ memberCount }} miembros",
+ "{{ typing }} are typing": "{{ typing }} están escribiendo",
+ "{{ typing }} is typing": "{{ typing }} está escribiendo",
"{{ user }} has been muted": "{{ user }} ha sido silenciado",
"{{ user }} has been unmuted": "Se ha desactivado el silencio de {{ user }}",
"{{ user }} is typing...": "{{ user }} está escribiendo...",
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 83a4e7f754..c409bbb6f3 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -4,6 +4,9 @@
"{{ count }} files_one": "{{ count }} fichier",
"{{ count }} files_many": "{{ count }} fichiers",
"{{ count }} files_other": "{{ count }} fichiers",
+ "{{ count }} people are typing_one": "{{ count }} personne écrit",
+ "{{ count }} people are typing_many": "{{ count }} personnes écrivent",
+ "{{ count }} people are typing_other": "{{ count }} personnes écrivent",
"{{ count }} photos_one": "{{ count }} photo",
"{{ count }} photos_many": "{{ count }} photos",
"{{ count }} photos_other": "{{ count }} photos",
@@ -16,6 +19,8 @@
"{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} et {{ secondUser }}",
"{{ imageCount }} more": "{{ imageCount }} supplémentaires",
"{{ memberCount }} members": "{{ memberCount }} membres",
+ "{{ typing }} are typing": "{{ typing }} écrivent",
+ "{{ typing }} is typing": "{{ typing }} écrit",
"{{ user }} has been muted": "{{ user }} a été mis en sourdine",
"{{ user }} has been unmuted": "{{ user }} n'est plus en sourdine",
"{{ user }} is typing...": "{{ user }} est en train d'écrire...",
diff --git a/src/i18n/hi.json b/src/i18n/hi.json
index 430b66e3c4..8c2d5545a3 100644
--- a/src/i18n/hi.json
+++ b/src/i18n/hi.json
@@ -3,6 +3,9 @@
"{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} और {{ lastUser }}",
"{{ count }} files_one": "{{ count }} फ़ाइल",
"{{ count }} files_other": "{{ count }} फ़ाइलें",
+ "{{ count }} people are typing_one": "{{ count }} व्यक्ति टाइप कर रहा है",
+ "{{ count }} people are typing_many": "{{ count }} लोग टाइप कर रहे हैं",
+ "{{ count }} people are typing_other": "{{ count }} लोग टाइप कर रहे हैं",
"{{ count }} photos_one": "{{ count }} फ़ोटो",
"{{ count }} photos_other": "{{ count }} फ़ोटो",
"{{ count }} reactions_one": "{{ count }} प्रतिक्रिया",
@@ -12,6 +15,8 @@
"{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} और {{ secondUser }}",
"{{ imageCount }} more": "{{ imageCount }} और",
"{{ memberCount }} members": "{{ memberCount }} मेंबर्स",
+ "{{ typing }} are typing": "{{ typing }} टाइप कर रहे हैं",
+ "{{ typing }} is typing": "{{ typing }} टाइप कर रहा है",
"{{ user }} has been muted": "{{ user }} को म्यूट कर दिया गया है",
"{{ user }} has been unmuted": "{{ user }} को अनम्यूट कर दिया गया है",
"{{ user }} is typing...": "{{ user }} टाइप कर रहा है...",
diff --git a/src/i18n/it.json b/src/i18n/it.json
index 33a13b5518..b26f4be7fe 100644
--- a/src/i18n/it.json
+++ b/src/i18n/it.json
@@ -4,6 +4,9 @@
"{{ count }} files_one": "{{ count }} file",
"{{ count }} files_many": "{{ count }} file",
"{{ count }} files_other": "{{ count }} file",
+ "{{ count }} people are typing_one": "{{ count }} persona sta scrivendo",
+ "{{ count }} people are typing_many": "{{ count }} persone stanno scrivendo",
+ "{{ count }} people are typing_other": "{{ count }} persone stanno scrivendo",
"{{ count }} photos_one": "{{ count }} foto",
"{{ count }} photos_many": "{{ count }} foto",
"{{ count }} photos_other": "{{ count }} foto",
@@ -16,6 +19,8 @@
"{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} e {{ secondUser }}",
"{{ imageCount }} more": "+ {{ imageCount }}",
"{{ memberCount }} members": "{{ memberCount }} membri",
+ "{{ typing }} are typing": "{{ typing }} stanno scrivendo",
+ "{{ typing }} is typing": "{{ typing }} sta scrivendo",
"{{ user }} has been muted": "{{ user }} è stato silenziato",
"{{ user }} has been unmuted": "Notifiche riattivate per {{ user }}",
"{{ user }} is typing...": "{{ user }} sta digitando...",
diff --git a/src/i18n/ja.json b/src/i18n/ja.json
index 61aab57116..7676d68388 100644
--- a/src/i18n/ja.json
+++ b/src/i18n/ja.json
@@ -3,6 +3,9 @@
"{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} と {{ lastUser }}",
"{{ count }} files_one": "{{ count }} ファイル",
"{{ count }} files_other": "{{ count }} ファイル",
+ "{{ count }} people are typing_one": "{{ count }}人が入力中です",
+ "{{ count }} people are typing_many": "{{ count }}人が入力中です",
+ "{{ count }} people are typing_other": "{{ count }}人が入力中です",
"{{ count }} photos_one": "{{ count }} 写真",
"{{ count }} photos_other": "{{ count }} 写真",
"{{ count }} reactions_other": "{{ count }}件のリアクション",
@@ -11,6 +14,8 @@
"{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} と {{ secondUser }}",
"{{ imageCount }} more": "{{ imageCount }} イメージ",
"{{ memberCount }} members": "{{ memberCount }} メンバー",
+ "{{ typing }} are typing": "{{ typing }}が入力中です",
+ "{{ typing }} is typing": "{{ typing }}が入力中です",
"{{ user }} has been muted": "{{ user }} 無音されています",
"{{ user }} has been unmuted": "{{ user }} 無音されていません",
"{{ user }} is typing...": "{{ user }} が入力中...",
diff --git a/src/i18n/ko.json b/src/i18n/ko.json
index d7f585959b..20c907e07b 100644
--- a/src/i18n/ko.json
+++ b/src/i18n/ko.json
@@ -3,6 +3,9 @@
"{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} 그리고 {{ lastUser }}",
"{{ count }} files_one": "{{ count }}개 파일",
"{{ count }} files_other": "{{ count }}개 파일",
+ "{{ count }} people are typing_one": "{{ count }}명이 입력 중입니다",
+ "{{ count }} people are typing_many": "{{ count }}명이 입력 중입니다",
+ "{{ count }} people are typing_other": "{{ count }}명이 입력 중입니다",
"{{ count }} photos_one": "{{ count }}개 사진",
"{{ count }} photos_other": "{{ count }}개 사진",
"{{ count }} reactions_other": "{{ count }}개 반응",
@@ -11,6 +14,8 @@
"{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} 그리고 {{ secondUser }}",
"{{ imageCount }} more": "{{ imageCount }}개 더",
"{{ memberCount }} members": "{{ memberCount }}명",
+ "{{ typing }} are typing": "{{ typing }} 입력 중입니다",
+ "{{ typing }} is typing": "{{ typing }} 입력 중입니다",
"{{ user }} has been muted": "{{ user }} 음소거되었습니다",
"{{ user }} has been unmuted": "{{ user }} 음소거가 해제되었습니다",
"{{ user }} is typing...": "{{ user }}이(가) 입력 중입니다...",
diff --git a/src/i18n/nl.json b/src/i18n/nl.json
index 991d992877..e6cc4e8b20 100644
--- a/src/i18n/nl.json
+++ b/src/i18n/nl.json
@@ -3,6 +3,9 @@
"{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} en {{ lastUser }}",
"{{ count }} files_one": "{{ count }} bestand",
"{{ count }} files_other": "{{ count }} bestanden",
+ "{{ count }} people are typing_one": "{{ count }} persoon typt",
+ "{{ count }} people are typing_many": "{{ count }} personen typen",
+ "{{ count }} people are typing_other": "{{ count }} personen typen",
"{{ count }} photos_one": "{{ count }} foto",
"{{ count }} photos_other": "{{ count }} foto's",
"{{ count }} reactions_one": "{{ count }} reactie",
@@ -12,6 +15,8 @@
"{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} en {{ secondUser }}",
"{{ imageCount }} more": "+{{ imageCount }}",
"{{ memberCount }} members": "{{ memberCount }} deelnemers",
+ "{{ typing }} are typing": "{{ typing }} typen",
+ "{{ typing }} is typing": "{{ typing }} typt",
"{{ user }} has been muted": "{{ user }} is gedempt",
"{{ user }} has been unmuted": "{{ user }} is niet meer gedempt",
"{{ user }} is typing...": "{{ user }} is aan het typen...",
diff --git a/src/i18n/pt.json b/src/i18n/pt.json
index 83d874a24c..abe61037af 100644
--- a/src/i18n/pt.json
+++ b/src/i18n/pt.json
@@ -4,6 +4,9 @@
"{{ count }} files_one": "{{ count }} arquivo",
"{{ count }} files_many": "{{ count }} arquivos",
"{{ count }} files_other": "{{ count }} arquivos",
+ "{{ count }} people are typing_one": "{{ count }} pessoa está digitando",
+ "{{ count }} people are typing_many": "{{ count }} pessoas estão digitando",
+ "{{ count }} people are typing_other": "{{ count }} pessoas estão digitando",
"{{ count }} photos_one": "{{ count }} foto",
"{{ count }} photos_many": "{{ count }} fotos",
"{{ count }} photos_other": "{{ count }} fotos",
@@ -16,6 +19,8 @@
"{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} e {{ secondUser }}",
"{{ imageCount }} more": "{{ imageCount }} mais",
"{{ memberCount }} members": "{{ memberCount }} membros",
+ "{{ typing }} are typing": "{{ typing }} estão digitando",
+ "{{ typing }} is typing": "{{ typing }} está digitando",
"{{ user }} has been muted": "{{ user }} foi silenciado",
"{{ user }} has been unmuted": "{{ user }} foi reativado",
"{{ user }} is typing...": "{{ user }} está digitando...",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index d20fdbf954..8e598eb0e5 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -5,6 +5,10 @@
"{{ count }} files_few": "{{ count }} файла",
"{{ count }} files_many": "{{ count }} файлов",
"{{ count }} files_other": "{{ count }} файла",
+ "{{ count }} people are typing_one": "{{ count }} человек печатает",
+ "{{ count }} people are typing_few": "{{ count }} человека печатают",
+ "{{ count }} people are typing_many": "{{ count }} человек печатают",
+ "{{ count }} people are typing_other": "{{ count }} человека печатают",
"{{ count }} photos_one": "{{ count }} фото",
"{{ count }} photos_few": "{{ count }} фото",
"{{ count }} photos_many": "{{ count }} фото",
@@ -20,6 +24,8 @@
"{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} и {{ secondUser }}",
"{{ imageCount }} more": "Ещё {{ imageCount }}",
"{{ memberCount }} members": "{{ memberCount }} участников",
+ "{{ typing }} are typing": "{{ typing }} печатают",
+ "{{ typing }} is typing": "{{ typing }} печатает",
"{{ user }} has been muted": "Вы отписались от уведомлений от {{ user }}",
"{{ user }} has been unmuted": "Уведомления от {{ user }} были включены",
"{{ user }} is typing...": "{{ user }} печатает...",
diff --git a/src/i18n/tr.json b/src/i18n/tr.json
index 2b72a13356..3fb3b83e7b 100644
--- a/src/i18n/tr.json
+++ b/src/i18n/tr.json
@@ -3,6 +3,9 @@
"{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} ve {{ lastUser }}",
"{{ count }} files_one": "{{ count }} dosya",
"{{ count }} files_other": "{{ count }} dosya",
+ "{{ count }} people are typing_one": "{{ count }} kişi yazıyor",
+ "{{ count }} people are typing_many": "{{ count }} kişi yazıyor",
+ "{{ count }} people are typing_other": "{{ count }} kişi yazıyor",
"{{ count }} photos_one": "{{ count }} fotoğraf",
"{{ count }} photos_other": "{{ count }} fotoğraf",
"{{ count }} reactions_one": "{{ count }} tepki",
@@ -12,6 +15,8 @@
"{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} ve {{ secondUser }}",
"{{ imageCount }} more": "{{ imageCount }} adet daha",
"{{ memberCount }} members": "{{ memberCount }} üye",
+ "{{ typing }} are typing": "{{ typing }} yazıyor",
+ "{{ typing }} is typing": "{{ typing }} yazıyor",
"{{ user }} has been muted": "{{ user }} sessize alındı",
"{{ user }} has been unmuted": "{{ user }} sesi açıldı",
"{{ user }} is typing...": "{{ user }} yazıyor...",
diff --git a/src/styling/_global-theme-variables.scss b/src/styling/_global-theme-variables.scss
index 73fbab07cd..e748847d1f 100644
--- a/src/styling/_global-theme-variables.scss
+++ b/src/styling/_global-theme-variables.scss
@@ -77,6 +77,12 @@
var(--typography-font-size-xl) / var(--typography-line-height-relaxed)
var(--str-chat__font-family);
+ --str-chat__numeric-md-text: normal var(--typography-font-weight-bold)
+ var(--typography-font-size-xxs) / 100% var(--str-chat__font-family);
+
+ --str-chat__numeric-xl-text: normal var(--typography-font-weight-bold)
+ var(--typography-font-size-sm) / 100% var(--str-chat__font-family);
+
color: var(--text-primary, #1a1b25);
// todo: adapt the old text variables to so that they use the new semantic text variables
diff --git a/src/styling/_utils.scss b/src/styling/_utils.scss
index 3b19eb0385..4b524a7926 100644
--- a/src/styling/_utils.scss
+++ b/src/styling/_utils.scss
@@ -117,7 +117,7 @@
overflow-y: hidden; // for Edge
overflow-x: hidden; // for ellipsis text
flex: 1;
- row-gap: var(--spacing-none);
+ row-gap: var(--spacing-xxs);
}
@mixin empty-theme($component-name) {
@@ -130,14 +130,11 @@
}
@mixin message-list-spacing {
- // 800px container, 16px padding, 768px content — align with composer
- $spacing: var(--str-chat__message-composer-padding, 16px);
padding: 0;
- padding-inline: $spacing;
// Need this trick to be able to apply full-width background color on hover to messages / full-width separator to thread header
.str-chat__li {
- margin-inline: calc(-1 * #{$spacing});
+ max-width: 100%;
}
}
diff --git a/src/styling/index.scss b/src/styling/index.scss
index ddfe09f9ad..111cb67a8a 100644
--- a/src/styling/index.scss
+++ b/src/styling/index.scss
@@ -45,13 +45,14 @@
@use '../components/MessageList/styling' as MessageList;
@use '../components/Poll/styling' as Poll;
@use '../components/Reactions/styling' as Reactions;
+@use '../experimental/Search/styling' as Search;
+@use '../components/SummarizedMessagePreview/styling' as SummarizedMessagePreview;
@use '../components/TextareaComposer/styling' as TextareaComposer;
@use '../components/Thread/styling' as Thread;
@use '../components/Threads/styling' as Threads;
@use '../components/Threads/ThreadList/styling' as ThreadList;
+@use '../components/TypingIndicator/styling' as TypingIndicator;
@use '../components/VideoPlayer/styling' as VideoPlayer;
-@use '../components/SummarizedMessagePreview/styling' as SummarizedMessagePreview;
-@use '../experimental/Search/styling/' as Search;
// Layers have to be kept the last
@import 'modern-normalize' layer(css-reset);