From 3b892f4114bccf20e1d0ad4d1b7a0f337ee96715 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 27 Sep 2026 09:03:07 +0200 Subject: [PATCH] fix(settings): drop withheld Stalker search rows at relock time and keep the M3U unlock row when every group is locked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A relock during a page-1 Stalker search now filters the rows already on screen at once, so old unlocked results are not clickable while the replacement page is pending. - When every M3U group is locked the groups rail still renders, with its "N locked · Enter PIN to show" row, instead of the plain empty state. Co-Authored-By: Claude Opus 5.5 --- docs/architecture/parental-lock.md | 4 ++ .../stalker-search.component.spec.ts | 18 +++++++++ .../stalker-search.component.ts | 38 ++++++++++++------- .../groups-view/groups-view.component.html | 2 +- .../groups-view/groups-view.component.spec.ts | 15 ++++++++ 5 files changed, 63 insertions(+), 14 deletions(-) diff --git a/docs/architecture/parental-lock.md b/docs/architecture/parental-lock.md index c448cb308..01aa1226c 100644 --- a/docs/architecture/parental-lock.md +++ b/docs/architecture/parental-lock.md @@ -255,6 +255,10 @@ on either side. `stalkerWithheldRowKey` (`id`, else `stream_id`/`movie_id`/`series_id`, else the row's `cmd`/`name`), never by a shared `''`, so a page of new locked rows is not mistaken for a stalled portal. +- **Stalker search relock:** a lock change drops the withheld rows already + on screen at once (`applyRelockToResults`, page 1 included) and closes an + open detail of a now-withheld genre, before the replacement page is + awaited — otherwise they stay clickable while that request is pending. - **Stalker search staleness:** portal requests are not aborted, so a search page issued before a relock can finish after it, filtered with the pre-relock withheld set; `isStalkerSearchRequestCurrent` keys the diff --git a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.spec.ts b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.spec.ts index 1b260a890..51b4b20c8 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.spec.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.spec.ts @@ -460,6 +460,24 @@ describe('StalkerSearchComponent result paging', () => { expect(component.searchPage()).toBe(pageBefore + 1); }); + it('drops withheld rows on screen at relock time, page 1 included', () => { + component.applySearchPageSuccess( + 1, + [ + { id: 'news-1', name: 'News', category_id: '5' }, + { id: 'adult-1', name: 'Adult', category_id: '9' }, + ], + 2 + ); + expect(component.searchResults()).toHaveLength(2); + + component.applyRelockToResults(new Set(['9']), 'vod'); + + expect(component.searchResults().map((item) => item.id)).toEqual([ + 'news-1', + ]); + }); + it('closes an open detail whose genre became withheld on relock', () => { component.selectItem({ id: 'adult-9', name: 'A', category_id: '9' }); expect(component.itemDetails()).not.toBeNull(); diff --git a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts index 3406ae9df..4d71c418e 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts @@ -297,22 +297,13 @@ export class StalkerSearchComponent { this.searchResultsLockVersion !== params.parentalLockVersion; this.searchResultsLockVersion = params.parentalLockVersion; if (lockVersionChanged) { - // A relock must also close an open detail of a genre that is - // withheld now; the list alone hiding it is not enough. - this.closeWithheldDetail(withheldCategoryIds); + this.applyRelockToResults(withheldCategoryIds, contentType); } if (lockVersionChanged && params.page > 1) { - // A lock flip past page 1: drop the withheld rows on screen - // and rebuild from page 1 rather than appending to pages - // accumulated under the old lock state. - const retained = withoutWithheldStalkerItems( - this.accumulatedSearchResults(), - contentType, - withheldCategoryIds - ); - this.accumulatedSearchResults.set(retained); + // A lock flip past page 1: rebuild from page 1 rather than + // appending to pages accumulated under the old lock state. this.searchPage.set(1); - return retained; + return this.accumulatedSearchResults(); } const withheldKey = JSON.stringify([ params.playlistId, @@ -743,6 +734,27 @@ export class StalkerSearchComponent { * lock (Lock now, idle relock): the title, its playback actions and the * store's selected item must not outlive the list row. */ + /** + * A lock change reached the results on screen, which were read under + * the old lock state: close an open detail of a now-withheld genre (the + * list hiding it is not enough) and drop the withheld rows NOW, before + * the replacement page is awaited — on page 1 too, or they stay + * clickable while (or, if it hangs, after) that request is pending. + */ + applyRelockToResults( + withheldCategoryIds: ReadonlySet, + contentType: Parameters[1] + ): void { + this.closeWithheldDetail(withheldCategoryIds); + this.accumulatedSearchResults.set( + withoutWithheldStalkerItems( + this.accumulatedSearchResults(), + contentType, + withheldCategoryIds + ) + ); + } + closeWithheldDetail(withheldCategoryIds: ReadonlySet): void { const details = this.itemDetails(); const categoryId = details?.category_id; diff --git a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.html b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.html index c04e800a1..ce685206a 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.html +++ b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.html @@ -1,4 +1,4 @@ -@if (!hasAnyGroups()) { +@if (!hasAnyGroups() && withheldGroupCount() === 0) {
{{ searchTerm() ? 'search_off' : 'folder_off' diff --git a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts index 26f92865a..ddb7d3a4b 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts +++ b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.spec.ts @@ -580,6 +580,21 @@ describe('GroupsViewComponent', () => { expect(parentalLock.requestUnlock).toHaveBeenCalled(); }); + it('keeps the rail and its unlock row when every group is withheld', () => { + fixture.componentRef.setInput('groupedChannels', {}); + fixture.componentRef.setInput('withheldGroupCount', 3); + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector('.groups-view-empty-state') + ).toBeNull(); + expect( + fixture.nativeElement.querySelector( + '[data-test-id="groups-locked-row"]' + ) + ).not.toBeNull(); + }); + it('offers no group lock menu while the feature is off', () => { const event = new MouseEvent('contextmenu', { cancelable: true }); component.onGroupContextMenu('Sports', event);