Skip to content

Commit d08e5ee

Browse files
committed
fix(feed): refresh feed correctly on filter change/reset
- Use switchMap instead of concatMap so the latest filter wins and stale requests are cancelled - Dedupe paginated items by typeModel:id (ids can collide across types) - Re-observe items after initial load and filter change - Remove partnerprogram type/field: program news must stay out of the general feed
1 parent 0f47575 commit d08e5ee

5 files changed

Lines changed: 38 additions & 29 deletions

File tree

projects/social_platform/src/app/api/feed/facades/feed-info.service.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Observable,
1111
skip,
1212
Subject,
13+
switchMap,
1314
takeUntil,
1415
tap,
1516
throttleTime,
@@ -23,7 +24,7 @@ import { ToggleFeedLikeUseCase } from "../use-cases/toggle-feed-like.use-case";
2324
import { isSuccess, loading, success } from "@domain/shared/async-state";
2425
import { FILTER_SPLIT_SYMBOL } from "@core/consts/other/filter-split-symbol.const";
2526

26-
const DEFAULT_FEED_TYPES: FeedItemType[] = ["vacancy", "project", "news", "partnerprogram"];
27+
const DEFAULT_FEED_TYPES: FeedItemType[] = ["vacancy", "project", "news"];
2728

2829
/** Координирует глобальную ленту: данные резолвера, фильтры, пагинация, просмотры, лайки. */
2930
@Injectable()
@@ -39,7 +40,7 @@ export class FeedInfoService {
3940

4041
readonly feedItems = this.feedUIInfoService.feedItems;
4142

42-
private readonly includes = signal<FeedItemType[]>([]);
43+
private readonly includes = signal<FeedItemType[] | string>([]);
4344

4445
initializationFeedNews(feedRoot: ElementRef<HTMLElement>): void {
4546
this.route.data
@@ -56,6 +57,8 @@ export class FeedInfoService {
5657
root: document.querySelector(".office__body"),
5758
threshold: 0,
5859
});
60+
61+
queueMicrotask(() => this.observeFeedItems());
5962
});
6063

