Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/BootstrapBlazor.Server/Components/Components/GlobalToast.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@inherits WebSiteModuleComponentBase

@code {
RenderFragment RenderVote =>
@<div>
<div style="font-size: 48px; text-align: center;">🏆</div>
<p class="text-center mt-2">正在参加 <b>Gitee 2025 最受欢迎的开源软件</b> 投票活动,快来给我投票吧!</p>
<div class="my-3 text-center">您的每一票都是对开源社区的支持,感谢您的参与!</div>
<div style="display: flex; justify-content: space-around;" id="bb-g-toast">
<a href="https://gitee.com/activity/2025opensource?ident=I6MYBB" target="_blank" style="font-weight: bold; padding: 6px 12px; border-radius: var(--bs-border-radius); background: linear-gradient(135deg, #667eea, #764ba2); color: #fff;">🚀 必须投一票</a>
<a href="https://gitee.com/activity/2025opensource?ident=I6MYBB" target="_blank" class="text-muted" style="padding: 6px 12px;" title="老六你居然不投票">我知道了</a>
</div>
</div>;
}
Comment on lines +1 to +14
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GlobalToast component has been created but is not instantiated anywhere in the application. The vote toast functionality was removed from BaseLayout, but the new GlobalToast component needs to be added to a layout or component for the functionality to work. Consider adding <GlobalToast /> to BaseLayout.razor or another appropriate location.

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone

using Microsoft.JSInterop;
using System.Globalization;

namespace BootstrapBlazor.Server.Components.Components;

/// <summary>
/// 正站通知组件
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment contains a typo. It should be "全站通知组件" (full site notification component) instead of "正站通知组件".

Suggested change
/// 正站通知组件
/// 全站通知组件

Copilot uses AI. Check for mistakes.
/// </summary>
[JSModuleAutoLoader("Components/GlobalToast.razor.js", JSObjectReference = true)]
public partial class GlobalToast
{
[Inject]
[NotNull]
private ToastService? Toast { get; set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Interop, nameof(ShowToast));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): The JS init function defines a debug parameter that is never set, so the debug behavior cannot be controlled.

GlobalToast.razor.js exports init(invoke, method, debug) and uses if (debug !== true) to decide whether to schedule the toast. InvokeInitAsync only passes Interop and the method name, so debug is always undefined and the toast is always scheduled. Either pass an IsDevelopment/similar flag as the third argument, or remove the debug parameter and related branching in JS to avoid dead configuration.

Suggested implementation:

public partial class GlobalToast
{
    [Inject]
    [NotNull]
    private ToastService? Toast { get; set; }

    [Inject]
    private IWebHostEnvironment? HostEnvironment { get; set; }

