Skip to content

Make toast support preventDuplicates #31501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jun 27, 2024
11 changes: 0 additions & 11 deletions templates/devtest/gitea-ui.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,6 @@
</div>
</div>

<div>
<h1>Toast</h1>
<div>
<button class="ui button" id="info-toast">Show Info Toast</button>
<button class="ui button" id="warning-toast">Show Warning Toast</button>
<button class="ui button" id="error-toast">Show Error Toast</button>
</div>
</div>

<div>
<h1>ComboMarkdownEditor</h1>
<div>ps: no JS code attached, so just a layout</div>
Expand All @@ -201,7 +192,5 @@
<div>
<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>
</div>

<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>
</div>
{{template "base/footer" .}}
15 changes: 15 additions & 0 deletions templates/devtest/toast.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{{template "base/head" .}}

<div>
<h1>Toast</h1>
<div>
<button class="ui button toast-test-button" data-toast-level="info" data-toast-message="test info">Show Info Toast</button>
<button class="ui button toast-test-button" data-toast-level="warning" data-toast-message="test warning">Show Warning Toast</button>
<button class="ui button toast-test-button" data-toast-level="error" data-toast-message="test error">Show Error Toast</button>
<button class="ui button toast-test-button" data-toast-level="error" data-toast-message="very looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong message">Show Error Toast (long)</button>
</div>
</div>

<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>

{{template "base/footer" .}}
10 changes: 6 additions & 4 deletions web_src/css/modules/animations.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,22 @@ code.language-math.is-loading::after {
}
}

@keyframes pulse {
/* 1p5 means 1-point-5. it can't use "pulse" here, otherwise the animation is not right (maybe due to some conflicts */
@keyframes pulse-1p5 {
0% {
transform: scale(1);
}
50% {
transform: scale(1.8);
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}

.pulse {
animation: pulse 2s linear;
/* pulse animation for scale(1.5) in 200ms */
.pulse-1p5-200 {
animation: pulse-1p5 200ms linear;
}

.ui.modal,
Expand Down
26 changes: 20 additions & 6 deletions web_src/css/modules/toast.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,31 @@
overflow-wrap: anywhere;
}

.toast-close,
.toast-icon {
color: currentcolor;
.toast-close {
border-radius: var(--border-radius);
background: transparent;
border: none;
display: flex;
width: 30px;
height: 30px;
justify-content: center;
}

.toast-icon {
display: inline-flex;
width: 30px;
height: 30px;
align-items: center;
justify-content: center;
}

.toast-duplicate-number::before {
content: "(";
}
.toast-duplicate-number {
display: inline-block;
margin-right: 5px;
user-select: none;
}
.toast-duplicate-number::after {
content: ")";
}

.toast-close:hover {
Expand Down
8 changes: 2 additions & 6 deletions web_src/js/features/repo-diff.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js';
import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js';
import {initImageDiff} from './imagediff.js';
import {showErrorToast} from '../modules/toast.js';
import {submitEventSubmitter, queryElemSiblings, hideElem, showElem} from '../utils/dom.js';
import {submitEventSubmitter, queryElemSiblings, hideElem, showElem, animateOnce} from '../utils/dom.js';
import {POST, GET} from '../modules/fetch.js';

const {pageData, i18n} = window.config;
Expand All @@ -26,11 +26,7 @@ function initRepoDiffReviewButton() {
const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1;
counter.setAttribute('data-pending-comment-number', num);
counter.textContent = num;

reviewBox.classList.remove('pulse');
requestAnimationFrame(() => {
reviewBox.classList.add('pulse');
});
animateOnce(reviewBox, 'pulse-1p5-200');
});
});
}
Expand Down
23 changes: 20 additions & 3 deletions web_src/js/modules/toast.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {htmlEscape} from 'escape-goat';
import {svg} from '../svg.js';
import {animateOnce, showElem} from '../utils/dom.js';
import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown

const levels = {
Expand All @@ -21,13 +22,28 @@ const levels = {
};

// See https://github.com/apvarun/toastify-js#api for options
function showToast(message, level, {gravity, position, duration, useHtmlBody, ...other} = {}) {
function showToast(message, level, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other} = {}) {
const body = useHtmlBody ? String(message) : htmlEscape(message);
const key = `${level}-${body}`;

// prevent showing duplicate toasts with same level and message, and give a visual feedback for end users
if (preventDuplicates) {
const toastEl = document.querySelector(`.toastify[data-toast-unique-key="${CSS.escape(key)}"]`);
if (toastEl) {
const toastDupNumEl = toastEl.querySelector('.toast-duplicate-number');
showElem(toastDupNumEl);
toastDupNumEl.textContent = String(Number(toastDupNumEl.textContent) + 1);
animateOnce(toastDupNumEl, 'pulse-1p5-200');
return;
}
}

const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
const toast = Toastify({
text: `
<div class='toast-icon'>${svg(icon)}</div>
<div class='toast-body'>${useHtmlBody ? message : htmlEscape(message)}</div>
<button class='toast-close'>${svg('octicon-x')}</button>
<div class='toast-body'><span class="toast-duplicate-number tw-hidden">1</span>${body}</div>
<button class='btn toast-close'>${svg('octicon-x')}</button>
`,
escapeMarkup: false,
gravity: gravity ?? 'top',
Expand All @@ -39,6 +55,7 @@ function showToast(message, level, {gravity, position, duration, useHtmlBody, ..

toast.showToast();
toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast());
toast.toastElement.setAttribute('data-toast-unique-key', key);
return toast;
}

Expand Down
21 changes: 12 additions & 9 deletions web_src/js/standalone/devtest.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.js';

document.querySelector('#info-toast').addEventListener('click', () => {
showInfoToast('success 😀');
});
document.querySelector('#warning-toast').addEventListener('click', () => {
showWarningToast('warning 😐');
});
document.querySelector('#error-toast').addEventListener('click', () => {
showErrorToast('error 🙁');
});
function initDevtestToast() {
const levelMap = {info: showInfoToast, warning: showWarningToast, error: showErrorToast};
for (const el of document.querySelectorAll('.toast-test-button')) {
el.addEventListener('click', () => {
const level = el.getAttribute('data-toast-level');
const message = el.getAttribute('data-toast-message');
levelMap[level](message);
});
}
}

initDevtestToast();
11 changes: 11 additions & 0 deletions web_src/js/utils/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,14 @@ export function createElementFromAttrs(tagName, attrs) {
}
return el;
}

export function animateOnce(el, animationClassName) {
return new Promise((resolve) => {
el.addEventListener('animationend', function onAnimationEnd() {
el.classList.remove(animationClassName);
el.removeEventListener('animationend', onAnimationEnd);
resolve();
}, {once: true});
el.classList.add(animationClassName);
});
}