Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion 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.3.1",
"version": "1.4.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
6 changes: 2 additions & 4 deletions apps/web/src/components/admin-session-card.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toApiUrl } from "../lib/env";
import { getAdminUserAvatarUrl } from "../lib/admin-user-avatar";
import { getOpenMojiUrl, pickOpenMojiCode } from "../lib/openmoji";
import type { AdminSession } from "../types/admin";
import type { AuthUser } from "../types/auth";
Expand Down Expand Up @@ -26,9 +26,7 @@ function initials(value: string): string {
}

function avatarUrl(user: AuthUser | undefined, session: AdminSession): string | null {
if (user?.avatarType === "emoji" && user.avatarCode) return getOpenMojiUrl(user.avatarCode);
if (user?.avatarUrl) return toApiUrl(user.avatarUrl);
if (user) return getOpenMojiUrl(pickOpenMojiCode(`${user.id}:${user.email}`));
if (user) return getAdminUserAvatarUrl(user);
if (session.userId) return getOpenMojiUrl(pickOpenMojiCode(session.userId));
return null;
}
Expand Down
11 changes: 2 additions & 9 deletions apps/web/src/components/admin-user-avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { toApiUrl } from "../lib/env";
import { getOpenMojiUrl, pickOpenMojiCode } from "../lib/openmoji";
import { getAdminUserAvatarUrl } from "../lib/admin-user-avatar";
import type { AuthUser } from "../types/auth";

type AdminUserAvatarProps = {
Expand All @@ -8,13 +7,7 @@ type AdminUserAvatarProps = {
};

export function AdminUserAvatar({ user, className }: AdminUserAvatarProps) {
const seed = `${user.id}:${user.email}`;
const avatarUrl =
user.avatarType === "emoji" && typeof user.avatarCode === "string" && user.avatarCode.length > 0
? getOpenMojiUrl(user.avatarCode)
: user.avatarUrl
? toApiUrl(user.avatarUrl)
: getOpenMojiUrl(pickOpenMojiCode(seed));
const avatarUrl = getAdminUserAvatarUrl(user);

return (
<div
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/components/channel-page-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function ChannelPageContent({ sourceUrl, sort, searchQuery, tab, onNaviga
fetchNextPage,
} = useChannel(sourceUrl, sort, searchQuery, live);
const { add, remove, isSubscribed } = useSubscriptions();
const { filter } = useBlockedFilter();
const { filter, isChannelIdentityBlocked } = useBlockedFilter();
useDocumentTitle(meta?.name);

const subscribed = isSubscribed(sourceUrl);
Expand Down Expand Up @@ -95,6 +95,14 @@ export function ChannelPageContent({ sourceUrl, sort, searchQuery, tab, onNaviga
</div>
);
}
if (meta && isChannelIdentityBlocked({ url: sourceUrl, name: meta.name })) {
return (
<FamilyListEmptyState
title="This channel is blocked"
description="Unblock it from Settings to see this channel again."
/>
);
}

return (
<div className="flex flex-col gap-6">
Expand Down
15 changes: 11 additions & 4 deletions apps/web/src/components/continue-watching.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useMemo } from "react";
import { useBlockedFilter } from "../hooks/use-blocked-filter";
import { useHistory } from "../hooks/use-history";
import { isVideoInProgress } from "../lib/watch-progress";
import { ContinueCard } from "./continue-card";
Expand All @@ -6,10 +8,15 @@ const MAX_ITEMS = 12;

export function ContinueWatching() {
const { items } = useHistory();
const displayed = items
.filter((h) => isVideoInProgress(h.progress, h.duration))
.sort((a, b) => b.watchedAt - a.watchedAt)
.slice(0, MAX_ITEMS);
const { filter } = useBlockedFilter();
const displayed = useMemo(
() =>
filter(items)
.filter((h) => isVideoInProgress(h.progress, h.duration))
.sort((a, b) => b.watchedAt - a.watchedAt)
.slice(0, MAX_ITEMS),
[filter, items],
);
if (displayed.length === 0) return null;

return (
Expand Down
46 changes: 27 additions & 19 deletions apps/web/src/components/reset-token-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { copyText } from "../lib/copy-text";

type ResetTokenModalProps = {
email: string;
Expand All @@ -8,14 +9,7 @@ type ResetTokenModalProps = {
};

export function ResetTokenModal({ email, token, onClose, onCopied }: ResetTokenModalProps) {
useEffect(() => {
navigator.clipboard
.writeText(token)
.then(() => {
onCopied();
})
.catch(() => {});
}, [token, onCopied]);
const [copyState, setCopyState] = useState<"idle" | "copied" | "manual">("idle");

useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
Expand All @@ -25,10 +19,13 @@ export function ResetTokenModal({ email, token, onClose, onCopied }: ResetTokenM
return () => window.removeEventListener("keydown", handleEscape);
}, [onClose]);

const handleCopy = () => {
navigator.clipboard.writeText(token).then(() => {
const handleCopy = async () => {
if (await copyText(token)) {
setCopyState("copied");
onCopied();
});
return;
}
setCopyState("manual");
};

return (
Expand All @@ -52,18 +49,29 @@ export function ResetTokenModal({ email, token, onClose, onCopied }: ResetTokenM
<p className="text-sm text-fg-muted mt-1">{email}</p>
</div>

<div className="mb-4 p-3 rounded-lg bg-app border border-border">
<p className="text-xs font-mono text-fg break-all">{token}</p>
</div>
<textarea
readOnly
value={token}
aria-label="Password reset token"
onFocus={(event) => event.currentTarget.select()}
className="mb-4 block h-24 w-full resize-none rounded-lg border border-border bg-app p-3 font-mono text-xs text-fg"
/>

<div className="mb-3 text-xs text-emerald-200 bg-emerald-950/30 border border-emerald-800/50 p-2 rounded">
Token copied to clipboard automatically
</div>
{copyState === "copied" && (
<div className="mb-3 rounded border border-emerald-800/50 bg-emerald-950/30 p-2 text-xs text-emerald-200">
Token copied to clipboard
</div>
)}
{copyState === "manual" && (
<div className="mb-3 rounded border border-border-strong bg-surface-strong p-2 text-xs text-fg-muted">
Clipboard access is unavailable. Select the token above and copy it manually.
</div>
)}

<div className="flex gap-2">
<button
type="button"
onClick={handleCopy}
onClick={() => void handleCopy()}
className="flex-1 h-9 rounded-md border border-border-strong bg-surface-strong px-3 text-sm font-medium text-fg hover:border-border-strong hover:bg-surface-soft transition-colors"
>
Copy token
Expand Down
Loading