Skip to content

Commit ab344a3

Browse files
GiteaBotsilverwind
andauthored
Rework and fix stopwatch (#30732) (#30787)
Backport #30732 by @silverwind 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. Co-authored-by: silverwind <[email protected]>
1 parent 2bedd16 commit ab344a3

File tree

11 files changed

+99
-113
lines changed

11 files changed

+99
-113
lines changed

package-lock.json

Lines changed: 0 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
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

Lines changed: 39 additions & 30 deletions
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

Lines changed: 8 additions & 8 deletions
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

Lines changed: 3 additions & 4 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 40 additions & 42 deletions
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';
@@ -11,28 +10,31 @@ export function initStopwatch() {
1110
return;
1211
}
1312

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

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

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

3840
let usingPeriodicPoller = false;
@@ -125,10 +127,9 @@ async function updateStopwatch() {
125127

126128
function updateStopwatchData(data) {
127129
const watch = data[0];
128-
const btnEl = document.querySelector('.active-stopwatch-trigger');
130+
const btnEls = document.querySelectorAll('.active-stopwatch');
129131
if (!watch) {
130-
clearStopwatchTimer();
131-
hideElem(btnEl);
132+
hideElem(btnEls);
132133
} else {
133134
const {repo_owner_name, repo_name, issue_index, seconds} = watch;
134135
const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
@@ -138,31 +139,28 @@ function updateStopwatchData(data) {
138139
const stopwatchIssue = document.querySelector('.stopwatch-issue');
139140
if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`;
140141
updateStopwatchTime(seconds);
141-
showElem(btnEl);
142+
showElem(btnEls);
142143
}
143144
return Boolean(data.length);
144145
}
145146

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

web_src/js/modules/tippy.js

Lines changed: 4 additions & 2 deletions
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

Lines changed: 1 addition & 0 deletions
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)