[%# update last-visited %]
[% IF user.id && user.is_involved_in_bug(bug) %]
- $(function() {
- bugzilla_ajax({
- url: 'rest/bug_user_last_visit/[% bug.id FILTER none %]',
- type: 'POST'
- });
- });
+ document.addEventListener('DOMContentLoaded', () => show_new_changes_indicator(), { once: true });
[% END %]
[%# expose useful data to js %]
is_insider: [% user.is_insider ? "true" : "false" %],
is_timetracker: [% user.is_timetracker ? "true" : "false" %],
can_tag: [% user.can_tag_comments ? "true" : "false" %],
+ timezone: '[% user.timezone.name FILTER js %]',
settings: {
quote_replies: '[% user.settings.quote_replies.value FILTER js %]',
zoom_textareas: [% user.settings.zoom_textareas.value == "on" ? "true" : "false" %],
'The full URL is:\n\n' + url + '\n\nContinue?');
}
+function show_new_changes_indicator() {
+ const url = `rest/bug_user_last_visit/${BUGZILLA.bug_id}`;
+
+ // Get the last visited timestamp
+ bugzilla_ajax({ url }, data => {
+ // Save the current timestamp
+ bugzilla_ajax({ url, type: 'POST' });
+
+ if (!data[0] || !data[0].last_visit_ts) {
+ return;
+ }
+
+ const last_visit_ts = new Date(data[0].last_visit_ts);
+ const new_changes = [...document.querySelectorAll('main .change-set')].filter($change => {
+ // Exclude hidden CC changes
+ return $change.clientHeight > 0 &&
+ new Date($change.querySelector('[data-time]').getAttribute('data-time') * 1000) > last_visit_ts;
+ });
+
+ if (new_changes.length === 0) {
+ return;
+ }
+
+ const now = new Date();
+ const date_locale = document.querySelector('html').lang;
+ const date_options = {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ hour12: false,
+ timeZone: BUGZILLA.user.timezone,
+ timeZoneName: 'short',
+ };
+
+ if (last_visit_ts.getFullYear() === now.getFullYear()) {
+ delete date_options.year;
+
+ if (last_visit_ts.getMonth() === now.getMonth() && last_visit_ts.getDate() === now.getDate()) {
+ delete date_options.month;
+ delete date_options.day;
+ }
+ }
+
+ const $link = document.createElement('div');
+ const $separator = document.createElement('div');
+ const comments_count = new_changes.filter($change => !!$change.querySelector('.comment')).length;
+ const changes_count = new_changes.length - comments_count;
+ const date_attr = last_visit_ts.toISOString();
+ const date_label = last_visit_ts.toLocaleString(date_locale, date_options);
+
+ // Insert a link
+ $link.className = 'new-changes-link';
+ $link.innerHTML =
+ (c => c === 0 ? '' : (c === 1 ? `${c} new comment` : `${c} new comments`))(comments_count) +
+ (comments_count > 0 && changes_count > 0 ? ', ' : '') +
+ (c => c === 0 ? '' : (c === 1 ? `${c} new change` : `${c} new changes`))(changes_count) +
+ ` since <time datetime="${date_attr}">${date_label}</time>`;
+ $link.addEventListener('click', () => {
+ $link.remove();
+ scroll_element_into_view($separator);
+ }, { once: true });
+ document.querySelector('#changeform').insertAdjacentElement('beforebegin', $link);
+
+ // Insert a separator
+ $separator.className = 'new-changes-separator';
+ $separator.innerHTML = '<span>New</span>';
+ new_changes[0].insertAdjacentElement('beforebegin', $separator);
+
+ // Remove the link once the separator goes into the viewport
+ if ('IntersectionObserver' in window) {
+ const observer = new IntersectionObserver(entries => entries.forEach(entry => {
+ if (entry.intersectionRatio > 0) {
+ observer.unobserve($separator);
+ $link.remove();
+ }
+ }), { root: $separator.offsetParent });
+
+ observer.observe($separator);
+ }
+
+ // TODO: Enable auto-scroll once the modal page layout is optimized
+ // scroll_element_into_view($separator);
+ });
+}
+
// fix url after bug creation/update
if (history && history.replaceState) {
let bug_id = BUGZILLA.bug_id;