Somewhere to point the other device's camera

The dialog draws the QR code beside the emoji rather than instead of them.
Both are offered because both are needed: a code cannot be made without
cross-signing, and when there is no code the emoji are the only way through.

Asked for once the flow is past "requested" - a code can only be made after
both sides have agreed what they are verifying - and keyed on the verification
so a new one fetches its own picture rather than showing the last session's.

Drawn as an image from a data URI rather than injected as markup. The SVG is
the daemon's own drawing of a QR grid and carries nothing from anywhere else,
so injecting it would have been safe; not making the exception is the habit
worth keeping.

White behind it on both themes. A scanner reads contrast, and a code drawn
dark-on-dark is a code nothing can read.

Closes #163.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-10 17:05:05 -04:00
co-authored by Claude Opus 5
parent bd05e9c861
commit 715f999944
3 changed files with 77 additions and 1 deletions
+1 -1
Submodule nobilis updated: d06a449e92...2929ff7bde
+20
View File
@@ -3392,6 +3392,26 @@ img.smilie {
background: var(--primary-hover-light);
}
/* The QR code, where the homeserver and the other device can support one.
White behind it always, on both themes: a scanner reads contrast, and a
code drawn dark-on-dark is a code nothing can read. */
.verification-qr {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
padding-top: var(--spacing-xs);
}
.verification-qr-code {
width: 180px;
height: 180px;
display: block;
background: #fff;
padding: var(--spacing-xs);
border-radius: var(--corner-radius-small);
}
.sas-emoji {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
@@ -1,3 +1,4 @@
import { useEffect, useState } from 'react'
import { Icon } from './Icon'
import { useChat, useStore } from '../state/hooks'
@@ -14,8 +15,43 @@ import { useChat, useStore } from '../state/hooks'
* else's is a person you are vouching for.
*/
export function VerificationDialog(): JSX.Element | null {
const active = useChat((s) => s.matrixVerification)
if (!active) return null
// Keyed on the verification so its QR code is fetched afresh for a new one,
// rather than a picture of the last session being shown against this one.
return <VerificationBody key={active.verificationId} />
}
function VerificationBody(): JSX.Element | null {
const store = useStore()
const active = useChat((s) => s.matrixVerification)
const [qr, setQr] = useState<string | null>(null)
// Asked for once the flow is past "requested" - a code can only be made
// after both sides have agreed what they are verifying. Answering null is
// the ordinary case rather than a failure: a QR code proves an identity the
// other side can already check, so without cross-signing there is nothing
// to encode and emoji stay the way through.
useEffect(() => {
if (!active || active.emoji) return
let dropped = false
void window.moho
.rpc<{ svg: string | null }>('matrixVerificationQrCode', {
accountId: active.accountId,
verificationId: active.verificationId
})
.then((r) => {
if (!dropped) setQr(r.svg)
})
.catch(() => {
/* No code to show is not an error worth a toast; emoji still work. */
})
return () => {
dropped = true
}
}, [active?.verificationId, active?.state, active?.emoji])
if (!active) return null
const who = active.otherUser || active.fromUser || ''
@@ -71,6 +107,26 @@ export function VerificationDialog(): JSX.Element | null {
? 'Waiting for you to accept.'
: `Verification ${active.state || 'starting'}…`}
</div>
{/* Scanning is the way most people verify, and until now moho had
nothing to be scanned - Element would offer its camera and this
side could only answer with emoji. Shown rather than scanned:
on a desktop the device holding the camera is the other one. */}
{qr && (
<div className="verification-qr">
<div className="small muted">Or scan this from your other device.</div>
{/* As an image rather than injected markup. The SVG is the
daemon's own drawing of a QR grid and carries nothing from
anywhere else, but a data URI reaches the same picture
without this page ever parsing markup it was handed - and
the one habit worth keeping is not making that exception. */}
<img
className="verification-qr-code"
alt="Verification QR code"
src={`data:image/svg+xml;utf8,${encodeURIComponent(qr)}`}
/>
</div>
)}
<div className="button-row">
{active.state === 'requested' && (
<button type="button" className="button" onClick={() => rpc('respondMatrixVerification', { accept: true })}>