diff --git a/client/src/app/chat-page.component.scss b/client/src/app/chat-page.component.scss index cf2ffa8..688489e 100644 --- a/client/src/app/chat-page.component.scss +++ b/client/src/app/chat-page.component.scss @@ -54,116 +54,6 @@ backdrop-filter: blur(8px); } -.call-choice-backdrop { - position: fixed; - inset: 0; - z-index: 1240; - display: grid; - place-items: center; - padding: 1rem; - background: rgba(3, 8, 14, 0.46); - backdrop-filter: blur(6px); -} - -.call-choice-card { - 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; - border: 0; -} - -.conversation-modal-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding-bottom: 1rem; -} - -.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; - letter-spacing: 0.16em; - text-transform: uppercase; - color: var(--page-text-soft); -} - -.call-choice-actions { - display: grid; - gap: 0.85rem; -} - -.call-choice-button { - display: flex; - align-items: center; - gap: 0.85rem; - width: 100%; - padding: 1rem 1.1rem; - border: 1px solid var(--surface-border); - border-radius: 1rem; - color: var(--page-text); - background: var(--surface-background); - text-align: left; -} - -.call-choice-button:hover, -.call-choice-button:focus-visible { - border-color: color-mix(in srgb, var(--accent-color) 35%, transparent); - background: var(--surface-hover-background); -} - -.call-choice-icon { - display: inline-grid; - place-items: center; - width: 2.5rem; - height: 2.5rem; - border-radius: 999px; - background: var(--badge-background); - font-size: 1.2rem; -} - .back-link { display: inline-flex; align-items: center; @@ -191,51 +81,11 @@ color: var(--page-text-soft); } -.status-indicator-action { - padding: 0; - border: 0; - background: transparent; -} - -.status-indicator-action:not(:disabled) { - color: var(--page-text); - cursor: pointer; -} - -.status-indicator-action:not(:disabled):hover, -.status-indicator-action:not(:disabled):focus-visible { - color: var(--accent-color); -} - -.status-indicator-action:disabled { - cursor: default; - opacity: 1; -} - .expand-action-icon { font-size: 1.9rem; line-height: 1; } -.status-led { - width: 0.8rem; - height: 0.8rem; - border-radius: 999px; - box-shadow: 0 0 0 1px var(--input-border); -} - -.status-led-ok { - background: #59d66f; -} - -.status-led-connecting { - background: #f3ad3d; -} - -.status-led-offline { - background: #eb5d64; -} - .chat-layout { display: grid; flex: 1 1 auto; @@ -244,145 +94,6 @@ min-height: 0; } -.peer-dropdown { - position: relative; - min-width: min(18rem, 42vw); -} - -.peer-dropdown-trigger { - width: 100%; - padding-top: 0.56rem; - padding-bottom: 0.56rem; -} - -.peer-dropdown-menu { - position: absolute; - top: calc(100% + 0.65rem); - left: 0; - width: 100%; - z-index: 4; - display: grid; - gap: 0.75rem; - 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 { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 0.75rem; - align-items: center; - width: 100%; - padding: 0.8rem 0.85rem 0.8rem 1rem; - border: 1px solid var(--surface-border); - border-radius: 1rem; - color: inherit; - background: var(--surface-background); - font-size: 1.05em; - transition: transform 160ms ease, border-color 160ms ease, background 160ms ease; -} - -.peer-tile-main { - display: block; - min-width: 0; - padding: 0; - border: 0; - color: inherit; - background: transparent; -} - -.peer-tile-indicators { - display: inline-flex; - align-items: center; - gap: 0.38rem; - flex: 0 0 auto; -} - -.peer-dropdown-caret { - font-size: 4.02rem; - line-height: 1; - transition: transform 160ms ease; -} - -.peer-dropdown-caret-open { - transform: rotate(180deg); -} - -.peer-tile-delete { - width: 1.54rem; - height: 1.54rem; - padding: 0; - border: 0; - border-radius: 999px; - background: transparent; - font-size: 0.7rem; - line-height: 1; -} - -.peer-tile-delete:hover, -.peer-tile-delete:focus-visible { - background: var(--badge-background); -} - -.peer-tile:hover, -.peer-tile:focus-visible, -.peer-tile-active { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--accent-color) 35%, transparent); - background: var(--surface-hover-background); -} - -.peer-tile-unread { - border-color: #c62828; - box-shadow: inset 0 0 0 2px #c62828; -} - -.peer-tile-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.53rem; -} - -.peer-tile-title { - display: inline-flex; - align-items: center; - gap: 0.32rem; - min-width: 0; -} - -.peer-typing-dots { - display: inline-flex; - align-items: center; - gap: 0.2rem; - min-height: 0.9rem; -} - -.peer-typing-dots span { - width: 0.27rem; - height: 0.27rem; - border-radius: 999px; - background: var(--page-text); - opacity: 0.28; - animation: peer-typing-pulse 900ms infinite ease-in-out; -} - -.peer-typing-dots span:nth-child(2) { - animation-delay: 120ms; -} - -.peer-typing-dots span:nth-child(3) { - animation-delay: 240ms; -} - -.peer-tile-status { - flex: 0 0 auto; -} .chat-main { display: grid; @@ -455,80 +166,11 @@ background: var(--input-background); } -.bubble-incoming { - justify-self: start; - color: var(--incoming-bubble-text); - background: var(--incoming-bubble-background); -} - -.bubble-outgoing { - justify-self: end; - color: var(--outgoing-bubble-text); - background: var(--outgoing-bubble-background); -} - -.bubble-pending { - opacity: 0.58; - filter: grayscale(0.28); -} - -.bubble-system { - justify-self: center; - max-width: 90%; - color: var(--page-text-soft); - background: var(--badge-background); -} - -.bubble-emoji-only { - max-width: none; - padding: 0; - border-radius: 0; - background: transparent; - box-shadow: none; -} - -.bubble-meta { - display: grid; - gap: 0.12rem; - margin-bottom: 0.35rem; - font-size: 0.78rem; - opacity: 0.7; -} - -.bubble-time { - display: block; -} - -.bubble-delivery-state { - display: inline-block; - margin-top: 0.1rem; - font-size: 0.72rem; - letter-spacing: 0.06em; - text-transform: uppercase; -} - .emoji-only-text { font-size: clamp(2.1rem, 5vw, 3.4rem); line-height: 1.15; } -.bubble-system-status { - display: inline-flex; - align-items: center; - gap: 0.7rem; -} - -.bubble-spinner { - width: 1rem; - height: 1rem; - flex: 0 0 auto; - border: 0.15rem solid currentColor; - border-right-color: transparent; - border-radius: 999px; - opacity: 0.8; - animation: bubble-spin 700ms linear infinite; -} - .composer { display: grid; gap: 0.85rem; @@ -778,50 +420,16 @@ color: var(--page-text); } -@keyframes peer-typing-pulse { - 0%, - 80%, - 100% { - opacity: 0.28; - transform: translateY(0); - } - - 40% { - opacity: 1; - transform: translateY(-1px); - } -} - -@keyframes bubble-spin { - to { - transform: rotate(360deg); - } -} - @media (max-width: 767.98px) { .chat-layout { grid-template-columns: 1fr; } - .peer-dropdown { - min-width: min(100%, 18rem); - } - .status-indicators { width: 100%; 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 77f5cd4..d441669 100644 --- a/client/src/app/chat-page.component.ts +++ b/client/src/app/chat-page.component.ts @@ -55,7 +55,7 @@ export class ChatPageComponent implements OnDestroy { private resolveDictationCompletion: (() => void) | null = null; private dictationApplyToken = 0; private lastConversationSnapshot: { peerId: string; length: number; lastEntryId: string | null } | null = null; - private lastAutoConnectSnapshot: { peerId: string; hasLivePeer: boolean } | null = null; + private lastAutoConnectedPeerId: string | null = null; @ViewChild('callAudioElement') set callAudioElementRef(value: ElementRef | undefined) { this.callAudioElement = value; @@ -312,24 +312,26 @@ export class ChatPageComponent implements OnDestroy { effect(() => { const peerId = this.peerId(); const hasLivePeer = !!this.peer(); - const previousSnapshot = this.lastAutoConnectSnapshot; if (!peerId) { - this.lastAutoConnectSnapshot = null; + this.lastAutoConnectedPeerId = null; return; } - this.lastAutoConnectSnapshot = { peerId, hasLivePeer }; this.session.selectPeer(peerId); if (!hasLivePeer) { + if (this.lastAutoConnectedPeerId === peerId) { + this.lastAutoConnectedPeerId = null; + } return; } - if (previousSnapshot?.peerId === peerId && previousSnapshot.hasLivePeer) { + if (this.lastAutoConnectedPeerId === peerId) { return; } + this.lastAutoConnectedPeerId = peerId; void this.session.connectToPeer(peerId); }); diff --git a/client/src/styles.scss b/client/src/styles.scss index 9174570..151291d 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -176,3 +176,407 @@ textarea { .alert-warning { border: 1px solid var(--surface-border); } + +.call-choice-backdrop { + position: fixed; + inset: 0; + z-index: 1240; + display: grid; + place-items: center; + padding: 1rem; + background: rgba(3, 8, 14, 0.46); + backdrop-filter: blur(6px); +} + +.call-choice-card { + 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; + border: 0; +} + +.conversation-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding-bottom: 1rem; +} + +.conversation-modal-eyebrow, +.call-choice-eyebrow, +.bubble-delivery-state { + text-transform: uppercase; +} + +.conversation-modal-eyebrow { + font-size: 0.78rem; + letter-spacing: 0.14em; + 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; + letter-spacing: 0.16em; + color: var(--page-text-soft); +} + +.call-choice-actions { + display: grid; + gap: 0.85rem; +} + +.call-choice-button { + display: flex; + align-items: center; + gap: 0.85rem; + width: 100%; + padding: 1rem 1.1rem; + border: 1px solid var(--surface-border); + border-radius: 1rem; + color: var(--page-text); + background: var(--surface-background); + text-align: left; +} + +.call-choice-button:hover, +.call-choice-button:focus-visible, +.peer-tile:hover, +.peer-tile:focus-visible, +.peer-tile-active { + border-color: color-mix(in srgb, var(--accent-color) 35%, transparent); + background: var(--surface-hover-background); +} + +.call-choice-icon { + display: inline-grid; + place-items: center; + width: 2.5rem; + height: 2.5rem; + border-radius: 999px; + background: var(--badge-background); + font-size: 1.2rem; +} + +.status-indicator-action { + padding: 0; + border: 0; + background: transparent; +} + +.status-indicator-action:not(:disabled) { + color: var(--page-text); + cursor: pointer; +} + +.status-indicator-action:not(:disabled):hover, +.status-indicator-action:not(:disabled):focus-visible { + color: var(--accent-color); +} + +.status-indicator-action:disabled { + cursor: default; + opacity: 1; +} + +.status-led, +.peer-tile-delete, +.bubble-spinner { + border-radius: 999px; +} + +.status-led { + width: 0.8rem; + height: 0.8rem; + box-shadow: 0 0 0 1px var(--input-border); +} + +.status-led-ok { + background: #59d66f; +} + +.status-led-connecting { + background: #f3ad3d; +} + +.status-led-offline { + background: #eb5d64; +} + +.peer-dropdown { + position: relative; + min-width: min(18rem, 42vw); +} + +.peer-dropdown-trigger { + width: 100%; + padding-top: 0.56rem; + padding-bottom: 0.56rem; +} + +.peer-dropdown-menu { + position: absolute; + top: calc(100% + 0.65rem); + left: 0; + z-index: 4; + display: grid; + gap: 0.75rem; + width: 100%; + 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 { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: center; + width: 100%; + padding: 0.8rem 0.85rem 0.8rem 1rem; + border: 1px solid var(--surface-border); + border-radius: 1rem; + color: inherit; + background: var(--surface-background); + font-size: 1.05em; + transition: transform 160ms ease, border-color 160ms ease, background 160ms ease; +} + +.peer-tile-main { + display: block; + min-width: 0; + padding: 0; + border: 0; + color: inherit; + background: transparent; +} + +.peer-tile-indicators, +.bubble-system-status { + display: inline-flex; + align-items: center; +} + +.peer-tile-indicators { + gap: 0.38rem; + flex: 0 0 auto; +} + +.peer-dropdown-caret { + font-size: 4.02rem; + line-height: 1; + transition: transform 160ms ease; +} + +.peer-dropdown-caret-open { + transform: rotate(180deg); +} + +.peer-tile-delete { + width: 1.54rem; + height: 1.54rem; + padding: 0; + border: 0; + background: transparent; + font-size: 0.7rem; + line-height: 1; +} + +.peer-tile-delete:hover, +.peer-tile-delete:focus-visible { + background: var(--badge-background); +} + +.peer-tile:hover, +.peer-tile:focus-visible, +.peer-tile-active { + transform: translateY(-1px); +} + +.peer-tile-unread { + border-color: #c62828; + box-shadow: inset 0 0 0 2px #c62828; +} + +.peer-tile-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.53rem; +} + +.peer-tile-title { + display: inline-flex; + align-items: center; + gap: 0.32rem; + min-width: 0; +} + +.peer-typing-dots { + display: inline-flex; + align-items: center; + gap: 0.2rem; + min-height: 0.9rem; +} + +.peer-typing-dots span { + width: 0.27rem; + height: 0.27rem; + border-radius: 999px; + background: var(--page-text); + opacity: 0.28; + animation: peer-typing-pulse 900ms infinite ease-in-out; +} + +.peer-typing-dots span:nth-child(2) { + animation-delay: 120ms; +} + +.peer-typing-dots span:nth-child(3) { + animation-delay: 240ms; +} + +.peer-tile-status { + flex: 0 0 auto; +} + +.bubble-incoming { + justify-self: start; + color: var(--incoming-bubble-text); + background: var(--incoming-bubble-background); +} + +.bubble-outgoing { + justify-self: end; + color: var(--outgoing-bubble-text); + background: var(--outgoing-bubble-background); +} + +.bubble-pending { + opacity: 0.58; + filter: grayscale(0.28); +} + +.bubble-system { + justify-self: center; + max-width: 90%; + color: var(--page-text-soft); + background: var(--badge-background); +} + +.bubble-emoji-only { + max-width: none; + padding: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +.bubble-meta { + display: grid; + gap: 0.12rem; + margin-bottom: 0.35rem; + font-size: 0.78rem; + opacity: 0.7; +} + +.bubble-time { + display: block; +} + +.bubble-delivery-state { + display: inline-block; + margin-top: 0.1rem; + font-size: 0.72rem; + letter-spacing: 0.06em; +} + +.bubble-system-status { + gap: 0.7rem; +} + +.bubble-spinner { + width: 1rem; + height: 1rem; + flex: 0 0 auto; + border: 0.15rem solid currentColor; + border-right-color: transparent; + opacity: 0.8; + animation: bubble-spin 700ms linear infinite; +} + +@keyframes peer-typing-pulse { + 0%, + 80%, + 100% { + opacity: 0.28; + transform: translateY(0); + } + + 40% { + opacity: 1; + transform: translateY(-1px); + } +} + +@keyframes bubble-spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 767.98px) { + .peer-dropdown { + min-width: min(100%, 18rem); + } + + .conversation-modal-backdrop { + padding: 0; + } + + .conversation-modal { + width: 100vw; + height: 100dvh; + border-radius: 0; + } +}