On touch, a tap on a post opens its thread

On a phone the only way into a post's thread was a small Expand button (and now an entry in More), while a tap on the
post pinned it to a globe that is usually hidden there, outlining the card. Where the pointer can't hover, a tap on a
post that isn't on its links, controls or menus and doesn't end a text selection opens its thread through the card's
own thread link (main.js, data-thread on the card); cards are pinned to the globe with a mouse only. The globe's corner
stats have 12px text on touch. Phone flow: a tap in a post's margin opens its thread and pins nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-06 23:39:10 +02:00
1 parent da8f4ffcab
commit 7b1bfa71c0
5 files changed
+39 -5

No files matched your search

+1 -1
View File
@@ -17,7 +17,7 @@
data-src="@(Compact ? null : Post.Location?.Source.ToString().ToLowerInvariant())" data-cdn="@(Compact ? null : Post.Location?.CdnKey)" data-src="@(Compact ? null : Post.Location?.Source.ToString().ToLowerInvariant())" data-cdn="@(Compact ? null : Post.Location?.CdnKey)"
data-label="@(Compact ? null : Post.Location?.Label)" data-host="@(Compact ? null : Post.Host)" data-label="@(Compact ? null : Post.Location?.Label)" data-host="@(Compact ? null : Post.Host)"
data-range="@(Post.Located && !Compact ? Coordinate(Post.RangeKm) : null)" data-post-at="@(Compact ? null : Post.WrittenAt?.ToString("O"))" data-range="@(Post.Located && !Compact ? Coordinate(Post.RangeKm) : null)" data-post-at="@(Compact ? null : Post.WrittenAt?.ToString("O"))"
data-uri="@(Compact ? null : Post.Status?.Uri)"> data-uri="@(Compact ? null : Post.Status?.Uri)" data-thread="@(IncludeExpand && !Compact && Status is not null && !Post.Located ? "" : null)">
@if (Author is { } author) @if (Author is { } author)
{ {
+2 -2
View File
@@ -14,7 +14,7 @@
@if (stats?.Sun is { } sun) @if (stats?.Sun is { } sun)
{ {
<div class="absolute left-2 top-2 z-10 max-w-[75%] text-[11px] leading-tight pointer-events-auto"> <div class="absolute left-2 top-2 z-10 max-w-[75%] text-[11px] pointer-coarse:text-xs leading-tight pointer-events-auto">
<NerdTip Heading="@Localizer["Sun"]" Rows="SunRows(sun)" Placement="DTooltipPlacement.Bottom" Align="DTooltipAlign.Start"> <NerdTip Heading="@Localizer["Sun"]" Rows="SunRows(sun)" Placement="DTooltipPlacement.Bottom" Align="DTooltipAlign.Start">
<div class="flex flex-col gap-0.5 rounded-box px-2 py-1 bg-base-100/70 backdrop-blur-sm neo-raised neo-2xs"> <div class="flex flex-col gap-0.5 rounded-box px-2 py-1 bg-base-100/70 backdrop-blur-sm neo-raised neo-2xs">
<span class="flex items-center gap-1 font-semibold"> <span class="flex items-center gap-1 font-semibold">
@@ -50,7 +50,7 @@
</button> </button>
} }
<div class="absolute left-2 bottom-2 z-10 flex items-center gap-2 text-[10px] font-mono opacity-70 pointer-events-auto"> <div class="absolute left-2 bottom-2 z-10 flex items-center gap-2 text-[10px] pointer-coarse:text-xs font-mono opacity-70 pointer-events-auto">
<NerdTip Heading="@Localizer["Globe"]" Rows="StatsRows()" Placement="DTooltipPlacement.Top" Align="DTooltipAlign.Start"> <NerdTip Heading="@Localizer["Globe"]" Rows="StatsRows()" Placement="DTooltipPlacement.Top" Align="DTooltipAlign.Start">
<span class="badge badge-ghost badge-xs gap-1"> <span class="badge badge-ghost badge-xs gap-1">
<i class="ion-md-planet"></i> <i class="ion-md-planet"></i>
+15
View File
@@ -262,6 +262,21 @@ namespace decePubClient.E2E.Mobile
} }
} }
// a tap on a post (in its margin, not on a link or a control) opens its thread
[Fact]
public async Task A_tap_on_a_post_opens_its_thread()
{
await using var session = await SignIn.As(browser, Persona(), Phone);
await Flow(session, "thread", async page =>
{
var card = Card(page, "a[data-thread-link]");
var box = (await card.BoundingBoxAsync())!;
await card.TapAsync(new() { Position = new() { X = box.Width - 5, Y = box.Height * 0.35f } });
await page.WaitForURLAsync(url => new Uri(url).AbsolutePath.StartsWith("/expand/"), new() { Timeout = 15000 });
Assert.Empty(await page.Locator("[data-globe-pinned]").AllAsync());
});
}
static async Task<JsonNode> Latest(Pasture.Persona persona) => static async Task<JsonNode> Latest(Pasture.Persona persona) =>
(await PersonaApi.Call(persona, $"/api/v1/accounts/{persona.LocalId}/statuses?limit=1"))!.AsArray().FirstOrDefault(); (await PersonaApi.Call(persona, $"/api/v1/accounts/{persona.LocalId}/statuses?limit=1"))!.AsArray().FirstOrDefault();
} }
+4 -2
View File
@@ -21,6 +21,8 @@ const vendor = new URL("../vendor/globe/", import.meta.url).href
const textures = { day: vendor + "earth-blue-marble.jpg", night: vendor + "earth-night.jpg" } const textures = { day: vendor + "earth-blue-marble.jpg", night: vendor + "earth-night.jpg" }
const countriesUrl = vendor + "countries.json" const countriesUrl = vendor + "countries.json"
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)") const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)")
// a touch screen: a tap on a card opens its thread there (main.js), so cards are pinned with a mouse only
const canHover = matchMedia("(hover: hover)")
let loading = null let loading = null
let state = null let state = null
@@ -597,10 +599,10 @@ function watchCards() {
hover(null) hover(null)
}) })
// a click on a card (not on its links or controls) pins it: the globe keeps its moment and place until it is // a click on a card (not on its links or controls) pins it: the globe keeps its moment and place until it is
// clicked again or another card is pinned // clicked again or another card is pinned. With a mouse only: on a touch screen the tap opens the post's thread
listen(scroller, "click", event => { listen(scroller, "click", event => {
const card = event.target.closest?.("[data-post-id]") const card = event.target.closest?.("[data-post-id]")
if (!card || event.target.closest("a, button, input, textarea, select, label, summary, video, audio, [role=button]")) if (!canHover.matches || !card || event.target.closest("a, button, input, textarea, select, label, summary, video, audio, [role=button]"))
return return
const key = card.dataset.postId const key = card.dataset.postId
scroller.querySelectorAll("[data-globe-pinned]").forEach(c => c.removeAttribute("data-globe-pinned")) scroller.querySelectorAll("[data-globe-pinned]").forEach(c => c.removeAttribute("data-globe-pinned"))
+17
View File
@@ -123,6 +123,23 @@ window.clearAppBadge = () => {
window.logFromJs(e.message, "clearAppBadge") window.logFromJs(e.message, "clearAppBadge")
} }
} }
// On a touch screen a tap on a post (not on its links, controls or menus, and not ending a text selection) opens its
// thread, through the card's own thread link (Content.razor, data-thread-link), which Blazor's router takes.
document.addEventListener("click", event => {
try {
if (window.matchMedia("(hover: hover)").matches || !(event.target instanceof Element))
return
const card = event.target.closest("[data-thread]")
if (!card || event.target.closest("a, button, input, textarea, select, label, summary, video, audio, [role=button], .dropdown, .modal, [data-sheet]"))
return
if (!window.getSelection()?.isCollapsed)
return
card.querySelector("a[data-thread-link]")?.click()
} catch (e) {
console.error(e)
window.logFromJs(e.message, "openThread")
}
})
window.canHover = () => { window.canHover = () => {
try { try {
return window.matchMedia("(hover: hover)").matches return window.matchMedia("(hover: hover)").matches