From b13b894561f74c9bc67debb2aa57476155325738 Mon Sep 17 00:00:00 2001 From: DarkSkyXD Date: Mon, 14 Sep 2026 07:45:06 -0500 Subject: [PATCH 1/2] feat(mobile): close guest Home, Search, and Categories Clip Recent/Views was the last D07 product gap. Parity records, coverage tabs, and named catalog keys let #148 #149 #150 close without inventing device evidence. Co-authored-by: Cursor --- .../components/category-filter-bar.tsx | 10 +- .../discovery/domain/category-detail.ts | 13 +- .../discovery/domain/category-identity.ts | 2 +- .../discovery/domain/discovery-fixture.ts | 3 +- .../tests/category-detail-screen.test.ts | 25 ++ .../discovery/tests/category-detail.test.ts | 36 ++- .../category-discovery.md | 33 +++ .../home-live-discovery.md | 37 +++ .../coverage-matrix-check.mjs | 85 +++++++ .../coverage-matrix-ledger.json | 225 ++++++++++++------ .../streamfusion-mobile/coverage-matrix.md | 38 +-- .../mobile-screen-and-control-contract.md | 4 +- verification/catalog.json | 128 ++++++++++ 13 files changed, 540 insertions(+), 99 deletions(-) create mode 100644 docs/research/streamfusion-mobile/android-parity-records/category-discovery.md create mode 100644 docs/research/streamfusion-mobile/android-parity-records/home-live-discovery.md diff --git a/apps/mobile/src/features/discovery/components/category-filter-bar.tsx b/apps/mobile/src/features/discovery/components/category-filter-bar.tsx index 0ffe402f..e73640ae 100644 --- a/apps/mobile/src/features/discovery/components/category-filter-bar.tsx +++ b/apps/mobile/src/features/discovery/components/category-filter-bar.tsx @@ -15,6 +15,7 @@ import { import type { CategoryRequestIdentity, CategoryTab, + ClipSort, LiveSort, PlatformScope, VideoSort, @@ -167,7 +168,12 @@ function sortOptions(tab: CategoryTab): readonly { { label: "Viewers (low)", value: "viewers-asc" }, ]; } - if (tab === "clips") return [{ label: "Views", value: "views" }]; + if (tab === "clips") { + return [ + { label: "Views", value: "views" }, + { label: "Recent", value: "recent" }, + ]; + } return [ { label: "Recent", value: "recent" }, { label: "Views", value: "views" }, @@ -202,7 +208,7 @@ function applySort( if (identity.tab === "videos") { return { ...identity, videoSort: value as VideoSort }; } - return identity; + return { ...identity, clipSort: value as ClipSort }; } const styles = StyleSheet.create({ diff --git a/apps/mobile/src/features/discovery/domain/category-detail.ts b/apps/mobile/src/features/discovery/domain/category-detail.ts index 4e02cbc0..c2ad2537 100644 --- a/apps/mobile/src/features/discovery/domain/category-detail.ts +++ b/apps/mobile/src/features/discovery/domain/category-detail.ts @@ -101,7 +101,7 @@ function selectMedia(input: { } return { kind: "clips", - items: sortClips(clipsOf(input.twitch)), + items: sortClips(clipsOf(input.twitch), input.identity.clipSort), }; } if (input.identity.tab === "videos") { @@ -176,8 +176,15 @@ function sortLive(items: readonly Stream[], sort: "viewers-desc" | "viewers-asc" ); } -function sortClips(items: readonly Clip[]): Clip[] { - return sortCopy(items, (left, right) => right.viewCount - left.viewCount); +function sortClips( + items: readonly Clip[], + sort: CategoryRequestIdentity["clipSort"], +): Clip[] { + return sortCopy(items, (left, right) => + sort === "recent" + ? right.createdAt.localeCompare(left.createdAt) + : right.viewCount - left.viewCount, + ); } function sortVideos( diff --git a/apps/mobile/src/features/discovery/domain/category-identity.ts b/apps/mobile/src/features/discovery/domain/category-identity.ts index ef3add52..dc12706a 100644 --- a/apps/mobile/src/features/discovery/domain/category-identity.ts +++ b/apps/mobile/src/features/discovery/domain/category-identity.ts @@ -8,7 +8,7 @@ export type CategoryTab = "live" | "clips" | "videos"; export type PlatformScope = "all" | Platform; export type TagFilter = "all" | string; export type LiveSort = "viewers-desc" | "viewers-asc"; -export type ClipSort = "views"; +export type ClipSort = "views" | "recent"; export type VideoSort = "views" | "recent"; export type CategoryFollowState = { diff --git a/apps/mobile/src/features/discovery/domain/discovery-fixture.ts b/apps/mobile/src/features/discovery/domain/discovery-fixture.ts index d137b8d9..83b603bb 100644 --- a/apps/mobile/src/features/discovery/domain/discovery-fixture.ts +++ b/apps/mobile/src/features/discovery/domain/discovery-fixture.ts @@ -57,6 +57,7 @@ export function fixtureClip( platform: Platform, id: string, views: number, + createdAt = "2026-09-11T00:00:00.000Z" as SerializedTimestamp, ): Clip { return { channelAvatar: "", @@ -64,7 +65,7 @@ export function fixtureClip( channelId: `${platform}-${id}`, channelName: `${platform}-clip`, clipUrl: `https://example.com/clip/${id}`, - createdAt: "2026-09-11T00:00:00.000Z" as SerializedTimestamp, + createdAt, creatorName: "Creator", duration: 12, id, diff --git a/apps/mobile/src/features/discovery/tests/category-detail-screen.test.ts b/apps/mobile/src/features/discovery/tests/category-detail-screen.test.ts index 5c4c4efd..34d357b0 100644 --- a/apps/mobile/src/features/discovery/tests/category-detail-screen.test.ts +++ b/apps/mobile/src/features/discovery/tests/category-detail-screen.test.ts @@ -106,4 +106,29 @@ describe("Category detail screen", () => { nodes.some((node) => node.props.testID === "category-unsupported"), ).toBe(true); }); + + it("offers Views and Recent clip sort chips", () => { + const root = CategoryDetailView({ + onChangeIdentity: () => undefined, + onChangeQuery: () => undefined, + onOpenAccounts: () => undefined, + onRetry: () => undefined, + query: "", + view: composeCategoryDetail({ + identity: { + ...defaultCategoryRequest(chatting, "all", "all"), + tab: "clips", + }, + loading: false, + twitch: fixtureOutcome("twitch", "ready"), + }), + }); + const nodes = descendants(root); + expect(nodes.some((node) => node.props.testID === "category-sort-views")).toBe( + true, + ); + expect( + nodes.some((node) => node.props.testID === "category-sort-recent"), + ).toBe(true); + }); }); diff --git a/apps/mobile/src/features/discovery/tests/category-detail.test.ts b/apps/mobile/src/features/discovery/tests/category-detail.test.ts index aa236022..72693bb9 100644 --- a/apps/mobile/src/features/discovery/tests/category-detail.test.ts +++ b/apps/mobile/src/features/discovery/tests/category-detail.test.ts @@ -89,7 +89,7 @@ describe("composeCategoryDetail", () => { expect(view.media.items.map((item) => item.id)).toEqual(["new", "old"]); }); - it("sorts clips by views only", () => { + it("sorts clips by views by default", () => { const view = composeCategoryDetail({ identity: { ...defaultCategoryRequest(chatting, "all", "day"), @@ -111,4 +111,38 @@ describe("composeCategoryDetail", () => { if (view.media.kind !== "clips") throw new Error("expected clips"); expect(view.media.items.map((item) => item.id)).toEqual(["high", "low"]); }); + + it("sorts clips by created time when Recent is selected", () => { + const view = composeCategoryDetail({ + identity: { + ...defaultCategoryRequest(chatting, "all", "day"), + clipSort: "recent", + tab: "clips", + }, + loading: false, + twitch: { + cache: { kind: "miss" }, + items: [ + fixtureClip( + "twitch", + "old", + 40, + "2026-01-01T00:00:00.000Z" as SerializedTimestamp, + ), + fixtureClip( + "twitch", + "new", + 2, + "2026-09-01T00:00:00.000Z" as SerializedTimestamp, + ), + ], + path: { kind: "direct", platform: "twitch" }, + platform: "twitch", + status: "complete", + }, + }); + expect(view.media.kind).toBe("clips"); + if (view.media.kind !== "clips") throw new Error("expected clips"); + expect(view.media.items.map((item) => item.id)).toEqual(["new", "old"]); + }); }); diff --git a/docs/research/streamfusion-mobile/android-parity-records/category-discovery.md b/docs/research/streamfusion-mobile/android-parity-records/category-discovery.md new file mode 100644 index 00000000..f77ed578 --- /dev/null +++ b/docs/research/streamfusion-mobile/android-parity-records/category-discovery.md @@ -0,0 +1,33 @@ +# Android parity record: `category-discovery` + +- Capability ID: `category-discovery` +- Desktop baseline: `docs/research/streamfusion-mobile/desktop-parity-inventory.md` Category discovery section +- Observed `main` at branch start: `aa0e52ab71030daa48f72d5cea1d250e8be07cd8` +- Android owner: Mobile `discovery` +- Progress: `implemented` +- Delivery: `direct` +- Adaptation: Equivalent. Categories live under More. Category Detail keeps Live, Clips, and Videos with a bottom search dock. +- Freshness: `current` for APK `1596d999e5ca19d5404bcfc89a5c182bad55d6397c5cb1946eb5550022844d5e` via `verification/evidence/issue-150-categories.json` + +## Desktop outcome + +Browse categories and inspect category-specific streams, videos, and clips. Filter by platform, language, tags, time, and sort. Clips sort by views or recency. + +## Android outcome + +Guest Categories work signed-out. Categories list Twitch and Kick cards with a Search categories dock. Category Detail keeps Follow as explained-unavailable (`guest-category-follow-requires-account`). Tabs are LIVE, CLIPS, and VIDEOS. Platform All, Twitch, and Kick stay on the merged card. Language chips persist. Clip time chips are day, week, month, and all. Clip sort chips are Views and Recent. Helix Get Clips has no sort param, so recency sorts the fetched page in `composeCategoryDetail`. Kick clips and videos stay explained-unavailable on the official public catalog. + +## Required evidence + +- `change-gate` +- `api30-journey` +- `official-live-catalog` +- `clips-videos-language-and-tags` + +## Evidence residuals + +`official-live-catalog` and `clips-videos-language-and-tags` stay residual on this APK. Live catalog success still needs official provider secrets on the signed-out relay. Language chips apply to Live only. Helix has no clip or video language query. The 2026-09-12 frames show Sort stayed Recent and Views on Videos. They do not isolate a Clips Recent chip tap. Unit tests cover Views default and Recent created-time order. + +## Blocking for public release + +Live Twitch and Kick category success still depends on Relay credentials or official app tokens. Signed-in category Follow stays behind OAuth. OAuth stays on #145 and #146. diff --git a/docs/research/streamfusion-mobile/android-parity-records/home-live-discovery.md b/docs/research/streamfusion-mobile/android-parity-records/home-live-discovery.md new file mode 100644 index 00000000..5f6f371a --- /dev/null +++ b/docs/research/streamfusion-mobile/android-parity-records/home-live-discovery.md @@ -0,0 +1,37 @@ +# Android parity record: `home-live-discovery` + +- Capability ID: `home-live-discovery` +- Desktop baseline: `docs/research/streamfusion-mobile/desktop-parity-inventory.md` Home live discovery section +- Observed `main` at branch start: `aa0e52ab71030daa48f72d5cea1d250e8be07cd8` +- Android owner: Mobile `discovery` +- Progress: `implemented` +- Delivery: `direct` +- Adaptation: Equivalent. Home lives under More. Channel Detail is a stacked destination with Home, Videos, and Clips. +- Freshness: `current` for APK `1596d999e5ca19d5404bcfc89a5c182bad55d6397c5cb1946eb5550022844d5e` via `verification/evidence/issue-148-d05-home-channel.json` + +## Desktop outcome + +Browse live streams, channel cards, and current platform availability from Home. Open a channel to Home, Videos, and Clips without breaking the player on an ended channel. + +## Android outcome + +Guest Home works signed-out. More lists Home first. Combined Twitch and Kick recommendation cards use 16:9 thumbs, live state, and viewer counts. Twitch without Relay or an app token reports `guest-unavailable` with Retry. It does not require Sign in. Stale cache shows cached recommendations with age copy. + +Channel Detail keeps Home, Videos, and Clips. Kick videos and clips show first-class unsupported copy. Follow uses `channel-follow`. Guest Follow writes a local channel follow. Watch opens the guest live player when a playback URL exists. OAuth stays on #145 and #146. + +## Required evidence + +- `change-gate` +- `api30-journey` +- `offline-cache` +- `tablet-layout` +- `live-platforms` +- `accessibility` + +## Evidence residuals + +`tablet-layout`, `live-platforms`, and `accessibility` stay residual on this APK. Live catalog success still needs Relay credentials or official app tokens. TalkBack, large text, and a tablet AVD were not driven. The 2026-09-12 frames predate Guest Follow (#225) and guest Watch (#227). Those PNGs still show the earlier degrade copy. + +## Blocking for public release + +Live Twitch and Kick catalog success still depends on Relay credentials or official app tokens. OAuth stays on #145 and #146. diff --git a/docs/research/streamfusion-mobile/coverage-matrix-check.mjs b/docs/research/streamfusion-mobile/coverage-matrix-check.mjs index 7ac2a4ab..3f7ce729 100644 --- a/docs/research/streamfusion-mobile/coverage-matrix-check.mjs +++ b/docs/research/streamfusion-mobile/coverage-matrix-check.mjs @@ -132,6 +132,23 @@ const implemented = new Set([ "screen:following", "screen:channel", "screen:watch", + "tab:search:all", + "tab:search:channels", + "tab:search:streams", + "tab:search:videos", + "tab:search:clips", + "tab:search:categories", + "tab:category-detail:live", + "tab:category-detail:clips", + "tab:category-detail:videos", + "tab:channel:home", + "tab:channel:videos", + "tab:channel:clips", + "tab:following:live", + "tab:following:videos", + "tab:following:clips", + "tab:following:categories", + "tab:following:channels", "shell-route:search", "shell-route:following", "shell-route:following/manage", @@ -195,6 +212,23 @@ const owners = { "screen:settings-proxy": [162, 169], "panel:proxy": [162], "shell-route:more/settings": [162, 169], + "tab:search:all": [147, 149], + "tab:search:channels": [147, 149], + "tab:search:streams": [147, 149], + "tab:search:videos": [147, 149], + "tab:search:clips": [147, 149], + "tab:search:categories": [147, 149], + "tab:category-detail:live": [147, 150], + "tab:category-detail:clips": [147, 150], + "tab:category-detail:videos": [147, 150], + "tab:channel:home": [147, 148], + "tab:channel:videos": [147, 148], + "tab:channel:clips": [147, 148], + "tab:following:live": [147, 151], + "tab:following:videos": [147, 151], + "tab:following:clips": [147, 151], + "tab:following:categories": [147, 151], + "tab:following:channels": [147, 151], }; const paths = { @@ -232,6 +266,57 @@ const paths = { "screen:channel": [ "apps/mobile/src/features/discovery/components/channel-detail-screen.tsx", ], + "tab:search:all": [ + "apps/mobile/src/features/discovery/components/search-results-view.tsx", + ], + "tab:search:channels": [ + "apps/mobile/src/features/discovery/components/search-results-view.tsx", + ], + "tab:search:streams": [ + "apps/mobile/src/features/discovery/components/search-results-view.tsx", + ], + "tab:search:videos": [ + "apps/mobile/src/features/discovery/components/search-results-view.tsx", + ], + "tab:search:clips": [ + "apps/mobile/src/features/discovery/components/search-results-view.tsx", + ], + "tab:search:categories": [ + "apps/mobile/src/features/discovery/components/search-results-view.tsx", + ], + "tab:category-detail:live": [ + "apps/mobile/src/features/discovery/components/category-detail-view.tsx", + ], + "tab:category-detail:clips": [ + "apps/mobile/src/features/discovery/components/category-detail-view.tsx", + ], + "tab:category-detail:videos": [ + "apps/mobile/src/features/discovery/components/category-detail-view.tsx", + ], + "tab:channel:home": [ + "apps/mobile/src/features/discovery/components/channel-tabs.tsx", + ], + "tab:channel:videos": [ + "apps/mobile/src/features/discovery/components/channel-tabs.tsx", + ], + "tab:channel:clips": [ + "apps/mobile/src/features/discovery/components/channel-tabs.tsx", + ], + "tab:following:live": [ + "apps/mobile/src/features/follows/components/following-tab-body.tsx", + ], + "tab:following:videos": [ + "apps/mobile/src/features/follows/components/following-tab-body.tsx", + ], + "tab:following:clips": [ + "apps/mobile/src/features/follows/components/following-tab-body.tsx", + ], + "tab:following:categories": [ + "apps/mobile/src/features/follows/components/following-tab-body.tsx", + ], + "tab:following:channels": [ + "apps/mobile/src/features/follows/components/following-tab-body.tsx", + ], "shell-route:search": [ "apps/mobile/src/features/discovery/components/unified-search-screen.tsx", ], diff --git a/docs/research/streamfusion-mobile/coverage-matrix-ledger.json b/docs/research/streamfusion-mobile/coverage-matrix-ledger.json index 09d70b22..47ef8c58 100644 --- a/docs/research/streamfusion-mobile/coverage-matrix-ledger.json +++ b/docs/research/streamfusion-mobile/coverage-matrix-ledger.json @@ -8,10 +8,10 @@ ], "totals": { "discovered": 185, - "implemented": 32, + "implemented": 49, "partial": 14, "placeholder": 9, - "missing": 130 + "missing": 113 }, "entries": [ { @@ -689,10 +689,15 @@ "id": "tab:search:all", "kind": "tab", "name": "search:all", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 149 + ], + "implementation": [ + "apps/mobile/src/features/discovery/components/search-results-view.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -700,10 +705,15 @@ "id": "tab:search:channels", "kind": "tab", "name": "search:channels", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 149 + ], + "implementation": [ + "apps/mobile/src/features/discovery/components/search-results-view.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -711,10 +721,15 @@ "id": "tab:search:streams", "kind": "tab", "name": "search:streams", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 149 + ], + "implementation": [ + "apps/mobile/src/features/discovery/components/search-results-view.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -722,10 +737,15 @@ "id": "tab:search:videos", "kind": "tab", "name": "search:videos", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 149 + ], + "implementation": [ + "apps/mobile/src/features/discovery/components/search-results-view.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -733,10 +753,15 @@ "id": "tab:search:clips", "kind": "tab", "name": "search:clips", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 149 + ], + "implementation": [ + "apps/mobile/src/features/discovery/components/search-results-view.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -744,10 +769,15 @@ "id": "tab:search:categories", "kind": "tab", "name": "search:categories", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 149 + ], + "implementation": [ + "apps/mobile/src/features/discovery/components/search-results-view.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -755,10 +785,15 @@ "id": "tab:following:live", "kind": "tab", "name": "following:live", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 151 + ], + "implementation": [ + "apps/mobile/src/features/follows/components/following-tab-body.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -766,10 +801,15 @@ "id": "tab:following:videos", "kind": "tab", "name": "following:videos", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 151 + ], + "implementation": [ + "apps/mobile/src/features/follows/components/following-tab-body.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -777,10 +817,15 @@ "id": "tab:following:clips", "kind": "tab", "name": "following:clips", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 151 + ], + "implementation": [ + "apps/mobile/src/features/follows/components/following-tab-body.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -788,10 +833,15 @@ "id": "tab:following:categories", "kind": "tab", "name": "following:categories", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 151 + ], + "implementation": [ + "apps/mobile/src/features/follows/components/following-tab-body.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -799,10 +849,15 @@ "id": "tab:following:channels", "kind": "tab", "name": "following:channels", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 151 + ], + "implementation": [ + "apps/mobile/src/features/follows/components/following-tab-body.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -810,10 +865,15 @@ "id": "tab:category-detail:live", "kind": "tab", "name": "category-detail:live", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 150 + ], + "implementation": [ + "apps/mobile/src/features/discovery/components/category-detail-view.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -821,10 +881,15 @@ "id": "tab:category-detail:clips", "kind": "tab", "name": "category-detail:clips", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 150 + ], + "implementation": [ + "apps/mobile/src/features/discovery/components/category-detail-view.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -832,10 +897,15 @@ "id": "tab:category-detail:videos", "kind": "tab", "name": "category-detail:videos", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 150 + ], + "implementation": [ + "apps/mobile/src/features/discovery/components/category-detail-view.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -843,10 +913,15 @@ "id": "tab:channel:home", "kind": "tab", "name": "channel:home", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 148 + ], + "implementation": [ + "apps/mobile/src/features/discovery/components/channel-tabs.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -854,10 +929,15 @@ "id": "tab:channel:videos", "kind": "tab", "name": "channel:videos", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 148 + ], + "implementation": [ + "apps/mobile/src/features/discovery/components/channel-tabs.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, @@ -865,10 +945,15 @@ "id": "tab:channel:clips", "kind": "tab", "name": "channel:clips", - "status": "missing", - "owners": [], - "implementation": [], - "verification": "missing", + "status": "implemented", + "owners": [ + 147, + 148 + ], + "implementation": [ + "apps/mobile/src/features/discovery/components/channel-tabs.tsx" + ], + "verification": "tests", "evidence": [], "designRef": "docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md" }, diff --git a/docs/research/streamfusion-mobile/coverage-matrix.md b/docs/research/streamfusion-mobile/coverage-matrix.md index 75d7b523..7bfef162 100644 --- a/docs/research/streamfusion-mobile/coverage-matrix.md +++ b/docs/research/streamfusion-mobile/coverage-matrix.md @@ -9,10 +9,10 @@ Source revisions are the files in this commit. The checker fails when a contract | Status | Count | | --- | ---: | -| Implemented | 32 | +| Implemented | 49 | | Partial | 14 | | Placeholder | 9 | -| Missing | 130 | +| Missing | 113 | | Discovered | 185 | Implemented means the current candidate has a working control or screen for that row. @@ -84,23 +84,23 @@ Missing means no route or control exists yet. | `panel:logs` | missing | — | — | missing | | `panel:report-bug` | missing | — | — | missing | | `panel:about` | missing | — | — | missing | -| `tab:search:all` | missing | — | — | missing | -| `tab:search:channels` | missing | — | — | missing | -| `tab:search:streams` | missing | — | — | missing | -| `tab:search:videos` | missing | — | — | missing | -| `tab:search:clips` | missing | — | — | missing | -| `tab:search:categories` | missing | — | — | missing | -| `tab:following:live` | missing | — | — | missing | -| `tab:following:videos` | missing | — | — | missing | -| `tab:following:clips` | missing | — | — | missing | -| `tab:following:categories` | missing | — | — | missing | -| `tab:following:channels` | missing | — | — | missing | -| `tab:category-detail:live` | missing | — | — | missing | -| `tab:category-detail:clips` | missing | — | — | missing | -| `tab:category-detail:videos` | missing | — | — | missing | -| `tab:channel:home` | missing | — | — | missing | -| `tab:channel:videos` | missing | — | — | missing | -| `tab:channel:clips` | missing | — | — | missing | +| `tab:search:all` | implemented | #147, #149 | apps/mobile/src/features/discovery/components/search-results-view.tsx | tests | +| `tab:search:channels` | implemented | #147, #149 | apps/mobile/src/features/discovery/components/search-results-view.tsx | tests | +| `tab:search:streams` | implemented | #147, #149 | apps/mobile/src/features/discovery/components/search-results-view.tsx | tests | +| `tab:search:videos` | implemented | #147, #149 | apps/mobile/src/features/discovery/components/search-results-view.tsx | tests | +| `tab:search:clips` | implemented | #147, #149 | apps/mobile/src/features/discovery/components/search-results-view.tsx | tests | +| `tab:search:categories` | implemented | #147, #149 | apps/mobile/src/features/discovery/components/search-results-view.tsx | tests | +| `tab:following:live` | implemented | #147, #151 | apps/mobile/src/features/follows/components/following-tab-body.tsx | tests | +| `tab:following:videos` | implemented | #147, #151 | apps/mobile/src/features/follows/components/following-tab-body.tsx | tests | +| `tab:following:clips` | implemented | #147, #151 | apps/mobile/src/features/follows/components/following-tab-body.tsx | tests | +| `tab:following:categories` | implemented | #147, #151 | apps/mobile/src/features/follows/components/following-tab-body.tsx | tests | +| `tab:following:channels` | implemented | #147, #151 | apps/mobile/src/features/follows/components/following-tab-body.tsx | tests | +| `tab:category-detail:live` | implemented | #147, #150 | apps/mobile/src/features/discovery/components/category-detail-view.tsx | tests | +| `tab:category-detail:clips` | implemented | #147, #150 | apps/mobile/src/features/discovery/components/category-detail-view.tsx | tests | +| `tab:category-detail:videos` | implemented | #147, #150 | apps/mobile/src/features/discovery/components/category-detail-view.tsx | tests | +| `tab:channel:home` | implemented | #147, #148 | apps/mobile/src/features/discovery/components/channel-tabs.tsx | tests | +| `tab:channel:videos` | implemented | #147, #148 | apps/mobile/src/features/discovery/components/channel-tabs.tsx | tests | +| `tab:channel:clips` | implemented | #147, #148 | apps/mobile/src/features/discovery/components/channel-tabs.tsx | tests | | `tab:watch:chat` | missing | — | — | missing | | `tab:watch:info` | missing | — | — | missing | | `tab:watch:related` | missing | — | — | missing | diff --git a/docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md b/docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md index c093bc50..0280039e 100644 --- a/docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md +++ b/docs/research/streamfusion-mobile/mobile-screen-and-control-contract.md @@ -165,7 +165,7 @@ Source anchor: `channelScreen`. `channel` renders a channel identity header with avatar, follower and Platform identity, Guest Follow or account relationship, and a Follow or Following control. It renders channel tabs, selected content, then an About card. Home renders current live or offline notice before recent broadcasts. Videos and Clips render recorded rows with their respective order label. -`set-channel-follow` is an assigned control. It follows PRD 18.3: account writes require an approved official path; otherwise the UI offers Guest Follow and an explicit provider-page action. `open-channel-about` is a read-only route or disclosure action. The prototype's Follow and Following labels do not prove provider mutation support. +`channel-follow` is an assigned control. It follows PRD 18.3: account writes require an approved official path; otherwise the UI offers Guest Follow and an explicit provider-page action. `open-channel-about` is a read-only route or disclosure action. The prototype's Follow and Following labels do not prove provider mutation support. ### Watch @@ -175,7 +175,7 @@ Source anchors: `watchScreen`, `videoStage`, and `chatPanel`. The player stage contains preview or video surface, live or media state, effective quality, channel identity, title, viewer or media metadata, and mute state. It requires `play-pause`, `seek-back`, `seek-forward`, `set-quality`, `set-volume`, `toggle-mute`, `enter-fullscreen`, `enter-pip`, and `toggle-captions` controls when the current media and capability support them. The revised mockup exposes transport and player-settings controls, with sheets for secondary operations. Its simulated controls do not prove playback integration. Unsupported operations must explain their actual limitation. Player-controls Settings governs visibility only. It does not bypass capability policy. -The Info sheet renders channel and stream identity, Follow, Add to Multistream, Start recording, and local-caption status. `set-channel-follow`, `add-multistream-slot`, `start-recording`, and `manage-local-captions` are assigned controls. Recording opens a confirmation sheet that names foreground-service behavior and the active stream. Local captions opens model install, removal, unavailable, and resource-impact states. Captions use decoded program PCM only. They never request microphone permission, upload audio, or offer cloud fallback. +The Info sheet renders channel and stream identity, Follow, Add to Multistream, Start recording, and local-caption status. `channel-follow`, `add-multistream-slot`, `start-recording`, and `manage-local-captions` are assigned controls. Recording opens a confirmation sheet that names foreground-service behavior and the active stream. Local captions opens model install, removal, unavailable, and resource-impact states. Captions use decoded program PCM only. They never request microphone permission, upload audio, or offer cloud fallback. The Chat sheet renders a connection header, message list, composer, Send action, and close action. It also requires `open-emote-picker` and `chat-message-action` controls when the provider supports them. `send-chat-message` validates login, scope, message content, and current channel before one direct send. The prototype's "Message as Guest" placeholder does not authorize guest Twitch chat. Closed or hidden chat disconnects while playback remains. The list shows reconnect state and a gap marker after an unrecoverable replay gap. Message actions are provider-scoped and never notification actions or offline queued mutations. diff --git a/verification/catalog.json b/verification/catalog.json index 158c9457..7014b303 100644 --- a/verification/catalog.json +++ b/verification/catalog.json @@ -1252,6 +1252,134 @@ } ], "category-discovery": [ + { + "id": "change-gate", + "sourceCommit": "aa1668a4191451dffcf7926b1f9d5ee0c10be4c1", + "apkDigest": null, + "verifierVersion": "1.0.0", + "testVersion": "issue-150-d07-categories-v1", + "environment": { + "gate": "change", + "retention": "development", + "name": "hosted-workspace-change-gate" + }, + "device": { + "kind": "none", + "profile": null, + "apiLevel": null + }, + "artifacts": [ + { + "id": "categories-receipt", + "path": "verification/evidence/issue-150-categories.json", + "sha256": "sha256:e3dc021a9ab3286e60d9a168685ded61933206cbc6f2a7f0b5090b94c5c8541a", + "mediaType": "application/json" + } + ], + "result": "pass", + "observedAt": "2026-09-12T07:00:33.000Z", + "expiresAt": "2026-09-26T07:00:33.000Z", + "links": [ + "https://github.com/TheDarkSkyXD/StreamFusion/issues/150" + ] + }, + { + "id": "api30-journey", + "sourceCommit": "aa1668a4191451dffcf7926b1f9d5ee0c10be4c1", + "apkDigest": "sha256:1596d999e5ca19d5404bcfc89a5c182bad55d6397c5cb1946eb5550022844d5e", + "verifierVersion": "1.0.0", + "testVersion": "issue-150-d07-categories-v1", + "environment": { + "gate": "change", + "retention": "development", + "name": "local-adb-uiautomator-development-client-and-metro" + }, + "device": { + "kind": "emulator", + "profile": "StreamFusion Issue 142 API30", + "apiLevel": 30 + }, + "artifacts": [ + { + "id": "categories-receipt", + "path": "verification/evidence/issue-150-categories.json", + "sha256": "sha256:e3dc021a9ab3286e60d9a168685ded61933206cbc6f2a7f0b5090b94c5c8541a", + "mediaType": "application/json" + }, + { + "id": "drive-observations", + "path": "verification/evidence/issue-150/drive-observations.json", + "sha256": "sha256:17cc6a39aaaaefa1f96d066ec68e768be3fb83cf1cceba2dbbe302f2aeccaee2", + "mediaType": "application/json" + }, + { + "id": "home-open-categories", + "path": "verification/evidence/issue-150/01-home.png", + "sha256": "sha256:35c7a241f107a1e8af2ac25d57a3a320dcade18bd0d72d61d4513ac4cede72dd", + "mediaType": "image/png" + }, + { + "id": "categories-live", + "path": "verification/evidence/issue-150/02-categories-live.png", + "sha256": "sha256:2b9e8a0d3840ac994bed0d5f2aeae6904baa5e5d51e70b174e02c4ce95a9ebc5", + "mediaType": "image/png" + }, + { + "id": "categories-ready", + "path": "verification/evidence/issue-150/03-categories-ready.png", + "sha256": "sha256:e3e0c4d5d954f0a97fc0198ba29a2f1617e84c2ff031c5c697e948efba25d856", + "mediaType": "image/png" + }, + { + "id": "just-chatting-card", + "path": "verification/evidence/issue-150/04-just-chatting-card.png", + "sha256": "sha256:bc71f9628ee33eb437efc5c7a247b82d06ae5916ec4fe06ffe9ed07fc6b308ff", + "mediaType": "image/png" + }, + { + "id": "category-detail-follow", + "path": "verification/evidence/issue-150/05-category-detail.png", + "sha256": "sha256:c57c3e4a36a29a65df6188ea153628ae1ac452c87ab637acf0e2f8eeb82e9333", + "mediaType": "image/png" + }, + { + "id": "kick-videos-unsupported", + "path": "verification/evidence/issue-150/08-detail-kick-videos-scrolled.png", + "sha256": "sha256:721a66162c4272988b6a367f41cf706b1126a513d613d2b8014112f5b1a4703d", + "mediaType": "image/png" + }, + { + "id": "category-empty-filters", + "path": "verification/evidence/issue-150/10-detail-after-ready.png", + "sha256": "sha256:8e5dc5ed6fb0e7dfaca019cc71aaa0282964c010258bad01498aa63aebe954e9", + "mediaType": "image/png" + }, + { + "id": "categories-loading", + "path": "verification/evidence/issue-150/18-categories-loading-banner.png", + "sha256": "sha256:667e3f4acd508553eaa0a375d1a91467b55f08ff2ee65ac26e293a671dbba49f", + "mediaType": "image/png" + }, + { + "id": "twitch-live-fixture", + "path": "verification/evidence/issue-150/21-detail-twitch-live-stream.png", + "sha256": "sha256:6179343934f176ff1bf53f793635878bb7a1cdb81ef3992c3244b1a531f1d29e", + "mediaType": "image/png" + }, + { + "id": "kick-clips-unsupported", + "path": "verification/evidence/issue-150/22-detail-kick-clips-body.png", + "sha256": "sha256:57c690963a124996dd96c15d7cba0addfe23ee8c8a2efc273ac6c6654835f0f0", + "mediaType": "image/png" + } + ], + "result": "pass", + "observedAt": "2026-09-12T07:00:33.000Z", + "expiresAt": "2026-09-26T07:00:33.000Z", + "links": [ + "https://github.com/TheDarkSkyXD/StreamFusion/issues/150" + ] + }, { "id": "d07-guest-categories", "sourceCommit": "aa1668a4191451dffcf7926b1f9d5ee0c10be4c1", From 08559daaf6a46f7af84b9595bdbb7e2ff5d6a6d8 Mon Sep 17 00:00:00 2001 From: DarkSkyXD Date: Mon, 14 Sep 2026 07:52:25 -0500 Subject: [PATCH 2/2] fix(mobile): pin coverage matrix after guest discovery tabs The Verify mobile job failed because the ledger pin still expected 32 implemented rows after Search, Category Detail, Channel, and Following tabs were marked implemented. Co-authored-by: Cursor --- apps/mobile/tests/coverage-matrix.test.mjs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/mobile/tests/coverage-matrix.test.mjs b/apps/mobile/tests/coverage-matrix.test.mjs index 51c30500..8260c147 100644 --- a/apps/mobile/tests/coverage-matrix.test.mjs +++ b/apps/mobile/tests/coverage-matrix.test.mjs @@ -44,10 +44,10 @@ test("coverage matrix reconciles prototype, contract, and shell routes", () => { assert.equal(ledger.schemaVersion, 1); assert.equal(ledger.issue, 195); assert.equal(ledger.totals.discovered, 185); - assert.equal(ledger.totals.implemented, 32); + assert.equal(ledger.totals.implemented, 49); assert.equal(ledger.totals.partial, 14); assert.equal(ledger.totals.placeholder, 9); - assert.equal(ledger.totals.missing, 130); + assert.equal(ledger.totals.missing, 113); assert.equal(ledger.gaps.length, 5); assert.ok(ledger.gaps.some((gap) => gap.id === "GAP-195-01")); assert.ok( @@ -91,4 +91,17 @@ test("coverage matrix reconciles prototype, contract, and shell routes", () => { entry.status === "implemented", ), ); + for (const id of [ + "tab:search:all", + "tab:category-detail:clips", + "tab:channel:home", + "tab:following:live", + ]) { + assert.ok( + ledger.entries.some( + (entry) => entry.id === id && entry.status === "implemented", + ), + `${id} must stay implemented`, + ); + } });