Skip to content

Commit

Permalink
selection mode - clicking channel no longer selects
Browse files Browse the repository at this point in the history
  • Loading branch information
sphinxrave committed Dec 30, 2024
1 parent 96e2d22 commit 4437aea
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 81 deletions.
8 changes: 8 additions & 0 deletions packages/react/src/Global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ declare module "*.module.styl" {
}

declare module "jsonp-es6";

import "react-router-dom";

declare module "react-router-dom" {
export interface LinkProps {
dataBehavior?: string; // Add your custom attribute here
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import { useVideoSelection } from "@/hooks/useVideoSelection";

export function SelectionEditShortcuts() {
const { selectedVideos } = useVideoSelection();
const hasClip = selectedVideos.some(
(x) => x.type === "clip" || x.type === "placeholder",
const selectionContainsClips = selectedVideos.some((x) => x.type === "clip");
const selectionContainsNoClip = selectedVideos.every(
(x) => x.type !== "clip",
);
const selectionContainsNoPlaceholders = selectedVideos.every(
(x) => x.type !== "placeholder",
);
const hasNonClip = selectedVideos.some((x) => x.type !== "clip");
const hasMentions = selectedVideos.some((x) => x.mentions?.length);

const context = { pageVideo: null, pageChannel: null };
Expand All @@ -38,12 +41,17 @@ export function SelectionEditShortcuts() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{!hasClip && hasNonClip && (
<DropdownMenuItem>Make Collab</DropdownMenuItem>
{!selectionContainsClips && selectionContainsNoClip && (
<DropdownMenuItem>Merge Participant Lists</DropdownMenuItem>
)}
{hasClip && hasNonClip && (
{selectionContainsNoClip && selectionContainsNoPlaceholders && (
<DropdownMenuItem>Make Simulwatch</DropdownMenuItem>
)}
{selectionContainsNoClip &&
selectionContainsNoPlaceholders &&
selectedVideos?.length < 4 && (
<DropdownMenuItem>Make videos refer to each other</DropdownMenuItem>
)}
{context.pageVideo && (
<DropdownMenuItem onClick={dissociateVideo}>
Disassociate w/ Current Video
Expand Down
87 changes: 17 additions & 70 deletions packages/react/src/components/video/VideoCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { VideoCardCountdownToLive } from "./VideoCardCountdownToLive";
import { formatDuration } from "@/lib/time";
import { Button } from "@/shadcn/ui/button";
import { Link, useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import { VideoMenu } from "./VideoMenu";
import { cn, makeThumbnailUrl, resizeChannelPhoto } from "@/lib/utils";
import React, { Suspense, useCallback, useMemo, useState } from "react";
Expand All @@ -18,6 +18,7 @@ import {
import { isMobileAtom } from "@/hooks/useFrame";
import { ChannelImg } from "../channel/ChannelImg";
import { tldexLanguageAtom } from "@/store/tldex";
import { useDefaultVideoCardClickHandler } from "./VideoCard.utils";

export type VideoCardType = VideoRef &
Partial<VideoBase> &
Expand Down Expand Up @@ -63,8 +64,6 @@ export function VideoCard({
onClick,
showDuration = true,
}: VideoCardProps) {
const navigate = useNavigate();

const isTwitch = video.link?.includes("twitch");
const videoHref =
!isTwitch && video.status === "live" && video.link
Expand All @@ -85,70 +84,12 @@ export function VideoCard({
const [placeholderOpen, setPlaceholderOpen] = useState(false); // placeholder popup state.

const selectedSet = useAtomValue(selectedVideoSetReadonlyAtom);
const { selectionMode, addVideo, removeVideo } = useVideoSelection();
const { selectionMode } = useVideoSelection();
const isMobile = useAtomValue(isMobileAtom);

const goToVideoClickHandler = useCallback(
(evt: React.MouseEvent<HTMLElement, MouseEvent>) => {
console.info("JS Video Click Handling", evt);
const isLinkClick = (evt.target as HTMLElement).closest("a");
if (isLinkClick) {
// Handle selection mode
if (selectionMode) {
if (selectedSet.includes(video.id)) {
removeVideo(video.id);
} else {
addVideo(video as PlaceholderVideo);
}
evt.preventDefault();
evt.stopPropagation();
return;
}

if (videoIsPlaceholder) {
evt.preventDefault();
evt.stopPropagation();
return setPlaceholderOpen(true);
}

console.info("no action b/c closest element is a link.", evt);
return;
}
// clicked a non-link part of the video card. Prevent default behavior.
evt.preventDefault();
evt.stopPropagation();

if (evt.ctrlKey || evt.metaKey) {
/** Control clicking a non-link part always goes to the external link no matter what the context */
return window.open(videoHref, "_blank");
}

// Handle selection mode
if (selectionMode) {
if (selectedSet.includes(video.id)) {
removeVideo(video.id);
} else {
addVideo(video as PlaceholderVideo);
}
return;
}

if (videoIsPlaceholder) {
return setPlaceholderOpen(true);
}

navigate(videoHref, { state: { video } });
},
[
selectionMode,
videoIsPlaceholder,
navigate,
videoHref,
video,
selectedSet,
removeVideo,
addVideo,
],
const goToVideoClickHandler = useDefaultVideoCardClickHandler(
video,
setPlaceholderOpen,
);

/**
Expand All @@ -174,7 +115,7 @@ export function VideoCard({
(size == "md" || size == "lg") && "group flex w-full flex-col gap-4",
onClick && "cursor-pointer",
selectionMode &&
(selectedSet?.includes(video.id)
(selectedSet?.has(video.id)
? "ring-offset-base-2 ring-offset-2 ring-4 ring-primary-8 rounded-lg "
: "ring-offset-base-2 ring-offset-2 ring-4 ring-base-6 rounded-lg saturate-[0.75] brightness-75 opacity-50"),
]),
Expand Down Expand Up @@ -265,9 +206,11 @@ export function VideoCard({
{(size == "lg" || size == "md") && video.channel && (
<Link
to={`/channel/${video.channel.id}`}
id="channelLink"
dataBehavior="channelLink"
className="shrink-0"
onClick={(e) => onClick && onClick("channel", video, e)}
onClick={(e) =>
onClick ? onClick("channel", video, e) : goToVideoClickHandler(e)
}
>
<ChannelImg
channelId={video.channel.id}
Expand All @@ -282,7 +225,7 @@ export function VideoCard({
</Link>
)}

{/* This block contains the Video Text Info: Title, Channel, Schedule. */}
{/* This sub-block contains the Video Text Info: Title, Channel, Schedule. */}
<div
className={videoCardClasses.videoTextInfo}
onClick={(e) =>
Expand All @@ -304,7 +247,7 @@ export function VideoCard({
{video.channel && (
<Link
className={videoCardClasses.channelLink}
id="channelLink"
dataBehavior="channelLink"
to={`/channel/${video.channel.id}`}
onClick={(e) => onClick && onClick("channel", video, e)}
>
Expand All @@ -317,6 +260,7 @@ export function VideoCard({
</div>
)}
</div>
{/* The last sub-block is for the video menu dropdown */}
<VideoMenu url={externalLink} video={video}>
<Button
variant="ghost"
Expand All @@ -334,8 +278,11 @@ export function VideoCard({
</Button>
</VideoMenu>
{videoIsPlaceholder && (
// This block contains the pop-up card for the video which is rendered when clicked.
// the surrounding onclick and mousedown events are caught to prevent navigating the video.
<div onClick={stopPropagation} onMouseDown={stopPropagation}>
<Suspense fallback={null}>
{/* The `Suspense` prevents the lazy-loaded placeholder from blocking page rendering */}
<LazyVideoCardPlaceholder
open={placeholderOpen}
setOpen={setPlaceholderOpen}
Expand Down
91 changes: 91 additions & 0 deletions packages/react/src/components/video/VideoCard.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useCallback } from "react";
import { type VideoCardType } from "./VideoCard";
import {
selectedVideoSetReadonlyAtom,
useVideoSelection,
} from "@/hooks/useVideoSelection";
import { useAtomValue } from "jotai";
import { useNavigate } from "react-router-dom";

export function useDefaultVideoCardClickHandler(
video: VideoCardType,
setPlaceholderOpen: (open: boolean) => void,
) {
const navigate = useNavigate();
const { selectionMode, addVideo, removeVideo } = useVideoSelection();
const selectedSet = useAtomValue(selectedVideoSetReadonlyAtom);

return useCallback(
(evt: React.MouseEvent<HTMLElement, MouseEvent>) => {
const videoIsPlaceholder = video.type === "placeholder";
const isTwitch = video.link?.includes("twitch");
const videoHref =
!isTwitch && video.status === "live" && video.link
? video.link
: `/watch/${video.id}`;

console.info("JS Video Click Handling", evt);
const isLinkClick = (evt.target as HTMLElement).closest("a");
const isChannelClick =
isLinkClick?.getAttribute("dataBehavior") === "channelLink";
if (isChannelClick) {
return; // do not select, skip the entire handler
}
if (isLinkClick) {
// Handle selection mode
if (selectionMode) {
if (selectedSet.has(video.id)) {
removeVideo(video.id);
} else {
addVideo(video as PlaceholderVideo);
}
evt.preventDefault();
evt.stopPropagation();
return;
}

if (videoIsPlaceholder) {
evt.preventDefault();
evt.stopPropagation();
return setPlaceholderOpen(true);
}

console.info("no action b/c closest element is a link.", evt);
return;
}
// clicked a non-link part of the video card. Prevent default behavior.
evt.preventDefault();
evt.stopPropagation();

if (evt.ctrlKey || evt.metaKey) {
/** Control clicking a non-link part always goes to the external link no matter what the context */
return window.open(videoHref, "_blank");
}

// Handle selection mode
if (selectionMode) {
if (selectedSet.has(video.id)) {
removeVideo(video.id);
} else {
addVideo(video as PlaceholderVideo);
}
return;
}

if (videoIsPlaceholder) {
return setPlaceholderOpen(true);
}

navigate(videoHref, { state: { video } });
},
[
video,
selectionMode,
navigate,
selectedSet,
removeVideo,
addVideo,
setPlaceholderOpen,
],
);
}
4 changes: 1 addition & 3 deletions packages/react/src/components/video/VideoMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ export function VideoMenu({ children, video, url }: VideoMenuProps) {

// Selection behavior
const { addVideo, removeVideo, setSelectionMode } = useVideoSelection();
const isSelected = useAtomValue(selectedVideoSetReadonlyAtom).includes(
videoId,
);
const isSelected = useAtomValue(selectedVideoSetReadonlyAtom).has(videoId);

const [, copy] = useCopyToClipboard();
const setReportedVideo = useSetAtom(videoReportAtom);
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/hooks/useVideoSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { atom, useAtom } from "jotai";
export const selectionModeAtom = atom(false);
const selectedVideosAtom = atom<PlaceholderVideo[]>([]);

export const selectedVideoSetReadonlyAtom = atom((get) =>
get(selectedVideosAtom).map((v) => v.id),
export const selectedVideoSetReadonlyAtom = atom(
(get) => new Set(get(selectedVideosAtom).map((v) => v.id)),
);
export const useVideoSelection = () => {
const [selectionMode, setSelectionMode] = useAtom(selectionModeAtom);
Expand Down

0 comments on commit 4437aea

Please sign in to comment.