Skip to content

Commit c1fe6fb

Browse files
Make toast support preventDuplicates (#31501)
make preventDuplicates default to true, users get a clear UI feedback and know that "a new message appears". Fixes: #26651 --------- Co-authored-by: silverwind <[email protected]>
1 parent 9bc5552 commit c1fe6fb

File tree

8 files changed

+86
-39
lines changed

8 files changed

+86
-39
lines changed

Diff for: templates/devtest/gitea-ui.tmpl

-11
Original file line numberDiff line numberDiff line change
@@ -182,15 +182,6 @@
182182
</div>
183183
</div>
184184

185-
<div>
186-
<h1>Toast</h1>
187-
<div>
188-
<button class="ui button" id="info-toast">Show Info Toast</button>
189-
<button class="ui button" id="warning-toast">Show Warning Toast</button>
190-
<button class="ui button" id="error-toast">Show Error Toast</button>
191-
</div>
192-
</div>
193-
194185
<div>
195186
<h1>ComboMarkdownEditor</h1>
196187
<div>ps: no JS code attached, so just a layout</div>
@@ -201,7 +192,5 @@
201192
<div>
202193
<button class="{{if true}}tw-bg-red{{end}} tw-p-5 tw-border tw-rounded hover:tw-bg-blue active:tw-bg-yellow">Button</button>
203194
</div>
204-
205-
<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>
206195
</div>
207196
{{template "base/footer" .}}

Diff for: templates/devtest/toast.tmpl

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{{template "base/head" .}}
2+
3+
<div>
4+
<h1>Toast</h1>
5+
<div>
6+
<button class="ui button toast-test-button" data-toast-level="info" data-toast-message="test info">Show Info Toast</button>
7+
<button class="ui button toast-test-button" data-toast-level="warning" data-toast-message="test warning">Show Warning Toast</button>
8+
<button class="ui button toast-test-button" data-toast-level="error" data-toast-message="test error">Show Error Toast</button>
9+
<button class="ui button toast-test-button" data-toast-level="error" data-toast-message="very looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong message">Show Error Toast (long)</button>
10+
</div>
11+
</div>
12+
13+
<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>
14+
15+
{{template "base/footer" .}}

Diff for: web_src/css/modules/animations.css

+6-4
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,22 @@ code.language-math.is-loading::after {
9292
}
9393
}
9494

95-
@keyframes pulse {
95+
/* 1p5 means 1-point-5. it can't use "pulse" here, otherwise the animation is not right (maybe due to some conflicts */
96+
@keyframes pulse-1p5 {
9697
0% {
9798
transform: scale(1);
9899
}
99100
50% {
100-
transform: scale(1.8);
101+
transform: scale(1.5);
101102
}
102103
100% {
103104
transform: scale(1);
104105
}
105106
}
106107

107-
.pulse {
108-
animation: pulse 2s linear;
108+
/* pulse animation for scale(1.5) in 200ms */
109+
.pulse-1p5-200 {
110+
animation: pulse-1p5 200ms linear;
109111
}
110112

111113
.ui.modal,

Diff for: web_src/css/modules/toast.css

+20-6
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,31 @@
2222
overflow-wrap: anywhere;
2323
}
2424

25-
.toast-close,
26-
.toast-icon {
27-
color: currentcolor;
25+
.toast-close {
2826
border-radius: var(--border-radius);
29-
background: transparent;
30-
border: none;
31-
display: flex;
3227
width: 30px;
3328
height: 30px;
3429
justify-content: center;
30+
}
31+
32+
.toast-icon {
33+
display: inline-flex;
34+
width: 30px;
35+
height: 30px;
3536
align-items: center;
37+
justify-content: center;
38+
}
39+
40+
.toast-duplicate-number::before {
41+
content: "(";
42+
}
43+
.toast-duplicate-number {
44+
display: inline-block;
45+
margin-right: 5px;
46+
user-select: none;
47+
}
48+
.toast-duplicate-number::after {
49+
content: ")";
3650
}
3751

3852
.toast-close:hover {

Diff for: web_src/js/features/repo-diff.js

+2-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js';
77
import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js';
88
import {initImageDiff} from './imagediff.js';
99
import {showErrorToast} from '../modules/toast.js';
10-
import {submitEventSubmitter, queryElemSiblings, hideElem, showElem} from '../utils/dom.js';
10+
import {submitEventSubmitter, queryElemSiblings, hideElem, showElem, animateOnce} from '../utils/dom.js';
1111
import {POST, GET} from '../modules/fetch.js';
1212

1313
const {pageData, i18n} = window.config;
@@ -26,11 +26,7 @@ function initRepoDiffReviewButton() {
2626
const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1;
2727
counter.setAttribute('data-pending-comment-number', num);
2828
counter.textContent = num;
29-
30-
reviewBox.classList.remove('pulse');
31-
requestAnimationFrame(() => {
32-
reviewBox.classList.add('pulse');
33-
});
29+
animateOnce(reviewBox, 'pulse-1p5-200');
3430
});
3531
});
3632
}

