Skip to content

Commit

Permalink
Merge pull request #145 from mykhailodanilenko/feature/channel-page
Browse files Browse the repository at this point in the history
Add channel and channels pages
  • Loading branch information
mykhailodanilenko authored Sep 6, 2024
2 parents f30f4c1 + ebbf547 commit 327c7f9
Show file tree
Hide file tree
Showing 29 changed files with 336 additions and 39 deletions.
27 changes: 23 additions & 4 deletions OwnTube.tv/api/channelsApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { VideoChannel } from "@peertube/peertube-types";
import { VideoChannel, VideosCommonQuery } from "@peertube/peertube-types";
import { Video } from "@peertube/peertube-types/peertube-models/videos/video.model";
import { GetVideosVideo } from "./models";
import i18n from "../i18n";
Expand All @@ -15,6 +15,25 @@ export class ChannelsApi extends AxiosInstanceBasedApi {
super();
}

/**
* Get channel info
*
* @param [baseURL] - Selected instance url
* @param [channelHandle] - Channel identifier
* @returns Channel info
*/
async getChannelInfo(baseURL: string, channelHandle: string): Promise<VideoChannel> {
try {
const response = await this.instance.get<VideoChannel>(`video-channels/${channelHandle}`, {
baseURL: `https://${baseURL}/api/v1`,
});

return response.data;
} catch (error: unknown) {
throw new Error(i18n.t("errors.failedToFetchChannelInfo", { error: (error as Error).message }));
}
}

