Skip to content

Commit

Permalink
Merge pull request #96 from mykhailodanilenko/feature/sepia-instances…
Browse files Browse the repository at this point in the history
…-view

Select custom instances from Sepia
  • Loading branch information
mykhailodanilenko authored Jul 5, 2024
2 parents 26bc5df + f12c188 commit 2c8a586
Show file tree
Hide file tree
Showing 16 changed files with 376 additions and 46 deletions.
45 changes: 45 additions & 0 deletions OwnTube.tv/api/instanceSearchApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import axios, { AxiosInstance } from "axios";
import { PeertubeInstance } from "./models";

export class InstanceSearchApi {
private instance!: AxiosInstance;
constructor() {
this.createAxiosInstance();
}

/**
* Create the Axios instance with request/response interceptors
*/
private createAxiosInstance(): void {
this.instance = axios.create({
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/json",
},
});
}

// Common query parameters for fetching videos that are classified as "local", "non-live", and "Safe-For-Work"
private readonly commonQueryParams = {
start: 0,
count: 1000,
sort: "createdAt",
};

/**
* Get up to 1000 instances
*/
async searchInstances(): Promise<{ data: Array<PeertubeInstance> }> {
try {
const response = await this.instance.get("instances", {
params: this.commonQueryParams,
baseURL: "https://instances.joinpeertube.org/api/v1",
});
return response.data;
} catch (error: unknown) {
throw new Error(`Failed to fetch instances: ${(error as Error).message}`);
}
}
}

export const InstanceSearchServiceImpl = new InstanceSearchApi();
35 changes: 35 additions & 0 deletions OwnTube.tv/api/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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";

export type GetVideosVideo = Pick<VideoModel, "uuid" | "name" | "description" | "duration"> & {
thumbnailPath: string;
category: { id: number | null; label: string };
};

export type PeertubeInstance = {
id: number;
host: string;
name: string;
shortDescription: string;
version: string;
signupAllowed: boolean;
signupRequiresApproval: boolean;
userVideoQuota: number;
liveEnabled: boolean;
categories: number[];
languages: string[];
autoBlacklistUserVideosEnabled: boolean;
defaultNSFWPolicy: "do_not_list" | "display" | "blur";
isNSFW: boolean;
avatars: Array<{ width: number; url: string }>;
banners: Array<{ width: number; url: string }>;
totalUsers: number;
totalVideos: number;
totalLocalVideos: number;
totalInstanceFollowers: number;
totalInstanceFollowing: number;
supportsIPv6: boolean;
country: string;
health: number;
createdAt: number;
};
8 changes: 1 addition & 7 deletions OwnTube.tv/api/peertubeVideosApi.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import axios, { AxiosInstance } from "axios";
import { VideosCommonQuery } from "@peertube/peertube-types";
import { VideoModel } from "@peertube/peertube-types/server/core/models/video/video";
import { Video } from "@peertube/peertube-types/peertube-models/videos/video.model";

// Subset of a video object from the PeerTube backend API, https://github.com/Chocobozzz/PeerTube/blob/develop/server/core/models/video/video.ts#L460
export type GetVideosVideo = Pick<VideoModel, "uuid" | "name" | "description" | "duration"> & {
thumbnailPath: string;
category: { id: number | null; label: string };
};
import { GetVideosVideo } from "./models";

