mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(dashboard): localize progress labels
This commit is contained in:
1 parent
2ecb5c28f4
commit
5cfe3aef06
21 files changed
+152
-20
No files matched your search
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "متابعة المشاهدة",
|
||||
"LIVE_FAVORITES": "يُبث الآن على المفضلة لديك",
|
||||
"LIVE_RECENT": "متابعة البث المباشر",
|
||||
"REMAINING_SECONDS": "متبقٍ {{seconds}} ث",
|
||||
"REMAINING_MINUTES": "متبقٍ {{minutes}} د",
|
||||
"REMAINING_HOURS": "متبقٍ {{hours}} س",
|
||||
"REMAINING_HOURS_MINUTES": "متبقٍ {{hours}} س {{minutes}} د",
|
||||
"PERCENT_WATCHED": "تمت مشاهدة {{value}}%",
|
||||
"UNTITLED_SOURCE": "مصدر بدون عنوان",
|
||||
"GLOBAL_FAVORITES": "المفضلة العامة",
|
||||
"REMOVE_FROM_HISTORY": "إزالة من السجل",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "كمل المشاهدة",
|
||||
"LIVE_FAVORITES": "مباشر دابا على المفضلة ديالك",
|
||||
"LIVE_RECENT": "كمل مع المباشر",
|
||||
"REMAINING_SECONDS": "باقي {{seconds}} ث",
|
||||
"REMAINING_MINUTES": "باقي {{minutes}} د",
|
||||
"REMAINING_HOURS": "باقي {{hours}} س",
|
||||
"REMAINING_HOURS_MINUTES": "باقي {{hours}} س {{minutes}} د",
|
||||
"PERCENT_WATCHED": "تشاهد {{value}}%",
|
||||
"UNTITLED_SOURCE": "مصدر بلا عنوان",
|
||||
"GLOBAL_FAVORITES": "المفضلة العامة",
|
||||
"REMOVE_FROM_HISTORY": "حيد من السجل",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "Працягнуць прагляд",
|
||||
"LIVE_FAVORITES": "Цяпер у эфіры на выбраных",
|
||||
"LIVE_RECENT": "Працягнуць прагляд эфіру",
|
||||
"REMAINING_SECONDS": "Засталося {{seconds}} с",
|
||||
"REMAINING_MINUTES": "Засталося {{minutes}} хв",
|
||||
"REMAINING_HOURS": "Засталося {{hours}} г",
|
||||
"REMAINING_HOURS_MINUTES": "Засталося {{hours}} г {{minutes}} хв",
|
||||
"PERCENT_WATCHED": "Прагледжана {{value}}%",
|
||||
"UNTITLED_SOURCE": "Крыніца без назвы",
|
||||
"GLOBAL_FAVORITES": "Усе фаварыты",
|
||||
"REMOVE_FROM_HISTORY": "Выдаліць з гісторыі",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "Weiter schauen",
|
||||
"LIVE_FAVORITES": "Jetzt live auf Ihren Favoriten",
|
||||
"LIVE_RECENT": "Mit Live-TV weiterschauen",
|
||||
"REMAINING_SECONDS": "Noch {{seconds}} s",
|
||||
"REMAINING_MINUTES": "Noch {{minutes}} Min.",
|
||||
"REMAINING_HOURS": "Noch {{hours}} Std.",
|
||||
"REMAINING_HOURS_MINUTES": "Noch {{hours}} Std. {{minutes}} Min.",
|
||||
"PERCENT_WATCHED": "{{value}}% angesehen",
|
||||
"UNTITLED_SOURCE": "Unbenannte Quelle",
|
||||
"GLOBAL_FAVORITES": "Globale Favoriten",
|
||||
"REMOVE_FROM_HISTORY": "Aus Verlauf entfernen",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "Συνέχιση παρακολούθησης",
|
||||
"LIVE_FAVORITES": "Ζωντανά τώρα στα αγαπημένα σας",
|
||||
"LIVE_RECENT": "Συνέχιση με ζωντανή TV",
|
||||
"REMAINING_SECONDS": "Απομένουν {{seconds}} δ",
|
||||
"REMAINING_MINUTES": "Απομένουν {{minutes}} λ",
|
||||
"REMAINING_HOURS": "Απομένουν {{hours}} ώ",
|
||||
"REMAINING_HOURS_MINUTES": "Απομένουν {{hours}} ώ {{minutes}} λ",
|
||||
"PERCENT_WATCHED": "Προβλήθηκε {{value}}%",
|
||||
"UNTITLED_SOURCE": "Πηγή χωρίς τίτλο",
|
||||
"GLOBAL_FAVORITES": "Βασικά Αγαπημένα",
|
||||
"REMOVE_FROM_HISTORY": "Αφαίρεση από το ιστορικό",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "Continue Watching",
|
||||
"LIVE_FAVORITES": "Live now on your favorites",
|
||||
"LIVE_RECENT": "Continue with live TV",
|
||||
"REMAINING_SECONDS": "{{seconds}}s left",
|
||||
"REMAINING_MINUTES": "{{minutes}}m left",
|
||||
"REMAINING_HOURS": "{{hours}}h left",
|
||||
"REMAINING_HOURS_MINUTES": "{{hours}}h {{minutes}}m left",
|
||||
"PERCENT_WATCHED": "{{value}}% watched",
|
||||
"UNTITLED_SOURCE": "Untitled source",
|
||||
"GLOBAL_FAVORITES": "Global Favorites",
|
||||
"REMOVE_FROM_HISTORY": "Remove from history",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "Continuar viendo",
|
||||
"LIVE_FAVORITES": "Ahora en vivo en tus favoritos",
|
||||
"LIVE_RECENT": "Continuar con TV en vivo",
|
||||
"REMAINING_SECONDS": "Quedan {{seconds}} s",
|
||||
"REMAINING_MINUTES": "Quedan {{minutes}} min",
|
||||
"REMAINING_HOURS": "Quedan {{hours}} h",
|
||||
"REMAINING_HOURS_MINUTES": "Quedan {{hours}} h {{minutes}} min",
|
||||
"PERCENT_WATCHED": "{{value}}% visto",
|
||||
"UNTITLED_SOURCE": "Fuente sin título",
|
||||
"GLOBAL_FAVORITES": "Favoritos globales",
|
||||
"REMOVE_FROM_HISTORY": "Quitar del historial",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "Reprendre la lecture",
|
||||
"LIVE_FAVORITES": "En direct sur vos favoris",
|
||||
"LIVE_RECENT": "Reprendre la TV en direct",
|
||||
"REMAINING_SECONDS": "Il reste {{seconds}} s",
|
||||
"REMAINING_MINUTES": "Il reste {{minutes}} min",
|
||||
"REMAINING_HOURS": "Il reste {{hours}} h",
|
||||
"REMAINING_HOURS_MINUTES": "Il reste {{hours}} h {{minutes}} min",
|
||||
"PERCENT_WATCHED": "{{value}}% regardé",
|
||||
"UNTITLED_SOURCE": "Source sans titre",
|
||||
"GLOBAL_FAVORITES": "Favoris globaux",
|
||||
"REMOVE_FROM_HISTORY": "Retirer de l'historique",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "Continua a guardare",
|
||||
"LIVE_FAVORITES": "Live ora sui tuoi preferiti",
|
||||
"LIVE_RECENT": "Continua con la TV live",
|
||||
"REMAINING_SECONDS": "Mancano {{seconds}} s",
|
||||
"REMAINING_MINUTES": "Mancano {{minutes}} min",
|
||||
"REMAINING_HOURS": "Mancano {{hours}} h",
|
||||
"REMAINING_HOURS_MINUTES": "Mancano {{hours}} h {{minutes}} min",
|
||||
"PERCENT_WATCHED": "{{value}}% visto",
|
||||
"UNTITLED_SOURCE": "Sorgente senza titolo",
|
||||
"GLOBAL_FAVORITES": "Preferiti globali",
|
||||
"REMOVE_FROM_HISTORY": "Rimuovi dalla cronologia",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "視聴を再開",
|
||||
"LIVE_FAVORITES": "お気に入りで現在放送中",
|
||||
"LIVE_RECENT": "ライブTVの視聴を再開",
|
||||
"REMAINING_SECONDS": "残り{{seconds}}秒",
|
||||
"REMAINING_MINUTES": "残り{{minutes}}分",
|
||||
"REMAINING_HOURS": "残り{{hours}}時間",
|
||||
"REMAINING_HOURS_MINUTES": "残り{{hours}}時間{{minutes}}分",
|
||||
"PERCENT_WATCHED": "{{value}}% 視聴済み",
|
||||
"UNTITLED_SOURCE": "無題のソース",
|
||||
"GLOBAL_FAVORITES": "グローバルお気に入り",
|
||||
"REMOVE_FROM_HISTORY": "履歴から削除",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "이어서 시청",
|
||||
"LIVE_FAVORITES": "즐겨찾기에서 지금 라이브 중인 채널입니다",
|
||||
"LIVE_RECENT": "라이브 TV 이어서 시청",
|
||||
"REMAINING_SECONDS": "{{seconds}}초 남음",
|
||||
"REMAINING_MINUTES": "{{minutes}}분 남음",
|
||||
"REMAINING_HOURS": "{{hours}}시간 남음",
|
||||
"REMAINING_HOURS_MINUTES": "{{hours}}시간 {{minutes}}분 남음",
|
||||
"PERCENT_WATCHED": "{{value}}% 시청함",
|
||||
"UNTITLED_SOURCE": "제목 없는 소스",
|
||||
"GLOBAL_FAVORITES": "전체 즐겨찾기",
|
||||
"REMOVE_FROM_HISTORY": "기록에서 제거",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "Verder kijken",
|
||||
"LIVE_FAVORITES": "Nu live op je favorieten",
|
||||
"LIVE_RECENT": "Verder kijken met live tv",
|
||||
"REMAINING_SECONDS": "Nog {{seconds}} sec",
|
||||
"REMAINING_MINUTES": "Nog {{minutes}} min",
|
||||
"REMAINING_HOURS": "Nog {{hours}} u",
|
||||
"REMAINING_HOURS_MINUTES": "Nog {{hours}} u {{minutes}} min",
|
||||
"PERCENT_WATCHED": "{{value}}% bekeken",
|
||||
"UNTITLED_SOURCE": "Naamloze bron",
|
||||
"GLOBAL_FAVORITES": "Algemene favorieten",
|
||||
"REMOVE_FROM_HISTORY": "Verwijderen uit geschiedenis",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "Kontynuuj oglądanie",
|
||||
"LIVE_FAVORITES": "Na żywo w Twoich ulubionych",
|
||||
"LIVE_RECENT": "Kontynuuj oglądanie na żywo",
|
||||
"REMAINING_SECONDS": "Pozostało {{seconds}} s",
|
||||
"REMAINING_MINUTES": "Pozostało {{minutes}} min",
|
||||
"REMAINING_HOURS": "Pozostało {{hours}} godz.",
|
||||
"REMAINING_HOURS_MINUTES": "Pozostało {{hours}} godz. {{minutes}} min",
|
||||
"PERCENT_WATCHED": "Obejrzano {{value}}%",
|
||||
"UNTITLED_SOURCE": "Źródło bez tytułu",
|
||||
"GLOBAL_FAVORITES": "Globalne ulubione",
|
||||
"REMOVE_FROM_HISTORY": "Usuń z historii",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "Continuar assistindo",
|
||||
"LIVE_FAVORITES": "Ao vivo agora nos seus favoritos",
|
||||
"LIVE_RECENT": "Continuar com TV ao vivo",
|
||||
"REMAINING_SECONDS": "Restam {{seconds}} s",
|
||||
"REMAINING_MINUTES": "Restam {{minutes}} min",
|
||||
"REMAINING_HOURS": "Restam {{hours}} h",
|
||||
"REMAINING_HOURS_MINUTES": "Restam {{hours}} h {{minutes}} min",
|
||||
"PERCENT_WATCHED": "{{value}}% assistido",
|
||||
"UNTITLED_SOURCE": "Fonte sem título",
|
||||
"GLOBAL_FAVORITES": "Favoritos globais",
|
||||
"REMOVE_FROM_HISTORY": "Remover do histórico",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "Продолжить просмотр",
|
||||
"LIVE_FAVORITES": "Сейчас в эфире на избранных каналах",
|
||||
"LIVE_RECENT": "Продолжить просмотр прямого эфира",
|
||||
"REMAINING_SECONDS": "Осталось {{seconds}} с",
|
||||
"REMAINING_MINUTES": "Осталось {{minutes}} мин",
|
||||
"REMAINING_HOURS": "Осталось {{hours}} ч",
|
||||
"REMAINING_HOURS_MINUTES": "Осталось {{hours}} ч {{minutes}} мин",
|
||||
"PERCENT_WATCHED": "Просмотрено {{value}}%",
|
||||
"UNTITLED_SOURCE": "Источник без названия",
|
||||
"GLOBAL_FAVORITES": "Глобальные избранные",
|
||||
"REMOVE_FROM_HISTORY": "Удалить из истории",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "İzlemeye Devam Et",
|
||||
"LIVE_FAVORITES": "Favorilerinizde şu an canlı",
|
||||
"LIVE_RECENT": "Canlı TV ile devam et",
|
||||
"REMAINING_SECONDS": "{{seconds}} sn kaldı",
|
||||
"REMAINING_MINUTES": "{{minutes}} dk kaldı",
|
||||
"REMAINING_HOURS": "{{hours}} sa kaldı",
|
||||
"REMAINING_HOURS_MINUTES": "{{hours}} sa {{minutes}} dk kaldı",
|
||||
"PERCENT_WATCHED": "{{value}}% izlendi",
|
||||
"UNTITLED_SOURCE": "Adsız kaynak",
|
||||
"GLOBAL_FAVORITES": "Tüm Favoriler",
|
||||
"REMOVE_FROM_HISTORY": "Geçmişten kaldır",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "继续观看",
|
||||
"LIVE_FAVORITES": "收藏频道正在播放",
|
||||
"LIVE_RECENT": "继续观看直播",
|
||||
"REMAINING_SECONDS": "剩余 {{seconds}} 秒",
|
||||
"REMAINING_MINUTES": "剩余 {{minutes}} 分钟",
|
||||
"REMAINING_HOURS": "剩余 {{hours}} 小时",
|
||||
"REMAINING_HOURS_MINUTES": "剩余 {{hours}} 小时 {{minutes}} 分钟",
|
||||
"PERCENT_WATCHED": "已观看 {{value}}%",
|
||||
"UNTITLED_SOURCE": "未命名源",
|
||||
"GLOBAL_FAVORITES": "全局收藏",
|
||||
"REMOVE_FROM_HISTORY": "从历史记录中移除",
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
"CONTINUE_WATCHING": "繼續觀看",
|
||||
"LIVE_FAVORITES": "我的最愛正在直播",
|
||||
"LIVE_RECENT": "繼續觀看直播電視",
|
||||
"REMAINING_SECONDS": "剩餘 {{seconds}} 秒",
|
||||
"REMAINING_MINUTES": "剩餘 {{minutes}} 分鐘",
|
||||
"REMAINING_HOURS": "剩餘 {{hours}} 小時",
|
||||
"REMAINING_HOURS_MINUTES": "剩餘 {{hours}} 小時 {{minutes}} 分鐘",
|
||||
"PERCENT_WATCHED": "已觀看 {{value}}%",
|
||||
"UNTITLED_SOURCE": "未命名的來源",
|
||||
"GLOBAL_FAVORITES": "全域我的最愛",
|
||||
"REMOVE_FROM_HISTORY": "從歷史記錄中移除",
|
||||
|
||||
+8
-2
@@ -85,12 +85,18 @@
|
||||
</span>
|
||||
<span class="hero__progress-meta">
|
||||
@if (item.remainingLabel) {
|
||||
<span>{{ item.remainingLabel }}</span>
|
||||
<span>{{
|
||||
item.remainingLabel.key
|
||||
| translate: item.remainingLabel.params
|
||||
}}</span>
|
||||
<span class="hero__progress-meta-sep"
|
||||
>·</span
|
||||
>
|
||||
}
|
||||
<span>{{ item.watchProgress }}% watched</span>
|
||||
<span>{{
|
||||
'WORKSPACE.DASHBOARD.PERCENT_WATCHED'
|
||||
| translate: { value: item.watchProgress }
|
||||
}}</span>
|
||||
</span>
|
||||
}
|
||||
|
||||
|
||||
+22
-7
@@ -349,40 +349,55 @@ describe('playback-position helpers', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('formats sub-minute, minute, and hour-spanning remainders', () => {
|
||||
it('returns translation keys and params for sub-minute, minute, and hour-spanning remainders', () => {
|
||||
expect(
|
||||
formatRemainingLabel({
|
||||
positionSeconds: 5970,
|
||||
durationSeconds: 6000,
|
||||
})
|
||||
).toBe('30s left');
|
||||
).toEqual({
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_SECONDS',
|
||||
params: { seconds: 30 },
|
||||
});
|
||||
expect(
|
||||
formatRemainingLabel({
|
||||
positionSeconds: 0,
|
||||
durationSeconds: 1800,
|
||||
})
|
||||
).toBe('30m left');
|
||||
).toEqual({
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_MINUTES',
|
||||
params: { minutes: 30 },
|
||||
});
|
||||
expect(
|
||||
formatRemainingLabel({
|
||||
positionSeconds: 0,
|
||||
durationSeconds: 3600,
|
||||
})
|
||||
).toBe('1h left');
|
||||
).toEqual({
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_HOURS',
|
||||
params: { hours: 1 },
|
||||
});
|
||||
expect(
|
||||
formatRemainingLabel({
|
||||
positionSeconds: 600,
|
||||
durationSeconds: 6840,
|
||||
})
|
||||
).toBe('1h 44m left');
|
||||
).toEqual({
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_HOURS_MINUTES',
|
||||
params: { hours: 1, minutes: 44 },
|
||||
});
|
||||
});
|
||||
|
||||
it('clamps below zero — a completed program reads as "0s left" not negative', () => {
|
||||
it('clamps below zero to the translated zero-seconds state', () => {
|
||||
expect(
|
||||
formatRemainingLabel({
|
||||
positionSeconds: 7000,
|
||||
durationSeconds: 6000,
|
||||
})
|
||||
).toBe('0s left');
|
||||
).toEqual({
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_SECONDS',
|
||||
params: { seconds: 0 },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+32
-11
@@ -76,8 +76,12 @@ interface DashboardHeroModel {
|
||||
readonly title: string;
|
||||
/** 0–100 watched, when a resume position is known. */
|
||||
readonly watchProgress?: number | null;
|
||||
/** "1h 04m left" — pre-formatted for direct rendering. */
|
||||
readonly remainingLabel?: string | null;
|
||||
readonly remainingLabel?: DashboardRemainingLabel | null;
|
||||
}
|
||||
|
||||
export interface DashboardRemainingLabel {
|
||||
readonly key: string;
|
||||
readonly params: Record<string, number>;
|
||||
}
|
||||
|
||||
export type DashboardHeroBackdropSource = 'backdrop' | 'poster' | 'fallback';
|
||||
@@ -334,7 +338,7 @@ export function playbackProgressPercent(
|
||||
|
||||
export function formatRemainingLabel(
|
||||
position: { positionSeconds: number; durationSeconds?: number } | null
|
||||
): string | null {
|
||||
): DashboardRemainingLabel | null {
|
||||
if (
|
||||
!position ||
|
||||
position.durationSeconds == null ||
|
||||
@@ -347,18 +351,30 @@ export function formatRemainingLabel(
|
||||
Math.round(position.durationSeconds - position.positionSeconds)
|
||||
);
|
||||
if (remaining < 60) {
|
||||
return `${remaining}s left`;
|
||||
return {
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_SECONDS',
|
||||
params: { seconds: remaining },
|
||||
};
|
||||
}
|
||||
const totalMinutes = Math.round(remaining / 60);
|
||||
if (totalMinutes < 60) {
|
||||
return `${totalMinutes}m left`;
|
||||
return {
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_MINUTES',
|
||||
params: { minutes: totalMinutes },
|
||||
};
|
||||
}
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
if (minutes === 0) {
|
||||
return `${hours}h left`;
|
||||
return {
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_HOURS',
|
||||
params: { hours },
|
||||
};
|
||||
}
|
||||
return `${hours}h ${minutes.toString().padStart(2, '0')}m left`;
|
||||
return {
|
||||
key: 'WORKSPACE.DASHBOARD.REMAINING_HOURS_MINUTES',
|
||||
params: { hours, minutes },
|
||||
};
|
||||
}
|
||||
|
||||
function isXtreamAccountPlaylist(
|
||||
@@ -401,6 +417,10 @@ export class WorkspaceDashboardRailsComponent {
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly store = inject(Store);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly languageTick = toSignal(
|
||||
this.translate.onLangChange.pipe(startWith(null)),
|
||||
{ initialValue: null }
|
||||
);
|
||||
private readonly shellActions = inject(WORKSPACE_SHELL_ACTIONS);
|
||||
private readonly epgService = inject(EpgService);
|
||||
|
||||
@@ -447,12 +467,13 @@ export class WorkspaceDashboardRailsComponent {
|
||||
// (movies/series) get one rail; live channels get their own. Two card
|
||||
// formats — one rail each — so users can scan each surface at a glance
|
||||
// instead of decoding a mixed grid of fallback posters and channel logos.
|
||||
readonly continueWatchingCards = computed<DashboardRailCard[]>(() =>
|
||||
this.data
|
||||
readonly continueWatchingCards = computed<DashboardRailCard[]>(() => {
|
||||
this.languageTick();
|
||||
return this.data
|
||||
.globalRecentVodItems()
|
||||
.slice(0, RAIL_ITEM_LIMIT)
|
||||
.map((item) => this.toRecentCard(item))
|
||||
);
|
||||
.map((item) => this.toRecentCard(item));
|
||||
});
|
||||
|
||||
// Live rail source — favorited live channels take precedence so the EPG
|
||||
// enrichment lands on the channels the user actually cares about. When
|
||||
|
||||
Reference in new issue
Block a user