diff --git a/.gitignore b/.gitignore
index e24ed42..83f53b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,7 +16,7 @@ docker-compose.yml
 node_modules/
 
 # testing
-/coverage
+/ui/coverage
 
 # building
 .cache/
diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js
index e14b0ea..8f6d570 100644
--- a/ui/.eslintrc.js
+++ b/ui/.eslintrc.js
@@ -43,7 +43,7 @@ module.exports = {
   // parser: 'babel-eslint',
   overrides: [
     Object.assign(require('eslint-plugin-jest').configs.recommended, {
-      files: ['**/*.test.js'],
+      files: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'],
       env: { jest: true },
       plugins: ['jest', 'jest-dom'],
       rules: Object.assign(
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 221dce9..7fba8e7 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -56,7 +56,7 @@
         "@testing-library/jest-dom": "^5.11.10",
         "@testing-library/react": "^11.2.6",
         "@types/geojson": "^7946.0.7",
-        "@types/jest": "^26.0.22",
+        "@types/jest": "^26.0.23",
         "@types/mapbox-gl": "^2.1.1",
         "@types/react": "^17.0.3",
         "@types/react-dom": "^17.0.3",
@@ -2789,9 +2789,9 @@
       }
     },
     "node_modules/@types/jest": {
-      "version": "26.0.22",
-      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.22.tgz",
-      "integrity": "sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==",
+      "version": "26.0.23",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz",
+      "integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==",
       "dev": true,
       "dependencies": {
         "jest-diff": "^26.0.0",
@@ -16438,9 +16438,9 @@
       }
     },
     "@types/jest": {
-      "version": "26.0.22",
-      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.22.tgz",
-      "integrity": "sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==",
+      "version": "26.0.23",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz",
+      "integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==",
       "dev": true,
       "requires": {
         "jest-diff": "^26.0.0",
diff --git a/ui/package.json b/ui/package.json
index c643bbe..7e0bca4 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -69,7 +69,7 @@
     "@testing-library/jest-dom": "^5.11.10",
     "@testing-library/react": "^11.2.6",
     "@types/geojson": "^7946.0.7",
-    "@types/jest": "^26.0.22",
+    "@types/jest": "^26.0.23",
     "@types/mapbox-gl": "^2.1.1",
     "@types/react": "^17.0.3",
     "@types/react-dom": "^17.0.3",
@@ -97,6 +97,10 @@
     "arrowParens": "avoid"
   },
   "jest": {
+    "testMatch": [
+      "**/__tests__/**/*.+(ts|tsx|js)",
+      "**/?(*.)+(spec|test).+(ts|tsx|js)"
+    ],
     "transformIgnorePatterns": [
       "^.+\\.css$"
     ],
diff --git a/ui/src/components/albumGallery/AlbumGallery.tsx b/ui/src/components/albumGallery/AlbumGallery.tsx
index 365cea2..c916ba1 100644
--- a/ui/src/components/albumGallery/AlbumGallery.tsx
+++ b/ui/src/components/albumGallery/AlbumGallery.tsx
@@ -1,10 +1,11 @@
-import React, { useState, useEffect } from 'react'
+import React, { useReducer } from 'react'
 import AlbumTitle from '../AlbumTitle'
 import PhotoGallery from '../photoGallery/PhotoGallery'
 import AlbumBoxes from './AlbumBoxes'
 import AlbumFilter from '../AlbumFilter'
 import { albumQuery_album } from '../../Pages/AlbumPage/__generated__/albumQuery'
 import { OrderDirection } from '../../../__generated__/globalTypes'
+import { photoGalleryReducer } from '../photoGallery/photoGalleryReducer'
 
 type AlbumGalleryProps = {
   album?: albumQuery_album
@@ -29,71 +30,49 @@ const AlbumGallery = React.forwardRef(
       setOrdering,
       ordering,
       onlyFavorites = false,
-      onFavorite,
     }: AlbumGalleryProps,
     ref: React.ForwardedRef<HTMLDivElement>
   ) => {
-    type ImageStateType = {
-      activeImage: number
-      presenting: boolean
-    }
+    // const [imageState, setImageState] = useState<ImageStateType>({
+    //   activeImage: -1,
+    //   presenting: false,
+    // })
 
-    const [imageState, setImageState] = useState<ImageStateType>({
-      activeImage: -1,
+    const [mediaState, dispatchMedia] = useReducer(photoGalleryReducer, {
       presenting: false,
+      activeIndex: -1,
+      media: album?.media || [],
     })
 
-    const setPresenting = (presenting: boolean) =>
-      setImageState(state => ({ ...state, presenting }))
+    // const setPresentingWithHistory = (presenting: boolean) => {
+    //   setPresenting(presenting)
+    //   if (presenting) {
+    //     history.pushState({ imageState }, '')
+    //   } else {
+    //     history.back()
+    //   }
+    // }
 
-    const setPresentingWithHistory = (presenting: boolean) => {
-      setPresenting(presenting)
-      if (presenting) {
-        history.pushState({ imageState }, '')
-      } else {
-        history.back()
-      }
-    }
+    // const updateHistory = (imageState: ImageStateType) => {
+    //   history.replaceState({ imageState }, '')
+    //   return imageState
+    // }
 
-    const updateHistory = (imageState: ImageStateType) => {
-      history.replaceState({ imageState }, '')
-      return imageState
-    }
+    // useEffect(() => {
+    //   const updateImageState = (event: PopStateEvent) => {
+    //     setImageState(event.state.imageState)
+    //   }
 
-    const setActiveImage = (activeImage: number) => {
-      setImageState(state => updateHistory({ ...state, activeImage }))
-    }
+    //   window.addEventListener('popstate', updateImageState)
 
-    const nextImage = () => {
-      if (album === undefined) return
-      setActiveImage((imageState.activeImage + 1) % album.media.length)
-    }
+    //   return () => {
+    //     window.removeEventListener('popstate', updateImageState)
+    //   }
+    // }, [imageState])
 
-    const previousImage = () => {
-      if (album === undefined) return
-
-      if (imageState.activeImage <= 0) {
-        setActiveImage(album.media.length - 1)
-      } else {
-        setActiveImage(imageState.activeImage - 1)
-      }
-    }
-
-    useEffect(() => {
-      const updateImageState = (event: PopStateEvent) => {
-        setImageState(event.state.imageState)
-      }
-
-      window.addEventListener('popstate', updateImageState)
-
-      return () => {
-        window.removeEventListener('popstate', updateImageState)
-      }
-    }, [imageState])
-
-    useEffect(() => {
-      setActiveImage(-1)
-    }, [album])
+    // useEffect(() => {
+    //   setActiveImage(-1)
+    // }, [album])
 
     let subAlbumElement = null
 
@@ -135,16 +114,8 @@ const AlbumGallery = React.forwardRef(
         }
         <PhotoGallery
           loading={loading}
-          media={album?.media || []}
-          activeIndex={imageState.activeImage}
-          presenting={imageState.presenting}
-          onSelectImage={index => {
-            setActiveImage(index)
-          }}
-          onFavorite={onFavorite}
-          setPresenting={setPresentingWithHistory}
-          nextImage={nextImage}
-          previousImage={previousImage}
+          mediaState={mediaState}
+          dispatchMedia={dispatchMedia}
         />
       </div>
     )
diff --git a/ui/src/components/photoGallery/MediaThumbnail.tsx b/ui/src/components/photoGallery/MediaThumbnail.tsx
index 2ddb98c..45c2083 100644
--- a/ui/src/components/photoGallery/MediaThumbnail.tsx
+++ b/ui/src/components/photoGallery/MediaThumbnail.tsx
@@ -1,22 +1,8 @@
 import React, { useCallback, useState } from 'react'
-import { useMutation, gql } from '@apollo/client'
 import styled from 'styled-components'
 import { Icon } from 'semantic-ui-react'
 import { ProtectedImage } from './ProtectedMedia'
 import { MediaType } from '../../../__generated__/globalTypes'
-import {
-  markMediaFavorite,
-  markMediaFavoriteVariables,
-} from './__generated__/markMediaFavorite'
-
-const markFavoriteMutation = gql`
-  mutation markMediaFavorite($mediaId: ID!, $favorite: Boolean!) {
-    favoriteMedia(mediaId: $mediaId, favorite: $favorite) {
-      id
-      favorite
-    }
-  }
-`
 
 const MediaContainer = styled.div`
   flex-grow: 1;
@@ -119,26 +105,19 @@ type MediaThumbnailProps = {
       height: number
     }
   }
-  onSelectImage(index: number): void
-  index: number
   active: boolean
-  setPresenting(presenting: boolean): void
-  onFavorite?(): void
+  selectImage(): void
+  clickPresent(): void
+  clickFavorite(): void
 }
 
 export const MediaThumbnail = ({
   media,
-  onSelectImage,
-  index,
   active,
-  setPresenting,
-  onFavorite,
+  selectImage,
+  clickPresent,
+  clickFavorite,
 }: MediaThumbnailProps) => {
-  const [markFavorite] = useMutation<
-    markMediaFavorite,
-    markMediaFavoriteVariables
-  >(markFavoriteMutation)
-
   let heartIcon = null
   if (media.favorite !== undefined) {
     heartIcon = (
@@ -147,21 +126,7 @@ export const MediaThumbnail = ({
         name={media.favorite ? 'heart' : 'heart outline'}
         onClick={(event: MouseEvent) => {
           event.stopPropagation()
-          const favorite = !media.favorite
-          markFavorite({
-            variables: {
-              mediaId: media.id,
-              favorite: favorite,
-            },
-            optimisticResponse: {
-              favoriteMedia: {
-                id: media.id,
-                favorite: favorite,
-                __typename: 'Media',
-              },
-            },
-          })
-          onFavorite && onFavorite()
+          clickFavorite()
         }}
       />
     )
@@ -187,7 +152,7 @@ export const MediaThumbnail = ({
         minWidth: `clamp(124px, ${minWidth}px, 100% - 8px)`,
       }}
       onClick={() => {
-        onSelectImage(index)
+        selectImage()
       }}
     >
       <div
@@ -203,7 +168,7 @@ export const MediaThumbnail = ({
         <HoverIcon
           name="expand"
           onClick={() => {
-            setPresenting(true)
+            clickPresent()
           }}
         />
         {heartIcon}
diff --git a/ui/src/components/photoGallery/PhotoGallery.tsx b/ui/src/components/photoGallery/PhotoGallery.tsx
index dc02e94..6773c5a 100644
--- a/ui/src/components/photoGallery/PhotoGallery.tsx
+++ b/ui/src/components/photoGallery/PhotoGallery.tsx
@@ -4,10 +4,28 @@ import { Loader } from 'semantic-ui-react'
 import { MediaThumbnail, PhotoThumbnail } from './MediaThumbnail'
 import PresentView from './presentView/PresentView'
 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'
+import {
+  PhotoGalleryAction,
+  PhotoGalleryState,
+  selectImageAction,
+} from './photoGalleryReducer'
+import { gql, useMutation } from '@apollo/client'
+import {
+  markMediaFavorite,
+  markMediaFavoriteVariables,
+} from './__generated__/markMediaFavorite'
+
+const markFavoriteMutation = gql`
+  mutation markMediaFavorite($mediaId: ID!, $favorite: Boolean!) {
+    favoriteMedia(mediaId: $mediaId, favorite: $favorite) {
+      id
+      favorite
+    }
+  }
+`
 
 const Gallery = styled.div`
   display: flex;
@@ -32,37 +50,32 @@ const ClearWrap = styled.div`
   clear: both;
 `
 
-interface PhotoGalleryProps_Media extends PresentMediaProps_Media {
+export interface PhotoGalleryProps_Media extends PresentMediaProps_Media {
   thumbnail: sidebarPhoto_media_thumbnail | null
+  favorite?: boolean
 }
 
 type PhotoGalleryProps = {
   loading: boolean
-  media: PhotoGalleryProps_Media[]
-  activeIndex: number
-  presenting: boolean
-  onSelectImage(index: number): void
-  setPresenting(presenting: boolean): void
-  nextImage(): void
-  previousImage(): void
-  onFavorite?(): void
+  mediaState: PhotoGalleryState
+  dispatchMedia: React.Dispatch<PhotoGalleryAction>
 }
 
 const PhotoGallery = ({
-  activeIndex = -1,
-  media,
+  mediaState,
   loading,
-  onSelectImage,
-  presenting,
-  setPresenting,
-  nextImage,
-  previousImage,
-  onFavorite,
+  dispatchMedia,
 }: PhotoGalleryProps) => {
   const { t } = useTranslation()
+
   const { updateSidebar } = useContext(SidebarContext)
 
-  const activeImage: PhotoGalleryProps_Media | undefined = media[activeIndex]
+  const [markFavorite] = useMutation<
+    markMediaFavorite,
+    markMediaFavoriteVariables
+  >(markFavoriteMutation)
+
+  const { media, activeIndex, presenting } = mediaState
 
   let photoElements = []
   if (media) {
@@ -73,14 +86,36 @@ const PhotoGallery = ({
         <MediaThumbnail
           key={media.id}
           media={media}
-          onSelectImage={index => {
-            updateSidebar(<MediaSidebar media={media} />)
-            onSelectImage(index)
-          }}
-          onFavorite={onFavorite}
-          setPresenting={setPresenting}
-          index={index}
           active={active}
+          selectImage={() => {
+            selectImageAction({
+              index,
+              mediaState,
+              dispatchMedia,
+              updateSidebar,
+            })
+          }}
+          clickFavorite={() => {
+            markFavorite({
+              variables: {
+                mediaId: media.id,
+                favorite: !media.favorite,
+              },
+              optimisticResponse: {
+                favoriteMedia: {
+                  id: media.id,
+                  favorite: !media.favorite,
+                  __typename: 'Media',
+                },
+              },
+            })
+          }}
+          clickPresent={() => {
+            dispatchMedia({
+              type: 'setPresenting',
+              presenting: true,
+            })
+          }}
         />
       )
     })
@@ -100,10 +135,7 @@ const PhotoGallery = ({
         <PhotoFiller />
       </Gallery>
       {presenting && (
-        <PresentView
-          media={activeImage}
-          {...{ nextImage, previousImage, setPresenting }}
-        />
+        <PresentView mediaState={mediaState} dispatchMedia={dispatchMedia} />
       )}
     </ClearWrap>
   )
diff --git a/ui/src/components/photoGallery/photoGalleryReducer.test.ts b/ui/src/components/photoGallery/photoGalleryReducer.test.ts
new file mode 100644
index 0000000..d82966e
--- /dev/null
+++ b/ui/src/components/photoGallery/photoGalleryReducer.test.ts
@@ -0,0 +1,91 @@
+import { photoGalleryReducer, PhotoGalleryState } from './photoGalleryReducer'
+import { MediaType } from '../../../__generated__/globalTypes'
+
+describe('photo gallery reducer', () => {
+  const defaultState: PhotoGalleryState = {
+    presenting: false,
+    activeIndex: 0,
+    media: [
+      {
+        __typename: 'Media',
+        id: '1',
+        highRes: null,
+        thumbnail: null,
+        type: MediaType.Photo,
+      },
+      {
+        __typename: 'Media',
+        id: '2',
+        highRes: null,
+        thumbnail: null,
+        type: MediaType.Photo,
+      },
+      {
+        __typename: 'Media',
+        id: '3',
+        highRes: null,
+        thumbnail: null,
+        type: MediaType.Photo,
+      },
+    ],
+  }
+
+  test('next image', () => {
+    expect(photoGalleryReducer(defaultState, { type: 'nextImage' })).toEqual({
+      ...defaultState,
+      activeIndex: 1,
+    })
+
+    expect(
+      photoGalleryReducer(
+        { ...defaultState, activeIndex: 2 },
+        { type: 'nextImage' }
+      )
+    ).toEqual({
+      ...defaultState,
+      activeIndex: 0,
+    })
+  })
+
+  test('previous image', () => {
+    expect(
+      photoGalleryReducer(defaultState, { type: 'previousImage' })
+    ).toEqual({
+      ...defaultState,
+      activeIndex: 2,
+    })
+
+    expect(
+      photoGalleryReducer(
+        { ...defaultState, activeIndex: 2 },
+        { type: 'previousImage' }
+      )
+    ).toEqual({
+      ...defaultState,
+      activeIndex: 1,
+    })
+  })
+
+  test('select image', () => {
+    expect(
+      photoGalleryReducer(defaultState, { type: 'selectImage', index: 1 })
+    ).toEqual({
+      ...defaultState,
+      activeIndex: 1,
+    })
+
+    expect(
+      photoGalleryReducer(defaultState, { type: 'selectImage', index: 100 })
+    ).toEqual({
+      ...defaultState,
+      activeIndex: 2,
+    })
+
+    expect(
+      photoGalleryReducer(defaultState, { type: 'selectImage', index: -5 })
+    ).toEqual({
+      ...defaultState,
+      activeIndex: 0,
+    })
+  })
+})
diff --git a/ui/src/components/photoGallery/photoGalleryReducer.tsx b/ui/src/components/photoGallery/photoGalleryReducer.tsx
new file mode 100644
index 0000000..22f393e
--- /dev/null
+++ b/ui/src/components/photoGallery/photoGalleryReducer.tsx
@@ -0,0 +1,74 @@
+import React from 'react'
+import { UpdateSidebarFn } from '../sidebar/Sidebar'
+import { PhotoGalleryProps_Media } from './PhotoGallery'
+import MediaSidebar from '../sidebar/MediaSidebar'
+
+export type PhotoGalleryState = {
+  presenting: boolean
+  activeIndex: number
+  media: PhotoGalleryProps_Media[]
+}
+
+export type PhotoGalleryAction =
+  | { type: 'nextImage' }
+  | { type: 'previousImage' }
+  | { type: 'setPresenting'; presenting: boolean }
+  | { type: 'selectImage'; index: number }
+
+export function photoGalleryReducer(
+  state: PhotoGalleryState,
+  action: PhotoGalleryAction
+): PhotoGalleryState {
+  switch (action.type) {
+    case 'nextImage':
+      return {
+        ...state,
+        activeIndex: (state.activeIndex + 1) % state.media.length,
+      }
+    case 'previousImage':
+      if (state.activeIndex <= 0) {
+        return {
+          ...state,
+          activeIndex: state.media.length - 1,
+        }
+      } else {
+        return {
+          ...state,
+          activeIndex: state.activeIndex - 1,
+        }
+      }
+    case 'setPresenting':
+      return {
+        ...state,
+        presenting: action.presenting,
+      }
+    case 'selectImage':
+      return {
+        ...state,
+        activeIndex: Math.max(
+          0,
+          Math.min(state.media.length - 1, action.index)
+        ),
+      }
+  }
+}
+
+export const selectImageAction = ({
+  index,
+  mediaState,
+  dispatchMedia,
+  updateSidebar,
+}: {
+  index: number
+  mediaState: PhotoGalleryState
+  dispatchMedia: React.Dispatch<PhotoGalleryAction>
+  updateSidebar: UpdateSidebarFn
+}) => {
+  updateSidebar(
+    <MediaSidebar media={mediaState.media[mediaState.activeIndex]} />
+  )
+  dispatchMedia({
+    type: 'selectImage',
+    index,
+  })
+}
diff --git a/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.tsx b/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.tsx
index 8d99e05..ff6da1e 100644
--- a/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.tsx
+++ b/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.tsx
@@ -1,6 +1,7 @@
 import React, { useState, useRef, useEffect } from 'react'
 import styled from 'styled-components'
 import { debounce, DebouncedFn } from '../../../helpers/utils'
+import { PhotoGalleryAction } from '../photoGalleryReducer'
 
 import ExitIcon from './icons/Exit'
 import NextIcon from './icons/Next'
@@ -65,16 +66,12 @@ const NavigationButton = styled(OverlayButton)<{ float: 'left' | 'right' }>`
 
 type PresentNavigationOverlayProps = {
   children?: React.ReactChild
-  nextImage(): void
-  previousImage(): void
-  setPresenting(presenting: boolean): void
+  dispatchMedia: React.Dispatch<PhotoGalleryAction>
 }
 
 const PresentNavigationOverlay = ({
   children,
-  nextImage,
-  previousImage,
-  setPresenting,
+  dispatchMedia,
 }: PresentNavigationOverlayProps) => {
   const [hide, setHide] = useState(true)
   const onMouseMove = useRef<null | DebouncedFn<() => void>>(null)
@@ -103,20 +100,22 @@ const PresentNavigationOverlay = ({
       <NavigationButton
         className={hide ? 'hide' : undefined}
         float="left"
-        onClick={() => previousImage()}
+        onClick={() => dispatchMedia({ type: 'previousImage' })}
       >
         <PrevIcon />
       </NavigationButton>
       <NavigationButton
         className={hide ? 'hide' : undefined}
         float="right"
-        onClick={() => nextImage()}
+        onClick={() => dispatchMedia({ type: 'nextImage' })}
       >
         <NextIcon />
       </NavigationButton>
       <ExitButton
         className={hide ? 'hide' : undefined}
-        onClick={() => setPresenting(false)}
+        onClick={() =>
+          dispatchMedia({ type: 'setPresenting', presenting: false })
+        }
       >
         <ExitIcon />
       </ExitButton>
diff --git a/ui/src/components/photoGallery/presentView/PresentView.tsx b/ui/src/components/photoGallery/presentView/PresentView.tsx
index c8fb3b4..beb9a11 100644
--- a/ui/src/components/photoGallery/presentView/PresentView.tsx
+++ b/ui/src/components/photoGallery/presentView/PresentView.tsx
@@ -1,7 +1,8 @@
 import React, { useEffect } from 'react'
 import styled, { createGlobalStyle } from 'styled-components'
 import PresentNavigationOverlay from './PresentNavigationOverlay'
-import PresentMedia, { PresentMediaProps_Media } from './PresentMedia'
+import PresentMedia from './PresentMedia'
+import { PhotoGalleryAction, PhotoGalleryState } from '../photoGalleryReducer'
 
 const StyledContainer = styled.div`
   position: fixed;
@@ -21,37 +22,33 @@ const PreventScroll = createGlobalStyle`
 `
 
 type PresentViewProps = {
-  media: PresentMediaProps_Media
   className?: string
   imageLoaded?(): void
-  nextImage(): void
-  previousImage(): void
-  setPresenting(presenting: boolean): void
+  mediaState: PhotoGalleryState
+  dispatchMedia: React.Dispatch<PhotoGalleryAction>
 }
 
 const PresentView = ({
   className,
-  media,
   imageLoaded,
-  nextImage,
-  previousImage,
-  setPresenting,
+  mediaState,
+  dispatchMedia,
 }: PresentViewProps) => {
   useEffect(() => {
     const keyDownEvent = (e: KeyboardEvent) => {
       if (e.key == 'ArrowRight') {
-        nextImage()
         e.stopPropagation()
+        dispatchMedia({ type: 'nextImage' })
       }
 
       if (e.key == 'ArrowLeft') {
-        previousImage()
         e.stopPropagation()
+        dispatchMedia({ type: 'previousImage' })
       }
 
       if (e.key == 'Escape') {
-        setPresenting(false)
         e.stopPropagation()
+        dispatchMedia({ type: 'setPresenting', presenting: false })
       }
     }
 
@@ -65,10 +62,11 @@ const PresentView = ({
   return (
     <StyledContainer {...className}>
       <PreventScroll />
-      <PresentNavigationOverlay
-        {...{ nextImage, previousImage, setPresenting }}
-      >
-        <PresentMedia media={media} imageLoaded={imageLoaded} />
+      <PresentNavigationOverlay dispatchMedia={dispatchMedia}>
+        <PresentMedia
+          media={mediaState.media[mediaState.activeIndex]}
+          imageLoaded={imageLoaded}
+        />
       </PresentNavigationOverlay>
     </StyledContainer>
   )
diff --git a/ui/tsconfig.json b/ui/tsconfig.json
index 49652a2..fb059ea 100644
--- a/ui/tsconfig.json
+++ b/ui/tsconfig.json
@@ -52,9 +52,10 @@
     // "paths": {},                                 /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
     // "rootDirs": [],                              /* List of root folders whose combined content represents the structure of the project at runtime. */
     "typeRoots": [
+      "node_modules/@types",
       "./src/@types/"
     ] /* List of folders to include type definitions from. */,
-    // "types": [],                                 /* Type declaration files to be included in compilation. */
+    "types": ["jest"],                      /* Type declaration files to be included in compilation. */
     // "allowSyntheticDefaultImports": true,        /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
     "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
     // "preserveSymlinks": true,                    /* Do not resolve the real path of symlinks. */
