Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 5,
// viewport-fit=cover lets the app draw into the iOS safe-area insets and makes
// env(safe-area-inset-*) non-zero, so safe-area padding (e.g. the messaging
// input row, #30 fix #1) actually clears the home indicator. Without this,
// env() resolves to 0 and the padding is a no-op.
viewportFit: 'cover',
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#f5f0eb' },
{ media: '(prefers-color-scheme: dark)', color: '#1a1a2e' },
Expand Down
2 changes: 1 addition & 1 deletion src/app/messages/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function MessagesLayout() {
<SearchParamsReader onParams={handleParams} />
</Suspense>

<div className="bg-base-100 fixed inset-x-0 top-16 bottom-28 overflow-hidden">
<div className="bg-base-100 fixed inset-x-0 top-16 bottom-[calc(7rem+env(safe-area-inset-bottom))] overflow-hidden">
<div className="drawer md:drawer-open h-full">
<input
id="sidebar-drawer"
Expand Down
6 changes: 4 additions & 2 deletions src/components/organisms/ChatWindow/ChatWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,10 @@ export default function ChatWindow({
/>
</div>

{/* Row 3: Message Input (auto height) */}
<div className="border-base-300 bg-base-100 border-t px-4 pt-4 pb-6">
{/* Row 3: Message Input (auto height). pb keeps the 1.5rem base but
extends past the iOS home indicator via the safe-area inset (#30
fix #1; needs viewport-fit=cover, set in the root layout). */}
<div className="border-base-300 bg-base-100 border-t px-4 pt-4 pb-[max(1.5rem,env(safe-area-inset-bottom))]">
<MessageInput
onSend={onSendMessage}
disabled={isBlocked}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import ConversationView from './ConversationView';
import ConversationView, { resolveParticipantName } from './ConversationView';

// ConversationView is a state-owning wrapper around ChatWindow. We mock
// the service layer and the ChatWindow organism so tests assert wiring,
Expand Down Expand Up @@ -149,3 +149,28 @@ describe('ConversationView', () => {
}
});
});

describe('resolveParticipantName (#30 fix #5)', () => {
it('returns Unknown User on a query error (transient — do not mislabel)', () => {
expect(resolveParticipantName(null, true)).toBe('Unknown User');
expect(resolveParticipantName({ display_name: 'Ada' }, true)).toBe(
'Unknown User'
);
});

it('returns Deleted User when the profile row is null (no error)', () => {
expect(resolveParticipantName(null, false)).toBe('Deleted User');
});

it('prefers display_name, then username, for a present profile', () => {
expect(
resolveParticipantName({ display_name: 'Ada', username: 'ada99' }, false)
).toBe('Ada');
expect(
resolveParticipantName({ display_name: null, username: 'ada99' }, false)
).toBe('ada99');
expect(
resolveParticipantName({ display_name: '', username: '' }, false)
).toBe('Unknown User');
});
});
25 changes: 19 additions & 6 deletions src/components/organisms/ConversationView/ConversationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ import type { DecryptedMessage } from '@/types/messaging';

const logger = createLogger('organisms:ConversationView');

/**
* Resolve the display name for the other 1-to-1 participant, distinguishing
* three cases (#30 fix #5):
* - query failed (transient/RLS) → 'Unknown User' (neutral; don't mislabel)
* - no profile row (null) → 'Deleted User' (account genuinely gone)
* - profile present → display_name || username || 'Unknown User'
*/
export function resolveParticipantName(
profile: { username?: string | null; display_name?: string | null } | null,
hadError: boolean
): string {
if (hadError) return 'Unknown User';
if (!profile) return 'Deleted User';
return profile.display_name || profile.username || 'Unknown User';
}

export interface ConversationViewProps {
/** Conversation to display. The component owns all message state internally;
* changing this prop resets and reloads. */
Expand Down Expand Up @@ -121,13 +137,10 @@ export default function ConversationView({

if (profileError) {
logger.warn('Profile query error', { error: profileError.message });
setParticipantName('Unknown User');
return;
}

setParticipantName(
profile?.display_name || profile?.username || 'Unknown User'
);
// Distinguish deleted account (null row) from a transient query error and
// from a present-but-unnamed profile (#30 fix #5).
setParticipantName(resolveParticipantName(profile, !!profileError));
} catch (err) {
logger.warn('Error loading participant info', { error: err });
setParticipantName('Unknown User');
Expand Down
Loading