    /// <summary>
    /// <inheritdoc/>
    protected override Task InvokeInitAsync() =>
        InvokeVoidAsync("init", Interop, nameof(ShowToast), HostEnvironment?.IsDevelopment() == true);
  1. Ensure the file has using Microsoft.AspNetCore.Hosting; or using Microsoft.Extensions.Hosting; at the top, depending on which hosting environment type your project already uses. If the project uses IHostEnvironment instead of IWebHostEnvironment, change the injected type accordingly and keep using .IsDevelopment().
  2. No changes are required in GlobalToast.razor.js if its exported init signature remains init(invoke, method, debug) and it already branches on debug using if (debug !== true) { ... }. With these changes, debug will be true in development and undefined/false otherwise.

Comment on lines +17 to +25
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GlobalToast component needs access to WebsiteOption to pass the IsDevelopment flag to the JavaScript init function (as done in the original BaseLayout implementation). Since WebSiteModuleComponentBase has WebsiteOption as a private field, GlobalToast should inject IOptions<WebsiteOptions> and pass WebsiteOption.Value.IsDevelopment as the third parameter to InvokeInitAsync.

Copilot uses AI. Check for mistakes.

/// <summary>
/// 显示通知窗口
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task ShowToast()
{
// 英文环境不通知
if (CultureInfo.CurrentUICulture.Name == "en-US")
{
return;
}

var option = new ToastOption()
{
Category = ToastCategory.Information,
Title = "Gitee 评选活动",
IsAutoHide = false,
ChildContent = RenderVote,
PreventDuplicates = true
};
await Toast.Show(option);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import EventHandler from "../../_content/BootstrapBlazor/modules/event-handler.js"

export function init(invoke, method, debug) {
const localstorageKey = 'bb-g-toast'
const v = localStorage.getItem(localstorageKey);
if (v) {
try {
const differ = new Date().getTime() - v;
if (differ < 86400000) {
return;
}
}
catch {
localStorage.removeItem(localstorageKey);
}
}

if (debug !== true) {
const handler = setTimeout(async () => {
clearTimeout(handler);
await invoke.invokeMethodAsync(method);
}, 10000);
}

EventHandler.on(document, 'click', '#bb-g-toast', e => {
const toast = e.delegateTarget.closest('.toast');
if (toast) {
toast.classList.remove('show');

localStorage.setItem(localstorageKey, new Date().getTime());
}
});
}

export function dispose() {
EventHandler.off(document, 'click', '#bb-g-toast');
}
13 changes: 0 additions & 13 deletions src/BootstrapBlazor.Server/Components/Layout/BaseLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,3 @@
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

@code {
RenderFragment RenderVote =>
@<div>
<div style="font-size: 48px; text-align: center;">🏆</div>
<p class="text-center mt-2">正在参加 <b>Gitee 2025 最受欢迎的开源软件</b> 投票活动,快来给我投票吧!</p>
<div class="my-3 text-center">您的每一票都是对开源社区的支持,感谢您的参与!</div>
<div style="display: flex; justify-content: space-around;" id="bb-gitee-vote">
<a href="https://gitee.com/activity/2025opensource?ident=I6MYBB" target="_blank" style="font-weight: bold; padding: 6px 12px; border-radius: var(--bs-border-radius); background: linear-gradient(135deg, #667eea, #764ba2); color: #fff;">🚀 必须投一票</a>
<a href="https://gitee.com/activity/2025opensource?ident=I6MYBB" target="_blank" class="text-muted" style="padding: 6px 12px;" title="老六你居然不投票">我知道了</a>
</div>
</div>;
}
57 changes: 9 additions & 48 deletions src/BootstrapBlazor.Server/Components/Layout/BaseLayout.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@

using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
using System.Globalization;

namespace BootstrapBlazor.Server.Components.Layout;

/// <summary>
/// 母版页基类
/// </summary>
public partial class BaseLayout : IAsyncDisposable
public partial class BaseLayout : IDisposable
{
[Inject]
[NotNull]
Expand Down Expand Up @@ -51,8 +50,6 @@ public partial class BaseLayout : IAsyncDisposable
private string? CancelText { get; set; }

private bool _init = false;
private JSModule? _module;
private DotNetObjectReference<BaseLayout>? _interop;

/// <summary>
/// <inheritdoc/>
Expand All @@ -74,18 +71,13 @@ protected override void OnInitialized()
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
protected override async Task OnInitializedAsync()
{
await base.OnAfterRenderAsync(firstRender);
await base.OnInitializedAsync();

if (firstRender)
{
_module = await JSRuntime.LoadModule($"{WebsiteOption.Value.JSModuleRootPath}Layout/BaseLayout.razor.js");
_interop = DotNetObjectReference.Create(this);
await _module.InvokeVoidAsync("doTask", _interop, WebsiteOption.Value.IsDevelopment);
_init = true;
StateHasChanged();
}
var module = await JSRuntime.LoadModule($"{WebsiteOption.Value.JSModuleRootPath}Layout/BaseLayout.razor.js");
await module.InvokeVoidAsync("initTheme");
Comment on lines +78 to +79
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JavaScript module loaded in OnInitializedAsync is not being disposed. The module reference should be stored in a field and disposed in the Dispose method to prevent memory leaks. Consider storing the module as a field and calling DisposeAsync on it during disposal.

Copilot uses AI. Check for mistakes.
_init = true;
Comment on lines +74 to +80
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving the module initialization from OnAfterRenderAsync to OnInitializedAsync may cause issues because the JavaScript may not be ready to execute at this lifecycle stage. The DOM is not guaranteed to be rendered during OnInitializedAsync, which could lead to JavaScript errors if the initTheme function interacts with DOM elements. Consider keeping this in OnAfterRenderAsync with a firstRender check.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The StateHasChanged call was removed when _init is set to true. Without StateHasChanged, the UI may not re-render to display the content inside the @if (_init) block in the razor file. This could cause the Header and main content not to appear until the next render cycle is triggered.

Suggested change
_init = true;
_init = true;
await InvokeAsync(StateHasChanged);

Copilot uses AI. Check for mistakes.
}

private async Task NotifyCommit(DispatchEntry<GiteePostBody> payload)
Expand Down Expand Up @@ -129,56 +121,25 @@ private async Task NotifyReboot(DispatchEntry<bool> payload)
}
}

/// <summary>
/// 显示投票弹窗
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task ShowVoteToast()
{
// 英文环境不投票
if (CultureInfo.CurrentUICulture.Name == "en-US")
{
return;
}

_option = new ToastOption()
{
Category = ToastCategory.Information,
Title = "邀请您支持 BB 参与 Gitee 项目评选活动",
IsAutoHide = false,
ChildContent = RenderVote,
PreventDuplicates = true,
ClassString = "bb-vote-toast"
};
await Toast.Show(_option);
}

/// <summary>
/// 释放资源
/// </summary>
/// <param name="disposing"></param>
private async ValueTask DisposeAsync(bool disposing)
private void Dispose(bool disposing)
{
if (disposing)
{
CommitDispatchService.UnSubscribe(NotifyCommit);
RebootDispatchService.UnSubscribe(NotifyReboot);

if (_module != null)
{
await _module.InvokeVoidAsync("dispose");
await _module.DisposeAsync();
}
}
}

/// <summary>
/// 释放资源
/// </summary>
public async ValueTask DisposeAsync()
public void Dispose()
Comment on lines +128 to +140
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change from IAsyncDisposable to IDisposable is problematic because the previous implementation needed to dispose of JSModule asynchronously. While the current code doesn't store the module reference, if async disposal is needed in the future (which is likely given that JSModule disposal is async), this change will require reverting back to IAsyncDisposable. Consider keeping IAsyncDisposable for consistency with JavaScript interop best practices.

Copilot uses AI. Check for mistakes.
{
await DisposeAsync(true);
Dispose(true);
GC.SuppressFinalize(this);
}
}
40 changes: 1 addition & 39 deletions src/BootstrapBlazor.Server/Components/Layout/BaseLayout.razor.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,6 @@
import { getTheme, setTheme } from "../../_content/BootstrapBlazor/modules/utility.js"
import EventHandler from "../../_content/BootstrapBlazor/modules/event-handler.js"

function initTheme() {
export function initTheme() {
const currentTheme = getTheme();
setTheme(currentTheme, false);
}

export function doTask(invoke, debug) {
initTheme();

const v = localStorage.getItem('bb-gitee-vote');
if (v) {
try {
const differ = new Date().getTime() - v;
if (differ < 86400000) {
return;
}
}
catch {
localStorage.removeItem('bb-gitee-vote');
}
}

if (debug !== true) {
const handler = setTimeout(async () => {
clearTimeout(handler);
await invoke.invokeMethodAsync("ShowVoteToast");
}, 10000);
}

EventHandler.on(document, 'click', '#bb-gitee-vote', e => {
const toast = e.delegateTarget.closest('.toast');
if (toast) {
toast.classList.remove('show');

localStorage.setItem('bb-gitee-vote', new Date().getTime());
}
});
}

export function dispose() {
EventHandler.off(document, 'click', '#bb-gitee-vote');
}
Loading