Repository navigation
feat(inbox): one data-bound grid — select many, Delete and Undo, mesh search, ordering, a switch per source #6475
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
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
81380cb
feat(inbox): select many, then Delete and Undo — one inbox for notifi…
rbuergi 1818bfa
feat(inbox): one grid, all data-bound - mesh search, ordering and a s…
rbuergi c86e428
fix(inbox): answer the review - no exception text to the viewer, loca…
rbuergi f8ca5e4
test(inbox): wait for written content instead of sampling it
rbuergi cc83ec0
Merge remote-tracking branch 'origin/main' into feat/unified-inbox
rbuergi fe88e1f
fix(inbox): answer the second review - nothing the client can write d…
rbuergi 2400bad
Merge remote-tracking branch 'origin/main' into feat/unified-inbox
rbuergi b1fec46
fix(inbox): answer the third review - only a real transition is done,…
rbuergi 0a3c873
Merge remote-tracking branch 'origin/main' into feat/unified-inbox
rbuergi 2ac9ab6
Merge remote-tracking branch 'origin/main' into feat/unified-inbox
rbuergi ec1d232
fix(inbox): answer review round 4 — write built inside the call, sear…
rbuergi 42614e8
fix(inbox): answer review round 5 — parser-read leg bound, per-leg ca…
rbuergi 32d73b0
fix(inbox): record the Undo batch per item as each write lands, so a …
rbuergi ee3a180
fix(inbox): every Undo batch change is one compare-and-swap of the wh…
rbuergi 8dda8dc
fix(inbox): page act state through one serialized reactive channel, n…
rbuergi 7931eef
fix(inbox): close the act channel by completing it, so a late change …
rbuergi 1d94431
Merge remote-tracking branch 'origin/main' into feat/unified-inbox
rbuergi ed11cd1
fix(inbox): answer review round 10 — runtime anchor guard, forced row…
rbuergi 6bce5ff
fix(inbox): the anchor guard checks what the parser runs — one struct…
rbuergi 634f07d
fix(inbox): answer review round 12 — posted writes outlive a cancelle…
rbuergi 05ecf33
fix(inbox): every leg is a live read bound to the page, never the pro…
rbuergi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
201 changes: 170 additions & 31 deletions
201
src/MeshWeaver.Documentation/Data/Architecture/CommunicationHub.md
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| using System.Reactive.Linq; | ||
| using MeshWeaver.Graph.Configuration; | ||
| using MeshWeaver.Mesh; | ||
| using MeshWeaver.Messaging; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace MeshWeaver.Graph; | ||
|
|
||
| /// <summary> | ||
| /// The inbox handlers core itself brings — for the two item types core owns. Every other type's | ||
| /// handler arrives with the package that owns the type (the Threads app brings the thread's). | ||
| /// </summary> | ||
| public static class InboxItemHandlers | ||
| { | ||
| /// <summary> | ||
| /// The handler for <paramref name="nodeType"/> among <paramref name="handlers"/>, or null when | ||
| /// nobody brought one. Ordinal: a node type is an identifier, never display text. | ||
| /// </summary> | ||
| /// <param name="handlers">The registered handlers.</param> | ||
| /// <param name="nodeType">The row's node type.</param> | ||
| public static IInboxItemHandler? For(IEnumerable<IInboxItemHandler> handlers, string? nodeType) | ||
| => nodeType is null | ||
| ? null | ||
| : handlers.FirstOrDefault(h => string.Equals(h.NodeType, nodeType, StringComparison.Ordinal)); | ||
|
|
||
| /// <summary> | ||
| /// One typed write as an outcome: the node's content read as <typeparamref name="TContent"/>, | ||
| /// changed, written through the owning hub. A fault becomes the item's own answer — a bulk act | ||
| /// names what happened to each item rather than aborting on the first refusal. | ||
| /// | ||
| /// <para>🚨 The fault is LOGGED here and the outcome carries NO reason: a typed-write failure can | ||
| /// quote the content it could not read, and an outcome's reason is shown to the viewer verbatim. | ||
| /// The page says "could not be changed"; the log says why.</para> | ||
| /// </summary> | ||
| /// <param name="hub">The hub the inbox renders on.</param> | ||
| /// <param name="path">The item's node path.</param> | ||
| /// <param name="change">The change; returns its argument unchanged when the item may not be changed.</param> | ||
| /// <param name="reached">Whether the content AS WRITTEN is in the state the action asked for.</param> | ||
| /// <remarks> | ||
| /// 🚨 <c>Done</c> needs BOTH: this call changed the content, and the content is now in the | ||
| /// asked-for state. "The update completed" proves neither — a no-op completes like a real write. | ||
| /// And the final state alone is not enough: an item someone ELSE already put away is in the | ||
| /// asked-for state too, and counting it would put it in this click's Undo batch, where Undo | ||
| /// would reverse the other writer's act. The update lambda can run more than once (a retried | ||
| /// write), so the flag records its LAST run — the one whose result was written. | ||
| /// <para>🚨 The write is BUILT here, inside the call, never in a <c>Defer</c>: <c>Update</c> | ||
| /// captures the caller's identity when it is constructed, and the inbox holds the clicker's | ||
| /// access scope open only around this call. A deferred build runs at subscription — for every | ||
| /// item after the first, on the previous write's completion thread, as nobody.</para> | ||
| /// </remarks> | ||
| internal static IObservable<InboxItemOutcome> Write<TContent>( | ||
| IMessageHub hub, string path, Func<TContent, TContent> change, Func<TContent, bool> reached) where TContent : class | ||
| { | ||
| // One flag per call — each call is one write, subscribed once by the inbox. | ||
| var changed = false; | ||
| return hub.GetMeshNodeStream(path).Update<TContent>(current => | ||
| { | ||
| var next = change(current); | ||
| changed = !Equals(next, current); | ||
| return next; | ||
| }) | ||
| .Take(1) | ||
| .Select(node => new InboxItemOutcome(path, | ||
| changed && node.ContentAs<TContent>(hub.JsonSerializerOptions) is { } content && reached(content))) | ||
| .Catch((Exception ex) => | ||
| { | ||
| hub.ServiceProvider.GetService<ILoggerFactory>()?.CreateLogger(typeof(InboxItemHandlers)) | ||
| .LogWarning(ex, "Inbox: {Path} could not be written", path); | ||
| return Observable.Return(new InboxItemOutcome(path, false)); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Notifications: Delete sets <see cref="Notification.Dismissed"/>, Undo clears it. The node stays — | ||
| /// retention removes it, exactly as it would have (Doc/Architecture/NotificationRetention). | ||
| /// </summary> | ||
| public sealed class NotificationInboxItemHandler : IInboxItemHandler | ||
| { | ||
| /// <inheritdoc /> | ||
| public string NodeType => NotificationNodeType.NodeType; | ||
|
|
||
| /// <inheritdoc /> | ||
| public IObservable<InboxItemOutcome> Dismiss(IMessageHub hub, string path) | ||
| => InboxItemHandlers.Write<Notification>(hub, path, n => n with { Dismissed = true }, n => n.Dismissed); | ||
|
|
||
| /// <inheritdoc /> | ||
| public IObservable<InboxItemOutcome> Restore(IMessageHub hub, string path) | ||
| => InboxItemHandlers.Write<Notification>(hub, path, n => n with { Dismissed = false }, n => !n.Dismissed); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// The user's stored mail (<c>{user}/_Email</c>): Delete archives it, Undo returns it to | ||
| /// <see cref="EmailStatus.Read"/> — the viewer has, by then, seen it. | ||
| /// | ||
| /// <para>Only an INBOUND mail that is not in flight is touched. An outbound mail's | ||
| /// <see cref="Email.Status"/> is its send state (<see cref="EmailStatus.Sending"/> / | ||
| /// <see cref="EmailStatus.Sent"/> / <see cref="EmailStatus.Failed"/>), which the sender watches; | ||
| /// archiving one would stop a queued mail from ever leaving.</para> | ||
| /// </summary> | ||
| public sealed class EmailInboxItemHandler : IInboxItemHandler | ||
| { | ||
| /// <inheritdoc /> | ||
| public string NodeType => EmailNodeType.NodeType; | ||
|
|
||
| /// <inheritdoc /> | ||
| public IObservable<InboxItemOutcome> Dismiss(IMessageHub hub, string path) | ||
| => InboxItemHandlers.Write<Email>(hub, path, | ||
| mail => CanFile(mail) ? mail with { Status = EmailStatus.Archived } : mail, | ||
| // Done only when THIS mail is now a filed inbound one — an outbound mail, or one the | ||
| // write left alone, was not put away and must never enter the Undo batch. | ||
| mail => mail is { Direction: EmailDirection.Inbound, Status: EmailStatus.Archived }); | ||
|
|
||
| /// <inheritdoc /> | ||
| public IObservable<InboxItemOutcome> Restore(IMessageHub hub, string path) | ||
| => InboxItemHandlers.Write<Email>(hub, path, | ||
| mail => mail is { Direction: EmailDirection.Inbound, Status: EmailStatus.Archived } | ||
| ? mail with { Status = EmailStatus.Read } | ||
| : mail, | ||
| mail => mail is { Direction: EmailDirection.Inbound, Status: EmailStatus.New or EmailStatus.Read }); | ||
|
|
||
| /// <summary>Whether <paramref name="mail"/> may be archived from the inbox — inbound, and not already archived.</summary> | ||
| internal static bool CanFile(Email mail) | ||
| => mail is { Direction: EmailDirection.Inbound, Status: EmailStatus.New or EmailStatus.Read }; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.