1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
|
/* eslint-disable no-await-in-loop */
import React from 'dom-chef';
import cache from 'webext-storage-cache';
import select from 'select-dom';
import onetime from 'onetime';
import ClockIcon from 'octicon/clock.svg';
import features from '.';
import * as api from '../github-helpers/api';
import {getUsername} from '../github-helpers';
import observeElement from '../helpers/simplified-element-observer';
interface Commit {
url: string;
sha: string;
}
const timeFormatter = new Intl.DateTimeFormat('en-GB', {
hour: 'numeric',
minute: 'numeric',
hour12: false
});
async function loadCommitPatch(commitUrl: string): Promise<string> {
const {textContent} = await api.v3(commitUrl, {
json: false,
headers: {
Accept: 'application/vnd.github.v3.patch'
}
});
return textContent;
}
const getLastCommitDate = cache.function(async (login: string): Promise<string | false> => {
for await (const page of api.v3paginated(`users/${login}/events`)) {
for (const event of page as any) {
if (event.type !== 'PushEvent') {
continue;
}
// Start from the latest commit, which is the last one in the list
for (const commit of event.payload.commits.reverse() as Commit[]) {
const response = await api.v3(commit.url, {ignoreHTTPStatus: true});
// Commits might not exist anymore even if they are listed in the events
// This can happen if the repository was deleted so we can also skip all other commits
if (response.httpStatus === 404) {
break;
}
if (!response.ok) {
throw await api.getError(response);
}
// `response.author` only appears if GitHub can match the email to a GitHub user
if (response.author?.id !== event.actor.id) {
continue;
}
const patch = await loadCommitPatch(commit.url);
// The patch of merge commits doesn't include the commit sha so the date might be from another user
if (patch.startsWith(`From ${commit.sha} `)) {
return /^Date: (.*)$/m.exec(patch)?.[1] ?? false;
}
}
}
}
return false;
}, {
maxAge: 10,
staleWhileRevalidate: 20,
cacheKey: ([login]) => __filebasename + ':' + login
});
function parseOffset(date: string): number {
const [, hourString, minuteString] = (/([-+]\d\d)(\d\d)$/).exec(date) ?? [];
const hours = Number.parseInt(hourString, 10);
const minutes = Number.parseInt(minuteString, 10);
return (hours * 60) + (hours < 0 ? -minutes : minutes);
}
function init(): void {
const hovercard = select('.js-hovercard-content > .Popover-message')!;
observeElement(hovercard, async () => {
if (
select.exists('.rgh-local-user-time', hovercard) || // Time already added
!select.exists('[data-hydro-view*="user-hovercard-hover"]', hovercard) // It's not the hovercard type we expect
) {
return;
}
const login = select<HTMLAnchorElement>('a[data-octo-dimensions="link_type:profile"]', hovercard)?.pathname.slice(1);
if (!login || login === getUsername()) {
return;
}
const placeholder = <span>Guessing local time…</span>;
const container = (
<div className="rgh-local-user-time mt-2 text-gray text-small">
<ClockIcon/> {placeholder}
</div>
);
// Adding the time element might change the height of the hovercard and thus break its positioning
const hovercardHeight = hovercard.offsetHeight;
select('div.d-flex.mt-3 > div.overflow-hidden.ml-3', hovercard)!.append(container);
if (hovercard.matches('.Popover-message--bottom-right, .Popover-message--bottom-left')) {
const diff = hovercard.offsetHeight - hovercardHeight;
if (diff > 0) {
const parent = hovercard.parentElement!;
const top = Number.parseInt(parent.style.top, 10);
parent.style.top = `${top - diff}px`;
}
}
const date = await getLastCommitDate(login);
if (!date) {
placeholder.textContent = 'Not found';
container.title = 'Timezone couldn’t be determined from their last commits';
return;
}
const now = new Date();
now.setMinutes(parseOffset(date) + now.getTimezoneOffset() + now.getMinutes());
placeholder.textContent = timeFormatter.format(now);
container.title = `Timezone guessed from their last commit: ${date}`;
});
}
void features.add({
id: __filebasename,
description: 'Shows the user local time in their hovercard (based on their last commit).',
screenshot: 'https://user-images.githubusercontent.com/1402241/69863648-ef449180-12cf-11ea-8f36-7c92fc487f31.gif'
}, {
init: onetime(init)
});
|