From 071bcce4b010acd65645b40ce19b1e6623f4d574 Mon Sep 17 00:00:00 2001 From: thepra Date: Tue, 6 Oct 2026 23:15:54 +0200 Subject: [PATCH] Writing on a phone: the composer rises as a sheet On a phone the composer opened at the top of the feed whether or not the feed was scrolled there, and a reply, quote or edit opened inside its card's 228px column. New PhoneSheet shows them as a sheet from the bottom of the screen below md, over a dimmed page, with a heading and a close button (a tap outside or Escape closes it too); from md they stay in place as before. On a phone a new post starts from a round button under the thumb (the feed keeps room for it at the bottom), and the tabs' Post button shows from md. Opening the composer scrolls the feed to its top, and the composer's words take the focus (DTextarea.Element). Picking a suggested mention or hashtag keeps the focus, and the phone's keyboard, in the words (DButton.KeepFocus, and the words are focused again). The character count sits under the words instead of over their last line, and the visibility menu names each choice. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw --- Components/Composer.razor | 36 +++++++++++++++------ Components/Content.razor | 16 ++++------ Components/Daisy/Actions/DButton.razor | 5 ++- Components/Daisy/DataInput/DTextarea.razor | 5 ++- Components/Feed/FeedTabs.razor | 2 +- Components/PhoneSheet.razor | 37 ++++++++++++++++++++++ Pages/Index.razor | 31 +++++++++++++----- Resources/AllStrings.it.resx | 3 ++ Resources/AllStrings.resx | 3 ++ 9 files changed, 108 insertions(+), 30 deletions(-) create mode 100644 Components/PhoneSheet.razor diff --git a/Components/Composer.razor b/Components/Composer.razor index 78ab2c9..f51b0f3 100644 --- a/Components/Composer.razor +++ b/Components/Composer.razor @@ -17,18 +17,19 @@ } -
- + - @ContentLength/@ComposeForm.MaxContentLength + @* under the words, not over their last line *@ + @ContentLength/@ComposeForm.MaxContentLength
@if (suggestedAccounts.Count > 0 || suggestedTags.Count > 0) {
@foreach (var account in suggestedAccounts) { - + @* alt="": the handle is next to it. *@ @("@" + account.Acct) @@ -36,7 +37,7 @@ } @foreach (var tag in suggestedTags) { - #@tag.Name + #@tag.Name }
} @@ -131,16 +132,18 @@
@if (!form.IsLocated && GroupId is null && Editing is null) { - + - + @foreach (var messageType in Enum.GetValues()) { - - + + + @Localizer[messageType.ToString()] } @@ -293,6 +296,7 @@ if (!typed.Success) return; form.Content = form.Content[..typed.Groups[1].Index] + written + " "; + focusPending = true; } string Placeholder => Editing is not null ? Localizer["Edit your post"] @@ -340,6 +344,18 @@ _ => MessageType.Public }; + DTextarea textarea; + // the words take the focus when the composer opens, and again after a suggestion is picked + bool focusPending = true; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!focusPending) + return; + focusPending = false; + await textarea.Element.FocusAsync(); + } + protected override async Task OnInitializedAsync() { form.GroupId = GroupId; diff --git a/Components/Content.razor b/Components/Content.razor index 92c3f47..7a3fcbc 100644 --- a/Components/Content.razor +++ b/Components/Content.razor @@ -406,18 +406,16 @@
- @if (isReplying) - { + @* under the post on a desktop, as a sheet over the page on a phone, where the card's column is too narrow *@ + - } - @if (isQuoting) - { + + - } - @if (isEditing) - { + + - } + @if (isReporting && status.Account is { } reported) { diff --git a/Components/Daisy/Actions/DButton.razor b/Components/Daisy/Actions/DButton.razor index 5e70cc2..9111f85 100644 --- a/Components/Daisy/Actions/DButton.razor +++ b/Components/Daisy/Actions/DButton.razor @@ -3,7 +3,8 @@ @* daisyUI Button: https://daisyui.com/components/button/ . Renders an when Href is set. *@ @if (Href is null) { - } @@ -27,6 +28,8 @@ else [Parameter] public string Type { get; set; } = "button"; [Parameter] public string Href { get; set; } [Parameter] public EventCallback OnClick { get; set; } + /// Pressing it doesn't take the focus from where it is (a chip that writes into a focused textarea). + [Parameter] public bool KeepFocus { get; set; } string CssClass => Cx("btn", ColorClass, VariantClass, SizeClass, ShapeClass, Active ? "btn-active" : null, Disabled && Href is not null ? "btn-disabled" : null, Class); diff --git a/Components/Daisy/DataInput/DTextarea.razor b/Components/Daisy/DataInput/DTextarea.razor index e77c774..dd6733d 100644 --- a/Components/Daisy/DataInput/DTextarea.razor +++ b/Components/Daisy/DataInput/DTextarea.razor @@ -3,10 +3,13 @@ @* daisyUI Textarea: https://daisyui.com/components/textarea/ . Blazor sets the textarea's value property, so the bound value is the content; rows, placeholder and the like pass through. *@ - @code { + /// The textarea itself, for focusing it. + public ElementReference Element { get; private set; } + [Parameter] public DColor Color { get; set; } [Parameter] public DSize Size { get; set; } [Parameter] public bool Ghost { get; set; } diff --git a/Components/Feed/FeedTabs.razor b/Components/Feed/FeedTabs.razor index 86e4551..afe0984 100644 --- a/Components/Feed/FeedTabs.razor +++ b/Components/Feed/FeedTabs.razor @@ -33,7 +33,7 @@ - + diff --git a/Components/PhoneSheet.razor b/Components/PhoneSheet.razor new file mode 100644 index 0000000..56ad5e6 --- /dev/null +++ b/Components/PhoneSheet.razor @@ -0,0 +1,37 @@ +@inherits LocalizableComponentBase + +@* What opens in place on a desktop (the composer at the top of the feed, a reply under its post) and as a sheet from the + bottom of a phone's screen (below md), over a dimmed page, with its heading and a close button; a tap outside it or + Escape closes it too. Class is the inline look from md (padding, a raised panel). *@ +@if (IsOpen) +{ +
+ +
+
+ @Heading + +
+ @ChildContent +
+
+} + +@code { + [Parameter] public bool IsOpen { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + /// The sheet's heading on a phone. + [Parameter] public string Heading { get; set; } + [Parameter] public RenderFragment ChildContent { get; set; } + [Parameter] public string Class { get; set; } + + protected override void OnInitialized() => IsLoading = false; + + Task Close() => OnClose.InvokeAsync(); + + Task OnKeyDown(KeyboardEventArgs e) => e.Key == "Escape" ? Close() : Task.CompletedTask; +} diff --git a/Pages/Index.razor b/Pages/Index.razor index 24ffff3..0fe53ba 100644 --- a/Pages/Index.razor +++ b/Pages/Index.razor @@ -18,19 +18,26 @@
- + + + } +
-
+ @* (on a phone the bottom stays clear of the New post button) *@ +
- @if (isComposing) - { -
- -
- } + + + @if (Filter.TimelineType is TimelineType.Notifications && filteredRequests > 0) { @@ -461,6 +468,14 @@ await InvokeAsync(StateHasChanged); } + // a composer that opens shows at the top of the feed, where it is, however far down the feed was read + async Task ToggleCompose() + { + isComposing = !isComposing; + if (isComposing && feedModule is not null) + await feedModule.InvokeVoidAsync("scrollToTop", ScrollId); + } + async Task ShowPending() { var shown = pending; diff --git a/Resources/AllStrings.it.resx b/Resources/AllStrings.it.resx index b1b6eef..e6b6410 100644 --- a/Resources/AllStrings.it.resx +++ b/Resources/AllStrings.it.resx @@ -1820,4 +1820,7 @@ Menu + + Nuovo post + diff --git a/Resources/AllStrings.resx b/Resources/AllStrings.resx index 406f821..70a33f7 100644 --- a/Resources/AllStrings.resx +++ b/Resources/AllStrings.resx @@ -1820,4 +1820,7 @@ Menu + + New post +