/**
* Get videos from the PeerTube backend `/api/v1/videos` API
Expand Down
16 changes: 15 additions & 1 deletion OwnTube.tv/api/queries.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { useQuery } from "@tanstack/react-query";
import { ApiServiceImpl, GetVideosVideo } from "./peertubeVideosApi";
import { ApiServiceImpl } from "./peertubeVideosApi";
import { SOURCES } from "../types";
import { getLocalData } from "./helpers";
import { Video } from "@peertube/peertube-types/peertube-models/videos/video.model";
import { useLocalSearchParams } from "expo-router";
import { RootStackParams } from "../app/_layout";
import { InstanceSearchServiceImpl } from "./instanceSearchApi";
import { GetVideosVideo } from "./models";

export enum QUERY_KEYS {
videos = "videos",
video = "video",
instances = "instances",
}

export const useGetVideosQuery = <TResult = GetVideosVideo[]>({
Expand Down Expand Up @@ -53,3 +56,14 @@ export const useGetVideoQuery = <TResult = Video>(id?: string, select?: (data: V
select,
});
};

export const useGetInstancesQuery = () => {
return useQuery({
queryKey: [QUERY_KEYS.instances],
queryFn: async () => {
return await InstanceSearchServiceImpl.searchInstances();
},
refetchOnWindowFocus: false,
select: ({ data }) => data,
});
};
1 change: 1 addition & 0 deletions OwnTube.tv/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export const colors = {
ghostwhite: "#F8F8FF",
gainsboro: "#DCDCDC",
_50percentBlackTint: "rgba(0, 0, 0, 0.5)",
sepia: "#704214",
};
35 changes: 35 additions & 0 deletions OwnTube.tv/components/AppConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DeviceCapabilities } from "./DeviceCapabilities";
import { StyleSheet, Switch, View } from "react-native";
import { Typography } from "./Typography";
import { useAppConfigContext, useColorSchemeContext } from "../contexts";

export const AppConfig = () => {
const { isDebugMode, setIsDebugMode } = useAppConfigContext();
const { scheme, toggleScheme } = useColorSchemeContext();

return (
<View style={styles.deviceInfoAndToggles}>
<DeviceCapabilities />
<View style={styles.togglesContainer}>
<View style={styles.option}>
<Typography>Debug logging</Typography>
<Switch value={isDebugMode} onValueChange={setIsDebugMode} />
</View>
<View style={styles.option}>
<Typography>Toggle Theme</Typography>
<Switch value={scheme === "light"} onValueChange={toggleScheme} />
</View>
</View>
</View>
);
};

const styles = StyleSheet.create({
deviceInfoAndToggles: { flexDirection: "row", flexWrap: "wrap", gap: 16, width: "100%" },
option: {
alignItems: "center",
flexDirection: "row",
gap: 5,
},
togglesContainer: { flex: 1, minWidth: 200 },
});
136 changes: 136 additions & 0 deletions OwnTube.tv/components/ComboBoxInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { FlatList, Pressable, StyleSheet, TextInput, View } from "react-native";
import { Typography } from "./Typography";
import { useCallback, useMemo, useState } from "react";
import { useTheme } from "@react-navigation/native";

interface DropDownItem {
label: string;
value: string;
}

interface ComboBoxInputProps {
value?: string;
onChange: (value: string) => void;
data?: Array<DropDownItem>;
testID: string;
}

const LIST_ITEM_HEIGHT = 50;

const DropdownItem = ({
onSelect,
item,
value,
}: {
onSelect: (item: DropDownItem) => () => void;
item: DropDownItem;
value: string;
}) => {
const [isHovered, setIsHovered] = useState(false);
const { colors } = useTheme();

return (
<Pressable
onHoverIn={() => setIsHovered(true)}
onHoverOut={() => setIsHovered(false)}
style={{
justifyContent: "center",
height: LIST_ITEM_HEIGHT,
backgroundColor: colors[isHovered ? "card" : "background"],
}}
onPress={onSelect(item)}
>
<Typography color={item.value === value ? colors.primary : undefined}>{item.label}</Typography>
</Pressable>
);
};

export const ComboBoxInput = ({ value = "", onChange, data = [], testID }: ComboBoxInputProps) => {
const { colors } = useTheme();
const [inputValue, setInputValue] = useState("");
const [isDropDownVisible, setIsDropDownVisible] = useState(false);

const onSelect = (item: { label: string; value: string }) => () => {
onChange(item.value);
setInputValue("");
setIsDropDownVisible(false);
};

const filteredList = useMemo(() => {
if (!inputValue) {
return data;
}

return data.filter(({ label }) => label.toLowerCase().includes(inputValue.toLowerCase()));
}, [data, inputValue]);

const initialScrollIndex = useMemo(() => {
if (value) {
const scrollTo = filteredList?.findIndex(({ value: itemValue }) => itemValue === value) || 0;

return scrollTo > 0 ? scrollTo : 0;
}

return 0;
}, [filteredList, value]);

const renderItem = useCallback(
({ item }: { item: DropDownItem }) => <DropdownItem item={item} onSelect={onSelect} value={value} />,
[value, onSelect],
);

return (
<View testID={testID} accessible={false}>
<TextInput
placeholder="Search instances..."
placeholderTextColor={colors.text}
style={[{ color: colors.primary, backgroundColor: colors.card, borderColor: colors.primary }, styles.input]}
onFocus={() => setIsDropDownVisible(true)}
onBlur={() => {
setTimeout(() => {
setIsDropDownVisible(false);
}, 300);
}}
value={inputValue}
onChangeText={setInputValue}
/>
{isDropDownVisible && (
<View style={[{ borderColor: colors.border }, styles.optionsContainer]}>
<FlatList
data={filteredList}
renderItem={renderItem}
extraData={filteredList?.length}
initialScrollIndex={initialScrollIndex}
keyExtractor={({ value }) => value}
getItemLayout={(_, index) => ({
length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index,
index,
})}
maxToRenderPerBatch={50}
/>
</View>
)}
</View>
);
};

const styles = StyleSheet.create({
input: {
borderRadius: 8,
borderWidth: 1,
height: 30,
width: 300,
},
optionsContainer: {
borderRadius: 8,
borderWidth: 1,
flex: 1,
height: 400,
padding: 8,
position: "absolute",
top: 30,
width: 500,
zIndex: 1,
},
});
23 changes: 23 additions & 0 deletions OwnTube.tv/components/SourceSelect.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { render, screen } from "@testing-library/react-native";
import { SourceSelect } from "./SourceSelect";

jest.mock("../api", () => ({
...jest.requireActual("../api"),
useGetInstancesQuery: () => ({
data: [
{ id: "withLocalVids", totalLocalVideos: 100 },
{ id: "withoutLocalVids", totalLocalVideos: 0 },
],
}),
}));

jest.mock("./ComboBoxInput", () => ({
ComboBoxInput: "ComboBoxInput",
}));

describe("SourceSelect", () => {
it("should filter out 0-video instances", () => {
render(<SourceSelect />);
expect(screen.getByTestId("custom-instance-select").props.data.length).toBe(1);
});
});
Loading

0 comments on commit 2c8a586

Please sign in to comment.