feat(dm): read-only composer notice for Deleted-User 1-on-1 threads (C2)

This commit is contained in:
Jannis Braun
2026-07-02 16:20:38 +02:00
parent 2a0574ee6c
commit ae07ae66e4
5 changed files with 56 additions and 5 deletions
@@ -0,0 +1,16 @@
/**
* Read-only composer replacement shown in a 1-on-1 DM whose partner's account
* was deleted. Occupies the same bottom slot as the MessageInput glass bubble.
*/
export function DmDeletedNotice() {
return (
<div className="px-4 pb-4 pt-1">
<div className="glass-bubble flex items-center gap-2 px-4 py-3 rounded-[14px] text-txt-tertiary text-[14px] justify-center select-none">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className="opacity-70 flex-shrink-0">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
</svg>
<span>This user&rsquo;s account was deleted</span>
</div>
</div>
);
}
@@ -18,7 +18,8 @@ import { wsSend } from '../../hooks/useWebSocket';
import { MemberListToggleButton } from './MemberListToggleButton';
import { TransferIndicator } from './TransferIndicator';
import { isSelf, parseFederatedUsername, isFederationGlobeApplicable } from '../../utils/identity';
import { formatDmHeaderName, formatDmInputLabel } from '../../utils/dmFormatters';
import { formatDmHeaderName, formatDmInputLabel, isDeletedPartnerDm } from '../../utils/dmFormatters';
import { DmDeletedNotice } from '../chat/DmDeletedNotice';
import { useCanonicalUserView } from '../../utils/userViewLookup';
import type { User } from '@backspace/shared';
import { Tooltip } from '../ui/Tooltip';
@@ -179,6 +180,7 @@ export function MainContent() {
: dmChannel
? `Message @${dmName}`
: undefined;
const dmPartnerDeleted = dmChannel ? isDeletedPartnerDm(dmChannel, authUser) : false;
const isInDmCall = activeDmCall?.dmChannelId === currentChannelId;
const isCallingThisDm = outgoingCall?.dmChannelId === currentChannelId;
@@ -365,7 +367,9 @@ export function MainContent() {
</div>
</div>
<MessageList channelId={currentChannelId} jumpToMessageId={jumpToMessageId} onJumpComplete={() => setJumpToMessageId(null)} />
<MessageInput channelId={currentChannelId} channelName={`@${dmName}`} placeholder={dmInputPlaceholder} />
{dmPartnerDeleted
? <DmDeletedNotice />
: <MessageInput channelId={currentChannelId} channelName={`@${dmName}`} placeholder={dmInputPlaceholder} />}
<SearchPopover
open={searchOpen}
onClose={() => setSearchOpen(false)}
@@ -7,7 +7,8 @@ import { MessageList } from '../chat/MessageList';
import { MessageInput } from '../chat/MessageInput';
import { TransferIndicator } from './TransferIndicator';
import { parseFederatedUsername } from '../../utils/identity';
import { formatDmHeaderName, formatDmInputLabel } from '../../utils/dmFormatters';
import { formatDmHeaderName, formatDmInputLabel, isDeletedPartnerDm } from '../../utils/dmFormatters';
import { DmDeletedNotice } from '../chat/DmDeletedNotice';
import { useCanonicalUserView } from '../../utils/userViewLookup';
import type { User } from '@backspace/shared';
@@ -47,6 +48,7 @@ export function MobileChatScreen({ params }: MobileChatScreenProps) {
const isGroup = !!dm?.ownerId;
const rawMainOther = !isGroup ? otherMembers[0] : undefined;
const canonicalMainOther = useCanonicalUserView((rawMainOther as unknown as User) ?? FALLBACK_USER);
const dmPartnerDeleted = dm ? isDeletedPartnerDm(dm, authUser) : false;
// Resolve channel/DM name. Group DMs route through `formatDmHeaderName` so
// a renamed group shows `dm.name` (previously this surface silently dropped
@@ -127,7 +129,9 @@ export function MobileChatScreen({ params }: MobileChatScreenProps) {
`absolute bottom-full` to the bubble), so we don't render it here. */}
<div className="relative flex-1 min-h-0 flex flex-col overflow-hidden">
{channelId && <MessageList channelId={channelId} />}
{channelId && <MessageInput channelId={channelId} channelName={channelName} placeholder={inputPlaceholder} />}
{channelId && (dmPartnerDeleted
? <DmDeletedNotice />
: <MessageInput channelId={channelId} channelName={channelName} placeholder={inputPlaceholder} />)}
</div>
</div>
);