Skip to content

Commit 564102c

Browse files
authored
Rework and fix stopwatch (#30732)
Fixes #30721 and overhauls the stopwatch. Time is now shown inside the "dot" icon and on both mobile and desktop. All rendering is now done by `<relative-time>`, the `pretty-ms` dependency is dropped. Desktop: <img width="557" alt="Screenshot 2024-04-29 at 22 33 27" src="https://github.com/go-gitea/gitea/assets/115237/3a46cdbf-6af2-4bf9-b07f-021348badaac"> Mobile: <img width="640" alt="Screenshot 2024-04-29 at 22 34 19" src="https://github.com/go-gitea/gitea/assets/115237/8a2beea7-bd5d-473f-8fff-66f63fd50877"> Note for tippy: Previously, tippy instances defaulted to "menu" theme, but that theme is really only meant for `.ui.menu`, so it was not optimal for the stopwatch popover. This introduces a unopinionated `default` theme that has no padding and should be suitable for all content. I reviewed all existing uses and explicitely set the desired `theme` on all of them.
1 parent 5f05e7b commit 564102c

File tree

11 files changed

+99
-113
lines changed

11 files changed

+99
-113
lines changed

package-lock.json

-26
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
"postcss": "8.4.38",
4242
"postcss-loader": "8.1.1",
4343
"postcss-nesting": "12.1.2",
44-
"pretty-ms": "9.0.0",
4544
"sortablejs": "1.15.2",
4645
"swagger-ui-dist": "5.17.2",
4746
"tailwindcss": "3.4.3",

templates/base/head_navbar.tmpl

+39-30
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212

1313
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
1414
<div class="ui secondary menu item navbar-mobile-right only-mobile">
15+
{{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
16+
<a id="mobile-stopwatch-icon" class="active-stopwatch item tw-mx-0" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}">
17+
<div class="tw-relative">
18+
{{svg "octicon-stopwatch"}}
19+
<span class="header-stopwatch-dot"></span>
20+
</div>
21+
</a>
22+
{{end}}
1523
{{if .IsSigned}}
1624
<a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
1725
<div class="tw-relative">
@@ -74,41 +82,13 @@
7482
</div><!-- end content avatar menu -->
7583
</div><!-- end dropdown avatar menu -->
7684
{{else if .IsSigned}}
77-
{{if EnableTimetracking}}
78-
<a class="active-stopwatch-trigger item tw-mx-0{{if not .ActiveStopwatch}} tw-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}">
85+
{{if and EnableTimetracking .ActiveStopwatch}}
86+
<a class="item not-mobile active-stopwatch tw-mx-0" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}">
7987
<div class="tw-relative">
8088
{{svg "octicon-stopwatch"}}
8189
<span class="header-stopwatch-dot"></span>
8290
</div>
83-
<span class="only-mobile tw-ml-2">{{ctx.Locale.Tr "active_stopwatch"}}</span>
8491
</a>
85-
<div class="active-stopwatch-popup item tippy-target tw-p-2">
86-
<div class="tw-flex tw-items-center">
87-
<a class="stopwatch-link tw-flex tw-items-center" href="{{.ActiveStopwatch.IssueLink}}">
88-
{{svg "octicon-issue-opened" 16 "tw-mr-2"}}
89-
<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
90-
<span class="ui primary label stopwatch-time tw-my-0 tw-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
91-
{{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}}
92-
</span>
93-
</a>
94-
<form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
95-
{{.CsrfTokenHtml}}
96-
<button
97-
type="submit"
98-
class="ui button mini compact basic icon"
99-
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
100-
>{{svg "octicon-square-fill"}}</button>
101-
</form>
102-
<form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
103-
{{.CsrfTokenHtml}}
104-
<button
105-
type="submit"
106-
class="ui button mini compact basic icon"
107-
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
108-
>{{svg "octicon-trash"}}</button>
109-
</form>
110-
</div>
111-
</div>
11292
{{end}}
11393

11494
<a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
@@ -202,4 +182,33 @@
202182
</a>
203183
{{end}}
204184
</div><!-- end full right menu -->
185+
186+
{{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
187+
<div class="active-stopwatch-popup tippy-target">
188+
<div class="tw-flex tw-items-center tw-gap-2 tw-p-3">
189+
<a class="stopwatch-link tw-flex tw-items-center tw-gap-2 muted" href="{{.ActiveStopwatch.IssueLink}}">
190+
{{svg "octicon-issue-opened" 16}}
191+
<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
192+
</a>
193+
<div class="tw-flex tw-gap-1">
194+
<form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
195+
{{.CsrfTokenHtml}}
196+
<button
197+
type="submit"
198+
class="ui button mini compact basic icon tw-mr-0"
199+
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
200+
>{{svg "octicon-square-fill"}}</button>
201+
</form>
202+
<form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
203+
{{.CsrfTokenHtml}}
204+
<button
205+
type="submit"
206+
class="ui button mini compact basic icon tw-mr-0"
207+
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
208+
>{{svg "octicon-trash"}}</button>
209+
</form>
210+
</div>
211+
</div>
212+
</div>
213+
{{end}}
205214
</nav>

web_src/css/modules/navbar.css

+8-8
Original file line numberDiff line numberDiff line change
@@ -103,26 +103,24 @@
103103
width: 50%;
104104
min-height: 48px;
105105
}
106+
#navbar #mobile-stopwatch-icon,
106107
#navbar #mobile-notifications-icon {
107108
margin-right: 6px !important;
108109
}
109110
}
110111

