Skip to content

Commit 13ef183

Browse files
committed
feat: add connection streak achievement
Closes #9
1 parent bf127ee commit 13ef183

File tree

10 files changed

+271
-3
lines changed

10 files changed

+271
-3
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.2.0] - 2025-11-24
6+
7+
### 🚀 Features
8+
9+
- Add connection streak achievement
10+
511
## [0.1.2] - 2025-11-22
612

713
### 📚 Documentation
4.58 KB
Loading

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"author": {
88
"name": "BoxBoxJason"
99
},
10-
"version": "0.1.2",
10+
"version": "0.2.0",
1111
"engines": {
1212
"vscode": "^1.106.1"
1313
},

src/constants.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* @author BoxBoxJason
55
*/
66

7-
import path from "path";
7+
import path from "node:path";
88

99
/**
1010
* Constants for the achievements extension
@@ -48,6 +48,11 @@ export namespace constants {
4848
MERGES_AND_REBASES = "mergesAndRebasesCount",
4949
AMENDS = "amendsCount",
5050
FORCED_PUSHES = "forcedPushesCount",
51+
52+
// Connection streak
53+
CURRENT_CONNECTION_STREAK = "currentConnectionStreak",
54+
MAX_CONNECTION_STREAK = "maxConnectionStreak",
55+
LAST_STREAK_DATE = "lastStreakDate",
5156
PUSHES = "pushesCount",
5257
// Opened tabs
5358
NUMBER_OF_SIMULTANEOUS_TABS = "simultaneousTabsCount",

src/database/controller/timespent.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { DailySession } from "../model/tables/DailySession";
22
import { constants } from "../../constants";
33
import logger from "../../utils/logger";
44
import { ProgressionController } from "./progressions";
5+
import Progression from "../model/tables/Progression";
56

67
/**
78
* Controller for the time spent counters
@@ -108,4 +109,70 @@ export namespace TimeSpentController {
108109
)
109110
);
110111
}
112+
113+
/**
114+
* Update the connection streak
115+
*
116+
* @returns {Promise<void>}
117+
*/
118+
export async function updateConnectionStreak(): Promise<void> {
119+
const currentDate = new Date();
120+
const currentDateString = currentDate.toISOString().split("T")[0];
121+
122+
// Check if we already updated the streak today
123+
const lastStreakDateProgressions = await Progression.getProgressions({
124+
name: constants.criteria.LAST_STREAK_DATE,
125+
});
126+
const lastStreakDateProgression = lastStreakDateProgressions[0];
127+
128+
if (lastStreakDateProgression?.value === currentDateString) {
129+
return;
130+
}
131+
132+
// Get today's session
133+
const todaySession = await DailySession.getOrCreate(currentDateString);
134+
if (todaySession.duration <= 0) {
135+
return;
136+
}
137+
138+
// Get yesterday's session
139+
const yesterdayDate = new Date(currentDate);
140+
yesterdayDate.setDate(yesterdayDate.getDate() - 1);
141+
const yesterdayDateString = yesterdayDate.toISOString().split("T")[0];
142+
143+
const yesterdaySession = await DailySession.getOrCreate(yesterdayDateString);
144+
145+
let newStreak = 1;
146+
147+
if (yesterdaySession.duration > 0) {
148+
const currentStreakProgressions = await Progression.getProgressions({
149+
name: constants.criteria.CURRENT_CONNECTION_STREAK,
150+
});
151+
const currentStreakProgression = currentStreakProgressions[0];
152+
const currentStreak = currentStreakProgression
153+
? Number.parseInt(currentStreakProgression.value as string)
154+
: 0;
155+
156+
newStreak = currentStreak + 1;
157+
}
158+
159+
// Update current streak
160+
await ProgressionController.updateProgression(
161+
constants.criteria.CURRENT_CONNECTION_STREAK,
162+
newStreak
163+
);
164+
165+
// Update max streak
166+
await ProgressionController.updateProgression(
167+
constants.criteria.MAX_CONNECTION_STREAK,
168+
newStreak,
169+
true
170+
);
171+
172+
// Update last streak date
173+
await ProgressionController.updateProgression(
174+
constants.criteria.LAST_STREAK_DATE,
175+
currentDateString
176+
);
177+
}
111178
}

