From c72e24f841b5771d74e82114e40ed81f15ec97a7 Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Fri, 30 Jun 2023 11:34:04 -0500
Subject: [PATCH] [APP-716] Add 'save image' button to the lightbox (#926)

* Add 'save image' button to the lightbox

* Fix types

* Fix types
---
 src/lib/media/manip.ts             | 31 +++++++++++++++++++++++++++++-
 src/lib/media/manip.web.ts         |  7 ++++++-
 src/view/com/lightbox/Lightbox.tsx | 24 +++++++++++++++++++++--
 src/view/index.ts                  |  2 ++
 4 files changed, 60 insertions(+), 4 deletions(-)

diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts
index c3595370..2f6c25e8 100644
--- a/src/lib/media/manip.ts
+++ b/src/lib/media/manip.ts
@@ -5,6 +5,7 @@ import {Image} from 'react-native-image-crop-picker'
 import * as RNFS from 'react-native-fs'
 import uuid from 'react-native-uuid'
 import * as Sharing from 'expo-sharing'
+import * as MediaLibrary from 'expo-media-library'
 import {Dimensions} from './types'
 import {isAndroid, isIOS} from 'platform/detection'
 
@@ -75,7 +76,7 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
   }
 }
 
-export async function saveImageModal({uri}: {uri: string}) {
+export async function shareImageModal({uri}: {uri: string}) {
   if (!(await Sharing.isAvailableAsync())) {
     // TODO might need to give an error to the user in this case -prf
     return
@@ -107,6 +108,34 @@ export async function saveImageModal({uri}: {uri: string}) {
   RNFS.unlink(imagePath)
 }
 
+export async function saveImageToAlbum({
+  uri,
+  album,
+}: {
+  uri: string
+  album: string
+}) {
+  // download the file to cache
+  // NOTE
+  // assuming PNG
+  // we're currently relying on the fact our CDN only serves pngs
+  // -prf
+  const downloadResponse = await RNFetchBlob.config({
+    fileCache: true,
+  }).fetch('GET', uri)
+  let imagePath = downloadResponse.path()
+  imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
+
+  // save to the album (creating as needed)
+  const assetRef = await MediaLibrary.createAssetAsync(imagePath)
+  const albumRef = await MediaLibrary.getAlbumAsync(album)
+  if (albumRef) {
+    await MediaLibrary.addAssetsToAlbumAsync(assetRef, albumRef)
+  } else {
+    await MediaLibrary.createAlbumAsync(album, assetRef)
+  }
+}
+
 export function getImageDim(path: string): Promise<Dimensions> {
   return new Promise((resolve, reject) => {
     RNImage.getSize(
diff --git a/src/lib/media/manip.web.ts b/src/lib/media/manip.web.ts
index 464802c3..914b05d2 100644
--- a/src/lib/media/manip.web.ts
+++ b/src/lib/media/manip.web.ts
@@ -37,7 +37,12 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
   return await doResize(dataUri, opts)
 }
 
-export async function saveImageModal(_opts: {uri: string}) {
+export async function shareImageModal(_opts: {uri: string}) {
+  // TODO
+  throw new Error('TODO')
+}
+
+export async function saveImageToAlbum(_opts: {uri: string; album: string}) {
   // TODO
   throw new Error('TODO')
 }
diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx
index 18440c55..d1fd701c 100644
--- a/src/view/com/lightbox/Lightbox.tsx
+++ b/src/view/com/lightbox/Lightbox.tsx
@@ -5,7 +5,8 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import ImageView from './ImageViewing'
 import {useStores} from 'state/index'
 import * as models from 'state/models/ui/shell'
-import {saveImageModal} from 'lib/media/manip'
+import {shareImageModal, saveImageToAlbum} from 'lib/media/manip'
+import * as Toast from '../util/Toast'
 import {Text} from '../util/text/Text'
 import {s, colors} from 'lib/styles'
 import {Button} from '../util/forms/Button'
@@ -54,7 +55,16 @@ export const Lightbox = observer(function Lightbox() {
             <Button
               type="primary-outline"
               style={styles.footerBtn}
-              onPress={() => saveImageModal({uri})}>
+              onPress={() => saveImageToAlbumWithToasts(uri)}>
+              <FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} />
+              <Text type="xl" style={s.white}>
+                Save
+              </Text>
+            </Button>
+            <Button
+              type="primary-outline"
+              style={styles.footerBtn}
+              onPress={() => shareImageModal({uri})}>
               <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} />
               <Text type="xl" style={s.white}>
                 Share
@@ -96,6 +106,15 @@ export const Lightbox = observer(function Lightbox() {
   }
 })
 
+async function saveImageToAlbumWithToasts(uri: string) {
+  try {
+    await saveImageToAlbum({uri, album: 'Bluesky'})
+    Toast.show('Saved to the "Bluesky" album.')
+  } catch (e: any) {
+    Toast.show(`Failed to save image: ${String(e)}`)
+  }
+}
+
 const styles = StyleSheet.create({
   footer: {
     paddingTop: 16,
@@ -109,6 +128,7 @@ const styles = StyleSheet.create({
   footerBtns: {
     flexDirection: 'row',
     justifyContent: 'center',
+    gap: 8,
   },
   footerBtn: {
     flexDirection: 'row',
diff --git a/src/view/index.ts b/src/view/index.ts
index 118a6de2..0ab84fc0 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -37,6 +37,7 @@ import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope'
 import {faExclamation} from '@fortawesome/free-solid-svg-icons/faExclamation'
 import {faEye} from '@fortawesome/free-solid-svg-icons/faEye'
 import {faEyeSlash as farEyeSlash} from '@fortawesome/free-regular-svg-icons/faEyeSlash'
+import {faFloppyDisk} from '@fortawesome/free-regular-svg-icons/faFloppyDisk'
 import {faGear} from '@fortawesome/free-solid-svg-icons/faGear'
 import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe'
 import {faHand} from '@fortawesome/free-solid-svg-icons/faHand'
@@ -124,6 +125,7 @@ export function setup() {
     faEye,
     faExclamation,
     farEyeSlash,
+    faFloppyDisk,
     faGear,
     faGlobe,
     faHand,