From f412a0ae508e8a62a8f7f7e47cf9ea3c2cfae34a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Feb 2025 10:40:41 -0600 Subject: [PATCH 01/12] detectLanguage: test that a Japanese text with Han-only characters is ambiguous --- src/utils/language.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/language.test.ts b/src/utils/language.test.ts index f4025290..60b844f0 100644 --- a/src/utils/language.test.ts +++ b/src/utils/language.test.ts @@ -38,6 +38,7 @@ Deno.test('Detects definitive texts', () => { // ambiguous assertEquals(detectLanguage('你好', 1), undefined); + assertEquals(detectLanguage('東京', 1), undefined); assertEquals(detectLanguage('Привет', 1), undefined); assertEquals(detectLanguage('Hello', 1), undefined); }); From c2aab97018a18a583bcc5a1302a6c4213c151d5e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Feb 2025 11:24:49 -0600 Subject: [PATCH 02/12] indexExtensions: ensure kind 6 has reply:false to test the performance difference between -reply:true --- src/storages/EventsDB.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/storages/EventsDB.ts b/src/storages/EventsDB.ts index b22cd32a..0b538f81 100644 --- a/src/storages/EventsDB.ts +++ b/src/storages/EventsDB.ts @@ -68,7 +68,13 @@ class EventsDB extends NPostgres { if (event.kind === 1) { ext.reply = event.tags.some(([name]) => name === 'e').toString(); + } else if (event.kind === 1111) { + ext.reply = event.tags.some(([name]) => ['e', 'E'].includes(name)).toString(); + } else if (event.kind === 6) { + ext.reply = 'false'; + } + if ([1, 20, 30023].includes(event.kind)) { const language = detectLanguage(event.content, 0.90); if (language) { From 084df2b59d44c07187132083d84e002c45ff3304 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Feb 2025 13:51:21 -0600 Subject: [PATCH 03/12] Streaks API --- src/db/DittoTables.ts | 2 ++ src/db/migrations/045_streaks.ts | 17 +++++++++++++++ src/entities/MastodonAccount.ts | 5 +++++ src/interfaces/DittoEvent.ts | 2 ++ src/storages/hydrate.ts | 26 ++++++++++++++++------ src/utils/stats.ts | 37 ++++++++++++++++++++++++++++---- src/views/mastodon/accounts.ts | 14 ++++++++++++ 7 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 src/db/migrations/045_streaks.ts diff --git a/src/db/DittoTables.ts b/src/db/DittoTables.ts index 6ffed988..7baaa42c 100644 --- a/src/db/DittoTables.ts +++ b/src/db/DittoTables.ts @@ -17,6 +17,8 @@ interface AuthorStatsRow { following_count: number; notes_count: number; search: string; + streak_start: number | null; + streak_end: number | null; } interface EventStatsRow { diff --git a/src/db/migrations/045_streaks.ts b/src/db/migrations/045_streaks.ts new file mode 100644 index 00000000..553ef96a --- /dev/null +++ b/src/db/migrations/045_streaks.ts @@ -0,0 +1,17 @@ +import { Kysely } from 'kysely'; + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('author_stats') + .addColumn('streak_start', 'integer') + .addColumn('streak_end', 'integer') + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema + .alterTable('author_stats') + .dropColumn('streak_start') + .dropColumn('streak_end') + .execute(); +} diff --git a/src/entities/MastodonAccount.ts b/src/entities/MastodonAccount.ts index 99409c6a..eedaaa29 100644 --- a/src/entities/MastodonAccount.ts +++ b/src/entities/MastodonAccount.ts @@ -45,6 +45,11 @@ export interface MastodonAccount { ditto: { accepts_zaps: boolean; external_url: string; + streak: { + days: number; + start: string | null; + end: string | null; + }; }; domain?: string; pleroma: { diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index cca7c0ca..293a7ab4 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -6,6 +6,8 @@ export interface AuthorStats { followers_count: number; following_count: number; notes_count: number; + streak_start?: number; + streak_end?: number; } /** Ditto internal stats for the event. */ diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 28dcea47..160dd1cc 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -89,10 +89,22 @@ export function assembleEvents( ): DittoEvent[] { const admin = Conf.pubkey; - const eventStats = stats.events.map((stat) => ({ - ...stat, - reactions: JSON.parse(stat.reactions), - })); + const authorStats = stats.authors.reduce((result, { pubkey, ...stat }) => { + result[pubkey] = { + ...stat, + streak_start: stat.streak_start ?? undefined, + streak_end: stat.streak_end ?? undefined, + }; + return result; + }, {} as Record); + + const eventStats = stats.events.reduce((result, { event_id, ...stat }) => { + result[event_id] = { + ...stat, + reactions: JSON.parse(stat.reactions), + }; + return result; + }, {} as Record); for (const event of a) { event.author = b.find((e) => matchFilter({ kinds: [0], authors: [event.pubkey] }, e)); @@ -161,8 +173,8 @@ export function assembleEvents( event.zap_message = zapRequest?.content ?? ''; } - event.author_stats = stats.authors.find((stats) => stats.pubkey === event.pubkey); - event.event_stats = eventStats.find((stats) => stats.event_id === event.id); + event.author_stats = authorStats[event.pubkey]; + event.event_stats = eventStats[event.id]; } return a; @@ -383,6 +395,8 @@ async function gatherAuthorStats( following_count: Math.max(0, row.following_count), notes_count: Math.max(0, row.notes_count), search: row.search, + streak_start: row.streak_start, + streak_end: row.streak_end, })); } diff --git a/src/utils/stats.ts b/src/utils/stats.ts index e2fab440..64aaa66c 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -1,5 +1,5 @@ import { NostrEvent, NSchema as n, NStore } from '@nostrify/nostrify'; -import { Kysely, UpdateObject } from 'kysely'; +import { Insertable, Kysely, UpdateObject } from 'kysely'; import { SetRequired } from 'type-fest'; import { z } from 'zod'; @@ -18,6 +18,8 @@ interface UpdateStatsOpts { export async function updateStats({ event, kysely, store, x = 1 }: UpdateStatsOpts): Promise { switch (event.kind) { case 1: + case 20: + case 1111: return handleEvent1(kysely, event, x); case 3: return handleEvent3(kysely, event, x, store); @@ -34,7 +36,32 @@ export async function updateStats({ event, kysely, store, x = 1 }: UpdateStatsOp /** Update stats for kind 1 event. */ async function handleEvent1(kysely: Kysely, event: NostrEvent, x: number): Promise { - await updateAuthorStats(kysely, event.pubkey, ({ notes_count }) => ({ notes_count: Math.max(0, notes_count + x) })); + await updateAuthorStats(kysely, event.pubkey, (prev) => { + let start = prev.streak_start; + let end = prev.streak_end; + + if (start && end) { // Streak exists. + if (event.created_at <= end) { + // Streak cannot go backwards in time. Skip it. + } else if (end - start > 86400) { + // Streak is broken. Start a new streak. + start = event.created_at; + end = event.created_at; + } else { + // Extend the streak. + end = event.created_at; + } + } else { // New streak. + start = event.created_at; + end = event.created_at; + } + + return { + notes_count: Math.max(0, prev.notes_count + x), + streak_start: start || null, + streak_end: end || null, + }; + }); const replyId = findReplyTag(event.tags)?.[1]; const quoteId = findQuoteTag(event.tags)?.[1]; @@ -187,9 +214,9 @@ export function getAuthorStats( export async function updateAuthorStats( kysely: Kysely, pubkey: string, - fn: (prev: DittoTables['author_stats']) => UpdateObject, + fn: (prev: Insertable) => UpdateObject, ): Promise { - const empty: DittoTables['author_stats'] = { + const empty: Insertable = { pubkey, followers_count: 0, following_count: 0, @@ -290,6 +317,8 @@ export async function countAuthorStats( following_count: getTagSet(followList?.tags ?? [], 'p').size, notes_count, search, + streak_start: null, + streak_end: null, }; } diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 025737c3..7a252158 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -69,6 +69,15 @@ async function renderAccount( verified_at: null, })) ?? []; + let streakDays = 0; + const streakStart = event.author_stats?.streak_start; + const streakEnd = event.author_stats?.streak_end; + + if (streakStart && streakEnd) { + const delta = streakEnd - streakStart; + streakDays = Math.ceil(delta / 86400); + } + return { id: pubkey, acct, @@ -113,6 +122,11 @@ async function renderAccount( ditto: { accepts_zaps: Boolean(getLnurl({ lud06, lud16 })), external_url: Conf.external(nprofile), + streak: { + days: streakDays, + start: streakStart ? nostrDate(streakStart).toISOString() : null, + end: streakEnd ? nostrDate(streakEnd).toISOString() : null, + }, }, domain: parsed05?.domain, pleroma: { From abea4f17b31f8d0b6626ddc1c86a72d4978392a7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Feb 2025 14:44:01 -0600 Subject: [PATCH 04/12] Streak: report a 1 day streak after the first post --- src/views/mastodon/accounts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 7a252158..c456b18c 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -75,7 +75,7 @@ async function renderAccount( if (streakStart && streakEnd) { const delta = streakEnd - streakStart; - streakDays = Math.ceil(delta / 86400); + streakDays = Math.max(Math.ceil(delta / 86400), 1); } return { From 080c34d13fc2f32b46da70d10205293abcae24c1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Feb 2025 14:53:42 -0600 Subject: [PATCH 05/12] Fix streak broken logic --- src/utils/stats.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/utils/stats.ts b/src/utils/stats.ts index 64aaa66c..341174c5 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -37,23 +37,25 @@ export async function updateStats({ event, kysely, store, x = 1 }: UpdateStatsOp /** Update stats for kind 1 event. */ async function handleEvent1(kysely: Kysely, event: NostrEvent, x: number): Promise { await updateAuthorStats(kysely, event.pubkey, (prev) => { + const now = event.created_at; + let start = prev.streak_start; let end = prev.streak_end; if (start && end) { // Streak exists. - if (event.created_at <= end) { + if (now <= end) { // Streak cannot go backwards in time. Skip it. - } else if (end - start > 86400) { + } else if (now - end > 86400) { // Streak is broken. Start a new streak. - start = event.created_at; - end = event.created_at; + start = now; + end = now; } else { // Extend the streak. - end = event.created_at; + end = now; } } else { // New streak. - start = event.created_at; - end = event.created_at; + start = now; + end = now; } return { From b480947c4d8f7d06b87db28423940b1c38006efc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Feb 2025 15:56:49 -0600 Subject: [PATCH 06/12] Add a script to recompute the streak of all authors --- deno.json | 1 + scripts/db-streak-recompute.ts | 48 ++++++++++++++++++++++++++++++++++ src/utils/stats.ts | 1 + 3 files changed, 50 insertions(+) create mode 100644 scripts/db-streak-recompute.ts diff --git a/deno.json b/deno.json index 7501c3f6..562aab51 100644 --- a/deno.json +++ b/deno.json @@ -23,6 +23,7 @@ "clean:deps": "deno cache --reload src/app.ts", "db:populate-search": "deno run -A --env-file --deny-read=.env scripts/db-populate-search.ts", "db:populate-extensions": "deno run -A --env-file --deny-read=.env scripts/db-populate-extensions.ts", + "db:streak:recompute": "deno run -A --env-file --deny-read=.env scripts/db-streak-recompute.ts", "vapid": "deno run scripts/vapid.ts" }, "unstable": [ diff --git a/scripts/db-streak-recompute.ts b/scripts/db-streak-recompute.ts new file mode 100644 index 00000000..262f0427 --- /dev/null +++ b/scripts/db-streak-recompute.ts @@ -0,0 +1,48 @@ +import { Storages } from '@/storages.ts'; + +const kysely = await Storages.kysely(); +const statsQuery = kysely.selectFrom('author_stats').select('pubkey'); + +for await (const { pubkey } of statsQuery.stream(10)) { + const eventsQuery = kysely + .selectFrom('nostr_events') + .select('created_at') + .where('pubkey', '=', pubkey) + .where('kind', 'in', [1, 20, 1111, 30023]) + .orderBy('nostr_events.created_at', 'desc') + .orderBy('nostr_events.id', 'asc'); + + let end: number | null = null; + let start: number | null = null; + + for await (const { created_at } of eventsQuery.stream(20)) { + const createdAt = Number(created_at); + + if (!end) { + const now = Math.floor(Date.now() / 1000); + + if (now - createdAt > 86400) { + break; // streak broken + } + + end = createdAt; + } + + if (start && (start - createdAt > 86400)) { + break; // streak broken + } + + start = createdAt; + } + + await kysely + .updateTable('author_stats') + .set({ + streak_end: end, + streak_start: start, + }) + .where('pubkey', '=', pubkey) + .execute(); +} + +Deno.exit(); diff --git a/src/utils/stats.ts b/src/utils/stats.ts index 341174c5..64e7986d 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -20,6 +20,7 @@ export async function updateStats({ event, kysely, store, x = 1 }: UpdateStatsOp case 1: case 20: case 1111: + case 30023: return handleEvent1(kysely, event, x); case 3: return handleEvent3(kysely, event, x, store); From 30559ba043821ac3b6f474964008245bcdfe9418 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Feb 2025 16:04:25 -0600 Subject: [PATCH 07/12] streak-recompute: only update changed rows --- scripts/db-streak-recompute.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scripts/db-streak-recompute.ts b/scripts/db-streak-recompute.ts index 262f0427..e202baa5 100644 --- a/scripts/db-streak-recompute.ts +++ b/scripts/db-streak-recompute.ts @@ -35,14 +35,16 @@ for await (const { pubkey } of statsQuery.stream(10)) { start = createdAt; } - await kysely - .updateTable('author_stats') - .set({ - streak_end: end, - streak_start: start, - }) - .where('pubkey', '=', pubkey) - .execute(); + if (start && end) { + await kysely + .updateTable('author_stats') + .set({ + streak_end: end, + streak_start: start, + }) + .where('pubkey', '=', pubkey) + .execute(); + } } Deno.exit(); From 86ffa7f0cc2de1e20f7cee4a47b56d59e49c1ddc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Feb 2025 16:33:14 -0600 Subject: [PATCH 08/12] Don't display broken streak through the API --- src/views/mastodon/accounts.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index c456b18c..1261de92 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -70,12 +70,18 @@ async function renderAccount( })) ?? []; let streakDays = 0; - const streakStart = event.author_stats?.streak_start; - const streakEnd = event.author_stats?.streak_end; + let streakStart = event.author_stats?.streak_start ?? null; + let streakEnd = event.author_stats?.streak_end ?? null; if (streakStart && streakEnd) { - const delta = streakEnd - streakStart; - streakDays = Math.max(Math.ceil(delta / 86400), 1); + const broken = nostrNow() - streakEnd > 86400; + if (broken) { + streakStart = null; + streakEnd = null; + } else { + const delta = streakEnd - streakStart; + streakDays = Math.max(Math.ceil(delta / 86400), 1); + } } return { From 00e10eb19ffb9a9a491e69a4ac72cbedd5ff74ba Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 6 Feb 2025 18:42:29 -0600 Subject: [PATCH 09/12] detectLanguage: strip numbers from text before matching language patterns --- src/utils/language.test.ts | 7 +++++++ src/utils/language.ts | 13 +++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/utils/language.test.ts b/src/utils/language.test.ts index 60b844f0..66a26edd 100644 --- a/src/utils/language.test.ts +++ b/src/utils/language.test.ts @@ -35,6 +35,13 @@ Deno.test('Detects definitive texts', () => { assertEquals(detectLanguage('Γειά σου!', 1), 'el'); assertEquals(detectLanguage('שלום!', 1), 'he'); assertEquals(detectLanguage('こんにちは。', 1), 'ja'); + assertEquals( + detectLanguage( + '最近、長女から「中学生男子全員クソ」という話を良く聞き中学生女子側の視点が分かってよかった。父からは「中学生男子は自分がクソだということを3年間かかって学習するんだよ」と言っておいた', + 1, + ), + 'ja', + ); // ambiguous assertEquals(detectLanguage('你好', 1), undefined); diff --git a/src/utils/language.ts b/src/utils/language.ts index b95e3e78..9a713122 100644 --- a/src/utils/language.ts +++ b/src/utils/language.ts @@ -12,9 +12,10 @@ export function detectLanguage(text: string, minConfidence: number): LanguageCod // It's better to remove the emojis first const sanitizedText = linkify.tokenize( text - .replaceAll(/\p{Extended_Pictographic}/gu, '') - .replaceAll(/[\s\uFEFF\u00A0\u200B-\u200D\u{0FE0E}]+/gu, ' '), - ).reduce((acc, { t, v }) => t === 'text' ? acc + v : acc, '').trim(); + .replaceAll(/\p{Extended_Pictographic}/gu, '') // strip emojis + .replaceAll(/[\s\uFEFF\u00A0\u200B-\u200D\u{0FE0E}]+/gu, ' '), // strip invisible characters + ) + .reduce((acc, { t, v }) => t === 'text' ? acc + v : acc, '').trim(); // Definite patterns for some languages. // Text which matches MUST unambiguously be in the given language. @@ -30,7 +31,11 @@ export function detectLanguage(text: string, minConfidence: number): LanguageCod // If any pattern matches, the language is known. for (const [lang, pattern] of Object.entries(languagePatterns) as [LanguageCode, RegExp][]) { - if (pattern.test(text.replace(/[\p{P}\p{S}]/gu, ''))) { // strip punctuation and symbols before checking + const text = sanitizedText + .replaceAll(/[\p{P}\p{S}]/gu, '') // strip punctuation and symbols + .replaceAll(/\p{N}/gu, ''); // strip numbers + + if (pattern.test(text)) { return lang; } } From 46558a97e4386e315a8753e246ae0cab73fba620 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 7 Feb 2025 11:50:37 -0600 Subject: [PATCH 10/12] Make STREAK_WINDOW configurable --- scripts/db-streak-recompute.ts | 6 ++++-- src/config.ts | 4 ++++ src/utils/stats.ts | 3 ++- src/views/mastodon/accounts.ts | 5 +++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/scripts/db-streak-recompute.ts b/scripts/db-streak-recompute.ts index e202baa5..a05eb08b 100644 --- a/scripts/db-streak-recompute.ts +++ b/scripts/db-streak-recompute.ts @@ -1,7 +1,9 @@ +import { Conf } from '@/config.ts'; import { Storages } from '@/storages.ts'; const kysely = await Storages.kysely(); const statsQuery = kysely.selectFrom('author_stats').select('pubkey'); +const { streakWindow } = Conf; for await (const { pubkey } of statsQuery.stream(10)) { const eventsQuery = kysely @@ -21,14 +23,14 @@ for await (const { pubkey } of statsQuery.stream(10)) { if (!end) { const now = Math.floor(Date.now() / 1000); - if (now - createdAt > 86400) { + if (now - createdAt > streakWindow) { break; // streak broken } end = createdAt; } - if (start && (start - createdAt > 86400)) { + if (start && (start - createdAt > streakWindow)) { break; // streak broken } diff --git a/src/config.ts b/src/config.ts index 0164182a..0fee527e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -357,6 +357,10 @@ class Conf { return Number(Deno.env.get('PROFILE_FIELDS_VALUE_LENGTH') || 2047); }, }; + /** Maximum time between events before a streak is broken, *in seconds*. */ + static get streakWindow(): number { + return Number(Deno.env.get('STREAK_WINDOW') || 86400); + } } const optionalBooleanSchema = z diff --git a/src/utils/stats.ts b/src/utils/stats.ts index 64e7986d..0821fed2 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -3,6 +3,7 @@ import { Insertable, Kysely, UpdateObject } from 'kysely'; import { SetRequired } from 'type-fest'; import { z } from 'zod'; +import { Conf } from '@/config.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts'; @@ -46,7 +47,7 @@ async function handleEvent1(kysely: Kysely, event: NostrEvent, x: n if (start && end) { // Streak exists. if (now <= end) { // Streak cannot go backwards in time. Skip it. - } else if (now - end > 86400) { + } else if (now - end > Conf.streakWindow) { // Streak is broken. Start a new streak. start = now; end = now; diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 1261de92..0c2d1dcc 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -72,15 +72,16 @@ async function renderAccount( let streakDays = 0; let streakStart = event.author_stats?.streak_start ?? null; let streakEnd = event.author_stats?.streak_end ?? null; + const { streakWindow } = Conf; if (streakStart && streakEnd) { - const broken = nostrNow() - streakEnd > 86400; + const broken = nostrNow() - streakEnd > streakWindow; if (broken) { streakStart = null; streakEnd = null; } else { const delta = streakEnd - streakStart; - streakDays = Math.max(Math.ceil(delta / 86400), 1); + streakDays = Math.max(Math.ceil(delta / streakWindow), 1); } } From ea8ef0904556a4299bf4f146c466424b242c8a74 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 7 Feb 2025 11:54:47 -0600 Subject: [PATCH 11/12] Change default streak window to 36 hours --- src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 0fee527e..cdd88705 100644 --- a/src/config.ts +++ b/src/config.ts @@ -359,7 +359,7 @@ class Conf { }; /** Maximum time between events before a streak is broken, *in seconds*. */ static get streakWindow(): number { - return Number(Deno.env.get('STREAK_WINDOW') || 86400); + return Number(Deno.env.get('STREAK_WINDOW') || 129600); } } From af262b5d524993da1b733acdd235ccb1e47f27d1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 7 Feb 2025 12:06:34 -0600 Subject: [PATCH 12/12] Whoops, fix streak days calculation --- src/views/mastodon/accounts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 0c2d1dcc..99dd3523 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -81,7 +81,7 @@ async function renderAccount( streakEnd = null; } else { const delta = streakEnd - streakStart; - streakDays = Math.max(Math.ceil(delta / streakWindow), 1); + streakDays = Math.max(Math.ceil(delta / 86400), 1); } }