src/database/model/init/StackingTemplates.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,22 @@ export namespace StackingTemplates {
345345
hidden: false,
346346
requires: [],
347347
});
348+
349+
export const connectionStreakTemplate = (): StackingAchievementTemplate => ({
350+
title: "Connection Streak %d",
351+
icon: "CONNECTION_STREAK",
352+
category: constants.category.PRODUCTIVITY,
353+
group: "Connection Streak",
354+
labels: [constants.category.PRODUCTIVITY, "streak"],
355+
criterias: [constants.criteria.CURRENT_CONNECTION_STREAK],
356+
criteriasFunctions: [(x: number) => x + 1],
357+
description: `Connect for ${constants.criteria.CURRENT_CONNECTION_STREAK} consecutive days`,
358+
minTier: 0,
359+
maxTier: 999,
360+
expFunction: (x: number) => 1 + 2 * (x + 1),
361+
hidden: false,
362+
requires: [],
363+
});
348364
}
349365

350366
export namespace vscode {

src/database/model/init/init.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,22 @@ export namespace db_init {
102102
for (const criteria of Object.values(constants.criteria)) {
103103
if (
104104
criteria !== constants.criteria.LINES_OF_CODE_LANGUAGE &&
105-
criteria !== constants.criteria.FILES_CREATED_LANGUAGE
105+
criteria !== constants.criteria.FILES_CREATED_LANGUAGE &&
106+
criteria !== constants.criteria.LAST_STREAK_DATE
106107
) {
107108
const progression = new Progression({
108109
name: criteria,
109110
type: "integer",
110111
value: 0,
111112
});
112113
progressions.push(progression);
114+
} else if (criteria === constants.criteria.LAST_STREAK_DATE) {
115+
const progression = new Progression({
116+
name: criteria,
117+
type: "string",
118+
value: "",
119+
});
120+
progressions.push(progression);
113121
} else if (criteria === constants.criteria.LINES_OF_CODE_LANGUAGE) {
114122
for (const language of Object.values(constants.labels.LANGUAGES)) {
115123
const progression = new Progression({

src/listeners/time.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export namespace timeListeners {
5353
dailySession = await getCurrentDailySession();
5454
await dailySession.increase(sessionDuration);
5555
sessionStart = sessionEnd;
56+
await TimeSpentController.updateConnectionStreak();
5657
}
5758
}, 60000);
5859

@@ -83,6 +84,7 @@ export namespace timeListeners {
8384
// Increase daily session duration in the database
8485
await dailySession.increase(sessionDuration);
8586
sessionStart = undefined;
87+
await TimeSpentController.updateConnectionStreak();
8688
}
8789
}
8890

src/test/connection-streak.test.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import * as assert from 'node:assert';
2+
import * as vscode from 'vscode';
3+
import * as path from 'node:path';
4+
import { db_model } from '../database/model/model';
5+
import { db_init } from '../database/model/init/init';
6+
import { TimeSpentController } from '../database/controller/timespent';
7+
import { DailySession } from '../database/model/tables/DailySession';
8+
import { ProgressionController } from '../database/controller/progressions';
9+
import { constants } from '../constants';
10+
import { getMockContext, cleanupMockContext } from './utils';
11+
12+
suite('Connection Streak Test Suite', () => {
13+
let context: vscode.ExtensionContext;
14+
let dbPath: string;
15+
let originalDate: any;
16+
17+
setup(async () => {
18+
context = getMockContext();
19+
dbPath = path.join(context.globalStorageUri.fsPath, 'achievements.sqlite');
20+
await db_model.activate(context, dbPath);
21+
await db_init.activate(); // Initialize progressions
22+
originalDate = global.Date;
23+
});
24+
25+
teardown(() => {
26+
db_model.deactivate();
27+
cleanupMockContext(context);
28+
global.Date = originalDate;
29+
});
30+
31+
function mockDate(isoDate: string) {
32+
const date = new originalDate(isoDate);
33+
global.Date = class extends originalDate {
34+
constructor(args: any) {
35+
super(args);
36+
if (args) {
37+
return new originalDate(args);
38+
}
39+
return date;
40+
}
41+
static now() {
42+
return date.getTime();
43+
}
44+
45+
// Need to override other methods if used, but new Date() is the main one
46+
// Also need to handle new Date(string) which is used in the code
47+
// The constructor above handles it?
48+
// Wait, `new Date()` calls constructor with no args.
49+
// `new Date(currentDate)` calls constructor with args.
50+
} as any;
51+
}
52+
53+
test('1 day connected is counted', async () => {
54+
mockDate('2023-01-01T12:00:00Z');
55+
56+
// Create session for today
57+
const session = await DailySession.getOrCreate('2023-01-01');
58+
await session.increase(100);
59+
60+
await TimeSpentController.updateConnectionStreak();
61+
62+
const progs = await ProgressionController.getProgressions();
63+
assert.strictEqual(progs[constants.criteria.CURRENT_CONNECTION_STREAK], 1);
64+
assert.strictEqual(progs[constants.criteria.MAX_CONNECTION_STREAK], 1);
65+
});
66+
67+
test('Consecutive days increment streak', async () => {
68+
// Day 1
69+
mockDate('2023-01-01T12:00:00Z');
70+
let session = await DailySession.getOrCreate('2023-01-01');
71+
await session.increase(100);
72+
await TimeSpentController.updateConnectionStreak();
73+
74+
// Day 2
75+
mockDate('2023-01-02T12:00:00Z');
76+
session = await DailySession.getOrCreate('2023-01-02');
77+
await session.increase(100);
78+
await TimeSpentController.updateConnectionStreak();
79+
80+
let progs = await ProgressionController.getProgressions();
81+
assert.strictEqual(progs[constants.criteria.CURRENT_CONNECTION_STREAK], 2);
82+
83+
// Day 3
84+
mockDate('2023-01-03T12:00:00Z');
85+
session = await DailySession.getOrCreate('2023-01-03');
86+
await session.increase(100);
87+
await TimeSpentController.updateConnectionStreak();
88+
89+
progs = await ProgressionController.getProgressions();
90+
assert.strictEqual(progs[constants.criteria.CURRENT_CONNECTION_STREAK], 3);
91+
});
92+
93+
test('Streak reset on missed day', async () => {
94+
// Day 1
95+
mockDate('2023-01-01T12:00:00Z');
96+
let session = await DailySession.getOrCreate('2023-01-01');
97+
await session.increase(100);
98+
await TimeSpentController.updateConnectionStreak();
99+
100+
// Skip Day 2
101+
102+
// Day 3
103+
mockDate('2023-01-03T12:00:00Z');
104+
session = await DailySession.getOrCreate('2023-01-03');
105+
await session.increase(100);
106+
await TimeSpentController.updateConnectionStreak();
107+
108+
const progs = await ProgressionController.getProgressions();
109+
assert.strictEqual(progs[constants.criteria.CURRENT_CONNECTION_STREAK], 1);
110+
assert.strictEqual(progs[constants.criteria.MAX_CONNECTION_STREAK], 1); // Max was 1
111+
});
112+
113+
test('Max streak updates', async () => {
114+
// Day 1
115+
mockDate('2023-01-01T12:00:00Z');
116+
let session = await DailySession.getOrCreate('2023-01-01');
117+
await session.increase(100);
118+
await TimeSpentController.updateConnectionStreak();
119+
120+
// Day 2
121+
mockDate('2023-01-02T12:00:00Z');
122+
session = await DailySession.getOrCreate('2023-01-02');
123+
await session.increase(100);
124+
await TimeSpentController.updateConnectionStreak();
125+
126+
let progs = await ProgressionController.getProgressions();
127+
assert.strictEqual(progs[constants.criteria.MAX_CONNECTION_STREAK], 2);
128+
129+
// Reset
130+
mockDate('2023-01-04T12:00:00Z');
131+
session = await DailySession.getOrCreate('2023-01-04');
132+
await session.increase(100);
133+
await TimeSpentController.updateConnectionStreak();
134+
135+
progs = await ProgressionController.getProgressions();
136+
assert.strictEqual(progs[constants.criteria.CURRENT_CONNECTION_STREAK], 1);
137+
assert.strictEqual(progs[constants.criteria.MAX_CONNECTION_STREAK], 2);
138+
});
139+
140+
test('Idempotency within same day', async () => {
141+
mockDate('2023-01-01T12:00:00Z');
142+
143+
// First update
144+
let session = await DailySession.getOrCreate('2023-01-01');
145+
await session.increase(100);
146+
await TimeSpentController.updateConnectionStreak();
147+
148+
let progs = await ProgressionController.getProgressions();
149+
assert.strictEqual(progs[constants.criteria.CURRENT_CONNECTION_STREAK], 1);
150+
151+
// Second update same day
152+
await session.increase(100);
153+
await TimeSpentController.updateConnectionStreak();
154+
155+
progs = await ProgressionController.getProgressions();
156+
assert.strictEqual(progs[constants.criteria.CURRENT_CONNECTION_STREAK], 1);
157+
});
158+
});

src/views/viewconst.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,12 @@ export namespace webview {
224224
TAB_HOARDER: ["icons", "achievements", "productivity", "tab.png"],
225225
ERROR_FIXER: ["icons", "achievements", "productivity", "fix.png"],
226226
SHOWER_AVOIDER: ["icons", "achievements", "productivity", "shower.png"],
227+
CONNECTION_STREAK: [
228+
"icons",
229+
"achievements",
230+
"productivity",
231+
"streak.png",
232+
],
227233
} as const;
228234
}
229235
}

0 commit comments

Comments
 (0)