Private
Public Access
Show the date on message timestamps that aren't from today
A bare "4:04 PM" gave no way to tell today's message from one sent last
week. Today stays time-only; older messages gain just enough date to
place them: "Yesterday 4:04 PM", a weekday for the past week ("Mon
4:04 PM"), then "Oct 2, 4:04 PM" (with the year if it isn't this one).
Hovering a time shows the full date. All in the viewer's local time.
A new calendar day now always starts a new message group. Previously a
same-sender run that crossed midnight within the 5-minute window (11:58
PM then 12:01 AM) stayed under one header, which would have put the
later message under the wrong day's timestamp. The grouping rule moves
out of MessageList into messageGrouping.ts so it's unit-tested.
useLocalDay refreshes "today" at local midnight and when a hidden tab
becomes visible again, so a tab left open overnight doesn't keep showing
yesterday's messages as a bare time. .message-time no longer wraps, and
.message-author can shrink, so a long display name can't push the longer
label out of the row on a phone.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eb30dc1753
commit
62b5eec2fb
9 files changed
+323
-19
No files matched your search
@@ -40,12 +40,17 @@
|
||||
}
|
||||
|
||||
.message-author {
|
||||
/* Lets a very long display name shrink/wrap instead of pushing the
|
||||
(now longer, e.g. "Yesterday 4:04 PM") timestamp out of the row. */
|
||||
min-width: 0;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 700;
|
||||
color: var(--ds-text);
|
||||
}
|
||||
|
||||
.message-time {
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
font-size: 0.68rem;
|
||||
color: var(--ds-muted);
|
||||
font-family: var(--mono);
|
||||
|
||||
@@ -2,8 +2,10 @@ import type { CSSProperties } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { getRoomFileUrl, getRoomImageUrl } from '../api/rooms'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useLocalDay } from '../hooks/useLocalDay'
|
||||
import { formatFileSize } from '../lib/fileSize'
|
||||
import { avatarUrlFor, displayNameFor, senderColorIndex, statusFor } from '../lib/messageGrouping'
|
||||
import { formatMessageTime, formatMessageTimeTitle } from '../lib/formatMessageTime'
|
||||
import { avatarUrlFor, displayNameFor, isGroupStart, senderColorIndex, statusFor } from '../lib/messageGrouping'
|
||||
import type { ChatMessageEnvelope, Message, MessageFileInfo, RoomMember } from '../types'
|
||||
import { EMOJI_PICKER_MAX_HEIGHT, EmojiPicker } from './EmojiPicker'
|
||||
import { FilePreviewModal, getPreviewKind } from './FilePreviewModal'
|
||||
@@ -69,13 +71,6 @@ function FileAttachmentCard({ file, roomId, onPreview }: FileAttachmentCardProps
|
||||
// trigger a download the moment the browser tries to fetch it).
|
||||
const PLAYABLE_VIDEO_CONTENT_TYPES = new Set(['video/mp4', 'video/webm', 'video/ogg'])
|
||||
|
||||
// #73: Slack's own threshold for the same "still grouped, but it's been a
|
||||
// while" call -- past this gap a same-sender message starts a new group
|
||||
// (its own avatar/name/timestamp) even with nobody else posting in
|
||||
// between, so a message sent minutes later doesn't hide under a stale
|
||||
// timestamp from the start of the run.
|
||||
const GROUP_BREAK_MS = 5 * 60 * 1000
|
||||
|
||||
interface VideoAttachmentProps {
|
||||
file: MessageFileInfo
|
||||
roomId: string
|
||||
@@ -134,6 +129,7 @@ export function MessageList({
|
||||
onDelete,
|
||||
}: MessageListProps) {
|
||||
const { user } = useAuth()
|
||||
const now = useLocalDay()
|
||||
// #71: same viewer preference MessageContent applies to in-text emoji,
|
||||
// looked up separately here since a reaction pill isn't a descendant of
|
||||
// that component's wrapper div (see EmojiGlyph's own comment).
|
||||
@@ -218,19 +214,15 @@ export function MessageList({
|
||||
// consecutive messages from the same sender only repeat the
|
||||
// avatar/name/timestamp header on the first one in the run --
|
||||
// applies uniformly, including to your own messages. Also breaks on
|
||||
// a long gap (see GROUP_BREAK_MS) so a message sent well after the
|
||||
// rest of the run still gets its own visible timestamp.
|
||||
const isGroupStart =
|
||||
!prev ||
|
||||
prev.user_id !== msg.user_id ||
|
||||
new Date(msg.created_at).getTime() - new Date(prev.created_at).getTime() > GROUP_BREAK_MS
|
||||
// a long gap or a new calendar day -- see isGroupStart.
|
||||
const groupStart = isGroupStart(prev, msg)
|
||||
const editing = editingId === msg.id
|
||||
const deleted = !!msg.deleted_at
|
||||
|
||||
return (
|
||||
<div key={msg.id} className={`message-row${isGroupStart ? ' message-row-start' : ''}`}>
|
||||
<div key={msg.id} className={`message-row${groupStart ? ' message-row-start' : ''}`}>
|
||||
<div className="message-avatar-slot">
|
||||
{isGroupStart && (
|
||||
{groupStart && (
|
||||
<UserAvatar
|
||||
username={msg.username}
|
||||
colorIndex={senderColorIndex(msg.username, members)}
|
||||
@@ -240,11 +232,11 @@ export function MessageList({
|
||||
)}
|
||||
</div>
|
||||
<div className="message-content">
|
||||
{isGroupStart && (
|
||||
{groupStart && (
|
||||
<div className="message-header">
|
||||
<span className="message-author">{displayNameFor(msg.username, members)}</span>
|
||||
<span className="message-time">
|
||||
{new Date(msg.created_at).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
|
||||
<span className="message-time" title={formatMessageTimeTitle(msg.created_at)}>
|
||||
{formatMessageTime(msg.created_at, now)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user