Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
},
"dependencies": {
"@deltablot/dropzone": "^7.4.3",
"@e4a/pg-js": "^2.0.0",
"@e4a/pg-js": "^2.0.1",
"@iconify/svelte": "^5.2.1",
"@privacybydesign/yivi-css": "^1.0.1",
"country-flag-icons": "^1.6.17",
Expand Down
106 changes: 100 additions & 6 deletions src/lib/components/filesharing/EmailPreviewModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
let data = $state<PreviewResponse | null>(null)
/** Index into the flat list of [...recipients, confirmation?] */
let activeIdx = $state(0)
/** Which MIME alternative to show in the body. Mail clients pick
* HTML when available; the plain-text branch is what text-only
* clients (or accessibility tools) actually render. */
let bodyView: 'html' | 'text' = $state('html')

let allEmails = $derived.by<RenderedEmail[]>(() => {
if (!data) return []
Expand Down Expand Up @@ -83,6 +87,24 @@
function handleKey(e: KeyboardEvent) {
if (e.key === 'Escape') onClose()
}

/** Inject `<base target="_blank">` into the rendered email HTML so
* anchor clicks open in a new top-level tab instead of trying to
* navigate the sandboxed iframe (which has no `allow-top-navigation`
* and would otherwise blank the body). The `allow-popups` /
* `allow-popups-to-escape-sandbox` flags already permit the new
* tab. We try to splice into an existing `<head>`; if cryptify ever
* changes the template shape, the prepended fallback still works
* because browsers tolerate a `<base>` before `<html>`. */
function withBaseTarget(html: string): string {
const tag = '<base target="_blank" rel="noopener">'
const headIdx = html.search(/<head[^>]*>/i)
if (headIdx >= 0) {
const end = html.indexOf('>', headIdx) + 1
return html.slice(0, end) + tag + html.slice(end)
}
return tag + html
}
</script>

<svelte:window onkeydown={handleKey} />
Expand Down Expand Up @@ -182,12 +204,37 @@
</div>
</section>

<iframe
class="email-frame"
title={$_('filesharing.emailPreview.iframeTitle')}
srcdoc={active.html}
sandbox="allow-popups allow-popups-to-escape-sandbox"
></iframe>
<div class="body-tabs" role="tablist">
<button
type="button"
role="tab"
aria-selected={bodyView === 'html'}
class="body-tab"
class:active={bodyView === 'html'}
onclick={() => (bodyView = 'html')}
>{$_('filesharing.emailPreview.viewHtml')}</button
>
<button
type="button"
role="tab"
aria-selected={bodyView === 'text'}
class="body-tab"
class:active={bodyView === 'text'}
onclick={() => (bodyView = 'text')}
>{$_('filesharing.emailPreview.viewText')}</button
>
</div>

{#if bodyView === 'html'}
<iframe
class="email-frame"
title={$_('filesharing.emailPreview.iframeTitle')}
srcdoc={withBaseTarget(active.html)}
sandbox="allow-popups allow-popups-to-escape-sandbox"
></iframe>
{:else}
<pre class="email-text">{active.text}</pre>
{/if}
{/if}
</div>
</div>
Expand Down Expand Up @@ -313,13 +360,60 @@
font-size: var(--pg-font-size-sm);
}

.body-tabs {
display: flex;
gap: 0.25rem;
padding: 0.5rem 1.5rem 0;
background: var(--pg-soft-background);
border-bottom: 1px solid var(--pg-strong-background);
}

.body-tab {
background: transparent;
border: 0;
border-bottom: 2px solid transparent;
padding: 0.4rem 0.8rem;
font-family: var(--pg-font-family);
font-size: var(--pg-font-size-sm);
color: var(--pg-text-secondary);
cursor: pointer;
}

.body-tab:hover {
color: var(--pg-text);
}

.body-tab.active {
color: var(--pg-text);
border-bottom-color: var(--pg-primary-contrast);
}

.body-tab:focus-visible {
outline: 2px solid var(--pg-text);
outline-offset: 2px;
}

.email-frame {
width: 100%;
height: min(70vh, 720px);
border: 0;
background: var(--pg-soft-background);
}

.email-text {
margin: 0;
padding: 1rem 1.5rem;
max-height: min(70vh, 720px);
overflow: auto;
background: var(--pg-soft-background);
color: var(--pg-text);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: var(--pg-font-size-sm);
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}

.state {
padding: 2rem 1.5rem;
text-align: center;
Expand Down
32 changes: 17 additions & 15 deletions src/lib/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,21 +229,6 @@
"addRecipient": "recipient",
"addRecipientButton": "Add another recipient",
"emailSenderConfirm": "Send me a confirmation",
"emailPreview": {
"title": "Notification email preview",
"subtitle": "Staging only — cryptify does not actually send notification emails on staging. This is what each recipient would have received.",
"loading": "Loading preview…",
"error": "Could not load preview.",
"empty": "No recipients to preview.",
"close": "Close",
"from": "From",
"replyTo": "Reply to",
"subject": "Subject",
"to": "To",
"confirmationTag": "sender copy",
"iframeTitle": "Notification email body",
"reopen": "Show email preview"
},
"timeremaining": {
"estimate": "Estimating...",
"unknown": "More then one day left.",
Expand Down Expand Up @@ -271,6 +256,23 @@
"serverBlocked": "The server rejected this upload because you have used {used} GB of your {limit} GB 2-week limit. Your limit resets on {resets}."
}
},
"emailPreview": {
"title": "Notification email preview",
"subtitle": "Staging only — cryptify does not actually send notification emails on staging. This is what each recipient would have received.",
"loading": "Loading preview…",
"error": "Could not load preview.",
"empty": "No recipients to preview.",
"close": "Close",
"from": "From",
"replyTo": "Reply to",
"subject": "Subject",
"to": "To",
"confirmationTag": "sender copy",
"iframeTitle": "Notification email body",
"reopen": "Show email preview",
"viewHtml": "HTML",
"viewText": "Plain text"
},
"attributes": {
"pbdf.sidn-pbdf.mobilenumber.mobilenumber": "Mobile phone number",
"pbdf.sidn-pbdf.mobilenumber.mobilenumber.placeholder": "612345678",
Expand Down
32 changes: 17 additions & 15 deletions src/lib/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,21 +228,6 @@
"addRecipient": "ontvanger",
"addRecipientButton": "Voeg nog een ontvanger toe",
"emailSenderConfirm": "Stuur mij een bevestiging",
"emailPreview": {
"title": "Voorbeeld notificatie-e-mail",
"subtitle": "Alleen staging — cryptify verstuurt op staging geen e-mails. Dit is wat elke ontvanger zou hebben gezien.",
"loading": "Voorbeeld laden…",
"error": "Voorbeeld kon niet worden geladen.",
"empty": "Geen ontvangers om weer te geven.",
"close": "Sluiten",
"from": "Van",
"replyTo": "Antwoord aan",
"subject": "Onderwerp",
"to": "Aan",
"confirmationTag": "bevestigingskopie",
"iframeTitle": "Inhoud van de notificatie-e-mail",
"reopen": "Toon e-mailvoorbeeld"
},
"timeremaining": {
"estimate": "Estimating...",
"unknown": "Nog meer dan een dag",
Expand Down Expand Up @@ -270,6 +255,23 @@
"serverBlocked": "De server weigerde deze upload omdat je {used} GB van je {limit} GB-limiet (2 weken) hebt gebruikt. De limiet reset op {resets}."
}
},
"emailPreview": {
"title": "Voorbeeld notificatie-e-mail",
"subtitle": "Alleen staging — cryptify verstuurt op staging geen e-mails. Dit is wat elke ontvanger zou hebben gezien.",
"loading": "Voorbeeld laden…",
"error": "Voorbeeld kon niet worden geladen.",
"empty": "Geen ontvangers om weer te geven.",
"close": "Sluiten",
"from": "Van",
"replyTo": "Antwoord aan",
"subject": "Onderwerp",
"to": "Aan",
"confirmationTag": "bevestigingskopie",
"iframeTitle": "Inhoud van de notificatie-e-mail",
"reopen": "Toon e-mailvoorbeeld",
"viewHtml": "HTML",
"viewText": "Platte tekst"
},
"attributes": {
"pbdf.sidn-pbdf.mobilenumber.mobilenumber": "Mobiel telefoonnummer",
"pbdf.sidn-pbdf.mobilenumber.mobilenumber.placeholder": "612345678",
Expand Down