From 32084a66d1a154ac18511738cd85cd0653ee16ae Mon Sep 17 00:00:00 2001 From: Laurent Dubertrand Date: Wed, 11 Mar 2026 16:48:39 +0100 Subject: [PATCH] full screen chat for iphone --- client/src/app/chat-page.component.html | 462 ++++++++++++++---------- client/src/app/chat-page.component.scss | 120 +++++- client/src/app/chat-page.component.ts | 54 ++- 3 files changed, 423 insertions(+), 213 deletions(-) diff --git a/client/src/app/chat-page.component.html b/client/src/app/chat-page.component.html index d3f2123..5edc7da 100644 --- a/client/src/app/chat-page.component.html +++ b/client/src/app/chat-page.component.html @@ -40,6 +40,31 @@ } + @if (conversationModalOpen()) { +
+
+
+
+

Fullscreen conversation

+

{{ peer()?.displayName ?? 'Conversation' }}

+
+ +
+ +
+ +
+
+
+ } +
@if (currentUser(); as connectedUser) {
@@ -61,6 +86,16 @@ WebRTC +
} @else { @@ -72,31 +107,24 @@
-
+ } @else { +
+ No peers are currently connected. +
+ } -
+
- @if (conversation().length === 0) { -
- No text messages yet. The chat page is ready as soon as the peer channel opens. -
- } - - @for (entry of conversation(); track entry.id) { -
- @if (entry.direction !== 'system') { -
- @if (isGeneratedImageEntry(entry)) { - - } - - - @if (isForwardMenuOpen(entry.id)) { -
- -
- } -
- } -
- {{ entry.authorLabel }} - -
- - @switch (entry.kind) { - @case ('text') { -

{{ entry.text }}

- } - @case ('json') { -
{{ entry.payload | json }}
- } - @case ('file') { -
- @if (isImageEntry(entry)) { - - } - - @if (isVideoEntry(entry)) { - - } - - @if (isIncomingJsonFileEntry(entry)) { - - } - -
-
{{ entry.fileName }}
- @if (entry.fileSize) { -
{{ entry.fileSize | number }} bytes
- } -
- - @if (entry.downloadUrl) { - Download - } - - @if (hasDocumentPreviewImage(entry)) { -
-
Preview
- -
- } -
- } - @case ('voice') { -
-
Voice message
- @if (entry.downloadUrl) { - - } -
- } - @default { - @if (entry.showSpinner) { -
- -

{{ entry.text }}

-
- } @else { -

{{ entry.text }}

- } - } - } -
- } +
@@ -428,3 +362,149 @@
+ + + @if (conversation().length === 0) { +
+ No text messages yet. The chat page is ready as soon as the peer channel opens. +
+ } + + @for (entry of conversation(); track entry.id) { +
+ @if (entry.direction !== 'system') { +
+ @if (isGeneratedImageEntry(entry)) { + + } + + + @if (isForwardMenuOpen(entry.id)) { +
+ +
+ } +
+ } +
+ {{ entry.authorLabel }} + +
+ + @switch (entry.kind) { + @case ('text') { +

{{ entry.text }}

+ } + @case ('json') { +
{{ entry.payload | json }}
+ } + @case ('file') { +
+ @if (isImageEntry(entry)) { + + } + + @if (isVideoEntry(entry)) { + + } + + @if (isIncomingJsonFileEntry(entry)) { + + } + +
+
{{ entry.fileName }}
+ @if (entry.fileSize) { +
{{ entry.fileSize | number }} bytes
+ } +
+ + @if (entry.downloadUrl) { + Download + } + + @if (hasDocumentPreviewImage(entry)) { +
+
Preview
+ +
+ } +
+ } + @case ('voice') { +
+
Voice message
+ @if (entry.downloadUrl) { + + } +
+ } + @default { + @if (entry.showSpinner) { +
+ +

{{ entry.text }}

+
+ } @else { +

{{ entry.text }}

+ } + } + } +
+ } +
diff --git a/client/src/app/chat-page.component.scss b/client/src/app/chat-page.component.scss index d75aae6..831201a 100644 --- a/client/src/app/chat-page.component.scss +++ b/client/src/app/chat-page.component.scss @@ -69,6 +69,59 @@ width: min(100%, 25rem); } +.conversation-modal-backdrop { + position: fixed; + inset: 0; + z-index: 1230; + display: grid; + place-items: center; + padding: 0.75rem; + background: rgba(3, 8, 14, 0.56); + backdrop-filter: blur(8px); +} + +.conversation-modal { + display: grid; + grid-template-rows: auto minmax(0, 1fr); + width: min(100%, 96rem); + height: min(100dvh - 1.5rem, 100%); + max-height: 100dvh; +} + +.conversation-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--surface-border-soft); +} + +.conversation-modal-eyebrow { + font-size: 0.78rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--page-text-soft); +} + +.conversation-modal-close { + width: 2.5rem; + height: 2.5rem; + padding: 0; + border: 0; + border-radius: 999px; + color: var(--page-text); + background: var(--badge-background); + font-size: 1.35rem; + line-height: 1; +} + +.conversation-modal-body { + min-height: 0; + max-height: none; + padding-top: 1rem; +} + .call-choice-eyebrow { margin-bottom: 0.45rem; font-size: 0.78rem; @@ -159,6 +212,11 @@ opacity: 1; } +.expand-action-icon { + font-size: 1.9rem; + line-height: 1; +} + .status-led { width: 0.8rem; height: 0.8rem; @@ -187,32 +245,36 @@ } .peer-sidebar { - display: flex; - flex-direction: column; - min-height: 0; padding:1rem; border-radius: 1.3rem; border: 1px solid var(--surface-border-soft); background: var(--panel-soft-background); + align-self: start; } -.peer-count { - display: inline-flex; - min-width: 2rem; - justify-content: center; - padding: 0.35rem 0.65rem; - border-radius: 999px; - font-size: 0.85rem; - background: var(--badge-background); +.peer-dropdown { + position: relative; } -.peer-list { +.peer-dropdown-trigger { + width: 100%; +} + +.peer-dropdown-menu { + position: absolute; + top: calc(100% + 0.65rem); + left: 0; + right: 0; + z-index: 4; display: grid; - align-content: start; - flex: 1 1 auto; gap: 0.75rem; - min-height: 0; + max-height: calc(3 * 4.55rem + 1.5rem); overflow: auto; + padding: 0.75rem; + border: 1px solid var(--surface-border); + border-radius: 1rem; + background: var(--panel-background); + box-shadow: 0 18px 36px rgba(0, 0, 0, 0.18); } .peer-tile { @@ -230,6 +292,7 @@ } .peer-tile-main { + display: block; min-width: 0; padding: 0; border: 0; @@ -237,6 +300,23 @@ background: transparent; } +.peer-tile-indicators { + display: inline-flex; + align-items: center; + gap: 0.55rem; + flex: 0 0 auto; +} + +.peer-dropdown-caret { + font-size: 0.95rem; + line-height: 1; + transition: transform 160ms ease; +} + +.peer-dropdown-caret-open { + transform: rotate(180deg); +} + .peer-tile-delete { width: 2.2rem; height: 2.2rem; @@ -673,6 +753,16 @@ margin-left: 0; } + .conversation-modal-backdrop { + padding: 0; + } + + .conversation-modal { + width: 100vw; + height: 100dvh; + border-radius: 0; + } + .bubble { max-width: 88%; } diff --git a/client/src/app/chat-page.component.ts b/client/src/app/chat-page.component.ts index 2c87342..9afbcbe 100644 --- a/client/src/app/chat-page.component.ts +++ b/client/src/app/chat-page.component.ts @@ -55,10 +55,17 @@ export class ChatPageComponent implements OnDestroy { this.conversationContainer = value; } private conversationContainer?: ElementRef; + @ViewChild('fullscreenConversationContainer') + set fullscreenConversationContainerRef(value: ElementRef | undefined) { + this.fullscreenConversationContainer = value; + } + private fullscreenConversationContainer?: ElementRef; messageText = ''; readonly forwardingEntryId = signal(null); readonly callChoicePeerId = signal(null); + readonly conversationModalOpen = signal(false); + readonly peerDropdownOpen = signal(false); readonly emojiPickerOpen = signal(false); readonly isRecordingVoice = signal(false); readonly isDictating = signal(false); @@ -75,6 +82,7 @@ export class ChatPageComponent implements OnDestroy { ]; readonly peerId = computed(() => this.routeParamMap().get('peerId') ?? ''); readonly peer = computed(() => this.session.peers().find((item) => item.id === this.peerId()) ?? null); + readonly displayedPeer = computed(() => this.peer() ?? this.session.peers()[0] ?? null); readonly currentUser = computed(() => this.session.currentUser()); readonly callModalPeerId = computed(() => this.session.activeVoiceCallPeerId() @@ -574,6 +582,33 @@ export class ChatPageComponent implements OnDestroy { return this.session.peers().filter((peer) => peer.id !== entry.peerId); } + togglePeerDropdown(): void { + if (this.session.peers().length === 0) { + return; + } + + this.peerDropdownOpen.update((open) => !open); + } + + closePeerDropdown(): void { + this.peerDropdownOpen.set(false); + } + + openConversationModal(): void { + this.closePeerDropdown(); + this.conversationModalOpen.set(true); + this.scrollConversationToBottom(); + } + + closeConversationModal(): void { + this.conversationModalOpen.set(false); + } + + async selectPeerFromDropdown(peerId: string): Promise { + this.closePeerDropdown(); + await this.switchPeer(peerId); + } + async forwardEntry(entry: ChatEntry, targetPeerId: string, select: HTMLSelectElement): Promise { if (!targetPeerId) { return; @@ -693,6 +728,8 @@ export class ChatPageComponent implements OnDestroy { this.stopVoiceRecording(true); this.forwardingEntryId.set(null); this.callChoicePeerId.set(null); + this.conversationModalOpen.set(false); + this.peerDropdownOpen.set(false); this.emojiPickerOpen.set(false); this.session.selectPeer(peerId); await this.router.navigate(['/chat', peerId]); @@ -874,15 +911,18 @@ export class ChatPageComponent implements OnDestroy { } private scrollConversationToBottom(): void { - const container = this.conversationContainer?.nativeElement; - - if (!container) { - return; - } - queueMicrotask(() => { requestAnimationFrame(() => { - container.scrollTop = container.scrollHeight; + for (const container of [ + this.conversationContainer?.nativeElement, + this.fullscreenConversationContainer?.nativeElement, + ]) { + if (!container) { + continue; + } + + container.scrollTop = container.scrollHeight; + } }); }); }