111-
#navbar a.item .notification_count {
112-
color: var(--color-nav-bg);
113-
padding: 0 3.75px;
114-
font-size: 12px;
115-
line-height: 12px;
116-
font-weight: var(--font-weight-bold);
117-
}
118-
119112
#navbar a.item:hover .notification_count,
120113
#navbar a.item:hover .header-stopwatch-dot {
121114
border-color: var(--color-nav-hover-bg);
122115
}
123116

124117
#navbar a.item .notification_count,
125118
#navbar a.item .header-stopwatch-dot {
119+
color: var(--color-nav-bg);
120+
padding: 0 3.75px;
121+
font-size: 12px;
122+
line-height: 12px;
123+
font-weight: var(--font-weight-bold);
126124
background: var(--color-primary);
127125
border: 2px solid var(--color-nav-bg);
128126
position: absolute;
@@ -135,6 +133,8 @@
135133
align-items: center;
136134
justify-content: center;
137135
z-index: 1; /* prevent menu button background from overlaying icon */
136+
user-select: none;
137+
white-space: nowrap;
138138
}
139139

140140
.secondary-nav {

web_src/css/modules/tippy.css

+3-4
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,15 @@
1616

1717
.tippy-box {
1818
position: relative;
19-
background-color: var(--color-body);
20-
color: var(--color-secondary-dark-6);
19+
background-color: var(--color-menu);
20+
color: var(--color-text);
2121
border: 1px solid var(--color-secondary);
2222
border-radius: var(--border-radius);
2323
font-size: 1rem;
2424
}
2525

2626
.tippy-content {
2727
position: relative;
28-
padding: 1rem; /* if you need different padding, use different data-theme */
2928
z-index: 1;
3029
}
3130

@@ -166,5 +165,5 @@
166165
}
167166

168167
.tippy-svg-arrow-inner {
169-
fill: var(--color-body);
168+
fill: var(--color-menu);
170169
}

