Redesign message notification email: add avatar, profile link, and metadata; refine translations and layout for better engagement.

This commit is contained in:
MartinBraquet
2026-08-01 14:51:55 +02:00
parent 292ba01383
commit 9bf1d1b8a3
4 changed files with 190 additions and 22 deletions

View File

@@ -17,9 +17,11 @@ export type PartialProfile = Pick<
> &
Partial<ProfileRow>
const avatarUrl =
'https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/user-images%2FMartin%2Flove-images%2FeD09lRtuRF.jpg?alt=media&token=db41b5f2-b1c0-4df9-aa9c-11d44c970947'
export const mockUser: User = {
createdTime: 0,
avatarUrl: 'https://martinbraquet.com/wp-content/uploads/BDo_Tbzj.jpeg',
avatarUrl: avatarUrl,
id: '0k1suGSJKVUnHbCPEhHNpgZPkUP2',
username: 'Martin',
name: 'Martin',
@@ -27,7 +29,7 @@ export const mockUser: User = {
export const jamesUser: User = {
createdTime: 0,
avatarUrl: 'https://martinbraquet.com/wp-content/uploads/BDo_Tbzj.jpeg',
avatarUrl: avatarUrl,
id: '5LZ4LgYuySdL1huCWe7bti02ghx2',
username: 'James',
name: 'James',

View File

@@ -4,6 +4,7 @@ import {
Container,
Head,
Html,
Img,
Link,
Preview,
Section,
@@ -13,7 +14,7 @@ import {ANDROID_APP_URL} from 'common/constants'
import {DOMAIN} from 'common/envs/constants'
import {type ProfileRow} from 'common/profiles/profile'
import {type User} from 'common/user'
import {container, content, Footer, main} from 'email/utils'
import {container, content, DARK_MODE_CSS, Footer, main} from 'email/utils'
import React from 'react'
import {createT} from 'shared/locale'
@@ -31,7 +32,7 @@ interface NewMessageEmailProps {
export const NewMessageEmail = ({
fromUser,
// fromUserProfile,
fromUserProfile,
toUser,
channelId,
unsubscribeUrl,
@@ -41,16 +42,25 @@ export const NewMessageEmail = ({
const name = toUser.name.split(' ')[0]
const creatorName = fromUser.name
const messagesUrl = `https://${DOMAIN}/messages/${channelId}`
// const userImgSrc = getOgImageUrl(fromUser, fromUserProfile)
const profileUrl = `https://${DOMAIN}/${fromUser.username}`
// Every client renders <img> reliably, none of them render a CSS-generated placeholder — so an absent
// avatar falls back to the same asset the web app uses rather than to a broken image icon.
const avatarUrl = fromUser.avatarUrl
const t = createT(locale)
// Age and city are the two facts a reader can act on at a glance; anything more turns the card into a
// profile page and competes with the one button this email has.
const meta = [fromUserProfile?.age, fromUserProfile?.city].filter(Boolean).join(' · ')
return (
<Html>
<Head />
<Head>
<style>{DARK_MODE_CSS}</style>
</Head>
<Preview>
{t('email.new_message.preview', 'New message from {creatorName}', {creatorName})}
</Preview>
<Body style={main}>
<Body style={main} className="cm-body">
<Container style={container}>
{/*<Section style={logoContainer}>*/}
{/* <Img*/}
@@ -61,52 +71,155 @@ export const NewMessageEmail = ({
{/* />*/}
{/*</Section>*/}
<Section style={content}>
<Section style={content} className="cm-surface">
<div
style={{
textAlign: 'center',
marginBottom: '32px',
marginBottom: '28px',
}}
>
{/* A pill rather than a headline: it names the event in one glance, above the fold and above
the greeting, which is what the reader is scanning for when the subject line already
said someone wrote to them. */}
<span
className="cm-chip"
style={{
display: 'inline-block',
backgroundColor: '#faf3e9',
border: '1px solid #e8c99e',
borderRadius: '9999px',
padding: '6px 14px',
fontFamily: "'DM Sans', sans-serif",
fontSize: '11px',
fontWeight: '600',
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: '#a6682e',
lineHeight: '1.2',
}}
>
{t('email.new_message.badge', 'New message')}
</span>
<Text
style={{
fontSize: '28px',
fontFamily: "'Cormorant Garamond', serif",
fontWeight: '500',
color: '#1e1a14',
marginBottom: '8px',
margin: '18px 0 0 0',
letterSpacing: '-0.01em',
lineHeight: '1.1',
}}
className="cm-name"
>
{t('email.new_message.greeting', 'Hi {name},', {name})}
</Text>
</div>
<div
className="cm-card"
style={{
backgroundColor: '#f7f4ef',
border: '1px solid #dee5b2',
borderRadius: '14px',
padding: '24px',
borderRadius: '18px',
padding: '28px 24px 26px 24px',
margin: '24px 0',
textAlign: 'center',
}}
>
{/* The avatar is the whole point of the redesign: a face converts where a name does not.
The ring is a padded round div rather than a box-shadow, which Gmail strips. */}
{avatarUrl && (
<Link href={profileUrl} style={{textDecoration: 'none'}}>
<span
style={{
display: 'inline-block',
padding: '4px',
backgroundColor: '#e8c99e',
borderRadius: '9999px',
lineHeight: '0',
}}
>
<Img
src={avatarUrl}
alt={creatorName}
width={96}
height={96}
style={{
width: '96px',
height: '96px',
borderRadius: '9999px',
objectFit: 'cover',
display: 'block',
border: '3px solid #ffffff',
}}
/>
</span>
</Link>
)}
<Text
className="cm-name"
style={{
fontSize: '18px',
fontSize: '22px',
fontFamily: "'Cormorant Garamond', serif",
fontWeight: '500',
fontWeight: '600',
color: '#1e1a14',
marginBottom: '20px',
margin: '16px 0 0 0',
lineHeight: '1.2',
letterSpacing: '0.01em',
}}
>
{creatorName}
</Text>
<Text
className="cm-accent"
style={{
fontSize: '13px',
fontFamily: "'DM Sans', sans-serif",
color: '#c17f3e',
margin: '4px 0 0 0',
lineHeight: '1.3',
}}
>
{t('email.new_message.message', '{creatorName} just messaged you!', {creatorName})}
@{fromUser.username}
{meta ? ` · ${meta}` : ''}
</Text>
{/* A chat bubble, not a paragraph: the asymmetric corner reads as a message in every client
without needing a CSS triangle that Outlook would drop. */}
<div
style={{
backgroundColor: '#ffffff',
border: '1px solid #e8c99e',
borderRadius: '18px 18px 18px 4px',
padding: '18px 20px',
margin: '22px 0 0 0',
textAlign: 'left',
}}
className="cm-chip"
>
<Text
style={{
fontSize: '18px',
fontFamily: "'Cormorant Garamond', serif",
fontStyle: 'italic',
fontWeight: '500',
color: '#1e1a14',
margin: '0',
letterSpacing: '0.01em',
lineHeight: '1.4',
}}
className="cm-name"
>
{t('email.new_message.message', '{creatorName} just messaged you!', {
creatorName,
})}
</Text>
</div>
<Button
href={messagesUrl}
style={{
@@ -119,22 +232,69 @@ export const NewMessageEmail = ({
textDecoration: 'none',
textAlign: 'center' as const,
display: 'inline-block',
padding: '14px 32px',
margin: '0',
padding: '14px 36px',
margin: '24px 0 0 0',
border: '1px solid #a6682e',
transition: 'all 0.12s ease',
}}
>
{t('email.new_message.viewButton', 'View message')}
{t('email.new_message.viewButton', 'Read & reply')}
</Button>
<Text
className="cm-muted"
style={{
fontSize: '12px',
fontFamily: "'DM Sans', sans-serif",
color: '#8c8070',
margin: '14px 0 0 0',
lineHeight: '1.5',
}}
>
<Link
href={profileUrl}
style={{color: '#c17f3e', textDecoration: 'none', fontWeight: '500'}}
>
{t('email.new_message.view_profile', 'View profile')}
</Link>
</Text>
</div>
{/* The nudge sits outside the card so the card stays a single, clean unit — and because the
reader who is going to reply has already clicked by the time they reach this line. */}
<Text
className="cm-muted"
style={{
fontSize: '14px',
fontFamily: "'Cormorant Garamond', serif",
fontStyle: 'italic',
lineHeight: '1.6',
color: '#8c8070',
marginTop: '24px',
textAlign: 'center',
}}
>
{t(
'email.new_message.nudge',
'One sentence back is usually all it takes to start something real.',
)}
</Text>
<hr
style={{
border: 'none',
borderTop: '1px solid #ece7de',
margin: '28px 0 0 0',
}}
/>
<Text
className="cm-muted"
style={{
fontSize: '13px',
lineHeight: '1.75',
color: '#8c8070',
marginTop: '32px',
marginTop: '20px',
textAlign: 'center',
}}
>

View File

@@ -314,7 +314,7 @@
"email.new_message.message": "{creatorName} hat Ihnen gerade eine Nachricht geschickt!",
"email.new_message.preview": "Neue Nachricht von {creatorName}",
"email.new_message.subject": "{creatorName} hat Ihnen eine Nachricht geschickt!",
"email.new_message.viewButton": "Nachricht anzeigen",
"email.new_message.viewButton": "Lesen und antworten",
"email.search_alerts.callToAction": "Wenn jemand zu Ihnen passt, treten Sie in Kontakt. Ein einfaches Hallo kann der Beginn einer bedeutungsvollen Freundschaft, Zusammenarbeit oder Beziehung sein.",
"email.search_alerts.communityNote": "Compass wird von der Community aufgebaut und erhalten — keine Werbung, keine versteckten Algorithmen, keine Abonnements. Ihre Präsenz und Teilnahme machen es möglich.",
"email.search_alerts.greeting": "Hallo {name},",
@@ -1749,6 +1749,9 @@
"email.new_message.daily_limit": "Um deine Inbox nicht zu überladen, erhältst du höchstens eine E-Mail pro Tag pro Konversation. Wenn {creatorName} heute weitere Nachrichten sendet, wirst du erst morgen wieder per E-Mail benachrichtigt.",
"email.new_message.app_prompt": "Willst du Echtzeit-Benachrichtigungen? Lade die Compass App für Android herunter:",
"email.new_message.app_link": "App herunterladen",
"email.new_message.badge": "Neue Nachricht",
"email.new_message.view_profile": "Profil ansehen",
"email.new_message.nudge": "Ein einziger Satz zurück reicht oft, damit etwas Echtes entsteht.",
"faq.empty.cta": "Frage stellen →",
"faq.empty.text": "Versuche ein allgemeineres Wort — oder frag uns direkt, wir beantworten jede Nachricht.",
"faq.empty.title": "Dazu passt hier nichts.",

View File

@@ -314,7 +314,7 @@
"email.new_message.message": "{creatorName} vient de vous envoyer un message !",
"email.new_message.preview": "Nouveau message de {creatorName}",
"email.new_message.subject": "{creatorName} vous a envoyé un message !",
"email.new_message.viewButton": "Voir le message",
"email.new_message.viewButton": "Lire et répondre",
"email.search_alerts.callToAction": "Si quelqu'un vous parle, contactez-les. Un simple bonjour peut être le début d'une amitié, d'une collaboration ou d'une relation significative.",
"email.search_alerts.communityNote": "Compass est construit et soutenu par la communauté — pas de publicité, pas d'algorithmes cachés, pas d'abonnements. Votre présence et votre participation le rendent possible.",
"email.search_alerts.greeting": "Bonjour {name},",
@@ -1748,6 +1748,9 @@
"email.new_message.daily_limit": "Pour éviter de surcharger votre boîte de réception, vous recevrez au plus un e-mail par jour par conversation. Si {creatorName} envoie d'autres messages aujourd'hui, vous ne serez pas notifié par e-mail.",
"email.new_message.app_prompt": "Vous voulez des notifications en temps réel ? Téléchargez l'application Compass pour Android :",
"email.new_message.app_link": "Obtenir l'app",
"email.new_message.badge": "Nouveau message",
"email.new_message.view_profile": "Voir le profil",
"email.new_message.nudge": "Une seule phrase en retour suffit souvent à faire naître quelque chose de vrai.",
"faq.empty.cta": "Poser une question →",
"faq.empty.text": "Essayez un mot plus général — ou posez-nous directement la question, nous répondons à chaque message.",
"faq.empty.title": "Aucun résultat pour cette recherche.",