/**
* Get a list of channels from the PeerTube instance
*
Expand All @@ -39,17 +58,17 @@ export class ChannelsApi extends AxiosInstanceBasedApi {
*
* @param [baseURL] - Selected instance url
* @param [channelHandle] - Channel handle
* @param [count] - Count of videos to fetch
* @param [queryParams] - Query params
* @returns List of channel videos
*/
async getChannelVideos(
baseURL: string,
channelHandle: string,
count: number,
queryParams: VideosCommonQuery,
): Promise<{ data: GetVideosVideo[]; total: number }> {
try {
const response = await this.instance.get(`video-channels/${channelHandle}/videos`, {
params: { ...commonQueryParams, sort: "-originallyPublishedAt", count },
params: { ...commonQueryParams, ...queryParams, sort: "-originallyPublishedAt" },
baseURL: `https://${baseURL}/api/v1`,
});

Expand Down
5 changes: 2 additions & 3 deletions OwnTube.tv/api/models.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Subset of a video object from the PeerTube backend API, https://github.com/Chocobozzz/PeerTube/blob/develop/server/core/models/video/video.ts#L460
import { VideoModel } from "@peertube/peertube-types/server/core/models/video/video";
import { VideoChannelSummary } from "@peertube/peertube-types";
import { Video } from "@peertube/peertube-types/peertube-models/videos/video.model";

export interface Channel {
id: number;
Expand All @@ -23,7 +22,7 @@ export interface Channel {
}

export type GetVideosVideo = Pick<
VideoModel,
Video,
"uuid" | "name" | "description" | "duration" | "publishedAt" | "originallyPublishedAt" | "views"
> & {
thumbnailPath: string;
Expand Down
48 changes: 44 additions & 4 deletions OwnTube.tv/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export enum QUERY_KEYS {
video = "video",
instances = "instances",
instance = "instance",
channel = "channel",
channels = "channels",
channelVideos = "channelVideos",
categories = "categories",
Expand Down Expand Up @@ -120,6 +121,17 @@ export const useGetInstanceInfoQuery = (backend?: string) => {
});
};

export const useGetChannelInfoQuery = (backend?: string, channelHandle?: string) => {
return useQuery({
queryKey: [QUERY_KEYS.channel, backend, channelHandle],
queryFn: async () => {
return await ChannelsApiImpl.getChannelInfo(backend!, channelHandle!);
},
enabled: !!backend && !!channelHandle,
refetchOnWindowFocus: false,
});
};

export const useGetChannelsQuery = () => {
const { backend } = useLocalSearchParams<RootStackParams["index"]>();

Expand All @@ -134,17 +146,45 @@ export const useGetChannelsQuery = () => {
});
};

export const useGetChannelVideosQuery = (channelHandle: string, videosCount = 4) => {
export const useGetChannelVideosQuery = (channelHandle?: string, queryParams: VideosCommonQuery = { count: 4 }) => {
const { backend } = useLocalSearchParams<RootStackParams["index"]>();

return useQuery<GetVideosVideo[]>({
queryKey: [QUERY_KEYS.channelVideos, backend, channelHandle],
queryKey: [QUERY_KEYS.channelVideos, backend, channelHandle, queryParams?.categoryOneOf],
queryFn: async () => {
const response = await ChannelsApiImpl.getChannelVideos(backend!, channelHandle, videosCount);
const response = await ChannelsApiImpl.getChannelVideos(backend!, channelHandle!, queryParams);

return response.data.map((video) => ({ ...video, thumbnailPath: `https://${backend}${video.thumbnailPath}` }));
},
enabled: !!backend,
enabled: !!backend && !!channelHandle,
refetchOnWindowFocus: false,
});
};

export const useInfiniteGetChannelVideosQuery = (channelHandle?: string, pageSize = 4) => {
const { backend } = useLocalSearchParams<RootStackParams["index"]>();

return useInfiniteQuery({
initialPageParam: 0,
getNextPageParam: (lastPage: { data: GetVideosVideo[]; total: number }, _nextPage, lastPageParam) => {
const nextCount = (lastPageParam === 0 ? pageSize * 3 : lastPageParam) + pageSize;

return nextCount > lastPage.total ? null : nextCount;
},
queryKey: [QUERY_KEYS.channelVideos, backend, channelHandle, "infinite"],
queryFn: async ({ pageParam }) => {
const response = await ChannelsApiImpl.getChannelVideos(backend!, channelHandle!, {
count: pageParam === 0 ? pageSize * 3 : pageSize,
start: pageParam,
sort: "-publishedAt",
});

return {
data: response.data.map((video) => ({ ...video, thumbnailPath: `https://${backend}${video.thumbnailPath}` })),
total: response.total,
};
},
enabled: !!backend && !!channelHandle,
refetchOnWindowFocus: false,
});
};
Expand Down
9 changes: 9 additions & 0 deletions OwnTube.tv/app/(home)/channel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ChannelScreen } from "../../screens";

export default function channel() {
return (
<>
<ChannelScreen />
</>
);
}
22 changes: 20 additions & 2 deletions OwnTube.tv/app/(home)/channels.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import { Loader } from "../../components";
import { ChannelsScreen } from "../../screens";
import { Platform } from "react-native";
import Head from "expo-router/head";
import { useTranslation } from "react-i18next";

export default function channels() {
return <Loader />;
const { t } = useTranslation();

return (
<>
{Platform.select({
default: null,
web: (
<Head>
<title>{t("channels")}</title>
<meta name="description" content="Channels list" />
</Head>
),
})}
<ChannelsScreen />
</>
);
}
2 changes: 2 additions & 0 deletions OwnTube.tv/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const RootStack = () => {
name={`(home)/video`}
options={{ drawerStyle: { display: "none" }, swipeEnabled: false, header: () => <></> }}
/>
<Drawer.Screen name={`(home)/${ROUTES.CHANNEL}`} />
<Drawer.Screen name={`(home)/${ROUTES.CHANNELS}`} />
<Drawer.Screen name={`(home)/${ROUTES.CATEGORIES}`} />
</Drawer>
Expand Down Expand Up @@ -132,6 +133,7 @@ export type RootStackParams = {
[ROUTES.INDEX]: { backend: string };
[ROUTES.SETTINGS]: { backend: string; tab: "history" | "instance" | "config" };
[ROUTES.VIDEO]: { backend: string; id: string; timestamp?: string };
[ROUTES.CHANNEL]: { backend: string; channel: string };
[ROUTES.CHANNELS]: { backend: string };
[ROUTES.CATEGORIES]: { backend: string };
};
3 changes: 2 additions & 1 deletion OwnTube.tv/components/ChannelLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { Typography } from "./Typography";
import { useTheme } from "@react-navigation/native";
import { useHoverState } from "../hooks";
import { Pressable } from "react-native";
import { LinkProps } from "expo-router/build/link/Link";

interface ChannelLinkProps {
href: string;
href: LinkProps["href"];
text: string;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { useGetChannelVideosQuery } from "../../../api";
import { VideoGrid } from "../../../components";
import { useGetChannelVideosQuery } from "../api";
import { VideoGrid } from "./index";
import { VideoChannel } from "@peertube/peertube-types";
import { useTranslation } from "react-i18next";
import { ROUTES } from "../types";
import { useLocalSearchParams } from "expo-router";
import { RootStackParams } from "../app/_layout";

interface ChannelViewProps {
channel: VideoChannel;
}

export const ChannelView = ({ channel }: ChannelViewProps) => {
const { backend } = useLocalSearchParams<RootStackParams[ROUTES.CHANNELS]>();
const { data, isFetching } = useGetChannelVideosQuery(channel.name);
const { t } = useTranslation();

Expand All @@ -17,7 +21,10 @@ export const ChannelView = ({ channel }: ChannelViewProps) => {

return (
<VideoGrid
headerLink={{ text: t("visitChannel"), href: { pathname: "#" } }}
headerLink={{
text: t("visitChannel"),
href: { pathname: ROUTES.CHANNEL, params: { backend, channelHandle: channel.name } },
}}
variant="channel"
key={channel.id}
title={channel.displayName}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Pressable, StyleSheet, View } from "react-native";
import { useTheme } from "@react-navigation/native";
import { Typography } from "../Typography";
import { getHumanReadableDuration } from "../../utils";
import { Link, useNavigation } from "expo-router";
import { Link, useLocalSearchParams, useNavigation } from "expo-router";
import { VolumeControl } from "./components/VolumeControl";
import * as Device from "expo-device";
import { DeviceType } from "expo-device";
Expand All @@ -15,6 +15,8 @@ import { ScrubBar } from "./components/ScrubBar";
import { LinearGradient } from "expo-linear-gradient";
import Animated, { SlideInDown, SlideInUp, SlideOutDown, SlideOutUp, FadeIn, FadeOut } from "react-native-reanimated";
import { useTranslation } from "react-i18next";
import { ROUTES } from "../../types";
import { RootStackParams } from "../../app/_layout";

interface VideoControlsOverlayProps {
isVisible: boolean;
Expand All @@ -32,7 +34,7 @@ interface VideoControlsOverlayProps {
handleReplay: () => void;
handleJumpTo: (position: number) => void;
title?: string;
channelName?: string;
channel?: Partial<{ name: string; handle: string }>;
handleVolumeControl: (volume: number) => void;
volume: number;
toggleFullscreen: () => void;
Expand All @@ -59,7 +61,7 @@ export const VideoControlsOverlay = ({
handleReplay,
handleJumpTo,
title,
channelName,
channel,
handleVolumeControl,
volume,
toggleFullscreen,
Expand All @@ -68,6 +70,7 @@ export const VideoControlsOverlay = ({
handleShare,
handleOpenSettings,
}: PropsWithChildren<VideoControlsOverlayProps>) => {
const { backend } = useLocalSearchParams<RootStackParams[ROUTES.VIDEO]>();
const { t } = useTranslation();
const { colors } = useTheme();
const navigation = useNavigation();
Expand Down Expand Up @@ -105,15 +108,15 @@ export const VideoControlsOverlay = ({
<View style={styles.topLeftControls} pointerEvents="box-none">
<PlayerButton onPress={navigation.goBack} icon="Arrow-Left" />
<View style={styles.videoInfoContainer}>
<Link href="#">
<Link href={{ pathname: ROUTES.CHANNEL, params: { backend, channelHandle: channel?.handle } }}>
<Typography
numberOfLines={1}
ellipsizeMode="tail"
color={colors.white80}
fontSize={isMobile ? "sizeXS" : "sizeSm"}
fontWeight="SemiBold"
>
{channelName}
{channel?.name}
</Typography>
</Link>
<Typography
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,38 @@ import { Typography } from "../../../Typography";
import { format } from "date-fns";
import { Spacer } from "../../../shared/Spacer";
import { useTheme } from "@react-navigation/native";
import { ROUTES } from "../../../../types";
import { useLocalSearchParams } from "expo-router";
import { RootStackParams } from "../../../../app/_layout";

interface VideoDetailsProps {
onClose: () => void;
name: string;
channelName: string;
channelHandle?: string;
datePublished: string | Date;
description: string;
}

export const VideoDetails = ({ onClose, name, channelName, datePublished, description }: VideoDetailsProps) => {
export const VideoDetails = ({
onClose,
name,
channelName,
channelHandle,
datePublished,
description,
}: VideoDetailsProps) => {
const { colors } = useTheme();
const { backend } = useLocalSearchParams<RootStackParams[ROUTES.VIDEO]>();

return (
<Animated.View entering={SlideInLeft} exiting={SlideOutLeft} style={styles.animatedContainer}>
<ModalContainer onClose={onClose} title={name} containerStyle={styles.modalContainer}>
<View style={styles.metadataContainer}>
<ChannelLink text={channelName} href="#" />
<ChannelLink
text={channelName}
href={{ pathname: ROUTES.CHANNEL, params: { backend, channel: channelHandle } }}
/>
<Typography fontSize="sizeSm" fontWeight="SemiBold" color={colors.themeDesaturated500}>
{format(datePublished, "dd MMMM yyyy")}
</Typography>
Expand Down
5 changes: 4 additions & 1 deletion OwnTube.tv/components/VideoGridCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ export const VideoGridCard = ({ video, backend }: VideoGridCardProps) => {
</View>
</Pressable>
<View style={styles.restInfoContainer}>
<ChannelLink href="#" text={video.channel?.displayName} />
<ChannelLink
href={{ pathname: ROUTES.CHANNEL, params: { backend, channel: video.channel.name } }}
text={video.channel?.displayName}
/>
<Typography fontSize="sizeXS" fontWeight="Medium" color={colors.themeDesaturated500}>
{`${video.publishedAt ? formatDistanceToNow(video.publishedAt, { addSuffix: true, locale: LANGUAGE_OPTIONS.find(({ value }) => value === i18n.language)?.dateLocale }) : ""}${t("views", { count: video.views })}`}
</Typography>
Expand Down
7 changes: 4 additions & 3 deletions OwnTube.tv/components/VideoView/VideoView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import { Gesture, GestureDetector } from "react-native-gesture-handler";
import * as Device from "expo-device";
import { DeviceType } from "expo-device";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { VideoChannelSummary } from "@peertube/peertube-types";

export interface VideoViewProps {
uri: string;
testID: string;
handleSetTimeStamp: (timestamp: number) => void;
timestamp?: string;
title?: string;
channelName?: string;
channel?: VideoChannelSummary;
toggleFullscreen: () => void;
isFullscreen: boolean;
handleOpenDetails: () => void;
Expand All @@ -29,7 +30,7 @@ const VideoView = ({
handleSetTimeStamp,
timestamp,
title,
channelName,
channel,
toggleFullscreen,
isFullscreen,
handleOpenDetails,
Expand Down Expand Up @@ -127,7 +128,7 @@ const VideoView = ({
handleReplay={handleReplay}
handleJumpTo={handleJumpTo}
title={title}
channelName={channelName}
channel={{ name: channel?.displayName, handle: channel?.name }}
handleVolumeControl={handleVolumeControl}
volume={playbackStatus?.volume ?? 0}
toggleFullscreen={toggleFullscreen}
Expand Down
Loading

0 comments on commit 327c7f9

Please sign in to comment.