web_src/js/features/contextpopup.js

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function attachRefIssueContextPopup(refIssues) {
1818
if (!owner) return;
1919

2020
const el = document.createElement('div');
21+
el.classList.add('tw-p-3');
2122
refIssue.parentNode.insertBefore(el, refIssue.nextSibling);
2223

2324
const view = createApp(ContextPopup);
@@ -30,6 +31,7 @@ export function attachRefIssueContextPopup(refIssues) {
3031
}
3132

3233
createTippy(refIssue, {
34+
theme: 'default',
3335
content: el,
3436
placement: 'top-start',
3537
interactive: true,

web_src/js/features/repo-code.js

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ function showLineButton() {
113113
btn.closest('.code-view').append(menu.cloneNode(true));
114114

115115
createTippy(btn, {
116+
theme: 'menu',
116117
trigger: 'click',
117118
hideOnClick: true,
118119
content: menu,

web_src/js/features/repo-issue.js

+1
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ export function initRepoPullRequestReview() {
502502
if ($reviewBtn.length && $panel.length) {
503503
const tippy = createTippy($reviewBtn[0], {
504504
content: $panel[0],
505+
theme: 'default',
505506
placement: 'bottom',
506507
trigger: 'click',
507508
maxWidth: 'none',

web_src/js/features/stopwatch.js

+40-42
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import prettyMilliseconds from 'pretty-ms';
21
import {createTippy} from '../modules/tippy.js';
32
import {GET} from '../modules/fetch.js';
43
import {hideElem, showElem} from '../utils/dom.js';
@@ -10,28 +9,31 @@ export function initStopwatch() {
109
return;
1110
}
1211

13-
const stopwatchEl = document.querySelector('.active-stopwatch-trigger');
12+
const stopwatchEls = document.querySelectorAll('.active-stopwatch');
1413
const stopwatchPopup = document.querySelector('.active-stopwatch-popup');
1514

16-
if (!stopwatchEl || !stopwatchPopup) {
15+
if (!stopwatchEls.length || !stopwatchPopup) {
1716
return;
1817
}
1918

20-
stopwatchEl.removeAttribute('href'); // intended for noscript mode only
21-
22-
createTippy(stopwatchEl, {
23-
content: stopwatchPopup,
24-
placement: 'bottom-end',
25-
trigger: 'click',
26-
maxWidth: 'none',
27-
interactive: true,
28-
hideOnClick: true,
29-
});
30-
3119
// global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used.
32-
const currSeconds = document.querySelector('.stopwatch-time')?.getAttribute('data-seconds');
33-
if (currSeconds) {
34-
updateStopwatchTime(currSeconds);
20+
const seconds = stopwatchEls[0]?.getAttribute('data-seconds');
21+
if (seconds) {
22+
updateStopwatchTime(parseInt(seconds));
23+
}
24+
25+
for (const stopwatchEl of stopwatchEls) {
26+
stopwatchEl.removeAttribute('href'); // intended for noscript mode only
27+
28+
createTippy(stopwatchEl, {
29+
content: stopwatchPopup.cloneNode(true),
30+
placement: 'bottom-end',
31+
trigger: 'click',
32+
maxWidth: 'none',
33+
interactive: true,
34+
hideOnClick: true,
35+
theme: 'default',
36+
});
3537
}
3638

3739
let usingPeriodicPoller = false;
@@ -124,10 +126,9 @@ async function updateStopwatch() {
124126

125127
function updateStopwatchData(data) {
126128
const watch = data[0];
127-
const btnEl = document.querySelector('.active-stopwatch-trigger');
129+
const btnEls = document.querySelectorAll('.active-stopwatch');
128130
if (!watch) {
129-
clearStopwatchTimer();
130-
hideElem(btnEl);
131+
hideElem(btnEls);
131132
} else {
132133
const {repo_owner_name, repo_name, issue_index, seconds} = watch;
133134
const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
@@ -137,31 +138,28 @@ function updateStopwatchData(data) {
137138
const stopwatchIssue = document.querySelector('.stopwatch-issue');
138139
if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`;
139140
updateStopwatchTime(seconds);
140-
showElem(btnEl);
141+
showElem(btnEls);
141142
}
142143
return Boolean(data.length);
143144
}
144145

145-
let updateTimeIntervalId = null; // holds setInterval id when active
146-
function clearStopwatchTimer() {
147-
if (updateTimeIntervalId !== null) {
148-
clearInterval(updateTimeIntervalId);
149-
updateTimeIntervalId = null;
150-
}
151-
}
146+
// TODO: This flickers on page load, we could avoid this by making a custom
147+
// element to render time periods. Feeding a datetime in backend does not work
148+
// when time zone between server and client differs.
152149
function updateStopwatchTime(seconds) {
153-
const secs = parseInt(seconds);
154-
if (!Number.isFinite(secs)) return;
155-
156-
clearStopwatchTimer();
157-
const stopwatch = document.querySelector('.stopwatch-time');
158-
// TODO: replace with <relative-time> similar to how system status up time is shown
159-
const start = Date.now();
160-
const updateUi = () => {
161-
const delta = Date.now() - start;
162-
const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
163-
if (stopwatch) stopwatch.textContent = dur;
164-
};
165-
updateUi();
166-
updateTimeIntervalId = setInterval(updateUi, 1000);
150+
if (!Number.isFinite(seconds)) return;
151+
const datetime = (new Date(Date.now() - seconds * 1000)).toISOString();
152+
for (const parent of document.querySelectorAll('.header-stopwatch-dot')) {
153+
const existing = parent.querySelector(':scope > relative-time');
154+
if (existing) {
155+
existing.setAttribute('datetime', datetime);
156+
} else {
157+
const el = document.createElement('relative-time');
158+
el.setAttribute('format', 'micro');
159+
el.setAttribute('datetime', datetime);
160+
el.setAttribute('lang', 'en-US');
161+
el.setAttribute('title', ''); // make <relative-time> show no title and therefor no tooltip
162+
parent.append(el);
163+
}
164+
}
167165
}

web_src/js/modules/tippy.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ export function createTippy(target, opts = {}) {
3737
return onShow?.(instance);
3838
},
3939
arrow: arrow || (theme === 'bare' ? false : arrowSvg),
40-
role: role || 'menu', // HTML role attribute
41-
theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare"
40+
// HTML role attribute, ideally the default role would be "popover" but it does not exist
41+
role: role || 'menu',
42+
// CSS theme, either "default", "tooltip", "menu", "box-with-header" or "bare"
43+
theme: theme || role || 'default',
4244
plugins: [followCursor],
4345
...other,
4446
});

web_src/js/webcomponents/overflow-menu.js

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
131131
interactive: true,
132132
placement: 'bottom-end',
133133
role: 'menu',
134+
theme: 'menu',
134135
content: this.tippyContent,
135136
onShow: () => { // FIXME: onShown doesn't work (never be called)
136137
setTimeout(() => {

0 commit comments

Comments
 (0)