Show the verification wherever it starts
The emoji comparison lived inside the Matrix account panel, which was fine while the only flow was one you started there. A request can now arrive from another of your sessions or from another person through a room you share, and a dialog only that panel can draw is a request nobody can answer - so it moved out over the window, beside the profile card and the call banner. An incoming request was also handled nowhere at all: the daemon recorded it and no case in the store ever read it. It now opens the dialog and says who is asking. Whose verification it is leads the dialog, because the two cases mean different things: your own session is a device you are letting in, and somebody else's is a person you are vouching for - and the emoji should be compared out loud, not typed into the chat you are verifying. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+1
-1
Submodule nobilis updated: 8d4855cfa6...e552bbc5e2
@@ -10,6 +10,7 @@ import { Composer } from './components/Composer'
|
||||
import { MembershipGate } from './components/MembershipGate'
|
||||
import { NickList } from './components/NickList'
|
||||
import { ThreadPanel } from './components/ThreadPanel'
|
||||
import { VerificationDialog } from './components/VerificationDialog'
|
||||
import { ProfileCard } from './components/ProfileCard'
|
||||
import { ConversationTools } from './components/ConversationTools'
|
||||
import { AccountsPanel } from './components/AccountsPanel'
|
||||
@@ -193,6 +194,12 @@ export default function App(): JSX.Element {
|
||||
{/* Over everything: an answer to something just asked. */}
|
||||
<ProfileCard />
|
||||
|
||||
{/* Over everything, because a verification can now start somewhere
|
||||
else entirely - another of your sessions, or another person in a
|
||||
room you share - and a dialog only the account panel can show is a
|
||||
request nobody can answer. */}
|
||||
<VerificationDialog />
|
||||
|
||||
{/* Over everything: a call arrives while you are looking elsewhere. */}
|
||||
<IncomingCallPanel />
|
||||
{/* Over everything for the same reason: a file is offered while you are
|
||||
|
||||
@@ -332,6 +332,32 @@
|
||||
|
||||
|
||||
|
||||
/* --- verifying somebody ------------------------------------------------
|
||||
Over the window rather than inside the account panel, because a request
|
||||
can arrive from another session or another person at any moment - and the
|
||||
panel it used to live in is usually closed when one does. */
|
||||
.verification-dialog {
|
||||
position: fixed;
|
||||
z-index: 70;
|
||||
right: var(--spacing-m);
|
||||
bottom: var(--spacing-m);
|
||||
width: min(360px, calc(100vw - var(--spacing-m) * 2));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-s);
|
||||
padding: var(--spacing-m);
|
||||
background: var(--surface-container-high);
|
||||
border: 1px solid var(--outline-strong);
|
||||
border-radius: var(--corner-radius);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.verification-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
/* --- panels hanging off the header -------------------------------------
|
||||
Fixed to the window rather than laid out in the header, because the header
|
||||
is a row that has to compact: a panel inside it either widens the row past
|
||||
|
||||
@@ -426,88 +426,14 @@ export function MatrixAccountTools({ account }: { account: Account }): JSX.Eleme
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* The emoji comparison is drawn over the whole window now rather
|
||||
than in here: a verification can arrive from another session, or
|
||||
from another person through a room you share, and this panel is
|
||||
not necessarily open when one does. */}
|
||||
{active && (
|
||||
<div className="verification-box">
|
||||
{active.emoji ? (
|
||||
<>
|
||||
<div className="setting-text">Do these match on the other device?</div>
|
||||
<div className="sas-emoji">
|
||||
{active.emoji.map((e, i) => (
|
||||
<div key={i} className="sas-cell">
|
||||
<span className="sas-symbol">{e.symbol}</span>
|
||||
<span className="small muted">{e.description}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="button-row">
|
||||
<button
|
||||
type="button"
|
||||
className="button"
|
||||
onClick={() =>
|
||||
void rpc('confirmMatrixVerification', {
|
||||
accountId: account.id,
|
||||
verificationId: active.verificationId,
|
||||
matches: true
|
||||
})
|
||||
}
|
||||
>
|
||||
They match
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button danger"
|
||||
onClick={() =>
|
||||
void rpc('confirmMatrixVerification', {
|
||||
accountId: account.id,
|
||||
verificationId: active.verificationId,
|
||||
matches: false
|
||||
})
|
||||
}
|
||||
>
|
||||
They don't
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="setting-text">
|
||||
Verification {active.state || 'starting'}…
|
||||
</div>
|
||||
<p className="small muted">
|
||||
Accept the request on your other session; the emoji to compare will appear here.
|
||||
</p>
|
||||
<div className="button-row">
|
||||
{active.state === 'requested' && (
|
||||
<button
|
||||
type="button"
|
||||
className="button"
|
||||
onClick={() =>
|
||||
void rpc('respondMatrixVerification', {
|
||||
accountId: account.id,
|
||||
verificationId: active.verificationId,
|
||||
accept: true
|
||||
})
|
||||
}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="button subtle"
|
||||
onClick={() =>
|
||||
void rpc('cancelMatrixVerification', {
|
||||
accountId: account.id,
|
||||
verificationId: active.verificationId
|
||||
})
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="small muted">
|
||||
A verification is in progress — the comparison is shown over the window.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="divider-h" />
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Icon } from './Icon'
|
||||
import { useChat, useStore } from '../state/hooks'
|
||||
|
||||
/**
|
||||
* The emoji comparison, over everything.
|
||||
*
|
||||
* It used to live inside the Matrix account panel, which was fine while the
|
||||
* only flow was one you started there. A verification can now arrive from
|
||||
* another person - through the room you share, the way Element sends one -
|
||||
* and a dialog nobody can see is a request nobody can answer.
|
||||
*
|
||||
* Whose verification it is leads, because the two cases mean different
|
||||
* things: your own session is a device you are letting in, and somebody
|
||||
* else's is a person you are vouching for.
|
||||
*/
|
||||
export function VerificationDialog(): JSX.Element | null {
|
||||
const store = useStore()
|
||||
const active = useChat((s) => s.matrixVerification)
|
||||
if (!active) return null
|
||||
|
||||
const who = active.otherUser || active.fromUser || ''
|
||||
const mine = active.isSelf !== false && (!who || who === active.accountId.replace(/^matrix:/, ''))
|
||||
const rpc = (method: string, params: Record<string, unknown>): void => {
|
||||
void window.moho
|
||||
.rpc(method, { accountId: active.accountId, verificationId: active.verificationId, ...params })
|
||||
.catch((e: Error) => store.toast('error', e.message))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="verification-dialog">
|
||||
<div className="verification-head">
|
||||
<Icon name={mine ? 'devices' : 'person_check'} size={18} />
|
||||
<div className="setting-text">
|
||||
<div>{mine ? 'Verifying your other session' : `Verifying ${who}`}</div>
|
||||
{active.otherDevice && <div className="small muted">{active.otherDevice}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{active.emoji ? (
|
||||
<>
|
||||
<div className="small muted">
|
||||
{mine
|
||||
? 'Do these match on your other device?'
|
||||
: 'Do these match on their screen? Ask them - out loud, not in this chat.'}
|
||||
</div>
|
||||
<div className="sas-emoji">
|
||||
{active.emoji.map((e, i) => (
|
||||
<div key={i} className="sas-cell">
|
||||
<span className="sas-symbol">{e.symbol}</span>
|
||||
<span className="small muted">{e.description}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="button-row">
|
||||
<button type="button" className="button" onClick={() => rpc('confirmMatrixVerification', { matches: true })}>
|
||||
They match
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button danger"
|
||||
onClick={() => rpc('confirmMatrixVerification', { matches: false })}
|
||||
>
|
||||
They don't
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="small muted">
|
||||
{active.state === 'requested'
|
||||
? 'Waiting for you to accept.'
|
||||
: `Verification ${active.state || 'starting'}…`}
|
||||
</div>
|
||||
<div className="button-row">
|
||||
{active.state === 'requested' && (
|
||||
<button type="button" className="button" onClick={() => rpc('respondMatrixVerification', { accept: true })}>
|
||||
Accept
|
||||
</button>
|
||||
)}
|
||||
<button type="button" className="button subtle" onClick={() => rpc('cancelMatrixVerification', {})}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1757,6 +1757,29 @@ export class ChatStore {
|
||||
|
||||
// Verification is a multi-step flow whose steps all arrive as pushes:
|
||||
// a status change, then the emoji to compare, then a final result.
|
||||
// Somebody asking to verify, rather than us asking them. Nothing
|
||||
// handled this, so a request arriving from another session - or from
|
||||
// another person, now that those reach here at all - was recorded by
|
||||
// the daemon and shown nowhere.
|
||||
case 'matrixVerificationIncoming':
|
||||
this.set({
|
||||
matrixVerification: {
|
||||
accountId: data.accountId,
|
||||
verificationId: data.verificationId,
|
||||
state: 'requested',
|
||||
otherUser: data.fromUser,
|
||||
otherDevice: data.fromDevice,
|
||||
isSelf: data.isSelf
|
||||
}
|
||||
})
|
||||
this.toast(
|
||||
'info',
|
||||
data.isSelf === false
|
||||
? `${data.fromUser} wants to verify with you`
|
||||
: 'Another of your sessions wants to verify'
|
||||
)
|
||||
break
|
||||
|
||||
case 'matrixVerificationStatus':
|
||||
this.set({
|
||||
matrixVerification: { ...(this.state.matrixVerification || {}), ...data }
|
||||
|
||||
@@ -508,6 +508,19 @@ export interface MatrixVerification {
|
||||
verificationId: string
|
||||
state?: string
|
||||
emoji?: SasEmoji[]
|
||||
/**
|
||||
* Who is on the other end.
|
||||
*
|
||||
* For one of your own sessions this is you and the device is the
|
||||
* interesting half; for somebody else it is the whole point - a dialog
|
||||
* that cannot say whose emoji these are is one nobody should press yes on.
|
||||
*/
|
||||
otherUser?: string
|
||||
otherDevice?: string | null
|
||||
isSelf?: boolean
|
||||
/** Set on an incoming request that has not been answered yet. */
|
||||
fromUser?: string
|
||||
fromDevice?: string
|
||||
}
|
||||
|
||||
export interface RoomPermissions {
|
||||
|
||||
Reference in New Issue
Block a user