Diff for: web_src/js/modules/toast.js

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {htmlEscape} from 'escape-goat';
22
import {svg} from '../svg.js';
3+
import {animateOnce, showElem} from '../utils/dom.js';
34
import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown
45

56
const levels = {
@@ -21,13 +22,28 @@ const levels = {
2122
};
2223

2324
// See https://github.com/apvarun/toastify-js#api for options
24-
function showToast(message, level, {gravity, position, duration, useHtmlBody, ...other} = {}) {
25+
function showToast(message, level, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other} = {}) {
26+
const body = useHtmlBody ? String(message) : htmlEscape(message);
27+
const key = `${level}-${body}`;
28+
29+
// prevent showing duplicate toasts with same level and message, and give a visual feedback for end users
30+
if (preventDuplicates) {
31+
const toastEl = document.querySelector(`.toastify[data-toast-unique-key="${CSS.escape(key)}"]`);
32+
if (toastEl) {
33+
const toastDupNumEl = toastEl.querySelector('.toast-duplicate-number');
34+
showElem(toastDupNumEl);
35+
toastDupNumEl.textContent = String(Number(toastDupNumEl.textContent) + 1);
36+
animateOnce(toastDupNumEl, 'pulse-1p5-200');
37+
return;
38+
}
39+
}
40+
2541
const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
2642
const toast = Toastify({
2743
text: `
2844
<div class='toast-icon'>${svg(icon)}</div>
29-
<div class='toast-body'>${useHtmlBody ? message : htmlEscape(message)}</div>
30-
<button class='toast-close'>${svg('octicon-x')}</button>
45+
<div class='toast-body'><span class="toast-duplicate-number tw-hidden">1</span>${body}</div>
46+
<button class='btn toast-close'>${svg('octicon-x')}</button>
3147
`,
3248
escapeMarkup: false,
3349
gravity: gravity ?? 'top',
@@ -39,6 +55,7 @@ function showToast(message, level, {gravity, position, duration, useHtmlBody, ..
3955

4056
toast.showToast();
4157
toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast());
58+
toast.toastElement.setAttribute('data-toast-unique-key', key);
4259
return toast;
4360
}
4461

Diff for: web_src/js/standalone/devtest.js

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.js';
22

3-
document.querySelector('#info-toast').addEventListener('click', () => {
4-
showInfoToast('success 😀');
5-
});
6-
document.querySelector('#warning-toast').addEventListener('click', () => {
7-
showWarningToast('warning 😐');
8-
});
9-
document.querySelector('#error-toast').addEventListener('click', () => {
10-
showErrorToast('error 🙁');
11-
});
3+
function initDevtestToast() {
4+
const levelMap = {info: showInfoToast, warning: showWarningToast, error: showErrorToast};
5+
for (const el of document.querySelectorAll('.toast-test-button')) {
6+
el.addEventListener('click', () => {
7+
const level = el.getAttribute('data-toast-level');
8+
const message = el.getAttribute('data-toast-message');
9+
levelMap[level](message);
10+
});
11+
}
12+
}
13+
14+
initDevtestToast();

Diff for: web_src/js/utils/dom.js

+11
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,14 @@ export function createElementFromAttrs(tagName, attrs) {
306306
}
307307
return el;
308308
}
309+
310+
export function animateOnce(el, animationClassName) {
311+
return new Promise((resolve) => {
312+
el.addEventListener('animationend', function onAnimationEnd() {
313+
el.classList.remove(animationClassName);
314+
el.removeEventListener('animationend', onAnimationEnd);
315+
resolve();
316+
}, {once: true});
317+
el.classList.add(animationClassName);
318+
});
319+
}

0 commit comments

Comments
 (0)