Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
157 commits
Select commit Hold shift + click to select a range
5d0a3da
fix: render HTML in YouTube comments
Priveetee Sep 11, 2026
858abd9
fix: keep rich text format type private
Priveetee Sep 11, 2026
a9a7da4
fix: remove duplicate player keyboard animation
Priveetee Sep 12, 2026
4e1afeb
feat: show remote login diagnostics with copy button
Priveetee Sep 13, 2026
b5c5c4d
fix: keep remote login diagnostics after the session ends
Priveetee Sep 13, 2026
c6844ae
feat: log client-side remote login diagnostics
Priveetee Sep 13, 2026
a6bb60c
feat: add subscription group models and selection helpers
kapdon Sep 15, 2026
889a6e4
feat: add subscription group API client and batch writes
kapdon Sep 15, 2026
f012052
feat: translate subscription group management messages
kapdon Sep 15, 2026
488d254
feat: load group memberships and refresh subscription caches
kapdon Sep 15, 2026
4b215fb
feat: add group forms and destructive action confirmations
kapdon Sep 15, 2026
84d5b5b
feat: add subscription group sidebar and group actions
kapdon Sep 15, 2026
3b48af0
feat: add searchable inline group membership editing
kapdon Sep 15, 2026
3b2b29f
feat: manage channel selection and group edit drafts
kapdon Sep 15, 2026
9060eb1
feat: show group editors for single selected channel rows
kapdon Sep 15, 2026
a364261
feat: add group membership filters and bulk action toolbar
kapdon Sep 15, 2026
8ce1ec8
feat: assemble the subscription group management page
kapdon Sep 15, 2026
5ff1f1f
feat: integrate group filters into subscription views
kapdon Sep 15, 2026
7d7c9c5
fix: preserve subscription feed after pagination errors
kapdon Sep 16, 2026
738dfc6
test: add varied subscription group fixture data
kapdon Sep 16, 2026
d91e730
test: serve a reproducible subscription group preview
kapdon Sep 16, 2026
0e3fb96
feat: add presence key settings
Priveetee Sep 16, 2026
a97716c
perf: group generated frontend chunks
Priveetee Sep 16, 2026
44963d6
feat: add dedicated API settings
Priveetee Sep 16, 2026
edd1b4d
feat: add viewport-aware subscription group pagination
web-flow Sep 16, 2026
4231388
style: compact group management with TypeType controls
web-flow Sep 16, 2026
ba33bf6
fix: pause group editing after failed data refreshes
web-flow Sep 16, 2026
4208664
test: cover group refresh recovery
web-flow Sep 16, 2026
729997d
fix: explain stream extraction failures
Priveetee Sep 16, 2026
f319d0c
fix: explain empty shorts feed
Priveetee Sep 16, 2026
e014c20
fix: guide empty shorts feeds
Priveetee Sep 16, 2026
84c23b0
refactor: share subscription queries and cache refreshes
web-flow Sep 17, 2026
3e7954f
fix: cancel subscription reads and preserve API errors
web-flow Sep 17, 2026
db5e12a
fix: bound membership writes and target query refreshes
web-flow Sep 17, 2026
e627d7c
fix: recover subscription filters and empty states
web-flow Sep 17, 2026
56607cb
fix: add accessible group action menus
web-flow Sep 17, 2026
1821d57
feat: query server pages and bounded membership selections
web-flow Sep 17, 2026
a18067d
feat: retain channel selection across server-paginated results
web-flow Sep 17, 2026
9460ecc
test: serve paginated subscription membership fixtures
web-flow Sep 17, 2026
3ba7460
feat: canonicalize provider channel routes
Priveetee Sep 17, 2026
caf0946
feat: unify provider channel routes
Priveetee Sep 17, 2026
3652964
fix: allow youtube provider channel routes
Priveetee Sep 17, 2026
8e1a4c1
fix: use landing page for logo
Priveetee Sep 17, 2026
0a8024a
feat: separate autoplay on open setting
Priveetee Sep 17, 2026
9dd3074
fix: use refreshed memberships for inline group edits
web-flow Sep 17, 2026
a6267cd
docs: link AI transparency statement
Priveetee Sep 18, 2026
4d95d29
docs: move AI transparency notice
Priveetee Sep 18, 2026
fbc6fb6
ci: route workflows to CI stack
Priveetee Sep 19, 2026
dbb0889
feat: add BiliBili session connect with QR login
Priveetee Sep 20, 2026
dfd51dc
feat: add BiliBili session banner on watch page
Priveetee Sep 20, 2026
d4a9dc9
feat: add expiring soon warning on BiliBili session card
Priveetee Sep 20, 2026
ea7f47d
style: format bilibili session card
Priveetee Sep 20, 2026
2de785a
fix: regenerate route tree and fix qr phase narrowing
Priveetee Sep 20, 2026
8d068fe
fix: remove unused BiliBiliSessionStatus type export
Priveetee Sep 20, 2026
10005b5
feat: add BiliBili session card to youtube-session page
Priveetee Sep 20, 2026
071f12e
feat: unified session layout for BiliBili with FR translations and se…
Priveetee Sep 20, 2026
121082f
fix: make BiliBili sign-in a dedicated flow
Priveetee Sep 20, 2026
a0a58d3
fix: use sidebar for service session switching
Priveetee Sep 20, 2026
12eb274
fix: keep BiliBili QR visible while waiting
Priveetee Sep 20, 2026
8995510
fix: align BiliBili sign-in button
Priveetee Sep 20, 2026
b039478
chore: bump frontend to 1.8.2
Priveetee Sep 20, 2026
8c9d85b
chore: align web version to 1.8.2
Priveetee Sep 20, 2026
b85ec58
fix: compact BiliBili QR layout
Priveetee Sep 20, 2026
dc24381
fix: keep BiliBili button compact
Priveetee Sep 20, 2026
e8ffb16
fix: consume MSE duration fix
Priveetee Sep 21, 2026
53aa48d
fix: select browser-compatible SABR audio
Priveetee Sep 21, 2026
c37c70d
fix: persist native SABR pause intent
Priveetee Sep 22, 2026
70959ca
fix: handle native SABR pause before autoplay races
Priveetee Sep 22, 2026
efb63e8
test: mirror merged membership page contract in fixture
web-flow Sep 23, 2026
d3a8147
Merge dev into subscription group management
web-flow Sep 23, 2026
915c9c6
fix: refresh memberships when reselecting channels
web-flow Sep 23, 2026
3f7e441
fix: force YouTube live HLS playback
Priveetee Sep 23, 2026
8c4bca1
perf: share HLS and DASH playback policy
Priveetee Sep 23, 2026
93cff73
fix: capture HLS transport errors
Priveetee Sep 23, 2026
b0a6ab0
fix: proxy signed stream manifests
Priveetee Sep 23, 2026
57bad5b
fix: proxy HLS media fragments
Priveetee Sep 23, 2026
78cbec3
perf: target PipePipe live edge latency
Priveetee Sep 24, 2026
6a666f7
perf: start live stream requests in parallel
Priveetee Sep 24, 2026
2a01f7e
perf: centralize frontend MSE transport settings
Priveetee Sep 24, 2026
b8b9e5e
perf: skip SABR bootstrap for known live videos
Priveetee Sep 24, 2026
063d1c8
perf: lower browser live HLS target latency
Priveetee Sep 24, 2026
9c8f901
perf: skip SABR bootstrap for known public lives
Priveetee Sep 24, 2026
637cc81
perf: defer SABR bootstrap until stream classification
Priveetee Sep 24, 2026
f7c0bd5
fix: stop stale player on watch navigation
Priveetee Sep 24, 2026
c7628c3
fix: unmount player before switching videos
Priveetee Sep 24, 2026
6a769d5
perf: prefetch live streams on intent
Priveetee Sep 24, 2026
1041835
chore: pause automatic frontend validation
Priveetee Sep 24, 2026
df8ad63
perf: overlap VOD SABR bootstrap extraction
Priveetee Sep 24, 2026
84a02e7
perf: preflight SABR playback from video cards
Priveetee Sep 24, 2026
128561e
perf: trace browser SABR playback startup
Priveetee Sep 24, 2026
a9d0236
perf: match prewarmed SABR playback options
Priveetee Sep 25, 2026
477e4fd
perf: keep browser playback traces correlated
Priveetee Sep 25, 2026
7fc774c
perf: trace MSE playback startup phases
Priveetee Sep 25, 2026
7caca84
fix: restore cursor visibility in paused player
Priveetee Sep 25, 2026
9a05061
feat: add configurable video layout density
Priveetee Sep 25, 2026
762fd07
chore: restore automatic dev validation
Priveetee Sep 25, 2026
e26f63e
style: format frontend playback diagnostics
Priveetee Sep 25, 2026
5d5198b
refactor: keep playback trace helper internal
Priveetee Sep 25, 2026
68487f1
ci: keep dev frontend builds amd64 only
Priveetee Sep 25, 2026
8872320
feat: move service picker into navbar
Priveetee Sep 25, 2026
d78f6d9
fix: prompt YouTube session reconnect
Priveetee Sep 25, 2026
2ae6eb1
feat: add YouTube reconnect GIF
Priveetee Sep 25, 2026
d00756d
feat: move service picker to top right
Priveetee Sep 25, 2026
20d2210
feat: show progressive Takeout import progress
Priveetee Sep 26, 2026
159ebd0
style: format Takeout progress UI
Priveetee Sep 26, 2026
1d2360d
chore: bump frontend version to 1.9.0
Priveetee Sep 26, 2026
42ea2db
fix: clear continue watching on logout
Priveetee Sep 26, 2026
1fe856a
fix: silence original audio unavailable toast on single-language streams
Aquarius-Situla Sep 26, 2026
af67b13
fix: improve quality resolution height resolution and fallback in Qua…
Aquarius-Situla Sep 26, 2026
8539848
fix: fallback to label height in PlayerDefaults quality matching
Aquarius-Situla Sep 26, 2026
6e76573
feat: persist prepared Takeout data in browser storage
Priveetee Sep 26, 2026
516a64e
refactor: stream nested YouTube Takeout archives
Priveetee Sep 26, 2026
23f6d61
feat: persist accepted portability jobs synchronously
Priveetee Sep 26, 2026
c7d938c
feat: show resumable Takeout progress across pages
Priveetee Sep 26, 2026
1eda93c
test: cover multipart and resumable Takeout imports
Priveetee Sep 26, 2026
b8fe20f
fix: satisfy frontend import lint checks
Priveetee Sep 26, 2026
d9cfb50
fix: remove unused Takeout progress export
Priveetee Sep 26, 2026
5eb0d8c
fix: handle wrapped offsets in Takeout ZIPs
Priveetee Sep 26, 2026
471a1ce
fix: stream YouTube Takeout metadata reliably
Priveetee Sep 26, 2026
4ac1907
fix: guide users to the correct import archive
Priveetee Sep 26, 2026
399d413
fix: keep ZIP import guidance format neutral
Priveetee Sep 26, 2026
c86aa68
feat: auto-detect portability import sources
Priveetee Sep 26, 2026
25f47ea
style: format automatic import changes
Priveetee Sep 26, 2026
b1b9430
fix: match Takeout roots case-insensitively
Priveetee Sep 26, 2026
58c29ee
fix: accept ZIP files in automatic import mode
Priveetee Sep 26, 2026
f21a198
fix: apply portability imports automatically
Priveetee Sep 26, 2026
c94537d
Merge remote-tracking branch 'origin/dev' into pr32-local-review
Priveetee Sep 26, 2026
2cd7890
feat: add granular channel notification settings
Priveetee Sep 26, 2026
dfc9ebf
fix: align portability query invalidation
Priveetee Sep 26, 2026
2d739bb
fix: normalize notification channel keys
Priveetee Sep 26, 2026
8df6c99
fix: keep notification query key private
Priveetee Sep 26, 2026
9ae91ac
fix: stop polling stale portability jobs
Priveetee Sep 26, 2026
43633bc
test: lock stale portability job polling
Priveetee Sep 26, 2026
7b65f53
fix: switch filter before deleting active group
Priveetee Sep 26, 2026
b05eb25
Merge pull request #32 from kapdon/codex/subscription-groups-ux
Priveetee Sep 26, 2026
5b28be0
Merge remote-tracking branch 'origin/dev' into review-pr33-local
Priveetee Sep 26, 2026
5f28872
Merge remote-tracking branch 'origin/dev' into review-pr34-local
Priveetee Sep 26, 2026
b2d0652
test: cover single-language audio defaults
Priveetee Sep 26, 2026
1b2f4b2
fix: handle zero quality heights
Priveetee Sep 26, 2026
8a525fd
fix: show avatar loading fallback
Priveetee Sep 26, 2026
b1dab1c
fix: show channel avatar loading skeleton
Priveetee Sep 26, 2026
8f83a0f
Merge pull request #34 from Aquarius-Situla/fix/player-quality-resolu…
Priveetee Sep 26, 2026
83fc64e
Merge pull request #33 from Aquarius-Situla/fix/silence-single-langua…
Priveetee Sep 26, 2026
35ef4a8
fix: correct playlist counts and avatar skeleton
Priveetee Sep 26, 2026
d1b103c
feat: animate pending subscription avatars
Priveetee Sep 26, 2026
b2b53c6
refactor: standardize avatar loading behavior
Priveetee Sep 26, 2026
a0671c1
feat: enable NicoNico live channel tab
Priveetee Sep 26, 2026
d264562
feat: add BiliBili live FLV playback
Priveetee Sep 26, 2026
7975bf2
test: cover BiliBili live FLV fallback
Priveetee Sep 26, 2026
2ff8ca1
fix: retry provider playback after network recovery
Priveetee Sep 27, 2026
f0d564f
style: order player recovery imports
Priveetee Sep 27, 2026
749ac32
fix: retry and resolve DeArrow branding
Priveetee Sep 27, 2026
aaf40ad
fix: accept nested YouTube Takeout parts
Priveetee Sep 27, 2026
24506ac
Revert "fix: accept nested YouTube Takeout parts"
Priveetee Sep 27, 2026
62dc345
fix: remove active service from picker
Priveetee Sep 27, 2026
e5e5d77
fix: load watch avatars immediately
Priveetee Sep 27, 2026
fdf9d02
chore: merge main for 1.9.0 release
Priveetee Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
image: ${{ steps.info.outputs.image }}
labels: ${{ steps.meta.outputs.labels }}
metadata-json: ${{ steps.meta.outputs.json }}
platform-matrix: ${{ steps.info.outputs.platform-matrix }}
version: ${{ steps.info.outputs.version }}
steps:
- uses: actions/checkout@v7.0.1
Expand Down Expand Up @@ -55,6 +56,12 @@ jobs:
echo "image=$image"
echo "version=$version"
} >> "$GITHUB_OUTPUT"
if [[ "$GITHUB_REF_NAME" == "dev" ]]; then
platform_matrix='{"include":[{"platform":"linux/amd64","arch":"amd64"}]}'
else
platform_matrix='{"include":[{"platform":"linux/amd64","arch":"amd64"},{"platform":"linux/arm64","arch":"arm64"}]}'
fi
echo "platform-matrix=$platform_matrix" >> "$GITHUB_OUTPUT"

