From 7f546e122eb1d47f3680a673faeef88df43d20ea Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Tue, 13 Apr 2021 21:21:43 +0200
Subject: [PATCH] Convert MediaSidebar to Typescript

---
 ui/src/Pages/SharePage/SharePage.tsx          |   3 +
 .../SharePage/__generated__/SharePageToken.ts | 105 ++++++-----
 .../components/photoGallery/PhotoGallery.tsx  |  15 +-
 .../photoGallery/ProtectedMedia.tsx           |  10 +-
 .../photoGallery/presentView/PresentMedia.tsx |   1 +
 .../{MediaSidebar.js => MediaSidebar.tsx}     | 167 ++++++++++++------
 6 files changed, 186 insertions(+), 115 deletions(-)
 rename ui/src/components/sidebar/{MediaSidebar.js => MediaSidebar.tsx} (72%)

diff --git a/ui/src/Pages/SharePage/SharePage.tsx b/ui/src/Pages/SharePage/SharePage.tsx
index 076d532..f2c752d 100644
--- a/ui/src/Pages/SharePage/SharePage.tsx
+++ b/ui/src/Pages/SharePage/SharePage.tsx
@@ -44,8 +44,11 @@ export const SHARE_TOKEN_QUERY = gql`
         }
         videoWeb {
           url
+          width
+          height
         }
         exif {
+          id
           camera
           maker
           lens
diff --git a/ui/src/Pages/SharePage/__generated__/SharePageToken.ts b/ui/src/Pages/SharePage/__generated__/SharePageToken.ts
index fd14709..68c3e54 100644
--- a/ui/src/Pages/SharePage/__generated__/SharePageToken.ts
+++ b/ui/src/Pages/SharePage/__generated__/SharePageToken.ts
@@ -3,163 +3,172 @@
 // @generated
 // This file was automatically generated and should not be edited.
 
-import { MediaType } from "./../../../../__generated__/globalTypes";
+import { MediaType } from './../../../../__generated__/globalTypes'
 
 // ====================================================
 // GraphQL query operation: SharePageToken
 // ====================================================
 
 export interface SharePageToken_shareToken_album {
-  __typename: "Album";
-  id: string;
+  __typename: 'Album'
+  id: string
 }
 
 export interface SharePageToken_shareToken_media_thumbnail {
-  __typename: "MediaURL";
+  __typename: 'MediaURL'
   /**
    * URL for previewing the image
    */
-  url: string;
+  url: string
   /**
    * Width of the image in pixels
    */
-  width: number;
+  width: number
   /**
    * Height of the image in pixels
    */
-  height: number;
+  height: number
 }
 
 export interface SharePageToken_shareToken_media_downloads_mediaUrl {
-  __typename: "MediaURL";
+  __typename: 'MediaURL'
   /**
    * URL for previewing the image
    */
-  url: string;
+  url: string
   /**
    * Width of the image in pixels
    */
-  width: number;
+  width: number
   /**
    * Height of the image in pixels
    */
-  height: number;
+  height: number
   /**
    * The file size of the resource in bytes
    */
-  fileSize: number;
+  fileSize: number
 }
 
 export interface SharePageToken_shareToken_media_downloads {
-  __typename: "MediaDownload";
-  title: string;
-  mediaUrl: SharePageToken_shareToken_media_downloads_mediaUrl;
+  __typename: 'MediaDownload'
+  title: string
+  mediaUrl: SharePageToken_shareToken_media_downloads_mediaUrl
 }
 
 export interface SharePageToken_shareToken_media_highRes {
-  __typename: "MediaURL";
+  __typename: 'MediaURL'
   /**
    * URL for previewing the image
    */
-  url: string;
+  url: string
   /**
    * Width of the image in pixels
    */
-  width: number;
+  width: number
   /**
    * Height of the image in pixels
    */
-  height: number;
+  height: number
 }
 
 export interface SharePageToken_shareToken_media_videoWeb {
-  __typename: "MediaURL";
+  __typename: 'MediaURL'
   /**
    * URL for previewing the image
    */
-  url: string;
+  url: string
+  /**
+   * Width of the image in pixels
+   */
+  width: number
+  /**
+   * Height of the image in pixels
+   */
+  height: number
 }
 
 export interface SharePageToken_shareToken_media_exif {
-  __typename: "MediaEXIF";
+  __typename: 'MediaEXIF'
+  id: string
   /**
    * The model name of the camera
    */
-  camera: string | null;
+  camera: string | null
   /**
    * The maker of the camera
    */
-  maker: string | null;
+  maker: string | null
   /**
    * The name of the lens
    */
-  lens: string | null;
-  dateShot: any | null;
+  lens: string | null
+  dateShot: any | null
   /**
    * The exposure time of the image
    */
-  exposure: number | null;
+  exposure: number | null
   /**
    * The aperature stops of the image
    */
-  aperture: number | null;
+  aperture: number | null
   /**
    * The ISO setting of the image
    */
-  iso: number | null;
+  iso: number | null
   /**
    * The focal length of the lens, when the image was taken
    */
-  focalLength: number | null;
+  focalLength: number | null
   /**
    * A formatted description of the flash settings, when the image was taken
    */
-  flash: number | null;
+  flash: number | null
   /**
    * An index describing the mode for adjusting the exposure of the image
    */
-  exposureProgram: number | null;
+  exposureProgram: number | null
 }
 
 export interface SharePageToken_shareToken_media {
-  __typename: "Media";
-  id: string;
-  title: string;
-  type: MediaType;
+  __typename: 'Media'
+  id: string
+  title: string
+  type: MediaType
   /**
    * URL to display the media in a smaller resolution
    */
-  thumbnail: SharePageToken_shareToken_media_thumbnail | null;
-  downloads: SharePageToken_shareToken_media_downloads[];
+  thumbnail: SharePageToken_shareToken_media_thumbnail | null
+  downloads: SharePageToken_shareToken_media_downloads[]
   /**
    * URL to display the photo in full resolution, will be null for videos
    */
-  highRes: SharePageToken_shareToken_media_highRes | null;
+  highRes: SharePageToken_shareToken_media_highRes | null
   /**
    * URL to get the video in a web format that can be played in the browser, will be null for photos
    */
-  videoWeb: SharePageToken_shareToken_media_videoWeb | null;
-  exif: SharePageToken_shareToken_media_exif | null;
+  videoWeb: SharePageToken_shareToken_media_videoWeb | null
+  exif: SharePageToken_shareToken_media_exif | null
 }
 
 export interface SharePageToken_shareToken {
-  __typename: "ShareToken";
-  token: string;
+  __typename: 'ShareToken'
+  token: string
   /**
    * The album this token shares
    */
-  album: SharePageToken_shareToken_album | null;
+  album: SharePageToken_shareToken_album | null
   /**
    * The media this token shares
    */
-  media: SharePageToken_shareToken_media | null;
+  media: SharePageToken_shareToken_media | null
 }
 
 export interface SharePageToken {
-  shareToken: SharePageToken_shareToken;
+  shareToken: SharePageToken_shareToken
 }
 
 export interface SharePageTokenVariables {
-  token: string;
-  password?: string | null;
+  token: string
+  password?: string | null
 }
diff --git a/ui/src/components/photoGallery/PhotoGallery.tsx b/ui/src/components/photoGallery/PhotoGallery.tsx
index 61d140c..dc02e94 100644
--- a/ui/src/components/photoGallery/PhotoGallery.tsx
+++ b/ui/src/components/photoGallery/PhotoGallery.tsx
@@ -7,6 +7,7 @@ import { SidebarContext } from '../sidebar/Sidebar'
 import MediaSidebar from '../sidebar/MediaSidebar'
 import { useTranslation } from 'react-i18next'
 import { PresentMediaProps_Media } from './presentView/PresentMedia'
+import { sidebarPhoto_media_thumbnail } from '../sidebar/__generated__/sidebarPhoto'
 
 const Gallery = styled.div`
   display: flex;
