]> source.dussan.org Git - gitea.git/commitdiff
Rework and fix stopwatch (#30732) (#30787)
authorGiteabot <teabot@gitea.io>
Tue, 30 Apr 2024 21:46:45 +0000 (05:46 +0800)
committerGitHub <noreply@github.com>
Tue, 30 Apr 2024 21:46:45 +0000 (21:46 +0000)
Backport #30732 by @silverwind

Fixes https://github.com/go-gitea/gitea/issues/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 <me@silverwind.io>
package-lock.json
package.json
templates/base/head_navbar.tmpl
web_src/css/modules/navbar.css
web_src/css/modules/tippy.css
web_src/js/features/contextpopup.js
web_src/js/features/repo-code.js
web_src/js/features/repo-issue.js
web_src/js/features/stopwatch.js
web_src/js/modules/tippy.js
web_src/js/webcomponents/overflow-menu.js

index 8e4eeb7fb8f925a9735c046aafd36da7694db3e1..917ff1029b2c3cb048699fd4e19dd46d1e212372 100644 (file)
@@ -42,7 +42,6 @@
         "postcss": "8.4.38",
         "postcss-loader": "8.1.1",
         "postcss-nesting": "12.1.2",
-        "pretty-ms": "9.0.0",
         "sortablejs": "1.15.2",
         "swagger-ui-dist": "5.17.2",
         "tailwindcss": "3.4.3",
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/parse-ms": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
-      "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
-      "engines": {
-        "node": ">=18"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
-    "node_modules/pretty-ms": {
-      "version": "9.0.0",
-      "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.0.0.tgz",
-      "integrity": "sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==",
-      "dependencies": {
-        "parse-ms": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=18"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/printable-characters": {
       "version": "1.0.42",
       "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
index 142b9bb3eeff9080bdffaf5c03d62332a26ff5a5..5f9b8103206c7f8ef2b7d28e46052df06cd2fcde 100644 (file)
@@ -41,7 +41,6 @@
     "postcss": "8.4.38",
     "postcss-loader": "8.1.1",
     "postcss-nesting": "12.1.2",
-    "pretty-ms": "9.0.0",
     "sortablejs": "1.15.2",
     "swagger-ui-dist": "5.17.2",
     "tailwindcss": "3.4.3",
index addff22c49774dc8b74a6310a05003a3e622f0e1..7a3e663c494f05575f3c759c461b0d90ce13dcd2 100644 (file)
 
                <!-- 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 -->
                <div class="ui secondary menu item navbar-mobile-right only-mobile">
+                       {{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
+                       <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}}">
+                               <div class="tw-relative">
+                                       {{svg "octicon-stopwatch"}}
+                                       <span class="header-stopwatch-dot"></span>
+                               </div>
+                       </a>
+                       {{end}}
                        {{if .IsSigned}}
                        <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"}}">
                                <div class="tw-relative">
                                </div><!-- end content avatar menu -->
                        </div><!-- end dropdown avatar menu -->
                {{else if .IsSigned}}
-                       {{if EnableTimetracking}}
-                       <a class="active-stopwatch-trigger item tw-mx-0{{if not .ActiveStopwatch}} tw-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}">
+                       {{if and EnableTimetracking .ActiveStopwatch}}
+                       <a class="item not-mobile active-stopwatch tw-mx-0" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}">
                                <div class="tw-relative">
                                        {{svg "octicon-stopwatch"}}
                                        <span class="header-stopwatch-dot"></span>
                                </div>
-                               <span class="only-mobile tw-ml-2">{{ctx.Locale.Tr "active_stopwatch"}}</span>
                        </a>
-                       <div class="active-stopwatch-popup item tippy-target tw-p-2">
-                               <div class="tw-flex tw-items-center">
-                                       <a class="stopwatch-link tw-flex tw-items-center" href="{{.ActiveStopwatch.IssueLink}}">
-                                               {{svg "octicon-issue-opened" 16 "tw-mr-2"}}
-                                               <span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
-                                               <span class="ui primary label stopwatch-time tw-my-0 tw-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
-                                                       {{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}}
-                                               </span>
-                                       </a>
-                                       <form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
-                                               {{.CsrfTokenHtml}}
-                                               <button
-                                                       type="submit"
-                                                       class="ui button mini compact basic icon"
-                                                       data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
-                                               >{{svg "octicon-square-fill"}}</button>
-                                       </form>
-                                       <form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
-                                               {{.CsrfTokenHtml}}
-                                               <button
-                                                       type="submit"
-                                                       class="ui button mini compact basic icon"
-                                                       data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
-                                               >{{svg "octicon-trash"}}</button>
-                                       </form>
-                               </div>
-                       </div>
                        {{end}}
 
                        <a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
                        </a>
                {{end}}
        </div><!-- end full right menu -->
+
+       {{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
+               <div class="active-stopwatch-popup tippy-target">
+                       <div class="tw-flex tw-items-center tw-gap-2 tw-p-3">
+                               <a class="stopwatch-link tw-flex tw-items-center tw-gap-2 muted" href="{{.ActiveStopwatch.IssueLink}}">
+                                       {{svg "octicon-issue-opened" 16}}
+                                       <span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
+                               </a>
+                               <div class="tw-flex tw-gap-1">
+                                       <form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
+                                               {{.CsrfTokenHtml}}
+                                               <button
+                                                       type="submit"
+                                                       class="ui button mini compact basic icon tw-mr-0"
+                                                       data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
+                                               >{{svg "octicon-square-fill"}}</button>
+                                       </form>
+                                       <form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
+                                               {{.CsrfTokenHtml}}
+                                               <button
+                                                       type="submit"
+                                                       class="ui button mini compact basic icon tw-mr-0"
+                                                       data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
+                                               >{{svg "octicon-trash"}}</button>
+                                       </form>
+                               </div>
+                       </div>
+               </div>
+       {{end}}
 </nav>
index d7aa197e02619aff6b23a7102938c394bea41aa9..848f9331d0f4c427e0753e432a616639bffb258d 100644 (file)
     width: 50%;
     min-height: 48px;
   }
+  #navbar #mobile-stopwatch-icon,
   #navbar #mobile-notifications-icon {
     margin-right: 6px !important;
   }
 }
 
-#navbar a.item .notification_count {
-  color: var(--color-nav-bg);
-  padding: 0 3.75px;
-  font-size: 12px;
-  line-height: 12px;
-  font-weight: var(--font-weight-bold);
-}
-
 #navbar a.item:hover .notification_count,
 #navbar a.item:hover .header-stopwatch-dot {
   border-color: var(--color-nav-hover-bg);
 
 #navbar a.item .notification_count,
 #navbar a.item .header-stopwatch-dot {
+  color: var(--color-nav-bg);
+  padding: 0 3.75px;
+  font-size: 12px;
+  line-height: 12px;
+  font-weight: var(--font-weight-bold);
   background: var(--color-primary);
   border: 2px solid var(--color-nav-bg);
   position: absolute;
   align-items: center;
   justify-content: center;
   z-index: 1; /* prevent menu button background from overlaying icon */
+  user-select: none;
+  white-space: nowrap;
 }
 
 .secondary-nav {
index 6ac7c37d934248ed1edd0a7471d390b1a690023d..53c3d5aaeac6efab49731b3067c6a0ef1f4590bc 100644 (file)
@@ -16,8 +16,8 @@
 
 .tippy-box {
   position: relative;
-  background-color: var(--color-body);
-  color: var(--color-secondary-dark-6);
+  background-color: var(--color-menu);
+  color: var(--color-text);
   border: 1px solid var(--color-secondary);
   border-radius: var(--border-radius);
   font-size: 1rem;
@@ -25,7 +25,6 @@
 
 .tippy-content {
   position: relative;
-  padding: 1rem; /* if you need different padding, use different data-theme */
   z-index: 1;
 }
 
 }
 
 .tippy-svg-arrow-inner {
-  fill: var(--color-body);
+  fill: var(--color-menu);
 }
index ce90f3e505f85ec41fa255194fae979100e5ac1b..6a9325ed1cd89a7d114209f838142fb15c297534 100644 (file)
@@ -18,6 +18,7 @@ export function attachRefIssueContextPopup(refIssues) {
     if (!owner) return;
 
     const el = document.createElement('div');
+    el.classList.add('tw-p-3');
     refIssue.parentNode.insertBefore(el, refIssue.nextSibling);
 
     const view = createApp(ContextPopup);
@@ -30,6 +31,7 @@ export function attachRefIssueContextPopup(refIssues) {
     }
 
     createTippy(refIssue, {
+      theme: 'default',
       content: el,
       placement: 'top-start',
       interactive: true,
index 63da5f20392b826ffcbf46c815fba18b38b88ede..7c74c253a2971a45e0069a43dea3828a9756a741 100644 (file)
@@ -113,6 +113,7 @@ function showLineButton() {
   btn.closest('.code-view').append(menu.cloneNode(true));
 
   createTippy(btn, {
+    theme: 'menu',
     trigger: 'click',
     hideOnClick: true,
     content: menu,
index 2b2eed58bbfb3470f38794c6b0cda0298d2a3ae1..c4e14c62c451b62db79b01870f011d2bca880ae1 100644 (file)
@@ -502,6 +502,7 @@ export function initRepoPullRequestReview() {
   if ($reviewBtn.length && $panel.length) {
     const tippy = createTippy($reviewBtn[0], {
       content: $panel[0],
+      theme: 'default',
       placement: 'bottom',
       trigger: 'click',
       maxWidth: 'none',
index c58a446075fc6bdb92eb8bf7c892f06056b326a8..79d9892b74821fa67fb59c45b5c5d7031cdc552a 100644 (file)
@@ -1,4 +1,3 @@
-import prettyMilliseconds from 'pretty-ms';
 import {createTippy} from '../modules/tippy.js';
 import {GET} from '../modules/fetch.js';
 import {hideElem, showElem} from '../utils/dom.js';
@@ -11,28 +10,31 @@ export function initStopwatch() {
     return;
   }
 
-  const stopwatchEl = document.querySelector('.active-stopwatch-trigger');
+  const stopwatchEls = document.querySelectorAll('.active-stopwatch');
   const stopwatchPopup = document.querySelector('.active-stopwatch-popup');
 
-  if (!stopwatchEl || !stopwatchPopup) {
+  if (!stopwatchEls.length || !stopwatchPopup) {
     return;
   }
 
-  stopwatchEl.removeAttribute('href'); // intended for noscript mode only
-
-  createTippy(stopwatchEl, {
-    content: stopwatchPopup,
-    placement: 'bottom-end',
-    trigger: 'click',
-    maxWidth: 'none',
-    interactive: true,
-    hideOnClick: true,
-  });
-
   // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used.
-  const currSeconds = document.querySelector('.stopwatch-time')?.getAttribute('data-seconds');
-  if (currSeconds) {
-    updateStopwatchTime(currSeconds);
+  const seconds = stopwatchEls[0]?.getAttribute('data-seconds');
+  if (seconds) {
+    updateStopwatchTime(parseInt(seconds));
+  }
+
+  for (const stopwatchEl of stopwatchEls) {
+    stopwatchEl.removeAttribute('href'); // intended for noscript mode only
+
+    createTippy(stopwatchEl, {
+      content: stopwatchPopup.cloneNode(true),
+      placement: 'bottom-end',
+      trigger: 'click',
+      maxWidth: 'none',
+      interactive: true,
+      hideOnClick: true,
+      theme: 'default',
+    });
   }
 
   let usingPeriodicPoller = false;
@@ -125,10 +127,9 @@ async function updateStopwatch() {
 
 function updateStopwatchData(data) {
   const watch = data[0];
-  const btnEl = document.querySelector('.active-stopwatch-trigger');
+  const btnEls = document.querySelectorAll('.active-stopwatch');
   if (!watch) {
-    clearStopwatchTimer();
-    hideElem(btnEl);
+    hideElem(btnEls);
   } else {
     const {repo_owner_name, repo_name, issue_index, seconds} = watch;
     const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
@@ -138,31 +139,28 @@ function updateStopwatchData(data) {
     const stopwatchIssue = document.querySelector('.stopwatch-issue');
     if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`;
     updateStopwatchTime(seconds);
-    showElem(btnEl);
+    showElem(btnEls);
   }
   return Boolean(data.length);
 }
 
-let updateTimeIntervalId = null; // holds setInterval id when active
-function clearStopwatchTimer() {
-  if (updateTimeIntervalId !== null) {
-    clearInterval(updateTimeIntervalId);
-    updateTimeIntervalId = null;
-  }
-}
+// TODO: This flickers on page load, we could avoid this by making a custom
+// element to render time periods. Feeding a datetime in backend does not work
+// when time zone between server and client differs.
 function updateStopwatchTime(seconds) {
-  const secs = parseInt(seconds);
-  if (!Number.isFinite(secs)) return;
-
-  clearStopwatchTimer();
-  const stopwatch = document.querySelector('.stopwatch-time');
-  // TODO: replace with <relative-time> similar to how system status up time is shown
-  const start = Date.now();
-  const updateUi = () => {
-    const delta = Date.now() - start;
-    const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
-    if (stopwatch) stopwatch.textContent = dur;
-  };
-  updateUi();
-  updateTimeIntervalId = setInterval(updateUi, 1000);
+  if (!Number.isFinite(seconds)) return;
+  const datetime = (new Date(Date.now() - seconds * 1000)).toISOString();
+  for (const parent of document.querySelectorAll('.header-stopwatch-dot')) {
+    const existing = parent.querySelector(':scope > relative-time');
+    if (existing) {
+      existing.setAttribute('datetime', datetime);
+    } else {
+      const el = document.createElement('relative-time');
+      el.setAttribute('format', 'micro');
+      el.setAttribute('datetime', datetime);
+      el.setAttribute('lang', 'en-US');
+      el.setAttribute('title', ''); // make <relative-time> show no title and therefor no tooltip
+      parent.append(el);
+    }
+  }
 }
index 83b28e57454d60526a1ad14b5df96f7862f1cbd9..a18c94cafb7247193c1b90b1022419a4ec0bbda0 100644 (file)
@@ -37,8 +37,10 @@ export function createTippy(target, opts = {}) {
       return onShow?.(instance);
     },
     arrow: arrow || (theme === 'bare' ? false : arrowSvg),
-    role: role || 'menu', // HTML role attribute
-    theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare"
+    // HTML role attribute, ideally the default role would be "popover" but it does not exist
+    role: role || 'menu',
+    // CSS theme, either "default", "tooltip", "menu", "box-with-header" or "bare"
+    theme: theme || role || 'default',
     plugins: [followCursor],
     ...other,
   });
index 0778c5990feb7b0fdf2e4545b87218919c0357b8..80dd1a545b4e1a41dafd7e5094add79ef0f50459 100644 (file)
@@ -131,6 +131,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
       interactive: true,
       placement: 'bottom-end',
       role: 'menu',
+      theme: 'menu',
       content: this.tippyContent,
       onShow: () => { // FIXME: onShown doesn't work (never be called)
         setTimeout(() => {