Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 4 additions & 2 deletions layers/identity/server/utils/weekly-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ function reasonFor(entry: {
attribution: 'social' | 'github' | 'both'
social: { authorCount: number, mentionCount: number, latestMentionAt: number } | null
github: { latestGain: number, observedDay: number } | null
mentionsByDay: number[] | null
}): WeeklyReason {
const authorCount = entry.social?.authorCount ?? 0
const mentionCount = entry.social?.mentionCount ?? 0
Expand All @@ -214,10 +215,11 @@ function reasonFor(entry: {
const latestAt = entry.social?.latestMentionAt ?? 0
const gain = entry.github?.latestGain ?? 0
const day = entry.github?.observedDay ?? 0
const mentionsByDay = entry.mentionsByDay

if (entry.attribution === 'both' && entry.social && entry.github)
return { _tag: 'named-and-stars', authorCount, mentionCount, latestAt, gain, day }
return { _tag: 'named-and-stars', authorCount, mentionCount, latestAt, mentionsByDay, gain, day }
if (entry.attribution === 'github' && entry.github)
return { _tag: 'stars', gain, day }
return { _tag: 'named', authorCount, mentionCount, latestAt }
return { _tag: 'named', authorCount, mentionCount, latestAt, mentionsByDay }
}
87 changes: 74 additions & 13 deletions layers/identity/server/utils/weekly-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

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

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

/**
* Why a skill is on the trending list, as one value rather than a bag of
Expand All @@ -90,17 +97,25 @@ const MONO = `'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospa
* reason.
*/
export type WeeklyReason
= { _tag: 'named', authorCount: number, mentionCount: number, latestAt: number }
| { _tag: 'stars', gain: number, day: number }
| {
_tag: 'named-and-stars'
authorCount: number
mentionCount: number
latestAt: number
gain: number
day: number
}
| { _tag: 'popular', stars: number }
= {
_tag: 'named'
authorCount: number
mentionCount: number
latestAt: number
/** Counted mentions per rolling day, oldest first, for the braille spark. */
mentionsByDay: number[] | null
}
| { _tag: 'stars', gain: number, day: number }
| {
_tag: 'named-and-stars'
authorCount: number
mentionCount: number
latestAt: number
mentionsByDay: number[] | null
gain: number
day: number
}
| { _tag: 'popular', stars: number }

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

/**
* The seven days a row's spark draws, or null when it draws none.
*
* Only a row somebody posted about has days to draw. A week of zeros means
* every counted post is older than the spark, and seven blank cells beside
* "0 mentions" say nothing, so that row draws none, as on the board.
*/
function sparkDays(reason: WeeklyReason): number[] | null {
if (reason._tag !== 'named' && reason._tag !== 'named-and-stars')
return null
const week = sparkWeek(reason.mentionsByDay ?? [])
return week.some(count => count > 0) ? week : null
}

function sparkTotal(days: readonly number[]): string {
return sparkCount(days.reduce((sum, count) => sum + count, 0))
}

/**
* The braille spark for the HTML half, in stone.
*
* The site draws today in rose. The email spends its rose on the button and
* the footer rule, so seven rose cells would break the one-rose budget. A day
* with no count draws the lowest bar faintly as a track, as the site does, so
* a lone bar still reads as one day of seven.
*
* The label carries the numbers and the bars are hidden from screen readers,
* because a braille cell read aloud is noise.
*/
function sparkHtml(t: Tokens, days: readonly number[]): string {
const label = `${sparkTotal(days)} in 7 days. Per day, oldest first: ${days.join(', ')}.`
const bars = sparkLevels(days)
.map(level => level === 0 ? `<span style="color:${t.borderStrong};">${SPARK_BARS[1]}</span>` : SPARK_BARS[level])
.join('')
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>`
}

/**
* A skill description cut to something a person reads in one glance.
*
Expand Down Expand Up @@ -449,6 +501,8 @@ function trendingRow(t: Tokens, options: {
description: string | null
meta: string
evidence: WeeklyEvidence | null
/** Seven days of mentions, or null for a row nobody posted about. */
spark: number[] | null
}): string {
const description = trimDescription(options.description)
const evidence = options.evidence
Expand All @@ -463,7 +517,12 @@ function trendingRow(t: Tokens, options: {
style="width:28px;height:28px;border-radius:14px;display:block;border:1px solid ${t.border};background:${t.quote};" />
</td>
<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>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
<tr>
<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>
${options.spark ? `<td valign="top" align="right" style="padding-left:12px;white-space:nowrap;">${sparkHtml(t, options.spark)}</td>` : ''}
</tr>
</table>
${description ? `<div style="margin-top:3px;font-family:${SANS};font-size:14px;line-height:1.55;color:${t.body};">${esc(description)}</div>` : ''}
<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>
</td>
Expand Down Expand Up @@ -715,6 +774,7 @@ export function renderWeekly(input: WeeklyRenderInput): WeeklyRender {
description: skill.description,
meta: trendingMeta(skill, now),
evidence: skill.evidence,
spark: sparkDays(skill.reason),
})
}).join('')

Expand Down Expand Up @@ -843,7 +903,8 @@ export function renderWeeklyText(input: WeeklyRenderInput): string {
if (input.trending.length) {
lines.push('', 'TRENDING THIS WEEK', '')
for (const skill of input.trending) {
lines.push(`- ${skill.owner}/${skill.repo} ${skill.canonicalName}`)
const spark = sparkDays(skill.reason)
lines.push(`- ${skill.owner}/${skill.repo} ${skill.canonicalName}${spark ? ` ${brailleSpark(spark)} ${sparkTotal(spark)}` : ''}`)
const description = trimDescription(skill.description)
if (description)
lines.push(` ${description}`)
Expand Down
17 changes: 9 additions & 8 deletions server/routes/_dev/weekly.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const SCENARIOS: Record<string, () => WeeklyRenderInput> = {
canonicalName: 'ship',
description: 'Take a change from branch to merged without babysitting it.',
stars: 12_400,
reason: { _tag: 'named', authorCount: 4, mentionCount: 9, latestAt: WINDOW_END - 2 * 86_400 },
reason: { _tag: 'named', authorCount: 4, mentionCount: 9, latestAt: WINDOW_END - 2 * 86_400, mentionsByDay: [0, 1, 1, 0, 2, 5, 0] },
evidence: {
url: 'https://x.com/garrytan/status/1',
authorHandle: 'garrytan',
Expand All @@ -116,7 +116,7 @@ const SCENARIOS: Record<string, () => WeeklyRenderInput> = {
canonicalName: 'swiftui-liquid-glass',
description: 'Build the new material effects without fighting the layout system.',
stars: 865,
reason: { _tag: 'named-and-stars', authorCount: 2, mentionCount: 3, latestAt: WINDOW_END - 86_400, gain: 412, day: WINDOW_END - 2 * 86_400 },
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 },
evidence: {
url: 'https://bsky.app/profile/dimillian/post/1',
authorHandle: 'dimillian.bsky.social',
Expand All @@ -141,7 +141,7 @@ const SCENARIOS: Record<string, () => WeeklyRenderInput> = {
canonicalName: 'vue-debug-guides',
description: null,
stars: 210,
reason: { _tag: 'named', authorCount: 1, mentionCount: 1, latestAt: WINDOW_END - 5 * 86_400 },
reason: { _tag: 'named', authorCount: 1, mentionCount: 1, latestAt: WINDOW_END - 5 * 86_400, mentionsByDay: [0, 1, 0, 0, 0, 0, 0] },
evidence: null,
},
{
Expand All @@ -161,7 +161,7 @@ const SCENARIOS: Record<string, () => WeeklyRenderInput> = {
canonicalName: 'web-interface-guidelines',
description: 'Review interface code against practical usability rules.',
stars: 19_200,
reason: { _tag: 'named', authorCount: 3, mentionCount: 4, latestAt: WINDOW_END - 3 * 86_400 },
reason: { _tag: 'named', authorCount: 3, mentionCount: 4, latestAt: WINDOW_END - 3 * 86_400, mentionsByDay: [1, 0, 0, 2, 1, 0, 0] },
evidence: null,
},
{
Expand Down Expand Up @@ -214,7 +214,7 @@ const SCENARIOS: Record<string, () => WeeklyRenderInput> = {
canonicalName: 'nuxt-seo',
description: null,
stars: null,
reason: { _tag: 'named', authorCount: 1, mentionCount: 1, latestAt: 0 },
reason: { _tag: 'named', authorCount: 1, mentionCount: 1, latestAt: 0, mentionsByDay: null },
evidence: null,
}],
}),
Expand Down Expand Up @@ -254,7 +254,7 @@ const SCENARIOS: Record<string, () => WeeklyRenderInput> = {
canonicalName: 'a-skill-with-an-unusually-long-canonical-name',
description: 'Unicode and entities: quotes "like this", ampersands & angle brackets <div>, emoji dashes, and accented names such as RenΓ©e MΓΌller.',
stars: 1_284_000,
reason: { _tag: 'named-and-stars', authorCount: 17, mentionCount: 42, latestAt: WINDOW_END - 3600, gain: 9_814, day: WINDOW_END - 3 * 86_400 },
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 },
evidence: {
url: 'https://x.com/someone/status/2',
authorHandle: 'a_very_long_handle_indeed',
Expand Down Expand Up @@ -287,6 +287,7 @@ export async function liveScenario(): Promise<WeeklyRenderInput> {
starGain: number | null
starGainDay: number | null
evidence: { url: string, authorHandle: string, text: string, platform: 'x' | 'bsky' } | null
mentionsByDay: number[] | null
}>
}>('https://skilld.dev/api/feed/trending', { query: { limit: MAX_TRENDING, window: 24 * 7 } })

Expand All @@ -303,10 +304,10 @@ export async function liveScenario(): Promise<WeeklyRenderInput> {
description: skill.description,
stars: skill.stars,
reason: skill.attribution === 'both' && skill.starGain !== null && skill.starGainDay !== null
? { _tag: 'named-and-stars' as const, authorCount: skill.authorCount, mentionCount: skill.mentionCount, latestAt: 0, gain: skill.starGain, day: skill.starGainDay }
? { _tag: 'named-and-stars' as const, authorCount: skill.authorCount, mentionCount: skill.mentionCount, latestAt: 0, mentionsByDay: skill.mentionsByDay, gain: skill.starGain, day: skill.starGainDay }
: skill.attribution === 'github' && skill.starGain !== null && skill.starGainDay !== null
? { _tag: 'stars' as const, gain: skill.starGain, day: skill.starGainDay }
: { _tag: 'named' as const, authorCount: skill.authorCount, mentionCount: skill.mentionCount, latestAt: 0 },
: { _tag: 'named' as const, authorCount: skill.authorCount, mentionCount: skill.mentionCount, latestAt: 0, mentionsByDay: skill.mentionsByDay },
evidence: skill.evidence,
})),
}
Expand Down
49 changes: 49 additions & 0 deletions test/unit/weekly-template.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { WeeklyRenderInput, WeeklyTrendingSkill } from '../../layers/identity/server/utils/weekly-template'
import { describe, expect, it } from 'vitest'
import { formatWindow, renderWeekly } from '../../layers/identity/server/utils/weekly-template'
import { SPARK_BARS } from '../../shared/braille-spark'

const WINDOW_END = 1_755_648_000
const WINDOW_START = WINDOW_END - 7 * 86_400
Expand Down Expand Up @@ -589,3 +590,51 @@ describe('weekly theme', () => {
expect(html).not.toContain('\u{1F525}')
})
})

describe('weekly trending spark', () => {
// Every glyph a spark can print, the blank cell included.
const BRAILLE = new RegExp(`[${SPARK_BARS.join('')}]`)
const week = [0, 1, 0, 2, 0, 3, 6]

it('draws a week of mentions as a braille spark beside its count', () => {
const { html, text } = renderWeekly(input({
trending: [trending({ reason: { _tag: 'named', authorCount: 3, mentionCount: 12, latestAt: WINDOW_END, mentionsByDay: week } })],
}))

expect(text).toContain('β €β£€β €β£€β €β£€β£Ώ 12 mentions')
expect(html).toContain('aria-label="12 mentions in 7 days. Per day, oldest first: 0, 1, 0, 2, 0, 3, 6."')
expect(html).toMatch(BRAILLE)
})

it('draws the spark on a row that was named and surged', () => {
const { text } = renderWeekly(input({
trending: [trending({
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] },
})],
}))

expect(text).toContain('β €β €β €β €β €β €β£Ώ 1 mention')
})

it.each([
{ name: 'a star surge', reason: { _tag: 'stars' as const, gain: 865, day: WINDOW_END } },
{ name: 'a popular filler row', reason: { _tag: 'popular' as const, stars: 46_712 } },
{ name: 'a named row with no counted days', reason: { _tag: 'named' as const, authorCount: 1, mentionCount: 1, latestAt: 0, mentionsByDay: null } },
{ 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] } },
])('draws no spark for $name', ({ reason }) => {
const { html, text } = renderWeekly(input({ trending: [trending({ reason })] }))

expect(html).not.toMatch(BRAILLE)
expect(text).not.toMatch(BRAILLE)
})

it('keeps the spark out of the rose budget', () => {
const { card } = renderWeekly(input({
edition: 'weekly',
trending: [trending({ reason: { _tag: 'named', authorCount: 3, mentionCount: 12, latestAt: WINDOW_END, mentionsByDay: week } })],
}))
const plain = renderWeekly(input({ edition: 'weekly', trending: [trending()] })).card

expect(card.match(/#e11d48|#be123c|#fb7185/gi)?.length ?? 0).toBe(plain.match(/#e11d48|#be123c|#fb7185/gi)?.length ?? 0)
})
})