|
1 | 1 | <script lang="ts"> |
2 | 2 | import { onMount, tick } from 'svelte' |
| 3 | + import { fade, slide } from 'svelte/transition' |
3 | 4 | import { browser, dev } from '$app/environment' |
4 | 5 | import { _ } from 'svelte-i18n' |
5 | 6 | import { pg, retryStatus } from '$lib/postguard' |
|
44 | 45 |
|
45 | 46 | let opened: Awaited<ReturnType<typeof pg.open>> | null = null |
46 | 47 |
|
| 48 | + // Hold on the QR card briefly after Yivi succeeds so its success |
| 49 | + // animation can finish before we swap in the progress banner. |
| 50 | + const YIVI_SUCCESS_HOLD_MS = 1400 |
| 51 | + let pendingDecryptFlip: ReturnType<typeof setTimeout> | null = null |
| 52 | +
|
47 | 53 | let isMobileDevice = isMobile() |
48 | 54 |
|
49 | 55 | onMount(() => { |
|
121 | 127 | recipient: key, |
122 | 128 | enableCache: true, |
123 | 129 | onDownloadProgress: (pct) => { |
124 | | - // Yivi scan happens before any bytes flow — the first |
125 | | - // progress tick is our cue to flip into Decrypting. |
126 | | - if (downloadState !== 'Decrypting') { |
127 | | - downloadState = 'Decrypting' |
128 | | - } |
129 | 130 | decryptPct = pct |
| 131 | + if ( |
| 132 | + downloadState !== 'Decrypting' && |
| 133 | + pendingDecryptFlip === null |
| 134 | + ) { |
| 135 | + pendingDecryptFlip = setTimeout(() => { |
| 136 | + pendingDecryptFlip = null |
| 137 | + if (downloadState === 'Ready') { |
| 138 | + downloadState = 'Decrypting' |
| 139 | + } |
| 140 | + }, YIVI_SUCCESS_HOLD_MS) |
| 141 | + } |
130 | 142 | }, |
131 | 143 | })) as DecryptFileResult |
132 | 144 |
|
|
226 | 238 | /> |
227 | 239 | </div> |
228 | 240 | {:else if downloadState === 'Ready'} |
229 | | - <p class="description"> |
230 | | - {$_('filesharing.decryptpanel.pageDescription')} |
231 | | - </p> |
232 | | - <div class="decrypt-card"> |
233 | | - <h3> |
234 | | - {isMobileDevice |
235 | | - ? $_( |
236 | | - 'filesharing.decryptpanel.irmaInstructionHeaderMobile' |
237 | | - ) |
238 | | - : $_( |
239 | | - 'filesharing.decryptpanel.irmaInstructionHeaderQr' |
240 | | - )} |
241 | | - </h3> |
242 | | - <p class="card-subtitle"> |
243 | | - {isMobileDevice |
244 | | - ? $_('filesharing.decryptpanel.irmaInstructionMobile') |
245 | | - : $_('filesharing.decryptpanel.irmaInstructionQr')} |
| 241 | + <div |
| 242 | + class="state-wrap" |
| 243 | + in:fade={{ duration: 250, delay: 200 }} |
| 244 | + out:fade={{ duration: 200 }} |
| 245 | + > |
| 246 | + <p class="description"> |
| 247 | + {$_('filesharing.decryptpanel.pageDescription')} |
246 | 248 | </p> |
247 | | - <YiviQRCode |
248 | | - id="yivi-download" |
249 | | - responsive |
250 | | - mode={isMobileDevice ? 'deeplink' : 'qr'} |
251 | | - /> |
252 | | - </div> |
253 | | - <HelpToggle |
254 | | - title={$_('filesharing.encryptPanel.yiviInfo')} |
255 | | - content={$_('filesharing.encryptPanel.yiviInfoText')} |
256 | | - linkText={$_('filesharing.encryptPanel.yiviInfoLink')} |
257 | | - linkUrl="https://yivi.app" |
258 | | - bordered |
259 | | - /> |
260 | | - {#if senderIdentity?.email} |
261 | | - <div class="sender-section"> |
262 | | - <svg |
263 | | - class="checkmark" |
264 | | - viewBox="0 0 12 10" |
265 | | - fill="none" |
266 | | - xmlns="http://www.w3.org/2000/svg" |
267 | | - > |
268 | | - <path |
269 | | - d="M1 5L4.5 8.5L11 1" |
270 | | - stroke="currentColor" |
271 | | - stroke-width="1.75" |
272 | | - stroke-linecap="round" |
273 | | - stroke-linejoin="round" |
274 | | - /> |
275 | | - </svg> |
276 | | - <p class="sender-label"> |
277 | | - {$_('filesharing.decryptpanel.verifiedEmail')} |
| 249 | + <div class="decrypt-card"> |
| 250 | + <h3> |
| 251 | + {isMobileDevice |
| 252 | + ? $_( |
| 253 | + 'filesharing.decryptpanel.irmaInstructionHeaderMobile' |
| 254 | + ) |
| 255 | + : $_( |
| 256 | + 'filesharing.decryptpanel.irmaInstructionHeaderQr' |
| 257 | + )} |
| 258 | + </h3> |
| 259 | + <p class="card-subtitle"> |
| 260 | + {isMobileDevice |
| 261 | + ? $_( |
| 262 | + 'filesharing.decryptpanel.irmaInstructionMobile' |
| 263 | + ) |
| 264 | + : $_('filesharing.decryptpanel.irmaInstructionQr')} |
278 | 265 | </p> |
279 | | - <strong class="sender-email">{senderIdentity.email}</strong> |
280 | | - </div> |
281 | | - {/if} |
282 | | - {:else if downloadState === 'Decrypting'} |
283 | | - <div class="decrypt-card"> |
284 | | - <DecryptionProgress percentage={decryptPct} /> |
285 | | - </div> |
286 | | - {#if $retryStatus} |
287 | | - <p class="retry-status" role="status"> |
288 | | - {$_('filesharing.encryptPanel.retrying', { |
289 | | - values: { |
290 | | - attempt: $retryStatus.attempt + 1, |
291 | | - max: $retryStatus.maxAttempts, |
292 | | - }, |
293 | | - })} |
294 | | - </p> |
295 | | - {/if} |
296 | | - {:else if downloadState === 'Done'} |
297 | | - <div class="success-banner"> |
298 | | - <svg |
299 | | - class="banner-check" |
300 | | - viewBox="0 0 12 10" |
301 | | - fill="none" |
302 | | - xmlns="http://www.w3.org/2000/svg" |
303 | | - > |
304 | | - <path |
305 | | - d="M1 5L4.5 8.5L11 1" |
306 | | - stroke="currentColor" |
307 | | - stroke-width="1.75" |
308 | | - stroke-linecap="round" |
309 | | - stroke-linejoin="round" |
| 266 | + <YiviQRCode |
| 267 | + id="yivi-download" |
| 268 | + responsive |
| 269 | + mode={isMobileDevice ? 'deeplink' : 'qr'} |
310 | 270 | /> |
311 | | - </svg> |
312 | | - <p>{$_('filesharing.decryptpanel.doneMessage')}</p> |
| 271 | + </div> |
| 272 | + <HelpToggle |
| 273 | + title={$_('filesharing.encryptPanel.yiviInfo')} |
| 274 | + content={$_('filesharing.encryptPanel.yiviInfoText')} |
| 275 | + linkText={$_('filesharing.encryptPanel.yiviInfoLink')} |
| 276 | + linkUrl="https://yivi.app" |
| 277 | + bordered |
| 278 | + /> |
| 279 | + {#if senderIdentity?.email} |
| 280 | + <div class="sender-section"> |
| 281 | + <svg |
| 282 | + class="checkmark" |
| 283 | + viewBox="0 0 12 10" |
| 284 | + fill="none" |
| 285 | + xmlns="http://www.w3.org/2000/svg" |
| 286 | + > |
| 287 | + <path |
| 288 | + d="M1 5L4.5 8.5L11 1" |
| 289 | + stroke="currentColor" |
| 290 | + stroke-width="1.75" |
| 291 | + stroke-linecap="round" |
| 292 | + stroke-linejoin="round" |
| 293 | + /> |
| 294 | + </svg> |
| 295 | + <p class="sender-label"> |
| 296 | + {$_('filesharing.decryptpanel.verifiedEmail')} |
| 297 | + </p> |
| 298 | + <strong class="sender-email" |
| 299 | + >{senderIdentity.email}</strong |
| 300 | + > |
| 301 | + </div> |
| 302 | + {/if} |
313 | 303 | </div> |
314 | | - |
315 | | - <FileList files={fileList} /> |
316 | | - |
317 | | - {#if senderIdentity?.email} |
318 | | - <div class="sender-section"> |
319 | | - <svg |
320 | | - class="checkmark" |
321 | | - viewBox="0 0 12 10" |
322 | | - fill="none" |
323 | | - xmlns="http://www.w3.org/2000/svg" |
324 | | - > |
325 | | - <path |
326 | | - d="M1 5L4.5 8.5L11 1" |
327 | | - stroke="currentColor" |
328 | | - stroke-width="1.75" |
329 | | - stroke-linecap="round" |
330 | | - stroke-linejoin="round" |
331 | | - /> |
332 | | - </svg> |
333 | | - <p class="sender-label"> |
334 | | - {$_('filesharing.decryptpanel.verifiedEmail')} |
335 | | - </p> |
336 | | - <strong class="sender-email">{senderIdentity.email}</strong> |
337 | | - {#if senderIdentity.attributes.filter((a) => !a.type.includes('email') && a.value).length > 0} |
338 | | - <div class="attr-chips"> |
339 | | - {#each senderIdentity.attributes.filter((a) => !a.type.includes('email') && a.value) as attr (attr.type)} |
340 | | - <span class="attr-chip">{attr.value}</span> |
341 | | - {/each} |
| 304 | + {:else if downloadState === 'Decrypting' || downloadState === 'Done'} |
| 305 | + <div |
| 306 | + class="state-wrap" |
| 307 | + in:fade={{ duration: 300, delay: 200 }} |
| 308 | + out:fade={{ duration: 200 }} |
| 309 | + > |
| 310 | + <div class="success-banner"> |
| 311 | + <div class="banner-row"> |
| 312 | + {#if downloadState === 'Done'} |
| 313 | + <svg |
| 314 | + class="banner-check" |
| 315 | + viewBox="0 0 12 10" |
| 316 | + fill="none" |
| 317 | + xmlns="http://www.w3.org/2000/svg" |
| 318 | + in:fade={{ duration: 250, delay: 100 }} |
| 319 | + > |
| 320 | + <path |
| 321 | + d="M1 5L4.5 8.5L11 1" |
| 322 | + stroke="currentColor" |
| 323 | + stroke-width="1.75" |
| 324 | + stroke-linecap="round" |
| 325 | + stroke-linejoin="round" |
| 326 | + /> |
| 327 | + </svg> |
| 328 | + {/if} |
| 329 | + <p> |
| 330 | + {downloadState === 'Done' |
| 331 | + ? $_( |
| 332 | + 'filesharing.decryptpanel.doneMessageComplete' |
| 333 | + ) |
| 334 | + : $_('filesharing.decryptpanel.doneMessage')} |
| 335 | + </p> |
| 336 | + </div> |
| 337 | + {#if downloadState === 'Decrypting'} |
| 338 | + <div transition:slide={{ duration: 280 }}> |
| 339 | + <DecryptionProgress percentage={decryptPct} /> |
342 | 340 | </div> |
343 | 341 | {/if} |
344 | 342 | </div> |
345 | | - {/if} |
| 343 | + {#if downloadState === 'Decrypting' && $retryStatus} |
| 344 | + <p class="retry-status" role="status"> |
| 345 | + {$_('filesharing.encryptPanel.retrying', { |
| 346 | + values: { |
| 347 | + attempt: $retryStatus.attempt + 1, |
| 348 | + max: $retryStatus.maxAttempts, |
| 349 | + }, |
| 350 | + })} |
| 351 | + </p> |
| 352 | + {/if} |
| 353 | + |
| 354 | + {#if downloadState === 'Done'} |
| 355 | + <div in:fade={{ duration: 300, delay: 200 }}> |
| 356 | + <FileList files={fileList} /> |
| 357 | + </div> |
| 358 | + {/if} |
| 359 | + |
| 360 | + {#if downloadState === 'Done' && senderIdentity?.email} |
| 361 | + <div |
| 362 | + class="sender-section" |
| 363 | + in:fade={{ duration: 300, delay: 280 }} |
| 364 | + > |
| 365 | + <svg |
| 366 | + class="checkmark" |
| 367 | + viewBox="0 0 12 10" |
| 368 | + fill="none" |
| 369 | + xmlns="http://www.w3.org/2000/svg" |
| 370 | + > |
| 371 | + <path |
| 372 | + d="M1 5L4.5 8.5L11 1" |
| 373 | + stroke="currentColor" |
| 374 | + stroke-width="1.75" |
| 375 | + stroke-linecap="round" |
| 376 | + stroke-linejoin="round" |
| 377 | + /> |
| 378 | + </svg> |
| 379 | + <p class="sender-label"> |
| 380 | + {$_('filesharing.decryptpanel.verifiedEmail')} |
| 381 | + </p> |
| 382 | + <strong class="sender-email" |
| 383 | + >{senderIdentity.email}</strong |
| 384 | + > |
| 385 | + {#if senderIdentity.attributes.filter((a) => !a.type.includes('email') && a.value).length > 0} |
| 386 | + <div class="attr-chips"> |
| 387 | + {#each senderIdentity.attributes.filter((a) => !a.type.includes('email') && a.value) as attr (attr.type)} |
| 388 | + <span class="attr-chip">{attr.value}</span> |
| 389 | + {/each} |
| 390 | + </div> |
| 391 | + {/if} |
| 392 | + </div> |
| 393 | + {/if} |
| 394 | + </div> |
346 | 395 | {:else if downloadState === 'SessionExpired'} |
347 | 396 | <p class="error-description"> |
348 | 397 | {$_('filesharing.decryptpanel.sessionExpiredSubtitle')} |
|
531 | 580 |
|
532 | 581 | .success-banner { |
533 | 582 | display: flex; |
534 | | - align-items: center; |
| 583 | + flex-direction: column; |
535 | 584 | gap: 0.75rem; |
536 | 585 | background: var(--pg-strong-background); |
537 | 586 | border-radius: var(--pg-border-radius-lg); |
|
546 | 595 | } |
547 | 596 | } |
548 | 597 |
|
| 598 | + .banner-row { |
| 599 | + display: flex; |
| 600 | + align-items: center; |
| 601 | + gap: 0.75rem; |
| 602 | + position: relative; |
| 603 | +
|
| 604 | + p { |
| 605 | + margin: 0; |
| 606 | + } |
| 607 | + } |
| 608 | +
|
| 609 | + .state-wrap { |
| 610 | + display: flex; |
| 611 | + flex-direction: column; |
| 612 | + gap: 1.25rem; |
| 613 | + } |
| 614 | +
|
| 615 | + .success-banner :global(.container) { |
| 616 | + padding: 0; |
| 617 | + } |
| 618 | +
|
| 619 | + .success-banner :global(.container .label) { |
| 620 | + display: none; |
| 621 | + } |
| 622 | +
|
549 | 623 | .banner-check { |
550 | 624 | width: 14px; |
551 | 625 | height: 14px; |
|
0 commit comments