diff --git a/src/LiveExample.test.tsx b/src/LiveExample.test.tsx index 166e0a1..3f87fc4 100644 --- a/src/LiveExample.test.tsx +++ b/src/LiveExample.test.tsx @@ -127,7 +127,7 @@ describe('React live demo', () => { expect(fake.sdk.markConversationRead).toHaveBeenCalledWith('room-1', { throughMessageId: 'message-1', privateStateVersion: 2 }), ) expect(screen.queryByRole('img', { name: 'Unread' })).toBeNull() - expect(screen.getByText('UI SDK 0.9.0')).toBeInTheDocument() + expect(screen.getByText('UI SDK 0.10.0')).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'Mark unread' })) // The action goes through the SDK's /unread route; the package patches the row and renders the diff --git a/src/LiveExample.tsx b/src/LiveExample.tsx index d537558..d738a38 100644 --- a/src/LiveExample.tsx +++ b/src/LiveExample.tsx @@ -32,6 +32,7 @@ import { personas, uploadAttachment, } from './demo' +import { ReadReceiptAvatars } from './ReadReceiptAvatars' import './live.css' export function LiveExample() { @@ -259,7 +260,7 @@ export function LiveExample() {
{state.status} - UI SDK 0.9.0 + UI SDK 0.10.0
@@ -494,6 +495,7 @@ function LiveConversation({ }) }} composerPlaceholder="Message your team…" + renderReadReceipt={({ readerIds }) => } formatTime={(date) => date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} /> diff --git a/src/ReadReceiptAvatars.test.tsx b/src/ReadReceiptAvatars.test.tsx new file mode 100644 index 0000000..22b1351 --- /dev/null +++ b/src/ReadReceiptAvatars.test.tsx @@ -0,0 +1,13 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { ReadReceiptAvatars } from './ReadReceiptAvatars' + +describe('live read receipts', () => { + it('shows reader identities as compact avatars', () => { + render() + const receipt = screen.getByRole('group', { name: 'Read by Alex, Sam' }) + expect(receipt.querySelectorAll('.demo-read-receipt__avatar')).toHaveLength(2) + expect(receipt).toHaveTextContent('AR') + expect(receipt).toHaveTextContent('SP') + }) +}) diff --git a/src/ReadReceiptAvatars.tsx b/src/ReadReceiptAvatars.tsx new file mode 100644 index 0000000..c05a90b --- /dev/null +++ b/src/ReadReceiptAvatars.tsx @@ -0,0 +1,30 @@ +import { displayName, personas } from './demo' + +/** Compact reader identities beside an outgoing message, like the Flutter demo. */ +export function ReadReceiptAvatars({ readerIds }: { readerIds: ReadonlySet }) { + const readers = [...readerIds].sort() + if (readers.length === 0) return + + const names = readers.map(displayName) + return ( + + {readers.slice(0, 3).map((id) => { + const persona = personas.find((entry) => entry.id === id) + const name = displayName(id) + const initials = persona?.initials ?? name.slice(0, 2).toUpperCase() + return ( + + ) + })} + {readers.length > 3 && +{readers.length - 3}} + + ) +} diff --git a/src/live.css b/src/live.css index 30cfb44..7a9886a 100644 --- a/src/live.css +++ b/src/live.css @@ -452,6 +452,35 @@ .demo-conversation .ckui-messages { background: #fcfdfb; } +.demo-conversation .ckui-message-bubble { order: 0; } +.demo-conversation .demo-read-receipt { order: 1; } +.demo-conversation .ckui-reactions { order: 2; } +.demo-conversation .ckui-message-actions, +.demo-conversation .ckui-message-confirm { order: 3; } +.demo-read-receipt { + display: flex; + align-items: center; + justify-content: flex-end; + min-height: 18px; + padding-top: 2px; + padding-right: 2px; +} +.demo-read-receipt--empty { display: none; } +.demo-read-receipt__avatar { + display: grid; + width: 18px; + height: 18px; + margin-left: -5px; + place-items: center; + border: 2px solid #fff; + border-radius: 50%; + color: #fff; + font-size: 7px; + font-weight: 700; + line-height: 1; +} +.demo-read-receipt__avatar:first-child { margin-left: 0; } +.demo-read-receipt__more { padding-left: 4px; color: var(--demo-muted); font-size: 10px; } .demo-conversation .ckui-media-card--image img { max-height: 230px; object-fit: contain;