From b92f6485554909d7d3ac17b78ae35fc3a7a79cf4 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 12 Feb 2026 20:37:34 +0100 Subject: [PATCH] feat: add dismiss functionality to instance notice banner --- options/locale/locale_en-US.json | 1 + routers/common/pagetmpl.go | 10 ++- templates/base/head_navbar.tmpl | 9 ++- web_src/js/features/instance-notice.test.ts | 84 +++++++++++++++++++++ web_src/js/features/instance-notice.ts | 28 +++++++ web_src/js/index-domready.ts | 2 + 6 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 web_src/js/features/instance-notice.test.ts create mode 100644 web_src/js/features/instance-notice.ts diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 987794373a..dd89976193 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3299,6 +3299,7 @@ "admin.config.instance_notice.delete": "Delete banner", "admin.config.instance_notice.delete_success": "Instance banner deleted.", "admin.config.instance_notice.edit_hint": "Edit this banner", + "admin.config.instance_notice.dismiss": "Dismiss", "admin.config.session_config": "Session Configuration", "admin.config.session_provider": "Session Provider", "admin.config.provider_config": "Provider Config", diff --git a/routers/common/pagetmpl.go b/routers/common/pagetmpl.go index 79267a76be..87c73a04f7 100644 --- a/routers/common/pagetmpl.go +++ b/routers/common/pagetmpl.go @@ -5,6 +5,8 @@ package common import ( goctx "context" + "crypto/sha256" + "encoding/hex" "errors" "sync" @@ -78,7 +80,8 @@ type pageGlobalDataType struct { } type InstanceNoticeBannerTmplInfo struct { - Message string + Message string + DismissKey string } func getInstanceNoticeBanner(ctx *context.Context) *InstanceNoticeBannerTmplInfo { @@ -86,8 +89,11 @@ func getInstanceNoticeBanner(ctx *context.Context) *InstanceNoticeBannerTmplInfo if !notice.IsActive(int64(timeutil.TimeStampNow())) { return nil } + h := sha256.Sum256([]byte(notice.Message)) + dismissKey := hex.EncodeToString(h[:])[:16] return &InstanceNoticeBannerTmplInfo{ - Message: notice.Message, + Message: notice.Message, + DismissKey: dismissKey, } } diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index ac31c957eb..0b5eb7bf93 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -178,9 +178,12 @@ {{if and .PageGlobalData .PageGlobalData.InstanceNoticeBanner}} {{$banner := .PageGlobalData.InstanceNoticeBanner}} -
-
-
{{ctx.RenderUtils.MarkdownToHtml $banner.Message}}
+
+
+
{{ctx.RenderUtils.MarkdownToHtml $banner.Message}}
+
{{if .PageGlobalData.IsSiteAdmin}}
diff --git a/web_src/js/features/instance-notice.test.ts b/web_src/js/features/instance-notice.test.ts new file mode 100644 index 0000000000..a83a4c4961 --- /dev/null +++ b/web_src/js/features/instance-notice.test.ts @@ -0,0 +1,84 @@ +import {beforeEach, describe, expect, test, vi} from 'vitest'; +import {initInstanceNotice} from './instance-notice.ts'; +import {localUserSettings} from '../modules/user-settings.ts'; + +vi.mock('../modules/user-settings.ts', () => ({ + localUserSettings: { + getString: vi.fn(), + setString: vi.fn(), + }, +})); + +function createBannerDOM(dismissKey: string) { + document.body.innerHTML = ` +
+
+
Maintenance in progress
+ +
+
+ `; +} + +describe('Instance Notice Banner Dismiss', () => { + beforeEach(() => { + vi.clearAllMocks(); + document.body.innerHTML = ''; + }); + + test('no banner in DOM does nothing', () => { + initInstanceNotice(); + expect(localUserSettings.getString).not.toHaveBeenCalled(); + }); + + test('hides banner when dismiss key matches stored value', () => { + createBannerDOM('abc123'); + vi.mocked(localUserSettings.getString).mockReturnValue('abc123'); + + initInstanceNotice(); + + const banner = document.querySelector('#instance-notice-banner')!; + expect(banner.style.display).toBe('none'); + expect(localUserSettings.setString).not.toHaveBeenCalled(); + }); + + test('does not hide banner when stored key differs', () => { + createBannerDOM('abc123'); + vi.mocked(localUserSettings.getString).mockReturnValue('old-key'); + + initInstanceNotice(); + + const banner = document.querySelector('#instance-notice-banner')!; + expect(banner.style.display).not.toBe('none'); + }); + + test('clicking dismiss button stores key and hides banner', () => { + createBannerDOM('abc123'); + vi.mocked(localUserSettings.getString).mockReturnValue(''); + + initInstanceNotice(); + + const banner = document.querySelector('#instance-notice-banner')!; + expect(banner.style.display).not.toBe('none'); + + const dismissBtn = banner.querySelector('.instance-notice-dismiss')!; + dismissBtn.click(); + + expect(localUserSettings.setString).toHaveBeenCalledWith('instance_notice_dismissed', 'abc123'); + expect(banner.style.display).toBe('none'); + }); + + test('banner without data-dismiss-key does nothing', () => { + document.body.innerHTML = ` +
+
Some message
+
+ `; + + initInstanceNotice(); + + const banner = document.querySelector('#instance-notice-banner')!; + expect(banner.style.display).not.toBe('none'); + expect(localUserSettings.getString).not.toHaveBeenCalled(); + }); +}); diff --git a/web_src/js/features/instance-notice.ts b/web_src/js/features/instance-notice.ts new file mode 100644 index 0000000000..0d380b3217 --- /dev/null +++ b/web_src/js/features/instance-notice.ts @@ -0,0 +1,28 @@ +import {localUserSettings} from '../modules/user-settings.ts'; + +const DISMISSED_KEY = 'instance_notice_dismissed'; + +function hideBanner(el: HTMLElement) { + el.style.display = 'none'; +} + +export function initInstanceNotice(): void { + const banner = document.querySelector('#instance-notice-banner'); + if (!banner) return; + + const dismissKey = banner.getAttribute('data-dismiss-key'); + if (!dismissKey) return; + + if (localUserSettings.getString(DISMISSED_KEY, '') === dismissKey) { + hideBanner(banner); + return; + } + + const dismissBtn = banner.querySelector('.instance-notice-dismiss'); + if (dismissBtn) { + dismissBtn.addEventListener('click', () => { + localUserSettings.setString(DISMISSED_KEY, dismissKey); + hideBanner(banner); + }); + } +} diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index 660e5c0989..80cc8bf4a3 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -64,6 +64,7 @@ import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts'; import {callInitFunctions} from './modules/init.ts'; import {initRepoViewFileTree} from './features/repo-view-file-tree.ts'; +import {initInstanceNotice} from './features/instance-notice.ts'; const initStartTime = performance.now(); const initPerformanceTracer = callInitFunctions([ @@ -75,6 +76,7 @@ const initPerformanceTracer = callInitFunctions([ initGlobalDropdown, initGlobalFetchAction, initGlobalTooltips, + initInstanceNotice, initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalCopyToClipboardListener,