From eaddd1d44e94c1485377cdb95759d1d25db06e28 Mon Sep 17 00:00:00 2001 From: Nariman Jelveh Date: Wed, 19 Aug 2026 18:49:11 -0700 Subject: [PATCH] redesign: share notification emails (#3612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * redesign: share notification emails The share digests were an unstyled fragment: a 520px div with a couple of paragraphs, no preview line, no small-screen or dark-mode handling, and an html-only body that left every text-only client reading a machine down-conversion of the markup. Both digests (`file_shared_with_you` and `file_shared_invite`) now render through one layout: a 600px table column that goes full width under 600px, every color written inline so a client that drops ` + + +
${parts.preheader}${PREHEADER_FILL}
+
+ + + + + +
+ + +`; + +/** The greeting line above the heading. */ +const greetingRow = (text: string): string => ` + + ${text} + `; + +const headingRow = (text: string): string => ` + + ${text} + `; + +/** + * One row per sender, hairline-separated. The wording comes pre-composed from + * the digest (`digestLines`), so the list stays a list however many senders and + * items fold into it. + */ +const SHARE_LIST_ROW = ` + + + + {{#each shares}} + + + + {{/each}} +
{{this.sender}} shared {{this.what}}
+ + `; + +/** A body paragraph. `padding` lets a caller tune the rhythm around it. */ +const textRow = (text: string, padding = '18px 0 0'): string => ` + + ${text} + `; + +/** + * The call to action. Padding sits on the cell and the color on `bgcolor` so + * Outlook still draws a real button (square-cornered, which is fine); the table + * goes full width under 600px so the tap target spans the card. + */ +const buttonRow = (label: string): string => ` + + + + + + + + + `; + export const EMAIL_TEMPLATES = { 'approved-for-listing': { subject: '🎉 Your app has been approved for listing!', @@ -190,85 +366,79 @@ immediately

*/ file_shared_with_you: { subject: '{{subject_line}}', - html: ` -
-

Hi {{recipient}},

-

Shared with you on Puter:

- - {{#each shares}} - - - - {{/each}} -
- {{this.sender}} shared {{this.what}} -
-

- Open Puter -

-

Sincerely,
Puter

- {{#if unsubscribe_uuid}} -

- Don't want these? Unsubscribe. -

- {{/if}} -
+ html: shareEmailLayout({ + preheader: 'Waiting for you under Shared with me.', + content: + greetingRow('Hi{{#if recipient}} {{recipient}}{{/if}},') + + headingRow('Shared with you') + + SHARE_LIST_ROW + + buttonRow('Open Puter') + + textRow( + 'Shared items live under Shared with me in your files. Nothing to download — the owner\'s changes show up as they make them.', + '24px 0 0', + ), + footer: `You're receiving this because someone shared with your Puter account.{{#if unsubscribe_uuid}} +
Unsubscribe from notification emails{{/if}}`, + }), + text: ` + Hi{{#if recipient}} {{recipient}}{{/if}}, + + Shared with you on Puter: + {{#each shares}} + - {{this.sender}} shared {{this.what}} + {{/each}} + + Open Puter: {{link}} + + Shared items live under "Shared with me" in your files. Nothing to + download — the owner's changes show up as they make them. + + -- + You're receiving this because someone shared with your Puter account. + {{#if unsubscribe_uuid}}Unsubscribe from notification emails: + {{link}}/unsubscribe?user_uuid={{unsubscribe_uuid}}{{/if}} `, }, // The only way to reach someone with no account. Same digest shape. file_shared_invite: { subject: '{{subject_line}}', - html: ` -
-

Hi there,

-

Shared with you on Puter:

- - {{#each shares}} - - - - {{/each}} -
- {{this.sender}} shared {{this.what}} -
-

- You don't have a Puter account for this address yet. Create one with - {{email}} and confirm it, and what was shared will be - waiting for you. -

-

- Create your account -

-

Sincerely,
Puter

-
- `, - }, - share_by_username: { - subject: 'Puter share from {{susername}}', - html: ` -

Hi there {{rusername}},

-

You've received a share from {{susername}} on Puter.

-

Go to puter.com to check it out.

-{{#if message}} -

The following message was included:

-
{{message}}
-{{/if}} -

Sincerely,

-

Puter

- `, - }, - share_by_email: { - subject: 'share by email', - html: ` -

Hi there,

-

You've received a share from {{sender_name}} on Puter:

-

{{link}}

-{{#if message}} -

The following message was included:

-
{{message}}
-{{/if}} -

Sincerely,

-

Puter

+ html: shareEmailLayout({ + preheader: 'Claim it with a free Puter account.', + content: + greetingRow('Hi there,') + + headingRow('Shared with you on Puter') + + SHARE_LIST_ROW + + textRow( + 'There\'s no Puter account for {{email}} yet. Create one with this address, confirm it, and everything above will be waiting for you. It\'s free and takes about a minute.', + ) + + buttonRow('Create your free account') + + textRow( + 'Already on Puter? Add {{email}} to your account and confirm it to get the same access.', + '24px 0 0', + ), + footer: `You're receiving this because someone shared with {{email}}. Nothing is shared until the address is confirmed, so you can ignore this email and nothing happens.`, + }), + text: ` + Hi there, + + Shared with you on Puter: + {{#each shares}} + - {{this.sender}} shared {{this.what}} + {{/each}} + + There's no Puter account for {{email}} yet. Create one with this + address, confirm it, and everything above will be waiting for you. + It's free and takes about a minute. + + Create your free account: {{link}} + + Already on Puter? Add {{email}} to your account and confirm it to get + the same access. + + -- + You're receiving this because someone shared with {{email}}. Nothing is + shared until the address is confirmed, so you can ignore this email + and nothing happens. `, }, } satisfies Record; diff --git a/src/backend/services/share/shareEmail.test.ts b/src/backend/services/share/shareEmail.test.ts index 9ca1a1c71..6054959f9 100644 --- a/src/backend/services/share/shareEmail.test.ts +++ b/src/backend/services/share/shareEmail.test.ts @@ -280,7 +280,7 @@ describe('share email', () => { // The address is in the body because it is the one to sign up with: an // account on any other address will not find the share. expect(mail.html).toContain(invitee); - expect(mail.html).toContain('Create your account'); + expect(mail.html).toContain('Create your free account'); // The full origin, port included — a rebuilt protocol://domain link // is dead on any self-host that doesn't run on the default port. expect(mail.html).toContain(`href="${env.origin}"`);