Skip to content

Commit 71ce691

Browse files
authored
Merge pull request #292 from Expensify/dangrous-relativeTimestamps
Add a toggle to add timestamps to relative dates
2 parents b04b883 + 28a2318 commit 71ce691

File tree

11 files changed

+310
-3
lines changed

11 files changed

+310
-3
lines changed

assets/manifest-firefox.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": 3,
33

44
"name": "K2 for GitHub",
5-
"version": "1.5.15",
5+
"version": "1.5.16",
66
"description": "Manage your Kernel Scheduling from directly inside GitHub",
77

88
"browser_specific_settings": {

assets/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": 3,
33

44
"name": "K2 for GitHub",
5-
"version": "1.5.15",
5+
"version": "1.5.16",
66
"description": "Manage your Kernel Scheduling from directly inside GitHub",
77

88
"icons": {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "k2-extension",
3-
"version": "1.5.15",
3+
"version": "1.5.16",
44
"description": "A browser extension for Kernel Schedule",
55
"private": true,
66
"scripts": {

src/js/lib/actions/Preferences.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import ReactNativeOnyx from 'react-native-onyx';
22
import ONYXKEYS from '../../ONYXKEYS';
33

44
let ghToken;
5+
let useAbsoluteTimestamps;
56
ReactNativeOnyx.connect({
67
key: ONYXKEYS.PREFERENCES,
78
callback: (preferences) => {
@@ -11,6 +12,7 @@ ReactNativeOnyx.connect({
1112
}
1213

1314
ghToken = preferences.ghToken;
15+
useAbsoluteTimestamps = !!preferences.useAbsoluteTimestamps;
1416
},
1517
});
1618

@@ -26,7 +28,21 @@ function setGitHubToken(value) {
2628
ReactNativeOnyx.merge(ONYXKEYS.PREFERENCES, {ghToken: value});
2729
}
2830

31+
function getUseAbsoluteTimestamps() {
32+
return !!useAbsoluteTimestamps;
33+
}
34+
35+
/**
36+
* @param {Boolean} value
37+
*/
38+
function setUseAbsoluteTimestamps(value) {
39+
useAbsoluteTimestamps = value;
40+
ReactNativeOnyx.merge(ONYXKEYS.PREFERENCES, {useAbsoluteTimestamps: value});
41+
}
42+
2943
export {
3044
getGitHubToken,
3145
setGitHubToken,
46+
getUseAbsoluteTimestamps,
47+
setUseAbsoluteTimestamps,
3248
};

src/js/lib/pages/github/_base.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import $ from 'jquery';
22
import _ from 'underscore';
3+
import ReactNativeOnyx from 'react-native-onyx';
34
import * as API from '../../api';
5+
import * as Preferences from '../../actions/Preferences';
6+
import ONYXKEYS from '../../../ONYXKEYS';
7+
import convertTimestamps from '../../timestampConverter';
48

59
/**
610
* This class is to be extended by each of the distinct types of webpages that the extension works on
@@ -280,5 +284,35 @@ export default function () {
280284
});
281285
};
282286

287+
/**
288+
* Listens for preference changes and applies the configured timestamp format once.
289+
* Call once during setup to register the Onyx listener and perform an initial conversion.
290+
*/
291+
Page.setupTimestampFormat = function setupTimestampFormat() {
292+
ReactNativeOnyx.connect({
293+
key: ONYXKEYS.PREFERENCES,
294+
callback: (preferences) => {
295+
if (!preferences) {
296+
return;
297+
}
298+
const preference = !!preferences.useAbsoluteTimestamps;
299+
convertTimestamps(preference);
300+
},
301+
});
302+
303+
const initialPreference = Preferences.getUseAbsoluteTimestamps();
304+
if (initialPreference !== undefined) {
305+
convertTimestamps(!!initialPreference);
306+
}
307+
};
308+
309+
/**
310+
* Applies the current timestamp format. Safe to invoke periodically for dynamic content.
311+
*/
312+
Page.applyTimestampFormat = function applyTimestampFormat() {
313+
const preference = Preferences.getUseAbsoluteTimestamps();
314+
convertTimestamps(!!preference);
315+
};
316+
283317
return Page;
284318
}

src/js/lib/pages/github/all.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export default function () {
2929
$('nav.js-repo-nav *[data-selected-links*="repo_pulls"]')
3030
.parent().after(k2Button({url: currentUrl}));
3131
}
32+
33+
// Set up timestamp format conversion
34+
setTimeout(() => AllPages.applyTimestampFormat(), 500);
35+
setInterval(() => AllPages.applyTimestampFormat(), 5000);
3236
};
3337

3438
return AllPages;

src/js/lib/pages/github/issue.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import K2picker from '../../../module/K2picker/K2picker';
77
import K2pickerarea from '../../../module/K2pickerarea/K2pickerarea';
88
import K2pickerType from '../../../module/K2pickertype/K2pickertype';
99
import ToggleReview from '../../../module/ToggleReview/ToggleReview';
10+
import ToggleTimestamps from '../../../module/ToggleTimestamps/ToggleTimestamps';
1011
import K2comments from '../../../module/K2comments/K2comments';
1112
import K2previousissues from '../../../module/K2previousissues/K2previousissues';
1213
import ONYXKEYS from '../../../ONYXKEYS';
@@ -133,12 +134,33 @@ const refreshPicker = function () {
133134
.before(sidebarWrapperHTML);
134135
}
135136

137+
// Add timestamp toggle wrapper at the bottom if it doesn't exist
138+
if (!$('.k2toggletimestamps-wrapper').length) {
139+
// Find the last sidebar item or sidebar container and append to bottom
140+
const lastSidebarItem = $('.discussion-sidebar-item').last();
141+
if (lastSidebarItem.length) {
142+
lastSidebarItem.after('<div class="discussion-sidebar-item js-discussion-sidebar-item k2toggletimestamps-wrapper"></div>');
143+
} else {
144+
// Fallback: append to sidebar container
145+
const sidebar = $('.discussion-sidebar, [role="complementary"]').first();
146+
if (sidebar.length) {
147+
sidebar.append('<div class="discussion-sidebar-item js-discussion-sidebar-item k2toggletimestamps-wrapper"></div>');
148+
}
149+
}
150+
}
151+
136152
new K2picker().draw();
137153
new K2pickerType().draw();
138154
new K2pickerarea().draw();
139155
new ToggleReview().draw();
140156
new K2comments().draw();
141157
new K2previousissues().draw();
158+
159+
// Draw timestamp toggle if wrapper exists and is empty
160+
const timestampWrapper = $('.k2toggletimestamps-wrapper');
161+
if (timestampWrapper.length && timestampWrapper.children().length === 0) {
162+
new ToggleTimestamps().draw();
163+
}
142164
};
143165

144166
/**

src/js/lib/pages/github/pr.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import $ from 'jquery';
22
import Base from './_base';
3+
import ToggleTimestamps from '../../../module/ToggleTimestamps/ToggleTimestamps';
34

45
/**
56
* Replaces all `- [ ]` with `- [x]` in textareas with specific names
@@ -21,6 +22,38 @@ const renderReplaceChecklistButton = () => {
2122
$('.k2-replace-checklist').off().on('click', replaceChecklistItems);
2223
};
2324

25+
const refreshSidebar = function () {
26+
// Add the timestamp toggle wrapper to the DOM if it doesn't exist
27+
if ($('.k2toggletimestamps-wrapper').length) {
28+
// Draw the toggle if wrapper exists but is empty
29+
const wrapper = $('.k2toggletimestamps-wrapper');
30+
if (wrapper.children().length === 0) {
31+
new ToggleTimestamps().draw();
32+
}
33+
return;
34+
}
35+
36+
// Add timestamp toggle wrapper at the bottom of the sidebar
37+
const lastSidebarItem = $('.discussion-sidebar-item').last();
38+
if (lastSidebarItem.length) {
39+
lastSidebarItem.after('<div class="discussion-sidebar-item js-discussion-sidebar-item k2toggletimestamps-wrapper"></div>');
40+
} else {
41+
// Fallback: append to sidebar container
42+
const sidebar = $('.discussion-sidebar, [role="complementary"], .Layout-sidebar').first();
43+
if (sidebar.length) {
44+
sidebar.append('<div class="discussion-sidebar-item js-discussion-sidebar-item k2toggletimestamps-wrapper"></div>');
45+
}
46+
}
47+
48+
// Draw the timestamp toggle if the wrapper exists and is empty
49+
const wrapper = $('.k2toggletimestamps-wrapper');
50+
if (!wrapper.length || wrapper.children().length > 0) {
51+
return;
52+
}
53+
54+
new ToggleTimestamps().draw();
55+
};
56+
2457
const refreshHold = function () {
2558
const prTitle = $('.js-issue-title').text();
2659

@@ -93,6 +126,7 @@ export default function () {
93126
* Add buttons to the page and setup the event handler
94127
*/
95128
PrPage.setup = function () {
129+
setTimeout(refreshSidebar, 500);
96130
setInterval(refreshHold, 1000);
97131
setInterval(renderReplaceChecklistButton, 2000);
98132

src/js/lib/timestampConverter.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Formats a datetime string to "Dec 16, 2025 6:30 PM EST" format
3+
* @param {String} datetimeString ISO 8601 datetime string
4+
* @returns {String} Formatted date string
5+
*/
6+
function formatTimestamp(datetimeString) {
7+
const date = new Date(datetimeString);
8+
const options = {
9+
month: 'short',
10+
day: 'numeric',
11+
year: 'numeric',
12+
hour: 'numeric',
13+
minute: 'numeric',
14+
hour12: true,
15+
timeZoneName: 'short',
16+
};
17+
return new Intl.DateTimeFormat('en-US', options).format(date);
18+
}
19+
20+
/**
21+
* Converts all relative-time elements on the page based on the preference
22+
* @param {Boolean} useAbsoluteTimestamps Whether to use absolute timestamps
23+
*/
24+
function convertTimestamps(useAbsoluteTimestamps) {
25+
const shouldUseAbsoluteTimestamps = !!useAbsoluteTimestamps;
26+
27+
if (shouldUseAbsoluteTimestamps) {
28+
// Find all relative-time elements that don't have absolute timestamp added yet
29+
const elements = document.querySelectorAll('relative-time:not([data-k2-absolute-added])');
30+
31+
Array.from(elements).forEach((el) => {
32+
const datetime = el.getAttribute('datetime');
33+
34+
if (!datetime) {
35+
return;
36+
}
37+
38+
// Create a span for the absolute timestamp
39+
const absoluteSpan = document.createElement('span');
40+
const absoluteTime = formatTimestamp(datetime);
41+
absoluteSpan.textContent = ` (${absoluteTime})`;
42+
absoluteSpan.dataset.k2AbsolutePart = 'true';
43+
44+
// Add the absolute span after the relative-time element
45+
const parent = el.parentNode;
46+
if (parent) {
47+
// Insert the absolute span right after the relative-time element
48+
if (el.nextSibling) {
49+
parent.insertBefore(absoluteSpan, el.nextSibling);
50+
} else {
51+
parent.appendChild(absoluteSpan);
52+
}
53+
54+
// Mark that we've added the absolute part
55+
// eslint-disable-next-line no-param-reassign
56+
el.dataset.k2AbsoluteAdded = 'true';
57+
58+
// Store datetime for updates
59+
// eslint-disable-next-line no-param-reassign
60+
el.dataset.k2OriginalDatetime = datetime;
61+
}
62+
});
63+
64+
// Update existing absolute parts (refresh the absolute timestamp)
65+
const elementsWithAbsolute = document.querySelectorAll('relative-time[data-k2-absolute-added]');
66+
Array.from(elementsWithAbsolute).forEach((el) => {
67+
const datetime = el.dataset.k2OriginalDatetime || el.getAttribute('datetime');
68+
if (datetime) {
69+
// Find the absolute span that follows this element
70+
let absoluteSpan = el.nextSibling;
71+
while (absoluteSpan && (!absoluteSpan.dataset || !absoluteSpan.dataset.k2AbsolutePart)) {
72+
absoluteSpan = absoluteSpan.nextSibling;
73+
}
74+
75+
if (absoluteSpan && absoluteSpan.dataset.k2AbsolutePart) {
76+
const absoluteTime = formatTimestamp(datetime);
77+
// eslint-disable-next-line no-param-reassign
78+
absoluteSpan.textContent = ` (${absoluteTime})`;
79+
}
80+
}
81+
});
82+
} else {
83+
// Find all relative-time elements that have absolute timestamps added
84+
const elements = document.querySelectorAll('relative-time[data-k2-absolute-added]');
85+
86+
Array.from(elements).forEach((el) => {
87+
// Find and remove the absolute timestamp span
88+
let absoluteSpan = el.nextSibling;
89+
while (absoluteSpan && (!absoluteSpan.dataset || !absoluteSpan.dataset.k2AbsolutePart)) {
90+
absoluteSpan = absoluteSpan.nextSibling;
91+
}
92+
93+
if (absoluteSpan && absoluteSpan.dataset.k2AbsolutePart) {
94+
const parent = absoluteSpan.parentNode;
95+
if (parent) {
96+
parent.removeChild(absoluteSpan);
97+
}
98+
}
99+
100+
// Remove our markers
101+
// eslint-disable-next-line no-param-reassign
102+
delete el.dataset.k2AbsoluteAdded;
103+
// eslint-disable-next-line no-param-reassign
104+
delete el.dataset.k2OriginalDatetime;
105+
});
106+
}
107+
}
108+
109+
export default convertTimestamps;

0 commit comments

Comments
 (0)