- name: Extract image metadata
id: meta
Expand All @@ -77,12 +84,7 @@ jobs:
packages: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
arch: amd64
- platform: linux/arm64
arch: arm64
matrix: ${{ fromJSON(needs.prepare.outputs.platform-matrix) }}
steps:
- uses: actions/checkout@v7.0.1

Expand Down Expand Up @@ -174,8 +176,10 @@ jobs:
METADATA_JSON: ${{ needs.prepare.outputs.metadata-json }}
run: |
mapfile -t digests < <(find "$DIGEST_DIR" -maxdepth 1 -type f -printf '%f\n' | sort)
if [[ "${#digests[@]}" -ne 2 ]]; then
echo "Expected two platform digests, found ${#digests[@]}"
expected_digests=2
if [[ "$GITHUB_REF_NAME" == "dev" ]]; then expected_digests=1; fi
if [[ "${#digests[@]}" -ne "$expected_digests" ]]; then
echo "Expected $expected_digests platform digests, found ${#digests[@]}"
exit 1
fi
mapfile -t tags < <(jq -r '.tags[]' <<< "$METADATA_JSON")
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<p>The React web client for TypeType.</p>
</div>

You want to know the current position of TypeType about AI ? Go check [this](https://github.com/TypeType-Video/TypeType/blob/dev/AI_TRANSPARENCY.md).

[![Translation status](https://translate.typetype.video/widgets/typetype/-/svg-badge.svg)](https://translate.typetype.video/engage/typetype/)

This repository contains the browser application and its nginx container. It owns the user interface, routing, settings, account flows, library views, and integration with the TypeType API and playback package.
Expand Down
177 changes: 175 additions & 2 deletions apps/web/messages/de.json

Large diffs are not rendered by default.

179 changes: 176 additions & 3 deletions apps/web/messages/en.json

Large diffs are not rendered by default.

183 changes: 178 additions & 5 deletions apps/web/messages/fr.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@typetype/web",
"private": true,
"version": "1.8.1",
"version": "1.9.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -14,13 +14,15 @@
"dependencies": {
"@tanstack/react-query": "^5.102.8",
"@tanstack/react-router": "^1.170.33",
"@typetype/mse": "0.1.60",
"@typetype/mse": "0.1.70",
"@vidstack/react": "1.12.13",
"@zip.js/zip.js": "^2.13.1",
"dashjs": "^5.2.1",
"hls.js": "1.7.2",
"lucide-react": "^1.43.0",
"media-icons": "1.1.5",
"mpegts.js": "1.8.2",
"qrcode.react": "4.2.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"simple-icons": "^16.30.0",
Expand Down
Binary file added apps/web/public/youtube-session-reconnect.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions apps/web/src/components/bilibili-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { SVGProps } from "react";
import { siBilibili } from "simple-icons";

type Props = SVGProps<SVGSVGElement> & {
title?: string;
};

export function BiliBiliIcon({ title, ...props }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
role={title ? "img" : undefined}
aria-hidden={title ? undefined : true}
aria-label={title}
{...props}
>
<path d={siBilibili.path} />
</svg>
);
}
25 changes: 25 additions & 0 deletions apps/web/src/components/bilibili-session-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "../hooks/use-auth";
import { fetchBiliBiliSessionStatus } from "../lib/api-bilibili-session";
import { m } from "../paraglide/messages.js";

export function BiliBiliSessionBanner({ sourceUrl }: { sourceUrl: string }) {
const { authReady, isAuthed } = useAuth();
const isBiliBili = sourceUrl.includes("bilibili.com") || sourceUrl.includes("b23.tv");
const shouldQuery = authReady && isAuthed && isBiliBili;
const { data: status } = useQuery({
queryKey: ["bilibili-session"],
queryFn: fetchBiliBiliSessionStatus,
enabled: shouldQuery,
staleTime: 5 * 60 * 1000,
});
if (!shouldQuery || !status || status.status === "connected") return null;
return (
<div className="mb-3 flex items-center gap-2 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-200">
<span>{m.ui_bilibili_session_not_connected()}</span>
<a href="/bilibili-session" className="font-medium underline">
{m.ui_bilibili_session_connect()}
</a>
</div>
);
}
91 changes: 69 additions & 22 deletions apps/web/src/components/channel-avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { useState } from "react";
import { UserRound } from "lucide-react";
import { useEffect, useState } from "react";
import { Skeleton } from "./skeleton";

type Props = {
src: string;
name: string;
className?: string;
pending?: boolean;
priority?: boolean;
};

function getInitial(name: string): string {
Expand All @@ -20,31 +24,74 @@ function getInitial(name: string): string {
return name[0].toUpperCase();
}

export function ChannelAvatar({ src, name, className = "w-8 h-8" }: Props) {
export function ChannelAvatar({ src, name, className = "w-8 h-8", pending, priority }: Props) {
const [failedSrc, setFailedSrc] = useState<string | null>(null);
const [loadedSrc, setLoadedSrc] = useState<string | null>(null);
const [missingExpired, setMissingExpired] = useState(false);
const [retryState, setRetryState] = useState({ src, count: 0 });
const hasSource = src.trim().length > 0;
const failed = failedSrc === src;

if (!src || failed) {
return (
<div
className={`${className} flex flex-shrink-0 select-none items-center justify-center rounded-full border border-border bg-gradient-to-br from-surface-strong to-surface-soft font-semibold text-fg-muted`}
title={name}
>
<span className="text-base leading-none">{getInitial(name)}</span>
</div>
);
}
const loaded = loadedSrc === src;
const retryCount = retryState.src === src ? retryState.count : 0;
useEffect(() => {
if (hasSource || pending === false) {
setMissingExpired(false);
return;
}
if (pending === true) return;
setMissingExpired(false);
const timer = window.setTimeout(() => setMissingExpired(true), MISSING_AVATAR_GRACE_MS);
return () => window.clearTimeout(timer);
}, [hasSource, pending]);
const missing =
!hasSource && !failed && (pending === true || (pending === undefined && !missingExpired));
const loading = missing || (hasSource && !failed && !loaded);
const state = loading ? "loading" : hasSource && !failed ? "ready" : "fallback";

return (
<img
src={src}
alt=""
aria-hidden="true"
className={`${className} flex-shrink-0 rounded-full object-cover`}
loading="lazy"
decoding="async"
onError={() => setFailedSrc(src)}
<div
className={`${className} relative flex flex-shrink-0 select-none items-center justify-center overflow-hidden rounded-full border border-border bg-gradient-to-br from-surface-strong to-surface-soft font-semibold text-fg-muted`}
title={name}
/>
data-avatar-state={state}
aria-busy={loading}
>
{loading && <Skeleton className="absolute inset-0 rounded-full" data-avatar-skeleton />}
{!loading &&
(name.trim() ? (
<span className="text-base leading-none">{getInitial(name)}</span>
) : (
<UserRound className="h-1/2 w-1/2" aria-hidden="true" />
))}
{hasSource && !failed && (
<img
key={retryCount}
src={avatarRetrySrc(src, retryCount)}
alt=""
aria-hidden="true"
className="absolute inset-0 h-full w-full object-cover"
loading={priority ? "eager" : "lazy"}
fetchPriority={priority ? "high" : "auto"}
decoding="async"
onLoad={() => setLoadedSrc(src)}
onError={() => {
if (retryCount < MAX_AVATAR_RETRIES) {
setRetryState({ src, count: retryCount + 1 });
return;
}
setFailedSrc(src);
}}
/>
)}
</div>
);
}

function avatarRetrySrc(src: string, retryCount: number): string {
if (retryCount === 0) return src;
const url = new URL(src, window.location.href);
url.searchParams.set("_tt_avatar_retry", String(retryCount));
return url.toString();
}

const MAX_AVATAR_RETRIES = 2;
const MISSING_AVATAR_GRACE_MS = 1_500;
4 changes: 3 additions & 1 deletion apps/web/src/components/channel-filter-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Props = {
sort: ChannelSort;
query: string;
tab: ChannelTab;
tabsAvailable: boolean;
searchAvailable: boolean;
onSearch: (query: string) => void;
onTabChange: (tab: ChannelTab) => void;
Expand All @@ -27,6 +28,7 @@ export function ChannelFilterBar({
sort,
query,
tab,
tabsAvailable,
searchAvailable,
onSearch,
onTabChange,
Expand All @@ -53,7 +55,7 @@ export function ChannelFilterBar({
return (
<section className="border-y border-border py-3">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
{searchAvailable && (
{tabsAvailable && (
<div className="flex max-w-full flex-wrap items-center gap-2 text-sm">
{channelTabs().map((item) => (
<button
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/channel-page-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ export function ChannelPageContent({ sourceUrl, sort, searchQuery, tab, onNaviga
useDocumentTitle(meta?.name);

const subscribed = isSubscribed(sourceUrl);
const searchAvailable = detectProvider(sourceUrl) === "youtube";
const provider = detectProvider(sourceUrl);
const searchAvailable = provider === "youtube";
const tabsAvailable = provider === "youtube" || provider === "nicovideo";
const visibleVideos = useMemo(() => filter(videos), [filter, videos]);
const progressByUrl = useVideoProgressMap(visibleVideos);
const isInitialLoading = isLoading && !meta;
Expand Down Expand Up @@ -128,6 +130,7 @@ export function ChannelPageContent({ sourceUrl, sort, searchQuery, tab, onNaviga
sort={sort}
query={searchQuery}
tab={tab}
tabsAvailable={tabsAvailable}
searchAvailable={searchAvailable}
onSearch={searchChannel}
onTabChange={selectTab}
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/components/channel-route-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ChannelSort } from "../lib/api-discovery";
import {
channelLegacySearch,
channelPathSearch,
toChannelPathParam,
toCanonicalChannelRoute,
} from "../lib/channel-route-url";

type Props = {
Expand All @@ -16,13 +16,13 @@ type Props = {
};

export function ChannelRouteLink({ url, children, className, sort = "latest", query = "" }: Props) {
const channelId = toChannelPathParam(url);
const canonicalRoute = toCanonicalChannelRoute(url);

if (channelId) {
if (canonicalRoute) {
return (
<Link
to="/channel/$channelId"
params={{ channelId }}
to="/channel/$provider/$channelId"
params={{ provider: canonicalRoute.provider, channelId: canonicalRoute.id }}
search={channelPathSearch(sort, query)}
className={className}
>
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/continue-watching.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo } from "react";
import { useAuth } from "../hooks/use-auth";
import { useBlockedFilter } from "../hooks/use-blocked-filter";
import { useHistory } from "../hooks/use-history";
import { isVideoInProgress } from "../lib/watch-progress";
Expand All @@ -9,6 +10,7 @@ const MAX_ITEMS = 12;

export function ContinueWatching() {
const { items } = useHistory();
const { isAuthed } = useAuth();
const { filter } = useBlockedFilter();
const displayed = useMemo(
() =>
Expand All @@ -18,7 +20,7 @@ export function ContinueWatching() {
.slice(0, MAX_ITEMS),
[filter, items],
);
if (displayed.length === 0) return null;
if (!isAuthed || displayed.length === 0) return null;

return (
<section className="mt-2 flex flex-col gap-3 sm:mt-3">
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/components/navbar-notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ const NotificationsDropdown = lazy(() =>
})),
);

const PortabilityProgressHost = lazy(() =>
import("./portability-progress-host").then((module) => ({
default: module.PortabilityProgressHost,
})),
);

const NotificationToastHost = lazy(() =>
import("./notification-toast-host").then((module) => ({
default: module.NotificationToastHost,
Expand All @@ -21,6 +27,9 @@ export function NavbarNotifications() {
<Suspense fallback={null}>
<NotificationToastHost />
</Suspense>
<Suspense fallback={null}>
<PortabilityProgressHost />
</Suspense>
</>
);
}
Loading