Skip to content

Commit 33d9e7e

Browse files
authored
feat(weekly): draw the braille spark on email trending rows (#447)
1 parent 7e2fa7e commit 33d9e7e

4 files changed

Lines changed: 136 additions & 23 deletions

File tree

‎layers/identity/server/utils/weekly-select.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ function reasonFor(entry: {
206206
attribution: 'social' | 'github' | 'both'
207207
social: { authorCount: number, mentionCount: number, latestMentionAt: number } | null
208208
github: { latestGain: number, observedDay: number } | null
209+
mentionsByDay: number[] | null
209210
}): WeeklyReason {
210211
const authorCount = entry.social?.authorCount ?? 0
211212
const mentionCount = entry.social?.mentionCount ?? 0
@@ -214,10 +215,11 @@ function reasonFor(entry: {
214215
const latestAt = entry.social?.latestMentionAt ?? 0
215216
const gain = entry.github?.latestGain ?? 0
216217
const day = entry.github?.observedDay ?? 0
218+
const mentionsByDay = entry.mentionsByDay
217219

218220
if (entry.attribution === 'both' && entry.social && entry.github)
219-
return { _tag: 'named-and-stars', authorCount, mentionCount, latestAt, gain, day }
221+
return { _tag: 'named-and-stars', authorCount, mentionCount, latestAt, mentionsByDay, gain, day }
220222
if (entry.attribution === 'github' && entry.github)
221223
return { _tag: 'stars', gain, day }
222-
return { _tag: 'named', authorCount, mentionCount, latestAt }
224+
return { _tag: 'named', authorCount, mentionCount, latestAt, mentionsByDay }
223225
}

‎layers/identity/server/utils/weekly-template.ts‎

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313

1414
import type { EmailPlacement } from './email-clicks'
15+
import { brailleSpark, SPARK_BARS, sparkCount, sparkLevels, sparkWeek } from '#shared/braille-spark'
1516
import { githubAvatarProxyUrl } from '#shared/image-proxy'
1617
import { canonicalRepoSkillPath } from '#shared/skill-routes'
1718
import { countedEmailUrl } from './email-clicks'
@@ -77,6 +78,12 @@ const PALETTE: Record<WeeklyTheme, Tokens> = {
7778

7879
const SANS = `'Plus Jakarta Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif`
7980
const MONO = `'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace`
81+
/**
82+
* Faces that carry braille, one per platform: Apple Mail and iOS, Windows,
83+
* Android, then Linux. IBM Plex Mono has none, and a mail client rarely loads
84+
* it anyway.
85+
*/
86+
const BRAILLE = `'Apple Braille','Segoe UI Symbol','Noto Sans Symbols 2','DejaVu Sans Mono',ui-monospace,monospace`
8087

8188
/**
8289
* Why a skill is on the trending list, as one value rather than a bag of
@@ -90,17 +97,25 @@ const MONO = `'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospa
9097
* reason.
9198
*/
9299
export type WeeklyReason
93-
= { _tag: 'named', authorCount: number, mentionCount: number, latestAt: number }
94-
| { _tag: 'stars', gain: number, day: number }
95-
| {
96-
_tag: 'named-and-stars'
97-
authorCount: number
98-
mentionCount: number
99-
latestAt: number
100-
gain: number
101-
day: number
102-
}
103-
| { _tag: 'popular', stars: number }
100+
= {
101+
_tag: 'named'
102+
authorCount: number
103+
mentionCount: number
104+
latestAt: number
105+
/** Counted mentions per rolling day, oldest first, for the braille spark. */
106+
mentionsByDay: number[] | null
107+
}
108+
| { _tag: 'stars', gain: number, day: number }
109+
| {
110+
_tag: 'named-and-stars'
111+
authorCount: number
112+
mentionCount: number
113+
latestAt: number
114+
mentionsByDay: number[] | null
115+
gain: number
116+
day: number
117+
}
118+
| { _tag: 'popular', stars: number }
104119

105120
export interface WeeklyEvidence {
106121
url: string
@@ -307,6 +322,43 @@ function trendingMeta(skill: WeeklyTrendingSkill, now: number): string {
307322
return joinMeta([`${skill.owner}/${skill.repo}`, reasonLine(skill.reason, now), stars])
308323
}
309324

325+
/**
326+
* The seven days a row's spark draws, or null when it draws none.
327+
*
328+
* Only a row somebody posted about has days to draw. A week of zeros means
329+
* every counted post is older than the spark, and seven blank cells beside
330+
* "0 mentions" say nothing, so that row draws none, as on the board.
331+
*/
332+
function sparkDays(reason: WeeklyReason): number[] | null {
333+
if (reason._tag !== 'named' && reason._tag !== 'named-and-stars')
334+
return null
335+
const week = sparkWeek(reason.mentionsByDay ?? [])
336+
return week.some(count => count > 0) ? week : null
337+
}
338+
339+
function sparkTotal(days: readonly number[]): string {
340+
return sparkCount(days.reduce((sum, count) => sum + count, 0))
341+
}
342+
343+
/**
344+
* The braille spark for the HTML half, in stone.
345+
*
346+
* The site draws today in rose. The email spends its rose on the button and
347+
* the footer rule, so seven rose cells would break the one-rose budget. A day
348+
* with no count draws the lowest bar faintly as a track, as the site does, so
349+
* a lone bar still reads as one day of seven.
350+
*
351+
* The label carries the numbers and the bars are hidden from screen readers,
352+
* because a braille cell read aloud is noise.
353+
*/
354+
function sparkHtml(t: Tokens, days: readonly number[]): string {
355+
const label = `${sparkTotal(days)} in 7 days. Per day, oldest first: ${days.join(', ')}.`
356+
const bars = sparkLevels(days)
357+
.map(level => level === 0 ? `<span style="color:${t.borderStrong};">${SPARK_BARS[1]}</span>` : SPARK_BARS[level])
358+
.join('')
359+
return `<span role="img" aria-label="${esc(label)}" style="white-space:nowrap;font-family:${MONO};font-size:14px;line-height:1.45;color:${t.muted};font-variant-numeric:tabular-nums;"><span aria-hidden="true" style="font-family:${BRAILLE};">${bars}</span> <span aria-hidden="true">${esc(sparkTotal(days))}</span></span>`
360+
}
361+
310362
/**
311363
* A skill description cut to something a person reads in one glance.
312364
*
@@ -449,6 +501,8 @@ function trendingRow(t: Tokens, options: {
449501
description: string | null
450502
meta: string
451503
evidence: WeeklyEvidence | null
504+
/** Seven days of mentions, or null for a row nobody posted about. */
505+
spark: number[] | null
452506
}): string {
453507
const description = trimDescription(options.description)
454508
const evidence = options.evidence
@@ -463,7 +517,12 @@ function trendingRow(t: Tokens, options: {
463517
style="width:28px;height:28px;border-radius:14px;display:block;border:1px solid ${t.border};background:${t.quote};" />
464518
</td>
465519
<td valign="top">
466-
<a href="${esc(options.href)}" style="display:inline-block;padding:0 0 3px;font-family:${MONO};font-size:14px;line-height:1.45;font-weight:600;color:${t.text};text-decoration:underline;text-decoration-color:${t.borderStrong};text-underline-offset:3px;">${esc(options.title)}</a>
520+
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
521+
<tr>
522+
<td valign="top"><a href="${esc(options.href)}" style="display:inline-block;padding:0 0 3px;font-family:${MONO};font-size:14px;line-height:1.45;font-weight:600;color:${t.text};text-decoration:underline;text-decoration-color:${t.borderStrong};text-underline-offset:3px;">${esc(options.title)}</a></td>
523+
${options.spark ? `<td valign="top" align="right" style="padding-left:12px;white-space:nowrap;">${sparkHtml(t, options.spark)}</td>` : ''}
524+
</tr>
525+
</table>
467526
${description ? `<div style="margin-top:3px;font-family:${SANS};font-size:14px;line-height:1.55;color:${t.body};">${esc(description)}</div>` : ''}
468527
<div style="margin-top:3px;font-family:${MONO};font-size:14px;line-height:1.45;color:${t.muted};font-variant-numeric:tabular-nums;">${esc(options.meta)}${evidence}</div>
469528
</td>
@@ -715,6 +774,7 @@ export function renderWeekly(input: WeeklyRenderInput): WeeklyRender {
715774
description: skill.description,
716775
meta: trendingMeta(skill, now),
717776
evidence: skill.evidence,
777+
spark: sparkDays(skill.reason),
718778
})
719779
}).join('')
720780

@@ -843,7 +903,8 @@ export function renderWeeklyText(input: WeeklyRenderInput): string {
843903
if (input.trending.length) {
844904
lines.push('', 'TRENDING THIS WEEK', '')
845905
for (const skill of input.trending) {
846-
lines.push(`- ${skill.owner}/${skill.repo} ${skill.canonicalName}`)
906+
const spark = sparkDays(skill.reason)
907+
lines.push(`- ${skill.owner}/${skill.repo} ${skill.canonicalName}${spark ? ` ${brailleSpark(spark)} ${sparkTotal(spark)}` : ''}`)
847908
const description = trimDescription(skill.description)
848909
if (description)
849910
lines.push(` ${description}`)

‎server/routes/_dev/weekly.get.ts‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const SCENARIOS: Record<string, () => WeeklyRenderInput> = {
101101
canonicalName: 'ship',
102102
description: 'Take a change from branch to merged without babysitting it.',
103103
stars: 12_400,
104-
reason: { _tag: 'named', authorCount: 4, mentionCount: 9, latestAt: WINDOW_END - 2 * 86_400 },
104+
reason: { _tag: 'named', authorCount: 4, mentionCount: 9, latestAt: WINDOW_END - 2 * 86_400, mentionsByDay: [0, 1, 1, 0, 2, 5, 0] },
105105
evidence: {
106106
url: 'https://x.com/garrytan/status/1',
107107
authorHandle: 'garrytan',
@@ -116,7 +116,7 @@ const SCENARIOS: Record<string, () => WeeklyRenderInput> = {
116116
canonicalName: 'swiftui-liquid-glass',
117117
description: 'Build the new material effects without fighting the layout system.',
118118
stars: 865,
119-
reason: { _tag: 'named-and-stars', authorCount: 2, mentionCount: 3, latestAt: WINDOW_END - 86_400, gain: 412, day: WINDOW_END - 2 * 86_400 },
119+
reason: { _tag: 'named-and-stars', authorCount: 2, mentionCount: 3, latestAt: WINDOW_END - 86_400, mentionsByDay: [0, 0, 0, 0, 1, 2, 0], gain: 412, day: WINDOW_END - 2 * 86_400 },
120120
evidence: {
121121
url: 'https://bsky.app/profile/dimillian/post/1',
122122
authorHandle: 'dimillian.bsky.social',
@@ -141,7 +141,7 @@ const SCENARIOS: Record<string, () => WeeklyRenderInput> = {
141141
canonicalName: 'vue-debug-guides',
142142
description: null,
143143
stars: 210,
144-
reason: { _tag: 'named', authorCount: 1, mentionCount: 1, latestAt: WINDOW_END - 5 * 86_400 },
144+
reason: { _tag: 'named', authorCount: 1, mentionCount: 1, latestAt: WINDOW_END - 5 * 86_400, mentionsByDay: [0, 1, 0, 0, 0, 0, 0] },
145145
evidence: null,
146146
},
147147
{
@@ -161,7 +161,7 @@ const SCENARIOS: Record<string, () => WeeklyRenderInput> = {
161161
canonicalName: 'web-interface-guidelines',
162162
description: 'Review interface code against practical usability rules.',
163163
stars: 19_200,
164-
reason: { _tag: 'named', authorCount: 3, mentionCount: 4, latestAt: WINDOW_END - 3 * 86_400 },
164+
reason: { _tag: 'named', authorCount: 3, mentionCount: 4, latestAt: WINDOW_END - 3 * 86_400, mentionsByDay: [1, 0, 0, 2, 1, 0, 0] },
165165
evidence: null,
166166
},
167167
{
@@ -214,7 +214,7 @@ const SCENARIOS: Record<string, () => WeeklyRenderInput> = {
214214
canonicalName: 'nuxt-seo',
215215
description: null,
216216
stars: null,
217-
reason: { _tag: 'named', authorCount: 1, mentionCount: 1, latestAt: 0 },
217+
reason: { _tag: 'named', authorCount: 1, mentionCount: 1, latestAt: 0, mentionsByDay: null },
218218
evidence: null,
219219
}],
220220
}),
@@ -254,7 +254,7 @@ const SCENARIOS: Record<string, () => WeeklyRenderInput> = {
254254
canonicalName: 'a-skill-with-an-unusually-long-canonical-name',
255255
description: 'Unicode and entities: quotes "like this", ampersands & angle brackets <div>, emoji dashes, and accented names such as Renée Müller.',
256256
stars: 1_284_000,
257-
reason: { _tag: 'named-and-stars', authorCount: 17, mentionCount: 42, latestAt: WINDOW_END - 3600, gain: 9_814, day: WINDOW_END - 3 * 86_400 },
257+
reason: { _tag: 'named-and-stars', authorCount: 17, mentionCount: 42, latestAt: WINDOW_END - 3600, mentionsByDay: [2, 3, 5, 4, 8, 9, 11], gain: 9_814, day: WINDOW_END - 3 * 86_400 },
258258
evidence: {
259259
url: 'https://x.com/someone/status/2',
260260
authorHandle: 'a_very_long_handle_indeed',
@@ -287,6 +287,7 @@ export async function liveScenario(): Promise<WeeklyRenderInput> {
287287
starGain: number | null
288288
starGainDay: number | null
289289
evidence: { url: string, authorHandle: string, text: string, platform: 'x' | 'bsky' } | null
290+
mentionsByDay: number[] | null
290291
}>
291292
}>('https://skilld.dev/api/feed/trending', { query: { limit: MAX_TRENDING, window: 24 * 7 } })
292293

@@ -303,10 +304,10 @@ export async function liveScenario(): Promise<WeeklyRenderInput> {
303304
description: skill.description,
304305
stars: skill.stars,
305306
reason: skill.attribution === 'both' && skill.starGain !== null && skill.starGainDay !== null
306-
? { _tag: 'named-and-stars' as const, authorCount: skill.authorCount, mentionCount: skill.mentionCount, latestAt: 0, gain: skill.starGain, day: skill.starGainDay }
307+
? { _tag: 'named-and-stars' as const, authorCount: skill.authorCount, mentionCount: skill.mentionCount, latestAt: 0, mentionsByDay: skill.mentionsByDay, gain: skill.starGain, day: skill.starGainDay }
307308
: skill.attribution === 'github' && skill.starGain !== null && skill.starGainDay !== null
308309
? { _tag: 'stars' as const, gain: skill.starGain, day: skill.starGainDay }
309-
: { _tag: 'named' as const, authorCount: skill.authorCount, mentionCount: skill.mentionCount, latestAt: 0 },
310+
: { _tag: 'named' as const, authorCount: skill.authorCount, mentionCount: skill.mentionCount, latestAt: 0, mentionsByDay: skill.mentionsByDay },
310311
evidence: skill.evidence,
311312
})),
312313
}

‎test/unit/weekly-template.test.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { WeeklyRenderInput, WeeklyTrendingSkill } from '../../layers/identity/server/utils/weekly-template'
22
import { describe, expect, it } from 'vitest'
33
import { formatWindow, renderWeekly } from '../../layers/identity/server/utils/weekly-template'
4+
import { SPARK_BARS } from '../../shared/braille-spark'
45

56
const WINDOW_END = 1_755_648_000
67
const WINDOW_START = WINDOW_END - 7 * 86_400
@@ -589,3 +590,51 @@ describe('weekly theme', () => {
589590
expect(html).not.toContain('\u{1F525}')
590591
})
591592
})
593+
594+
describe('weekly trending spark', () => {
595+
// Every glyph a spark can print, the blank cell included.
596+
const BRAILLE = new RegExp(`[${SPARK_BARS.join('')}]`)
597+
const week = [0, 1, 0, 2, 0, 3, 6]
598+
599+
it('draws a week of mentions as a braille spark beside its count', () => {
600+
const { html, text } = renderWeekly(input({
601+
trending: [trending({ reason: { _tag: 'named', authorCount: 3, mentionCount: 12, latestAt: WINDOW_END, mentionsByDay: week } })],
602+
}))
603+
604+
expect(text).toContain('⠀⣀⠀⣀⠀⣤⣿ 12 mentions')
605+
expect(html).toContain('aria-label="12 mentions in 7 days. Per day, oldest first: 0, 1, 0, 2, 0, 3, 6."')
606+
expect(html).toMatch(BRAILLE)
607+
})
608+
609+
it('draws the spark on a row that was named and surged', () => {
610+
const { text } = renderWeekly(input({
611+
trending: [trending({
612+
reason: { _tag: 'named-and-stars', authorCount: 1, mentionCount: 1, latestAt: WINDOW_END, gain: 40, day: WINDOW_END, mentionsByDay: [0, 0, 0, 0, 0, 0, 1] },
613+
})],
614+
}))
615+
616+
expect(text).toContain('⠀⠀⠀⠀⠀⠀⣿ 1 mention')
617+
})
618+
619+
it.each([
620+
{ name: 'a star surge', reason: { _tag: 'stars' as const, gain: 865, day: WINDOW_END } },
621+
{ name: 'a popular filler row', reason: { _tag: 'popular' as const, stars: 46_712 } },
622+
{ name: 'a named row with no counted days', reason: { _tag: 'named' as const, authorCount: 1, mentionCount: 1, latestAt: 0, mentionsByDay: null } },
623+
{ name: 'a named row whose mentions are older than the spark', reason: { _tag: 'named' as const, authorCount: 1, mentionCount: 1, latestAt: 0, mentionsByDay: [0, 0, 0, 0, 0, 0, 0] } },
624+
])('draws no spark for $name', ({ reason }) => {
625+
const { html, text } = renderWeekly(input({ trending: [trending({ reason })] }))
626+
627+
expect(html).not.toMatch(BRAILLE)
628+
expect(text).not.toMatch(BRAILLE)
629+
})
630+
631+
it('keeps the spark out of the rose budget', () => {
632+
const { card } = renderWeekly(input({
633+
edition: 'weekly',
634+
trending: [trending({ reason: { _tag: 'named', authorCount: 3, mentionCount: 12, latestAt: WINDOW_END, mentionsByDay: week } })],
635+
}))
636+
const plain = renderWeekly(input({ edition: 'weekly', trending: [trending()] })).card
637+
638+
expect(card.match(/#e11d48|#be123c|#fb7185/gi)?.length ?? 0).toBe(plain.match(/#e11d48|#be123c|#fb7185/gi)?.length ?? 0)
639+
})
640+
})

0 commit comments

Comments
 (0)