diff --git a/.eslintrc.js b/.eslintrc.js index bf8aa8d..b20e65a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,7 +31,7 @@ module.exports = { // Best Practices eqeqeq: ["error", "always"], // Enforce '===' instead of '==' curly: ["error", "multi-line", "consistent"], // Require curly braces for all control statements - "no-unused-vars": "warn", // Warn about variables that are declared but not used + "no-unused-vars": "off", // Not needed with TypeScript // React Specific "react/jsx-filename-extension": [1, { extensions: [".tsx"] }], // Allow jsx syntax in .tsx files diff --git a/app.json b/app.json index 9e0404e..e68d44e 100644 --- a/app.json +++ b/app.json @@ -38,7 +38,7 @@ "googleServicesFile": "./google-services.json" }, "web": { - "favicon": "./src/assets/favicon.png" + "favicon": "./src/assets/icons/favicon.png" } } } diff --git a/src/assets/favicon.png b/src/assets/icons/favicon.png similarity index 100% rename from src/assets/favicon.png rename to src/assets/icons/favicon.png diff --git a/src/component/video/camera.tsx b/src/component/video/camera.tsx index c1ed84b..8dbb6cb 100644 --- a/src/component/video/camera.tsx +++ b/src/component/video/camera.tsx @@ -4,7 +4,6 @@ import * as gql from "railbird-gql"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { Button, StyleSheet, Text, View } from "react-native"; import * as RNFS from "react-native-fs"; - import { Camera, CameraRuntimeError, @@ -19,6 +18,7 @@ import { RecordingButton } from "./capture-button"; import { useIsForeground } from "./is-foreground"; type Dictionary = { + // eslint-disable-next-line no-unused-vars [key in KeyType]: ValueType; }; @@ -111,6 +111,7 @@ export default function CameraScreen({ navigation, }): React.ReactElement { const apolloClient = useApolloClient(); + const { params } = route; const [createUpload, { data, loading, error }] = gql.useCreateUploadStreamMutation(); @@ -127,6 +128,7 @@ export default function CameraScreen({ console.log(`VideoId: ${newVideoId}`); setUploadManager(new StreamUploadManager(apolloClient, newVideoId)); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, uploadManager]); const camera = useRef(null); @@ -143,14 +145,24 @@ export default function CameraScreen({ }, []); const onInitialized = useCallback(() => { - console.log("Camera initialized!"); setIsCameraInitialized(true); - createUpload({ variables: { videoName: "Test" } }); + createUpload({ + variables: { videoName: params?.sessionName ?? "New session" }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const onMediaCaptured = useCallback((media: PhotoFile | VideoFile) => { - console.log(`Media captured! ${JSON.stringify(media)}`); - }, []); + const onMediaCaptured = useCallback( + (media: PhotoFile | VideoFile) => { + console.log(`Media captured! ${JSON.stringify(media)}`); + navigation.push("SaveVideo", { + videoId: uploadManager?.videoId, + ...params, + }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [uploadManager, navigation, params], + ); const onVideoChunkReady = useCallback( (event) => { @@ -179,7 +191,6 @@ export default function CameraScreen({ // Replace with error handling if (device === null) { - console.log(device); // hasPermission redundant here - user should not be able to launch camera without permissions return ( diff --git a/src/component/video/use-video-details.tsx b/src/component/video/use-video-details.tsx new file mode 100644 index 0000000..6b323dd --- /dev/null +++ b/src/component/video/use-video-details.tsx @@ -0,0 +1,154 @@ +import * as gql from "railbird-gql"; +import { useCallback, useState } from "react"; +// @ts-ignore +import { useCameraPermission } from "react-native-vision-camera"; +import { showAlert } from "../../lib/alert-messages"; + +// TODO: #122 should decide on what values/labels go here. +enum GameType { + FreePlay = "freePlay", + StraightPool = "straightPool", + NineBall = "nineBall", +} +// TODO: #122 should be int -- inch value of table size. +enum TableSize { + NineFoot = "nineFoot", + EightFoot = "eightFoot", + SevenFoot = "sevenFoot", +} + +interface VideoUserInputMeta { + sessionName: string; + gameType: GameType | null; + tableSize: TableSize | null; +} + +interface VideoFlowInputParams extends VideoUserInputMeta { + videoId?: number; +} + +function useDropdown( + initialValue: T | null, + options: Array<{ label: string; value: T }>, + closeOthers: () => void, +) { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [value, setValue] = useState(initialValue); + const [optionsList, setOptionsList] = useState(options); + + const toggleOpen = useCallback(() => { + if (!isDropdownOpen) { + closeOthers(); + } + setIsDropdownOpen(!isDropdownOpen); + }, [isDropdownOpen, closeOthers]); + + return { + isDropdownOpen, + toggleOpen, + value, + setValue, + optionsList, + setOptionsList, + setIsDropdownOpen, + }; +} + +export const useVideoDetails = ({ + params: { + mode, + videoId, + sessionName: initialSessionName, + gameType: initialGameType, + tableSize: initialTableSize, + }, + navigation, +}) => { + const initialState = { + sessionName: initialSessionName || "", + gameType: initialGameType || null, + tableSize: initialTableSize || null, + }; + const { hasPermission, requestPermission } = useCameraPermission(); + + const [sessionName, setSessionName] = useState( + initialState.sessionName, + ); + + const closeAllDropdowns = useCallback(() => { + gameType.setIsDropdownOpen(false); + tableSize.setIsDropdownOpen(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const gameType = useDropdown( + initialState.gameType, + [ + { label: "Free Play", value: GameType.FreePlay }, + { label: "Straight Pool", value: GameType.StraightPool }, + { label: "Nine Ball", value: GameType.NineBall }, + ], + closeAllDropdowns, + ); + + const tableSize = useDropdown( + initialState.tableSize, + [ + { label: `9'`, value: TableSize.NineFoot }, + { label: `8'`, value: TableSize.EightFoot }, + { label: `7'`, value: TableSize.SevenFoot }, + ], + closeAllDropdowns, + ); + + const [terminateUploadStream, { loading, error }] = + gql.useTerminateUploadStreamMutation(); + + const handleSubmit = async () => { + // Check permissions + if (!hasPermission) { + try { + const permissionResult = await requestPermission(); + if (permissionResult.status !== "granted") { + return showAlert("camera"); + } + } catch (err) { + return showAlert("permissionError"); + } + } + + // Navigate if starting flow, terminateUploadStream if completing flow + if (mode === "start-video") { + const params: VideoFlowInputParams = { + sessionName, + gameType: gameType.value, + tableSize: tableSize.value, + }; + navigation.push("Camera", params); + } else { + try { + await terminateUploadStream({ + variables: { + videoId, + videoName: sessionName, + tableSize: tableSize.value, + gameType: gameType.value, + }, + }); + navigation.push("Tabs"); + } catch (err) { + showAlert("terminateUpload"); + } + } + }; + + return { + sessionName, + setSessionName, + gameType, + tableSize, + handleSubmit, + loading, + error, + }; +}; diff --git a/src/graphql/client.tsx b/src/graphql/client.tsx index 7194399..21fb914 100644 --- a/src/graphql/client.tsx +++ b/src/graphql/client.tsx @@ -50,7 +50,6 @@ export const ClientProvider: React.FC = ({ children }) => { const authMiddleware = new ApolloLink((operation, forward) => { const { key, value } = authHeader; - console.log("Auth Key", key, "Value", value); if (key && value) { operation.setContext({ headers: { diff --git a/src/lib/alert-messages/constants.ts b/src/lib/alert-messages/constants.ts index 02e5d94..4f1895f 100644 --- a/src/lib/alert-messages/constants.ts +++ b/src/lib/alert-messages/constants.ts @@ -8,6 +8,10 @@ interface PermissionMessage { message: string; }; } +interface ApiError { + title: string; + message: string; +} export const CAMERA_PERMISSION_DENIED: PermissionMessage = { android: { @@ -19,3 +23,13 @@ export const CAMERA_PERMISSION_DENIED: PermissionMessage = { message: "Please go to Settings > Railbird > Camera and grant permissions.", }, }; + +export const PERMISSION_ERROR: ApiError = { + title: "There was an issue accessing permissions. ", + message: "Please check you settings and try again", +}; + +export const TERMINATE_UPLOAD_ERROR: ApiError = { + title: "There was an issue.", + message: "Please try again", +}; diff --git a/src/lib/alert-messages/index.ts b/src/lib/alert-messages/index.ts index 4c76604..b71bebe 100644 --- a/src/lib/alert-messages/index.ts +++ b/src/lib/alert-messages/index.ts @@ -1,12 +1,18 @@ import { Alert, Platform } from "react-native"; -import { CAMERA_PERMISSION_DENIED } from "./constants"; +import { + CAMERA_PERMISSION_DENIED, + PERMISSION_ERROR, + TERMINATE_UPLOAD_ERROR, +} from "./constants"; const ALERT_TYPE = { camera: CAMERA_PERMISSION_DENIED, + permissionError: PERMISSION_ERROR, + terminateUpload: TERMINATE_UPLOAD_ERROR, }; export const showAlert = (alertType: string) => { const alert = ALERT_TYPE[alertType]; - const { title, message } = alert[Platform.OS]; + const { title, message } = alert[Platform.OS] ?? alert; Alert.alert(title, message); }; diff --git a/src/navigation/tab-navigator.tsx b/src/navigation/tab-navigator.tsx index 1800ce6..90ca9eb 100644 --- a/src/navigation/tab-navigator.tsx +++ b/src/navigation/tab-navigator.tsx @@ -4,10 +4,10 @@ import { Image } from "react-native"; import CameraScreen from "../component/video/camera"; import Profile from "../screens/profile"; import Session from "../screens/session"; -import RecordScreen from "../screens/video-stack/record"; +import VideoDetails from "../screens/video-stack/video-details"; import { tabIconColors } from "../styles"; -import Icon from "../assets/favicon.png"; +import Icon from "../assets/icons/favicon.png"; const Tab = createBottomTabNavigator(); const RecordStack = createNativeStackNavigator(); @@ -22,8 +22,17 @@ const tabIcons = { function VideoTabStack() { return ( - + + ); } diff --git a/src/screens/video-stack/record.tsx b/src/screens/video-stack/record.tsx deleted file mode 100644 index 9235498..0000000 --- a/src/screens/video-stack/record.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { - Keyboard, - Text, - TextInput, - TouchableOpacity, - TouchableWithoutFeedback, - View, -} from "react-native"; -import DropDownPicker from "react-native-dropdown-picker"; -// @ts-ignore -import { useCameraPermission } from "react-native-vision-camera"; -import { showAlert } from "../../lib/alert-messages"; -import { recordStyles as styles } from "./styles"; - -interface CameraScreenParams { - gameType: string; - tableSize: string; - tags: Array; - location: string; -} - -export default function RecordScreen({ navigation }): React.JSX.Element { - // Permissions - const { hasPermission, requestPermission } = useCameraPermission(); - - if (!hasPermission) { - requestPermission(); - } - - // Game type dropdown - const [gameTypeOpen, setGameTypeOpen] = useState(false); - const [gameType, setGameType] = useState(null); // This is a dropdown - const [gameTypes, setGameTypes] = useState([ - { label: "Free Play", value: "freePlay" }, - { label: "Straight Pool", value: "straightPool" }, - { label: "Nine Ball", value: "nineBall" }, - ]); - const onGameTypeOpen = useCallback(() => { - setTableSizeOpen(false); - setTagsOpen(false); - }, []); - - // Table size dropdown - const [tableSizeOpen, setTableSizeOpen] = useState(false); - const [tableSize, setTableSize] = useState(""); - const [tableSizes, setTableSizes] = useState([ - { label: `9'`, value: "nineFoot" }, - { label: `8'`, value: "eightFoot" }, - { label: "7", value: "sevenFoot" }, - ]); - const onTableSizeOpen = useCallback(() => { - setGameTypeOpen(false); - setTagsOpen(false); - }, []); - - // Tags multi-select dropdown - const [tagsOpen, setTagsOpen] = useState(false); - const [tags, setTags] = useState>([]); - const [tagsList, setTagsList] = useState([ - { label: `Tag1`, value: "tag1" }, - { label: `Tag2`, value: "tag2" }, - { label: "Tag3", value: "tag3" }, - ]); - const onTagsOpen = useCallback(() => { - setTableSizeOpen(false); - setGameTypeOpen(false); - }, []); - - // Location - const [location, setLocation] = useState(""); - - const handleSubmit = () => { - if (!hasPermission) { - return showAlert("camera"); - } - - // needs to pass info as params or store in a context/state provider - const params: CameraScreenParams = { - gameType: gameType, - tableSize: tableSize, - tags: tags, - location: location, - }; - navigation.push("Camera", params); - }; - - const dropDownStyles = { - style: styles.dropdownStyle, - }; - - return ( - Keyboard.dismiss()}> - - - Game Type - - Table size - - Tags - - - Location - setLocation(value)} - /> - - - navigation.goBack()} - > - Back - - - Next - - - - - ); -} diff --git a/src/screens/video-stack/styles.ts b/src/screens/video-stack/styles.ts index 458d271..21e1ce3 100644 --- a/src/screens/video-stack/styles.ts +++ b/src/screens/video-stack/styles.ts @@ -7,26 +7,13 @@ export const recordStyles = StyleSheet.create({ justifyContent: "center", padding: 20, }, - dropdownContainer: { - width: "100%", - marginBottom: 20, - zIndex: 50, - }, - dropdownTitle: { - fontSize: 16, + headerText: { + fontSize: 22, fontWeight: "500", - marginBottom: 5, - alignSelf: "flex-start", - }, - input: { - width: "100%", - marginBottom: 20, - borderWidth: 1, - borderColor: "grey", - borderRadius: 5, - padding: 10, + paddingBottom: "10%", }, buttonContainer: { + marginTop: 10, flexDirection: "row", justifyContent: "space-between", width: "100%", @@ -42,16 +29,4 @@ export const recordStyles = StyleSheet.create({ color: "white", textAlign: "center", }, - dropdownStyle: { - backgroundColor: "#ffffff", - borderColor: "#D1D1D1", - borderWidth: 1, - borderRadius: 4, - }, - dropdownContainerStyle: { - marginBottom: 10, - borderColor: "#D1D1D1", - borderWidth: 1, - borderRadius: 4, - }, }); diff --git a/src/screens/video-stack/video-details.tsx b/src/screens/video-stack/video-details.tsx new file mode 100644 index 0000000..324bb2c --- /dev/null +++ b/src/screens/video-stack/video-details.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { + ActivityIndicator, + Keyboard, + Text, + TextInput, + TouchableOpacity, + TouchableWithoutFeedback, + View, +} from "react-native"; +import DropDownPicker from "react-native-dropdown-picker"; +import { useVideoDetails } from "../../component/video/use-video-details"; +import { globalInputStyles } from "../../styles"; +import { recordStyles as styles } from "./styles"; + +export default function VideoDetails({ navigation, route }): React.JSX.Element { + const { mode } = route.params; + const { + sessionName, + setSessionName, + gameType, + tableSize, + handleSubmit, + loading, + } = useVideoDetails({ params: route.params, navigation }); + + const dropDownStyles = { + style: globalInputStyles.dropdownStyle, + }; + + return ( + Keyboard.dismiss()}> + + + {mode === "start-video" ? "Record Session" : "Save Session"} + + + Session Name + + Game Type + + Table Size + + + + + {mode === "start-video" && ( + navigation.goBack()} + > + Back + + )} + + {loading ? ( + + ) : ( + + {mode === "start-video" ? "Next" : "Save"} + + )} + + + + + ); +} diff --git a/src/styles.ts b/src/styles.ts index 1cc4549..9b229b2 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -1,4 +1,33 @@ // GLOBAL STYLES +import { StyleSheet } from "react-native"; + +export const globalInputStyles = StyleSheet.create({ + input: { + width: "100%", + padding: 10, + backgroundColor: "#ffffff", + borderColor: "#D1D1D1", + borderWidth: 1, + borderRadius: 4, + }, + dropdownContainer: { + width: "100%", + marginBottom: 20, + zIndex: 50, + }, + dropdownTitle: { + fontSize: 16, + fontWeight: "500", + marginBottom: 5, + alignSelf: "flex-start", + }, + dropdownStyle: { + backgroundColor: "#ffffff", + borderColor: "#D1D1D1", + borderWidth: 1, + borderRadius: 4, + }, +}); // COLORS: // can be made more granular to specify utility (ex: fontColors vs backgroundColors) export const colors = { diff --git a/yarn.lock b/yarn.lock index 74b6712..03a9858 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9800,7 +9800,7 @@ queue@6.0.2: "railbird-gql@git+https://dev.railbird.ai/railbird/railbird-gql.git#master": version "1.0.0" - resolved "git+https://dev.railbird.ai/railbird/railbird-gql.git#204e289627b08a96b947b6f23ed4a843d9e8abff" + resolved "git+https://dev.railbird.ai/railbird/railbird-gql.git#ce8cfd6a68d7b27f478c99dd5ae74a411d4d0c77" dependencies: "@apollo/client" "^3.9.2" "@graphql-codegen/cli" "^5.0.0"