Conversation
|
Hey @Priveetee, where would you like the group management page to live? Currently it is accessed through a Manage groups button under Subscriptions → Channels, as shown below. Would you prefer to:
Current placement (desktop preview with sample data):
|
|
Mmmh, I think that keeping it as a separate page for now makes the most sense, opened through the Manage groups button under Subscriptions → Channels. This keeps the existing Videos / Channels navigation simple and preserves Channels as the normal subscription list, while the group manager stays focused on organization. I wouldn’t replace the Channels page. We can revisit a dedicated Groups tab later if discoverability becomes an issue. For now, the current placement feels right to me. |
|
And please don't forget to split ur commit ;p |
4405d41 to
8683521
Compare
|
|
||
| export function GroupChannelList(props: Props): React.JSX.Element { | ||
| const [anchor, setAnchor] = useState<string | null>(null); | ||
| const pagination = useGroupPagination({ |
There was a problem hiding this comment.
In TypeType #172, the scope explicitly targets large subscription lists. This pagination only slices props.channels after /subscriptions/group-memberships has already returned and parsed the complete projection. The Server currently also builds that projection in memory. For those accounts this still means one large response, database work, and browser allocation before the first page is usable. Can we move page/search to the Server contract and add the corresponding OpenAPI/tests, or narrow the feature scope before merging?
There was a problem hiding this comment.
Implemented the server contract in TypeType-Server #86: SQL filtering/counting with page, limit, search and named/ungrouped/inverted membership filters, plus OpenAPI and route/service tests with 1,001 subscriptions. f14b697 and 681c587 render returned pages directly. “Select page” is explicit, and a bounded lookup refreshes selected channels retained across pages; the manager no longer downloads the complete projection.
| }; | ||
|
|
||
| export function useSubscriptionFeed(): Result { | ||
| export function useSubscriptionFeed(filter = "all"): Result { |
There was a problem hiding this comment.
The feed now accepts a filter, but the avatar fallback still calls useSubscriptions() without it. On every named-group Videos view, the page fetches the filtered subscriptions and this hook fetches the complete list again just to build avatarMap. Pass the active filter, or reuse the page query, so a group view does not double the subscription payload.
There was a problem hiding this comment.
Fixed in 3656733: the feed passes its active filter to useSubscriptions(filter), sharing the page query/cache for avatar fallback. Named and ungrouped regression cases confirm that a filtered view does not fetch the default full subscription list for avatars.
| const failed = new Set<string>(); | ||
| for (const change of changes) { | ||
| const urls = [...new Set(change.channelUrls)]; | ||
| for (let offset = 0; offset < urls.length; offset += 500) { |
There was a problem hiding this comment.
The API limit is not only 500 items; the Server also rejects bodies over 1 MiB and individual URLs over 2048 characters. Count-only chunks are not byte-safe for multibyte or escaped URLs. Please chunk by serialized byte size and cover the upper boundary, otherwise a valid large edit can fail with 413.
There was a problem hiding this comment.
Fixed in 991c863: batches are bounded by the UTF-8 byte size of the complete serialized JSON body as well as 500 URLs, and URLs over 2048 characters are rejected before sending. Tests cover multibyte and escaped URLs at the body-size boundary. The new selected-channel lookup reuses the same batch helper.
| <p role="alert" className="py-10 text-center text-sm text-fg-muted"> | ||
| {m.sg_load_error()} | ||
| </p> | ||
| ) : subscriptions.length === 0 ? ( |
There was a problem hiding this comment.
This replaces the existing empty-account state for the default all view with sg_no_channel_match(). A user with no subscriptions now sees a “no channel match” message instead of the normal empty state. Keep the old message when group === "all" and use the match message only for an actual filter.
There was a problem hiding this comment.
Fixed in 3656733: the all Channels view keeps the existing no-subscriptions message. The no-match message is used only for an actual group filter.
| {query.isLoading ? ( | ||
| <VideoGridSkeleton idPrefix="subscription-channels" /> | ||
| ) : query.isError ? ( | ||
| <p role="alert" className="py-10 text-center text-sm text-fg-muted"> |
There was a problem hiding this comment.
This error branch leaves the user with no Retry action. A transient failure on the filtered /subscriptions request can only be recovered by navigating away or waiting for cache state to change. Please expose query.refetch() here, as the Videos page now does.
There was a problem hiding this comment.
Fixed in 3656733: the Channels error state now exposes a Retry action backed by query.refetch(), with a disabled/retrying state while the request is in flight. Verified transient-failure recovery in the browser.
| > | ||
| <MoreHorizontal size={16} /> | ||
| </button> | ||
| {menuOpen && ( |
There was a problem hiding this comment.
This action popup is a fieldset with ordinary buttons and has no outside-click or focus handling. Opening more than one item can leave multiple menus open, and keyboard users do not get menu semantics. Please make it a real menu or popover with a focus boundary and close behavior.
There was a problem hiding this comment.
Fixed in 855e7a2: group actions now use menu/menuitem semantics, focus movement, Escape dismissal with focus restoration, and outside-click/focus-out closure. Opening another group’s menu closes the previous one. Checked keyboard navigation and close behavior in the browser.
| className="h-9 max-w-64 border border-border-strong bg-app px-3 text-sm text-fg focus-visible:outline-2 focus-visible:outline-accent" | ||
| > | ||
| <option value="all">{m.sg_all_subscriptions()}</option> | ||
| <option value="ungrouped">{m.groups_preview_ungrouped()}</option> |
There was a problem hiding this comment.
Production code is using groups_preview_ungrouped, a key owned by the mock/prototype flow. Reusing it couples the real manager to prototype copy and makes future wording changes affect both surfaces. Please add a manager-specific message key.
There was a problem hiding this comment.
Fixed in 3656733: production group filters and the manager use the new sg_ungrouped message, with English, French and German translations. The groups_preview_ungrouped key remains confined to the preview flow.
| selection.select(new Set()); | ||
| setOnlySelected(false); | ||
| } | ||
| async function bulk(groupId: string, action: "add" | "remove"): Promise<void> { |
There was a problem hiding this comment.
bulk() can still call updateGroupMemberships with an empty channelUrls list when all selected channels already have, or do not have, the target group. That produces a success notice for zero changes and an unnecessary invalidation. Disable the action or return early when there is no effective change.
There was a problem hiding this comment.
Fixed in 991c863: bulk add/remove returns immediately when no selected membership would change; clearing an already-ungrouped selection does the same. No request, success notice, invalidation or selection clearing is produced for a no-op. Verified the no-op path in the browser.
| value={group} | ||
| onChange={(value) => void navigate({ search: { group: value } })} | ||
| /> | ||
| {query.isLoading || isLoading ? ( |
There was a problem hiding this comment.
The old early return avoided starting the feed query for an account with no subscriptions. Now the default empty account waits for both /subscriptions and /subscriptions/feed (which may return 202) before showing the empty state. Keep the fast empty path, or short-circuit the feed when the unfiltered subscription query is empty.
There was a problem hiding this comment.
Fixed in 3656733: the feed query is gated on subscription data, and prefetch also stops when the subscription list is empty. The default empty-account view can render without waiting for /subscriptions/feed. Added regression coverage and verified that the empty Videos view makes no feed request.
| queryKey: SUBSCRIPTION_FEED_KEY, | ||
| queryFn: ({ pageParam, signal }) => fetchSubscriptionFeed(pageParam as string | null, signal), | ||
| queryKey: filter === "all" ? SUBSCRIPTION_FEED_KEY : [...SUBSCRIPTION_FEED_KEY, filter], | ||
| queryFn: ({ pageParam, signal }) => |
There was a problem hiding this comment.
The new filter changes both query keys and request parameters, but the added hook tests only exercise the default all path. Please add coverage for named and ungrouped filters, including independent cursors, so a future change cannot cross-contaminate feed caches.
There was a problem hiding this comment.
Added named-group and ungrouped coverage in 3656733, including query parameters, independent feed cursors and avatar-query reuse. The tests exercise the shared query options used by the hook and verify that one filter cannot advance or overwrite another filter’s cached feed.
Priveetee
left a comment
There was a problem hiding this comment.
I went through the PR again. I like the direction of the UI and the amount of work that went into it! :)
There are two important points I’d like to sort out before merging.
The first one is the channel-list pagination. The UI only slices props.channels after /subscriptions/group-memberships has already returned and parsed the complete membership projection. The Server also builds that complete projection in memory before sending it. With a large subscription list, the first page still waits for the full response, all database work, and all browser allocations. Changing page only changes which already-loaded rows are rendered.
This matters for TypeType #172, which is specifically about managing large subscription lists. Could we move page, limit, search, and group-membership filtering into the Server contract, return the requested page with its total count, and add the matching OpenAPI and route/service tests? The frontend could then render the returned page directly instead of paginating a complete in-memory projection.
I’d also like to keep this PR focused on the actual frontend implementation and its tests. Could you remove all Markdown files introduced by the PR: DESIGN.md, PRODUCT.md, docs/subscription-groups-fixture.md, and docs/subscription-groups-ux.md? I don’t want any new .md files in this PR. They contain planning notes, design summaries, fixture instructions, and verification claims rather than files required for the feature itself. Some of the claims are also ahead of the current implementation, especially the statement that selection works across every page while the complete list is still loaded up front.
Once the pagination contract is handled on the Server side and the extra Markdown files are removed, I’ll gladly review the PR again! ;)
If you need a hand with the Server-side code or the integration, don’t hesitate to ask, I’ll be happy to help! :)
|
Hey @Priveetee, I marked this ready for review too early by mistake—sorry for the premature signal. It is back in draft while the coordinated frontend/server change is reviewed. Following your latest review:
Frontend checks passed (376 tests), and the server checks passed (1,248 tests, 3 skipped). Firefox/WebKit and a browser session against the running Kotlin server still need verification. Latest desktop UI at 1280 × 720, using the disposable 150-channel fixture: |
|
oki doki np, take ur time :) |


Summary
Add a desktop subscription group manager at
/subscriptions/groups, opened from Subscriptions → Channels → Manage groups, following the placement discussion.Refs TypeType #172. Requires TypeType-Server #86 for paginated membership reads and selected-channel refreshes. This PR remains a draft while the coordinated change is reviewed.
bun run dev:groups-fixture) with overlapping memberships and the paginated contract. No new Markdown files are included.Scope and acceptance criteria
Desktop group CRUD, inline/bulk membership editing, server pagination/search, retained selections and error recovery are included. Compact mobile composition and organization of only newly imported channels are explicitly deferred; the post-import link opens the full manager. The stacked narrow-screen fallback is not the proposed mobile workflow. This is a partial implementation of #172 and does not close that issue.
Validation
bun run check,bun run test(378 passed),bun run knip,bun run sherif,bun run buildandgit diff --checkpassed.Latest desktop preview
Disposable fixture data, with Cooking selected, its matching bulk target and the inline group combobox open.