6164
this.route.queryParams
@@ -65,7 +68,7 @@ export class FeedInfoService {
6568
this.includes.set(includes);
6669
}),
6770
skip(1),
68-
concatMap(includes => {
71+
switchMap(includes => {
6972
this.feedUIInfoService.totalItemsCount.set(0);
7073
this.feedUIInfoService.feedPage.set(0);
7174

@@ -83,6 +86,8 @@ export class FeedInfoService {
8386
.subscribe(feed => {
8487
this.feedUIInfoService.applyFeedFilters(feed);
8588

89+
queueMicrotask(() => this.observeFeedItems());
90+
8691
setTimeout(() => {
8792
feedRoot?.nativeElement.children[0].scrollIntoView({ behavior: "smooth" });
8893
});
@@ -110,8 +115,9 @@ export class FeedInfoService {
110115
this.includes()
111116
).pipe(
112117
tap((feedChunk: FeedItem[]) => {
113-
const existingIds = new Set(this.feedItems().map(item => item.content.id));
114-
const uniqueNewItems = feedChunk.filter(item => !existingIds.has(item.content.id));
118+
const keyOf = (item: FeedItem) => `${item.typeModel}:${item.content.id}`;
119+
const existingIds = new Set(this.feedItems().map(keyOf));
120+
const uniqueNewItems = feedChunk.filter(item => !existingIds.has(keyOf(item)));
115121

116122
if (uniqueNewItems.length > 0) {
117123
this.feedUIInfoService.feedPage.update(page => page + uniqueNewItems.length);
@@ -141,9 +147,15 @@ export class FeedInfoService {
141147
}
142148
}
143149

144-
private onFetch(offset: number, limit: number, includes: FeedItemType[] = DEFAULT_FEED_TYPES) {
150+
private onFetch(
151+
offset: number,
152+
limit: number,
153+
includes: FeedItemType[] | string = DEFAULT_FEED_TYPES
154+
) {
145155
const type =
146-
includes.length === 0
156+
typeof includes === "string"
157+
? includes || DEFAULT_FEED_TYPES.join(FILTER_SPLIT_SYMBOL)
158+
: includes.length === 0
147159
? DEFAULT_FEED_TYPES.join(FILTER_SPLIT_SYMBOL)
148160
: includes.join(FILTER_SPLIT_SYMBOL);
149161

projects/social_platform/src/app/domain/feed/feed-item.model.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/** @format */
22

3-
import { Program } from "../program/program.model";
43
import { FeedNews } from "../news/project-news.model";
54
import { Vacancy } from "../vacancy/vacancy.model";
65

@@ -14,14 +13,10 @@ export interface FeedProject {
1413
imageAddress: string;
1514
viewsCount: number;
1615
leader: number;
17-
partnerProgram: {
18-
id: Program["id"];
19-
name: Program["name"];
20-
} | null;
2116
}
2217

2318
/** Тип элемента ленты */
24-
export type FeedItemType = "vacancy" | "news" | "project" | "partnerprogram";
19+
export type FeedItemType = "vacancy" | "news" | "project";
2520

2621
/** Объединенный тип элемента ленты */
2722
export type FeedItem =

projects/social_platform/src/app/ui/pages/feed/feed.component.html

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,16 @@
1616
[attr.data-id]="item.content.id"
1717
[feedItem]="item.content"
1818
></app-new-project>
19-
} @else if (item.typeModel === "news") { @if (item.content.contentObject &&
20-
item.content.contentObject.hasOwnProperty("email")) {
19+
} @else if (item.typeModel === "news") {
2120
<app-news-card
2221
class="page__item"
2322
[attr.data-id]="item.content.id"
2423
[feedItem]="item.content"
2524
[contentId]="item.content.contentObject.id"
26-
[resourceLink]="[AppRoutes.profile.detail(item.content.contentObject.id)]"
25+
[resourceLink]="resourceLink(item.content)"
2726
(like)="onLike($event)"
2827
></app-news-card>
29-
} @else if ( item.content.contentObject && !item.content.contentObject.hasOwnProperty("email") )
30-
{
31-
<app-news-card
32-
class="page__item"
33-
[attr.data-id]="item.content.id"
34-
[feedItem]="item.content"
35-
[contentId]="item.content.contentObject.id"
36-
[resourceLink]="[AppRoutes.projects.detail(item.content.contentObject.id)]"
37-
(like)="onLike($event)"
38-
></app-news-card>
39-
} } } } @else {
28+
} } } @else {
4029
<div class="page__feed--no-items">
4130
<i appIcon icon="empty-chat" appSquare="38"></i>
4231
<p class="text-body-12">в данном разделе пока нет новостей</p>

projects/social_platform/src/app/ui/pages/feed/feed.component.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
AfterViewInit,
55
ChangeDetectionStrategy,
66
Component,
7+
computed,
78
ElementRef,
89
inject,
910
OnDestroy,
@@ -48,6 +49,15 @@ export class FeedComponent implements OnInit, AfterViewInit, OnDestroy {
4849
protected readonly feedItems = this.feedUIInfoService.feedItems;
4950
protected readonly AppRoutes = AppRoutes;
5051

52+
/** Ссылка-маршрут для карточки новости в зависимости от типа источника. */
53+
protected resourceLink(content: any): (string | number)[] {
54+
if (content.contentObject && "email" in content.contentObject) {
55+
return [AppRoutes.profile.detail(content.contentObject.id)];
56+
}
57+
58+
return [AppRoutes.projects.detail(content.contentObject.id)];
59+
}
60+
5161
ngOnInit() {
5262
this.feedInfoService.initializationFeedNews(this.feedRoot!);
5363
}

projects/social_platform/src/app/ui/pages/feed/feed.resolver.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import { inject } from "@angular/core";
44
import { ResolveFn } from "@angular/router";
55
import { map } from "rxjs";
66
import { ApiPagination } from "@domain/other/api-pagination.model";
7-
import { FeedItem } from "@domain/feed/feed-item.model";
7+
import { FeedItem, FeedItemType } from "@domain/feed/feed-item.model";
88
import { FetchFeedUseCase } from "@api/feed/use-cases/fetch-feed.use-case";
9+
import { FILTER_SPLIT_SYMBOL } from "@core/consts/other/filter-split-symbol.const";
10+
11+
const DEFAULT_FEED_TYPES: FeedItemType[] = ["vacancy", "news", "project"];
912

1013
/** Предзагружает ленту новостей. */
1114
export const FeedResolver: ResolveFn<ApiPagination<FeedItem>> = route => {
@@ -14,7 +17,7 @@ export const FeedResolver: ResolveFn<ApiPagination<FeedItem>> = route => {
1417
// Загружаем первую страницу ленты (offset: 0, limit: 20)
1518
// По умолчанию включаем вакансии, новости и проекты
1619
return fetchFeedUseCase
17-
.execute(0, 20, route.queryParams["includes"] ?? ["vacancy", "news", "project"])
20+
.execute(0, 20, route.queryParams["includes"] ?? DEFAULT_FEED_TYPES.join(FILTER_SPLIT_SYMBOL))
1821
.pipe(
1922
map(result =>
2023
result.ok

0 commit comments

Comments
 (0)