diff --git a/Components/Content.razor b/Components/Content.razor index 7e93bd4..d1df3d2 100644 --- a/Components/Content.razor +++ b/Components/Content.razor @@ -525,6 +525,7 @@ bool isReplying; bool isDetailsOpen; + MediaAttachment viewingPicture; bool isQuoting; bool isEditing; bool isActing; @@ -626,6 +627,11 @@ } + @if (viewingPicture is { } picture) + { + + } + @if (status.Card is { } card && status.MediaAttachments.Count == 0) { @@ -762,10 +768,11 @@ RenderFragment Media(MediaAttachment media, bool several) => media.Type switch { - "image" => @ + "image" => @, "gifv" => @, "video" => @, @@ -1036,6 +1043,10 @@ await OnReplied.InvokeAsync(reply); } + static List Pictures(Status status) => [.. status.MediaAttachments.Where(m => m.Type == "image")]; + + void OpenPicture(MediaAttachment media) => viewingPicture = media; + async Task OpenDetails() { isDetailsOpen = true; diff --git a/Components/MediaViewer.razor b/Components/MediaViewer.razor new file mode 100644 index 0000000..5a8b2d2 --- /dev/null +++ b/Components/MediaViewer.razor @@ -0,0 +1,75 @@ +@inherits LocalizableComponentBase + +@* A post's pictures, one at a time, in the app (a picture used to open its file in a new tab, leaving it): the whole + screen on a phone, a large dialog from md, with its alt text, the previous and next one (buttons, or the arrow + keys), and the original in a new tab for whoever wants the file. *@ + + + @Localizer["Picture {0} of {1}", index + 1, Pictures.Count] + + +
+
+ @Current.Description + @if (Current.Description is { Length: > 0 } description) + { +
@description
+ } +
+
+ @if (Pictures.Count > 1) + { + + + + } + + + @Localizer["Open original"] + + @if (Pictures.Count > 1) + { + + + + } +
+
+
+
+ +@code { + /// The post's pictures, in its order. + [Parameter, EditorRequired] public List Pictures { get; set; } = []; + /// The one opened. + [Parameter] public int Start { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + + int index; + + MediaAttachment Current => Pictures[Math.Clamp(index, 0, Pictures.Count - 1)]; + + protected override void OnInitialized() + { + index = Math.Clamp(Start, 0, Math.Max(0, Pictures.Count - 1)); + IsLoading = false; + } + + void Previous() => index = Math.Max(0, index - 1); + + void Next() => index = Math.Min(Pictures.Count - 1, index + 1); + + void OnKeyDown(KeyboardEventArgs e) + { + if (e.Key == "ArrowLeft") + Previous(); + else if (e.Key == "ArrowRight") + Next(); + } + + Task Closed(bool open) => open ? Task.CompletedTask : OnClose.InvokeAsync(); +} diff --git a/Resources/AllStrings.it.resx b/Resources/AllStrings.it.resx index 0137395..2f0decd 100644 --- a/Resources/AllStrings.it.resx +++ b/Resources/AllStrings.it.resx @@ -1859,4 +1859,16 @@ Dettagli + + Immagine + + + Immagine precedente + + + Immagine successiva + + + Immagine {0} di {1} + diff --git a/Resources/AllStrings.resx b/Resources/AllStrings.resx index e1a785f..34e2589 100644 --- a/Resources/AllStrings.resx +++ b/Resources/AllStrings.resx @@ -1859,4 +1859,16 @@ Details + + Picture + + + Previous picture + + + Next picture + + + Picture {0} of {1} + diff --git a/tests/e2e/Mobile/FlowTests.cs b/tests/e2e/Mobile/FlowTests.cs index 845b524..9da1b2b 100644 --- a/tests/e2e/Mobile/FlowTests.cs +++ b/tests/e2e/Mobile/FlowTests.cs @@ -277,6 +277,39 @@ namespace decePubClient.E2E.Mobile }); } + // a picture opens in the app, the whole screen, and closes + [Fact] + public async Task A_picture_opens_in_the_app() + { + var expected = (await Pasture.Town("expect")).AsArray(); + var personas = Pasture.Personas(); + var item = expected.FirstOrDefault(i => i!["alts"]!.AsArray().Count > 0 + && i["readers"]!.AsArray().Any(r => personas.Any(p => p.UserName == (string)r))); + Assert.SkipWhen(item is null, "the town holds no post with pictures"); + var reader = personas.First(p => item!["readers"]!.AsArray().Any(r => (string)r == p.UserName)); + await using var session = await SignIn.As(browser, reader, Phone); + await Flow(session, "picture", async page => + { + await page.GotoAsync($"{Pasture.Site}/expand/{item!["id"]}"); + var card = page.Locator($"article[data-uri='{item["uri"]}']").First; + await card.WaitForAsync(new() { Timeout = 30000 }); + var hidden = card.GetByRole(AriaRole.Button).Filter(new() { HasText = "Sensitive media" }); + for (var i = 0; i < 8 && await hidden.CountAsync() > 0; i++) + await hidden.First.TapAsync(); + await card.Locator("button[data-picture]").First.TapAsync(); + var viewer = page.Locator("[data-media-viewer].modal-open .modal-box"); + await Assertions.Expect(viewer).ToBeVisibleAsync(); + var box = (await viewer.BoundingBoxAsync())!; + var size = page.ViewportSize!; + Assert.True(box.Width >= size.Width - 2 && box.Height >= size.Height - 2, $"the picture viewer is {box.Width}x{box.Height}"); + await AssertWhole(page, "[data-media-viewer] .modal-box"); + if (await page.Locator("[data-picture-next]:enabled").CountAsync() > 0) + await page.Locator("[data-picture-next]").TapAsync(); + await page.Locator("[data-media-viewer] [data-modal-close]").TapAsync(); + await Assertions.Expect(page.Locator("[data-media-viewer].modal-open")).ToHaveCountAsync(0); + }); + } + static async Task Latest(Pasture.Persona persona) => (await PersonaApi.Call(persona, $"/api/v1/accounts/{persona.LocalId}/statuses?limit=1"))!.AsArray().FirstOrDefault(); }