@@ -32,11 +33,7 @@ const ClearWrap = styled.div`
 `
 
 interface PhotoGalleryProps_Media extends PresentMediaProps_Media {
-  thumbnail: null | {
-    url: string
-    width: number
-    height: number
-  }
+  thumbnail: sidebarPhoto_media_thumbnail | null
 }
 
 type PhotoGalleryProps = {
@@ -69,15 +66,15 @@ const PhotoGallery = ({
 
   let photoElements = []
   if (media) {
-    photoElements = media.map((photo, index) => {
+    photoElements = media.map((media, index) => {
       const active = activeIndex == index
 
       return (
         <MediaThumbnail
-          key={photo.id}
-          media={photo}
+          key={media.id}
+          media={media}
           onSelectImage={index => {
-            updateSidebar(<MediaSidebar media={photo} />)
+            updateSidebar(<MediaSidebar media={media} />)
             onSelectImage(index)
           }}
           onFavorite={onFavorite}
diff --git a/ui/src/components/photoGallery/ProtectedMedia.tsx b/ui/src/components/photoGallery/ProtectedMedia.tsx
index 87eaad7..bd95cfc 100644
--- a/ui/src/components/photoGallery/ProtectedMedia.tsx
+++ b/ui/src/components/photoGallery/ProtectedMedia.tsx
@@ -1,4 +1,5 @@
 import React, { DetailedHTMLProps, ImgHTMLAttributes } from 'react'
+import { isNil } from '../../helpers/utils'
 
 const isNativeLazyLoadSupported = 'loading' in HTMLImageElement.prototype
 const placeholder =
@@ -73,11 +74,14 @@ export const ProtectedImage = ({
 }
 
 export interface ProtectedVideoProps_Media {
+  __typename: 'Media'
   id: string
-  thumbnail: null | {
+  thumbnail?: null | {
+    __typename: 'MediaURL'
     url: string
   }
-  videoWeb: null | {
+  videoWeb?: null | {
+    __typename: 'MediaURL'
     url: string
   }
 }
@@ -87,7 +91,7 @@ export interface ProtectedVideoProps {
 }
 
 export const ProtectedVideo = ({ media, ...props }: ProtectedVideoProps) => {
-  if (media.videoWeb === null) {
+  if (isNil(media.videoWeb)) {
     console.error('ProetctedVideo called with media.videoWeb = null')
     return null
   }
diff --git a/ui/src/components/photoGallery/presentView/PresentMedia.tsx b/ui/src/components/photoGallery/presentView/PresentMedia.tsx
index 782f50d..c4ee5a7 100644
--- a/ui/src/components/photoGallery/presentView/PresentMedia.tsx
+++ b/ui/src/components/photoGallery/presentView/PresentMedia.tsx
@@ -28,6 +28,7 @@ const StyledVideo = styled(ProtectedVideo)`
 export interface PresentMediaProps_Media extends ProtectedVideoProps_Media {
   type: MediaType
   highRes: null | {
+    __typename: 'MediaURL'
     url: string
   }
 }
diff --git a/ui/src/components/sidebar/MediaSidebar.js b/ui/src/components/sidebar/MediaSidebar.tsx
similarity index 72%
rename from ui/src/components/sidebar/MediaSidebar.js
rename to ui/src/components/sidebar/MediaSidebar.tsx
index 0bc82be..f044ad2 100644
--- a/ui/src/components/sidebar/MediaSidebar.js
+++ b/ui/src/components/sidebar/MediaSidebar.tsx
@@ -1,17 +1,30 @@
-import PropTypes from 'prop-types'
 import React, { useEffect } from 'react'
 import { useLazyQuery, gql } from '@apollo/client'
 import styled from 'styled-components'
 import { authToken } from '../../helpers/authentication'
-import { ProtectedImage, ProtectedVideo } from '../photoGallery/ProtectedMedia'
+import {
+  ProtectedImage,
+  ProtectedVideo,
+  ProtectedVideoProps_Media,
+} from '../photoGallery/ProtectedMedia'
 import SidebarShare from './Sharing'
 import SidebarDownload from './SidebarDownload'
 import SidebarItem from './SidebarItem'
 import { SidebarFacesOverlay } from '../facesOverlay/FacesOverlay'
 import { isNil } from '../../helpers/utils'
 import { useTranslation } from 'react-i18next'
+import { MediaType } from '../../../__generated__/globalTypes'
+import { TranslationFn } from '../../localization'
+import {
+  sidebarPhoto,
+  sidebarPhotoVariables,
+  sidebarPhoto_media_exif,
+  sidebarPhoto_media_faces,
+  sidebarPhoto_media_thumbnail,
+  sidebarPhoto_media_videoMetadata,
+} from './__generated__/sidebarPhoto'
 
-const mediaQuery = gql`
+const SIDEBAR_MEDIA_QUERY = gql`
   query sidebarPhoto($id: ID!) {
     media(id: $id) {
       id
@@ -72,7 +85,7 @@ const mediaQuery = gql`
   }
 `
 
-const PreviewImageWrapper = styled.div`
+const PreviewImageWrapper = styled.div<{ imageAspect: number }>`
   width: 100%;
   height: 0;
   padding-top: ${({ imageAspect }) => Math.min(imageAspect, 0.75) * 100}%;
@@ -96,23 +109,29 @@ const PreviewVideo = styled(ProtectedVideo)`
   left: 0;
 `
 
-const PreviewMedia = ({ media, previewImage }) => {
-  if (media.type == null || media.type == 'photo') {
+interface PreviewMediaPropsMedia extends ProtectedVideoProps_Media {
+  type: MediaType
+}
+
+type PreviewMediaProps = {
+  media: PreviewMediaPropsMedia
+  previewImage?: {
+    url: string
+  }
+}
+
+const PreviewMedia = ({ media, previewImage }: PreviewMediaProps) => {
+  if (media.type === MediaType.Photo) {
     return <PreviewImage src={previewImage?.url} />
   }
 
-  if (media.type == 'video') {
+  if (media.type === MediaType.Video) {
     return <PreviewVideo media={media} />
   }
 
   throw new Error('Unknown media type')
 }
 
-PreviewMedia.propTypes = {
-  media: PropTypes.object.isRequired,
-  previewImage: PropTypes.object,
-}
-
 const Name = styled.div`
   text-align: center;
   font-size: 1.2rem;
@@ -123,37 +142,45 @@ const MetadataInfoContainer = styled.div`
   margin-bottom: 1.5rem;
 `
 
-export const MetadataInfo = ({ media }) => {
+type MediaInfoProps = {
+  media?: MediaSidebarMedia
+}
+
+export const MetadataInfo = ({ media }: MediaInfoProps) => {
   const { t } = useTranslation()
-  let exifItems = []
+  let exifItems: JSX.Element[] = []
 
   const exifName = exifNameLookup(t)
 
   if (media?.exif) {
-    let exifKeys = Object.keys(exifName).filter(
-      x => media.exif[x] !== null && x != '__typename'
+    const mediaExif = (media?.exif as unknown) as {
+      [key: string]: string | number | null
+    }
+
+    const exifKeys = Object.keys(exifName).filter(
+      x => mediaExif[x] !== null && x != '__typename'
     )
 
-    let exif = exifKeys.reduce(
+    const exif = exifKeys.reduce(
       (prev, curr) => ({
         ...prev,
-        [curr]: media.exif[curr],
+        [curr]: mediaExif[curr],
       }),
-      {}
+      {} as { [key: string]: string | number | null }
     )
 
     if (!isNil(exif.dateShot)) {
       exif.dateShot = new Date(exif.dateShot).toLocaleString()
     }
 
-    if (!isNil(exif.exposure) && exif.exposure !== 0) {
+    if (typeof exif.exposure === 'number' && exif.exposure !== 0) {
       exif.exposure = `1/${1 / exif.exposure}`
     }
 
     const exposurePrograms = exposureProgramsLookup(t)
 
     if (
-      !isNil(exif.exposureProgram) &&
+      typeof exif.exposureProgram === 'number' &&
       exposurePrograms[exif.exposureProgram]
     ) {
       exif.exposureProgram = exposurePrograms[exif.exposureProgram]
@@ -170,7 +197,7 @@ export const MetadataInfo = ({ media }) => {
     }
 
     const flash = flashLookup(t)
-    if (!isNil(exif.flash) && flash[exif.flash]) {
+    if (typeof exif.flash === 'number' && flash[exif.flash]) {
       exif.flash = flash[exif.flash]
     }
 
@@ -179,16 +206,20 @@ export const MetadataInfo = ({ media }) => {
     ))
   }
 
-  let videoMetadataItems = []
+  let videoMetadataItems: JSX.Element[] = []
   if (media?.videoMetadata) {
-    let metadata = Object.keys(media.videoMetadata)
+    const videoMetadata = (media.videoMetadata as unknown) as {
+      [key: string]: string | number | null
+    }
+
+    let metadata = Object.keys(videoMetadata)
       .filter(x => !['id', '__typename', 'width', 'height'].includes(x))
       .reduce(
         (prev, curr) => ({
           ...prev,
-          [curr]: media.videoMetadata[curr],
+          [curr]: videoMetadata[curr as string],
         }),
-        {}
+        {} as { [key: string]: string | number | null }
       )
 
     metadata = {
@@ -209,11 +240,7 @@ export const MetadataInfo = ({ media }) => {
   )
 }
 
-MetadataInfo.propTypes = {
-  media: PropTypes.object,
-}
-
-const exifNameLookup = t => ({
+const exifNameLookup = (t: TranslationFn): { [key: string]: string } => ({
   camera: t('sidebar.media.exif.name.camera', 'Camera'),
   maker: t('sidebar.media.exif.name.maker', 'Maker'),
   lens: t('sidebar.media.exif.name.lens', 'Lens'),
@@ -227,7 +254,9 @@ const exifNameLookup = t => ({
 })
 
 // From https://exiftool.org/TagNames/EXIF.html
-const exposureProgramsLookup = t => ({
+const exposureProgramsLookup = (
+  t: TranslationFn
+): { [key: number]: string } => ({
   0: t('sidebar.media.exif.exposure_program.not_defined', 'Not defined'),
   1: t('sidebar.media.exif.exposure_program.manual', 'Manual'),
   2: t('sidebar.media.exif.exposure_program.normal_program', 'Normal program'),
@@ -250,7 +279,7 @@ const exposureProgramsLookup = t => ({
 })
 
 // From https://exiftool.org/TagNames/EXIF.html#Flash
-const flashLookup = t => {
+const flashLookup = (t: TranslationFn): { [key: number]: string } => {
   const values = {
     no_flash: t('sidebar.media.exif.flash.no_flash', 'No Flash'),
     fired: t('sidebar.media.exif.flash.fired', 'Fired'),
@@ -303,7 +332,7 @@ const flashLookup = t => {
     0x58: `${values['auto']}, ${values['did_not_fire']}, ${values['red_eye_reduction']}`,
     0x59: `${values['auto']}, ${values['fired']}, ${values['red_eye_reduction']}`,
     0x5d: `${values['auto']}, ${values['red_eye_reduction']}, ${values['return_not_detected']}`,
-    0x5f: `${values['auto']}, ${values['red_eye_redcution']}, ${values['return_detected']}`,
+    0x5f: `${values['auto']}, ${values['red_eye_reduction']}, ${values['return_detected']}`,
   }
 }
 
@@ -319,26 +348,33 @@ const flashLookup = t => {
 //   8: 'Rotate 270 CW',
 // }
 
-const SidebarContent = ({ media, hidePreview }) => {
-  let previewImage = null
-  if (media) {
-    if (media.highRes) previewImage = media.highRes
-    else if (media.thumbnail) previewImage = media.thumbnail
-  }
+type SidebarContentProps = {
+  media: MediaSidebarMedia
+  hidePreview?: boolean
+}
 
-  const imageAspect = previewImage
-    ? previewImage.height / previewImage.width
-    : 3 / 2
+const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => {
+  let previewImage = null
+  if (media.highRes) previewImage = media.highRes
+  else if (media.thumbnail) previewImage = media.thumbnail
+
+  const imageAspect =
+    previewImage?.width && previewImage?.height
+      ? previewImage.height / previewImage.width
+      : 3 / 2
 
   return (
     <div>
       {!hidePreview && (
         <PreviewImageWrapper imageAspect={imageAspect}>
-          <PreviewMedia previewImage={previewImage} media={media} />
+          <PreviewMedia
+            previewImage={previewImage || undefined}
+            media={media}
+          />
           <SidebarFacesOverlay media={media} />
         </PreviewImageWrapper>
       )}
-      <Name>{media && media.title}</Name>
+      <Name>{media.title}</Name>
       <MetadataInfo media={media} />
       <SidebarDownload photo={media} />
       <SidebarShare photo={media} />
@@ -346,13 +382,39 @@ const SidebarContent = ({ media, hidePreview }) => {
   )
 }
 
-SidebarContent.propTypes = {
-  media: PropTypes.object,
-  hidePreview: PropTypes.bool,
+interface MediaSidebarMedia {
+  __typename: 'Media'
+  id: string
+  title?: string
+  type: MediaType
+  highRes?: null | {
+    __typename: 'MediaURL'
+    url: string
+    width?: number
+    height?: number
+  }
+  thumbnail?: sidebarPhoto_media_thumbnail | null
+  videoWeb?: null | {
+    __typename: 'MediaURL'
+    url: string
+    width?: number
+    height?: number
+  }
+  videoMetadata?: sidebarPhoto_media_videoMetadata | null
+  exif?: sidebarPhoto_media_exif | null
+  faces?: sidebarPhoto_media_faces[]
 }
 
-const MediaSidebar = ({ media, hidePreview }) => {
-  const [loadMedia, { loading, error, data }] = useLazyQuery(mediaQuery)
+type MediaSidebarType = {
+  media: MediaSidebarMedia
+  hidePreview?: boolean
+}
+
+const MediaSidebar = ({ media, hidePreview }: MediaSidebarType) => {
+  const [loadMedia, { loading, error, data }] = useLazyQuery<
+    sidebarPhoto,
+    sidebarPhotoVariables
+  >(SIDEBAR_MEDIA_QUERY)
 
   useEffect(() => {
     if (media != null && authToken()) {
@@ -379,9 +441,4 @@ const MediaSidebar = ({ media, hidePreview }) => {
   return <SidebarContent media={data.media} hidePreview={hidePreview} />
 }
 
-MediaSidebar.propTypes = {
-  media: PropTypes.object.isRequired,
-  hidePreview: PropTypes.bool,
-}
-
 export default MediaSidebar
