CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
- New rooms (public too) and direct messages are end-to-end encrypted by default. - Per-message trust: "Not verified" for sessions you haven't verified, and what is wrong for unsigned sessions, mismatched keys or plain text in an encrypted room (src/matrix/trust.ts). - Verify a person's identity in a room you share (your DM first), and QR-code verification both ways: show ours, scan theirs with the camera (qrcode-generator, jsqr). - Warnings when someone's identity changes, with acknowledge, verify again or withdraw verification. - Encrypted thumbnails for large images. - A tab-only session encrypts its crypto store with a key kept in sessionStorage; stores unused for two weeks are deleted. - Reset encryption fixed: new secret storage first, so the new cross-signing keys survive a stale /keys/query answer; status checks no longer query our own keys mid-setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
74 lines
2.6 KiB
Vue
74 lines
2.6 KiB
Vue
<script setup lang="ts">
|
||
import jsQR from 'jsqr'
|
||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||
import { WBanner, WSpinner } from '@warp/ui'
|
||
|
||
// The camera, looking for a QR code. Frames are read on a timer, decoded
|
||
// here in the page, and the camera is switched off as soon as a code is
|
||
// found or the scanner goes away.
|
||
|
||
const emit = defineEmits<{ found: [data: Uint8ClampedArray] }>()
|
||
|
||
const video = ref<HTMLVideoElement>()
|
||
const error = ref('')
|
||
const starting = ref(true)
|
||
let stream: MediaStream | null = null
|
||
let timer = 0
|
||
const canvas = document.createElement('canvas')
|
||
|
||
function stop() {
|
||
window.clearInterval(timer)
|
||
stream?.getTracks().forEach((t) => t.stop())
|
||
stream = null
|
||
}
|
||
|
||
function scan() {
|
||
const v = video.value
|
||
if (!v || v.readyState < 2 || !v.videoWidth) return
|
||
// Scaled down: decoding is the slow part, and codes are large on screen.
|
||
const scale = Math.min(1, 640 / v.videoWidth)
|
||
canvas.width = Math.round(v.videoWidth * scale)
|
||
canvas.height = Math.round(v.videoHeight * scale)
|
||
const g = canvas.getContext('2d', { willReadFrequently: true })
|
||
if (!g) return
|
||
g.drawImage(v, 0, 0, canvas.width, canvas.height)
|
||
const img = g.getImageData(0, 0, canvas.width, canvas.height)
|
||
const code = jsQR(img.data, img.width, img.height, { inversionAttempts: 'dontInvert' })
|
||
if (code?.binaryData.length) {
|
||
stop()
|
||
emit('found', Uint8ClampedArray.from(code.binaryData))
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
if (!navigator.mediaDevices?.getUserMedia) throw new Error('This browser can’t use a camera here.')
|
||
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false })
|
||
if (!video.value) return stop()
|
||
video.value.srcObject = stream
|
||
await video.value.play()
|
||
timer = window.setInterval(scan, 250)
|
||
} catch (e) {
|
||
error.value =
|
||
e instanceof DOMException && e.name === 'NotAllowedError'
|
||
? 'Camera access was turned down. Allow it in the browser’s site settings, or compare emoji instead.'
|
||
: e instanceof Error
|
||
? e.message
|
||
: 'Can’t start the camera.'
|
||
} finally {
|
||
starting.value = false
|
||
}
|
||
})
|
||
onBeforeUnmount(stop)
|
||
</script>
|
||
|
||
<template>
|
||
<div class="flex flex-col items-center gap-3">
|
||
<WBanner v-if="error" tone="warning" class="w-full">{{ error }}</WBanner>
|
||
<div v-else class="relative aspect-square w-full max-w-[280px] overflow-hidden rounded-md bg-surface-sunken">
|
||
<video ref="video" class="h-full w-full object-cover" muted playsinline />
|
||
<span v-if="starting" class="absolute inset-0 flex items-center justify-center"><WSpinner /></span>
|
||
</div>
|
||
</div>
|
||
</template>
|