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
17 changes: 16 additions & 1 deletion clients/react/src/i18n/strings.de.json
Original file line number Diff line number Diff line change
Expand Up @@ -981,5 +981,20 @@
"versions.viewRedline": "Nachverfolgte Änderungen anzeigen",
"settings.published": "Im Web veröffentlicht",
"ui.mdPublishedIntro": "Jede Seite, die ein **abgemeldeter Besucher** öffnen kann — dieselbe Liste, die `/sitemap.xml` an Suchmaschinen liefert, aus derselben Quelle gelesen, damit beide nicht auseinanderlaufen.\n\nEine Seite ist öffentlich, weil sie eine **Leseberechtigung für Anonymous** trägt. Es gibt keinen separaten Schalter und keinen separaten Ort: Die öffentliche URL ist ihr gewöhnlicher Pfad. Änderbar im Reiter **Zugriff** des Knotens.",
"ui.mdPublishedNone": "**Nichts ist veröffentlicht.** Kein Knoten trägt eine Leseberechtigung für Anonymous, daher sieht ein abgemeldeter Besucher nichts und `/sitemap.xml` ist leer."
"ui.mdPublishedNone": "**Nichts ist veröffentlicht.** Kein Knoten trägt eine Leseberechtigung für Anonymous, daher sieht ein abgemeldeter Besucher nichts und `/sitemap.xml` ist leer.",
"menu.hideInPresentation": "Im Präsentationsmodus ausblenden",
"menu.hideInPresentationTooltip": "Bleibt bei eingeschaltetem Präsentationsmodus aus deinen Kacheln und Vorschlägen heraus",
"menu.showInPresentation": "Im Präsentationsmodus anzeigen",
"menu.showInPresentationTooltip": "Beim Präsentieren nicht mehr aus deinen Kacheln und Vorschlägen ausblenden",
"presentation.mode": "Präsentationsmodus",
"presentation.modeTooltip": "Blendet die von dir markierten Einträge aus deinen Kacheln, Karten und Vorschlägen aus — nur für dich, und nur bis du ihn ausschaltest",
"presentation.on": "Ein",
"presentation.off": "Aus",
"presentation.indicator": "Präsentationsmodus ist aktiv — markierte Einträge sind aus deinen Kacheln und Vorschlägen ausgeblendet",
"presentation.markedTitle": "Im Präsentationsmodus ausgeblendet",
"presentation.markedBody": "Das bleibt bei eingeschaltetem Präsentationsmodus aus deinen Kacheln, Karten und Vorschlägen heraus. Für andere ändert sich nichts, und über den Link erreichst du es weiterhin.",
"presentation.unmarkedTitle": "Wieder sichtbar",
"presentation.unmarkedBody": "Das wird im Präsentationsmodus nicht mehr ausgeblendet.",
"presentation.signInRequiredTitle": "Anmeldung erforderlich",
"presentation.signInRequiredBody": "Der Präsentationsmodus ist eine persönliche Einstellung — dafür musst du angemeldet sein."
}
17 changes: 16 additions & 1 deletion clients/react/src/i18n/strings.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -981,5 +981,20 @@
"versions.viewRedline": "Show the tracked changes",
"settings.published": "Published to the web",
"ui.mdPublishedIntro": "Every page a **logged-out visitor** can open — the same list `/sitemap.xml` gives crawlers, read from the same enumeration so the two cannot drift.\n\nA page is public because it carries an **Anonymous Read grant**. There is no separate switch and no separate location: its public URL is its ordinary path. Change it on the node's **Access** tab.",
"ui.mdPublishedNone": "**Nothing is published.** No node carries an Anonymous Read grant, so a logged-out visitor sees nothing and `/sitemap.xml` is empty."
"ui.mdPublishedNone": "**Nothing is published.** No node carries an Anonymous Read grant, so a logged-out visitor sees nothing and `/sitemap.xml` is empty.",
"menu.hideInPresentation": "Hide in presentation mode",
"menu.hideInPresentationTooltip": "Keep this off your own tiles and suggestions while presentation mode is on",
"menu.showInPresentation": "Show in presentation mode",
"menu.showInPresentationTooltip": "Stop hiding this from your own tiles and suggestions while presenting",
"presentation.mode": "Presentation mode",
"presentation.modeTooltip": "Hide the items you marked from your own tiles, cards and suggestions — for you only, and only until you turn it off",
"presentation.on": "On",
"presentation.off": "Off",
"presentation.indicator": "Presentation mode is on — marked items are hidden from your tiles and suggestions",
"presentation.markedTitle": "Hidden in presentation mode",
"presentation.markedBody": "This will stay off your own tiles, cards and suggestions while presentation mode is on. Nobody else is affected, and it is still open to you by its link.",
"presentation.unmarkedTitle": "Shown again",
"presentation.unmarkedBody": "This is no longer hidden while presentation mode is on.",
"presentation.signInRequiredTitle": "Sign-in required",
"presentation.signInRequiredBody": "Presentation mode is a personal setting, so you have to be signed in to use it."
}
21 changes: 21 additions & 0 deletions src/MeshWeaver.Blazor.Portal/Components/UserProfile.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
<AuthorizeView>
<Authorized>
<div class="profile-menu-container">
@* The "clearly indicated while active" half of the quick toggle: a glyph in the header
itself, not only inside the menu, so a presenter can see at a glance that the screen
is up. Language-neutral icon + a translated tooltip. *@
@if (PresentationModeOn)
{
<span class="presentation-indicator" title="@Access.Localize("presentation.indicator")"
aria-label="@Access.Localize("presentation.indicator")">
<FluentIcon Value="@(new Icons.Filled.Size20.Glasses())" Color="Color.Accent" />
</span>
}
<FluentProfileMenu Initials="@initials"
EMail="@username"
FullName="@name"
Expand All @@ -24,6 +34,17 @@
<div class="user-id">@username</div>
</FluentPersona>
</FluentStack>
@* Presentation mode (#1803): the quick toggle, right where a presenter looks
for it. Display-only and personal — it changes nothing for anyone else. *@
<FluentStack VerticalAlignment="VerticalAlignment.Center" Style="margin-top: 8px;">
<FluentButton Appearance="Appearance.Stealth"
OnClick="TogglePresentationMode"
Title="@Access.Localize("presentation.modeTooltip")"
IconStart="@(new Icons.Regular.Size20.Glasses())">
@Access.Localize("presentation.mode") ·
@(PresentationModeOn ? Access.Localize("presentation.on") : Access.Localize("presentation.off"))
</FluentButton>
</FluentStack>
@if (showFrontendToggle)
{
<FluentStack VerticalAlignment="VerticalAlignment.Center" Style="margin-top: 8px;">
Expand Down
65 changes: 64 additions & 1 deletion src/MeshWeaver.Blazor.Portal/Components/UserProfile.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using MeshWeaver.Blazor.Portal.Authentication;
using MeshWeaver.Graph.Configuration;
using MeshWeaver.Mesh;
using MeshWeaver.Mesh.Security;
using MeshWeaver.Messaging;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
Expand All @@ -21,7 +22,7 @@ namespace MeshWeaver.Blazor.Portal.Components;
/// profile, settings, login, and logout actions. Resolves the display name and the
/// platform-admin flag from the authentication state and access context.
/// </summary>
public partial class UserProfile : ComponentBase
public partial class UserProfile : ComponentBase, IDisposable
{
/// <summary>Navigation manager used for login, logout, and profile/settings routing.</summary>
[Inject]
Expand All @@ -47,6 +48,13 @@ public partial class UserProfile : ComponentBase
[Inject]
public required PortalApplication PortalApp { get; init; }

/// <summary>
/// The MESH hub — deliberately not <see cref="PortalApp"/>'s per-circuit portal hub — used to
/// read and write the viewer's own profile for the presentation-mode toggle (#1803).
/// </summary>
[Inject]
public required IMessageHub MeshHub { get; init; }

/// <summary>Cascaded authentication state used to read the signed-in user's claims.</summary>
[CascadingParameter]
public required Task<AuthenticationState> AuthenticationState { get; set; }
Expand All @@ -63,6 +71,13 @@ public partial class UserProfile : ComponentBase
private string? username;
private string? initials;
private bool isPlatformAdmin;

// ----- Presentation mode (#1803) -----
// The quick toggle and its header indicator. The screen is read LIVE off the viewer's own
// profile so the header lights up the instant the mode is flipped — including from another tab
// — and so the state shown here can never disagree with what the tile surfaces are doing.
private PresentationScreen presentationScreen = PresentationScreen.Off;
private IDisposable? presentationSubscription;
private string NameClaimType { get; } = "name";
/// <summary>The claim type read for the user's preferred username (<c>preferred_username</c>).</summary>
public string UsernameClaimType { get; } = "preferred_username";
Expand Down Expand Up @@ -96,9 +111,55 @@ protected override async Task OnParametersSetAsync()
.Where(x => x).Take(1)
.Timeout(TimeSpan.FromSeconds(5), Observable.Return(false))
.FirstAsync().ToTask();

SubscribePresentationScreen();
}

}

/// <summary>
/// Subscribes the viewer's live presentation screen so the menu entry and the header indicator
/// track the real state. Idempotent — <c>OnParametersSetAsync</c> can run more than once.
/// </summary>
private void SubscribePresentationScreen()
{
if (presentationSubscription is not null)
return;
presentationSubscription = AccessService.ViewerScreen(MeshHub)
.Subscribe(
screen => InvokeAsync(() =>
{
if (presentationScreen == screen)
return;
presentationScreen = screen;
StateHasChanged();
}),
ex => Logger.LogWarning(ex, "Presentation screen stream ended for the user menu"));
}

/// <summary>Whether presentation mode is currently on for this viewer.</summary>
private bool PresentationModeOn => presentationScreen.Active;

/// <summary>
/// Flips the viewer's presentation mode. Writes their OWN profile through the one mutation API,
/// so the owning user hub serialises it and every surface bound to that node — this menu, the
/// header indicator, every open tile surface — re-renders from the same write.
/// </summary>
private void TogglePresentationMode()
{
var viewerId = AccessService.ViewerId();
if (!PresentationScreenExtensions.IsPersonalViewer(viewerId))
return;
var on = !presentationScreen.Active;
var options = MeshHub.JsonSerializerOptions;
MeshHub.GetMeshNodeStream(viewerId!)
.Update(node => node with
{
Content = PresentationPreference.SetMode(node.ContentAs<User>(options), on)
})
.Subscribe(_ => { }, ex => Logger.LogWarning(ex,
"Failed to set presentation mode for {Viewer}", viewerId));
}
/// <summary>
/// Derives the avatar initials from a name: the first letter for a single word, or the
/// first letters of the first and last words otherwise.
Expand Down Expand Up @@ -181,4 +242,6 @@ private void Logout()
Navigation.NavigateTo(logoutUrl, forceLoad: true);
}

/// <summary>Releases the presentation-screen subscription when the header component goes away.</summary>
public void Dispose() => presentationSubscription?.Dispose();
}
15 changes: 15 additions & 0 deletions src/MeshWeaver.Blazor.Portal/Components/UserProfile.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,18 @@
::deep .sign-out-form {
display: flex;
}

/* Presentation mode (#1803): the header indicator sits left of the avatar, on the same optical
line, and is present ONLY while the mode is on — it is the "clearly indicated" half of the
quick toggle. The container is laid out as a row for it, which is a no-op when the indicator
is absent (a single child). */
.profile-menu-container {
display: flex;
align-items: center;
}

.presentation-indicator {
display: inline-flex;
align-items: center;
margin-right: 6px;
}
Loading
Loading