Commit 961806bd authored by Luko van der Maas's avatar Luko van der Maas

Merge branch 'photos-share' into 'master'

Add share button when viewing photos

Closes #104

See merge request !333
parents 4b1cea4b 030f6b64
......@@ -17,3 +17,7 @@ NativeModules.RNCStatusBarManager = {
setBackgroundColor: jest.fn(),
setTranslucent: jest.fn(),
};
jest.mock('react-native-fs', () => ({
downloadFile: jest.fn(),
}));
......@@ -76,6 +76,7 @@ android {
}
dependencies {
implementation project(':react-native-fs')
implementation project(':@react-native-community_status-bar')
implementation project(':@react-native-community_async-storage')
implementation project(':react-native-device-info')
......
......@@ -3,6 +3,7 @@ package com.thaliapp;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.rnfs.RNFSPackage;
import com.reactnativecommunity.statusbar.RNCStatusBarPackage;
import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
......@@ -40,6 +41,7 @@ public class MainApplication extends Application implements ShareApplication, Re
protected List<ReactPackage> getPackages() {
return Arrays.asList(
new MainReactPackage(),
new RNFSPackage(),
new RNCStatusBarPackage(),
new AsyncStoragePackage(),
new RNDeviceInfo(),
......
rootProject.name = 'ThaliApp'
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android')
include ':@react-native-community_status-bar'
project(':@react-native-community_status-bar').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/status-bar/android')
include ':@react-native-community_async-storage'
......
......@@ -8,6 +8,8 @@ export const PHOTOS_ALBUM_SUCCESS = 'PHOTOS_ALBUM_SUCCESS';
export const PHOTOS_ALBUM_FAILURE = 'PHOTOS_ALBUM_FAILURE';
export const PHOTOS_ALBUM_FETCHING = 'PHOTOS_ALBUM_FETCHING';
export const PHOTOS_GALLERY_OPEN = 'PHOTOS_GALLERY_OPEN';
export const PHOTOS_PHOTO_DOWNLOAD = 'PHOTOS_PHOTO_DOWNLOAD';
export const PHOTOS_PHOTO_SHARE = 'PHOTOS_PHOTO_SHARE';
export function openAlbums() {
return {
......@@ -19,14 +21,21 @@ export function openAlbums() {
export function loadAlbums(keywords, next) {
return {
type: PHOTOS_ALBUMS_LOAD,
payload: { keywords, next },
payload: {
keywords,
next,
},
};
}
export function successAlbums(data, next, isNext) {
return {
type: PHOTOS_ALBUMS_SUCCESS,
payload: { data, next, isNext },
payload: {
data,
next,
isNext,
},
};
}
......@@ -77,3 +86,17 @@ export function fetchingAlbum() {
type: PHOTOS_ALBUM_FETCHING,
};
}
export function downloadPhoto(url) {
return {
type: PHOTOS_PHOTO_DOWNLOAD,
payload: { url },
};
}
export function sharePhoto(url) {
return {
type: PHOTOS_PHOTO_SHARE,
payload: { url },
};
}
......@@ -2,8 +2,14 @@ import {
call, put, select, takeEvery,
} from 'redux-saga/effects';
import Share from 'react-native-share';
import * as RNFS from 'react-native-fs';
import Snackbar from 'react-native-snackbar';
import { apiRequest, tokenSelector } from '../utils/url';
import * as photosActions from '../actions/photos';
import i18next from '../utils/i18n';
const t = i18next.getFixedT(undefined, 'sagas/photos');
function* loadAlbums({ payload: { keywords, next } }) {
const token = yield select(tokenSelector);
......@@ -80,8 +86,46 @@ function* loadAlbum(action) {
}
}
const downloadFile = (fromUrl, toFile) => RNFS.downloadFile({
fromUrl, toFile,
}).promise;
function* downloadPhotoToFile(url, root = RNFS.DocumentDirectoryPath) {
const toFile = `${root}/photo-download.jpg`;
const result = yield call(downloadFile, url, toFile);
if (result.statusCode === 200) {
return toFile;
}
return null;
}
function* downloadPhoto({ payload: { url } }) {
const file = yield call(downloadPhotoToFile, url, RNFS.PicturesDirectoryPath);
if (file === null) {
return;
}
yield call([Snackbar, 'show'], { title: t('Photo has been saved successfully') });
}
function* sharePhoto({ payload: { url } }) {
const file = yield call(downloadPhotoToFile, url);
if (file === null) {
return;
}
const data = yield call([RNFS, 'readFile'], file, 'base64');
try {
yield call([Share, 'open'], {
url: `data:image/png;base64,${data}`,
});
// Error is thrown when a share is cancelled
// eslint-disable-next-line no-empty
} catch (e) {}
}
export default function* photosSaga() {
yield takeEvery(photosActions.PHOTOS_ALBUMS_OPEN, loadAlbums);
yield takeEvery(photosActions.PHOTOS_ALBUMS_LOAD, loadAlbums);
yield takeEvery(photosActions.PHOTOS_ALBUM_OPEN, loadAlbum);
yield takeEvery(photosActions.PHOTOS_PHOTO_DOWNLOAD, downloadPhoto);
yield takeEvery(photosActions.PHOTOS_PHOTO_SHARE, sharePhoto);
}
import { View } from 'react-native';
import { TouchableOpacity, View, Platform } from 'react-native';
import ImageViewer from 'react-native-image-zoom-viewer';
import React from 'react';
import Icon from 'react-native-vector-icons/MaterialIcons';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Colors from '../../style/Colors';
import styles from './style/AlbumDetailScreen';
import IconButton from '../../components/button/IconButton';
const AlbumGalleryScreen = ({ photos, goBack, selection }) => (
<View style={styles.screenWrapper}>
<View style={styles.galleryWrapper}>
<ImageViewer
index={selection}
imageUrls={photos}
/>
class AlbumGalleryScreen extends Component {
constructor(props) {
super(props);
this.state = { index: props.selection };
}
render() {
const {
photos, goBack, downloadPhoto, sharePhoto,
} = this.props;
const buttons = [
<IconButton
onPress={goBack}
name="close"
style={styles.closeGalleryTouchable}
iconStyle={styles.icon}
/>
</View>
</View>
);
key="share-button"
name="share"
style={styles.sharePhotoTouchable}
onPress={() => sharePhoto(photos[this.state.index].url)}
/>,
];
if (Platform.OS === 'android') {
buttons.push(
<IconButton
key="download-button"
name="file-download"
style={styles.downloadPhotoTouchable}
onPress={() => downloadPhoto(photos[this.state.index].url)}
/>,
);
}
return (
<View style={styles.screenWrapper}>
<View style={styles.galleryWrapper}>
<ImageViewer
index={this.state.index}
imageUrls={photos}
onChange={index => this.setState({ index })}
/>
<TouchableOpacity
style={styles.closeGalleryTouchable}
onPress={goBack}
>
<Icon
name="close"
style={styles.icon}
size={24}
color={Colors.white}
/>
</TouchableOpacity>
{buttons}
</View>
</View>
);
}
}
AlbumGalleryScreen.propTypes = {
goBack: PropTypes.func.isRequired,
......@@ -28,6 +70,8 @@ AlbumGalleryScreen.propTypes = {
photos: PropTypes.arrayOf(PropTypes.shape({
url: PropTypes.string.isRequired,
})).isRequired,
downloadPhoto: PropTypes.func.isRequired,
sharePhoto: PropTypes.func.isRequired,
};
export default AlbumGalleryScreen;
......@@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import AlbumGalleryScreen from './AlbumGalleryScreen';
import * as navigationActions from '../../../actions/navigation';
import * as photosActions from '../../../actions/photos';
import { albumData, albumSelection } from '../../../selectors/photos';
const mapStateToProps = state => ({
......@@ -15,6 +16,8 @@ const mapStateToProps = state => ({
const mapDispatchToProps = {
goBack: navigationActions.goBack,
downloadPhoto: photosActions.downloadPhoto,
sharePhoto: photosActions.sharePhoto,
};
export default connect(mapStateToProps, mapDispatchToProps)(withTranslation(['screens/photos/AlbumDetail'])(AlbumGalleryScreen));
......@@ -3,6 +3,8 @@ import StyleSheet from '../../../style/StyleSheet';
import Colors from '../../../style/Colors';
import { TOTAL_BAR_HEIGHT, STATUSBAR_HEIGHT, APPBAR_HEIGHT } from '../../../components/standardHeader/style/StandardHeader';
const MARGIN_TOP = STATUSBAR_HEIGHT + ((APPBAR_HEIGHT - 24) / 2);
export default StyleSheet.create({
screenWrapper: {
flex: 1,
......@@ -21,7 +23,27 @@ export default StyleSheet.create({
left: 10,
},
position: 'absolute',
marginTop: STATUSBAR_HEIGHT + ((APPBAR_HEIGHT - 24) / 2),
marginTop: MARGIN_TOP,
},
downloadPhotoTouchable: {
android: {
right: 48 + 10,
},
ios: {
right: 10,
},
position: 'absolute',
marginTop: MARGIN_TOP,
},
sharePhotoTouchable: {
android: {
right: 20,
},
ios: {
right: 10,
},
position: 'absolute',
marginTop: MARGIN_TOP,
},
galleryWrapper: {
flex: 1,
......
......@@ -53,6 +53,8 @@
327DC550229EB8E100DE29E9 /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 327DC52E229EB6B500DE29E9 /* libRNVectorIcons.a */; };
327DC59C229EBC1500DE29E9 /* libRNGestureHandler.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 327DC548229EB76B00DE29E9 /* libRNGestureHandler.a */; };
551DB1D439844F9A9B05A645 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 33887E49731D4893B6E6ED7A /* libz.tbd */; };
60D59B4F22D141B500FBD174 /* libRCTCameraRoll.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 60D59B2122D1418D00FBD174 /* libRCTCameraRoll.a */; };
60D59B5922D1447400FBD174 /* libRNFS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 60D59B5622D1446200FBD174 /* libRNFS.a */; };
63A2AC2C5803461381047FEC /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 79D02DBB9DA749469711667C /* EvilIcons.ttf */; };
776C5691B1584DD2B8CF9F9B /* FontAwesome5_Brands.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4F5BE7DE856D40C8986B6AD3 /* FontAwesome5_Brands.ttf */; };
7F90E2A796DC4F62B4277702 /* FontAwesome5_Solid.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 04A371D759264AA1B4201AB4 /* FontAwesome5_Solid.ttf */; };
......@@ -439,6 +441,27 @@
remoteGlobalIDString = 2D2A28201D9B03D100D4039D;
remoteInfo = "RCTAnimation-tvOS";
};
60D59B2022D1418D00FBD174 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 60D59B1422D1418D00FBD174 /* RCTCameraRoll.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 58B5115D1A9E6B3D00147676;
remoteInfo = RCTCameraRoll;
};
60D59B5522D1446200FBD174 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 60D59B5022D1446200FBD174 /* RNFS.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = F12AFB9B1ADAF8F800E0535D;
remoteInfo = RNFS;
};
60D59B5722D1446200FBD174 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 60D59B5022D1446200FBD174 /* RNFS.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 6456441F1EB8DA9100672408;
remoteInfo = "RNFS-tvOS";
};
78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */;
......@@ -514,6 +537,8 @@
4F5BE7DE856D40C8986B6AD3 /* FontAwesome5_Brands.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Brands.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf"; sourceTree = "<group>"; };
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = "<group>"; };
606D56B0E26C6147A83359B5 /* Pods-ThaliApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ThaliApp.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ThaliApp/Pods-ThaliApp.debug.xcconfig"; sourceTree = "<group>"; };
60D59B1422D1418D00FBD174 /* RCTCameraRoll.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTCameraRoll.xcodeproj; path = "../node_modules/react-native/Libraries/CameraRoll/RCTCameraRoll.xcodeproj"; sourceTree = "<group>"; };
60D59B5022D1446200FBD174 /* RNFS.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNFS.xcodeproj; path = "../node_modules/react-native-fs/RNFS.xcodeproj"; sourceTree = "<group>"; };
620B1E415CFE41A486D41EF8 /* Entypo.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Entypo.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Entypo.ttf"; sourceTree = "<group>"; };
70F33973BD154E92BCA1836A /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = "<group>"; };
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; };
......@@ -548,6 +573,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
60D59B5922D1447400FBD174 /* libRNFS.a in Frameworks */,
60D59B4F22D141B500FBD174 /* libRCTCameraRoll.a in Frameworks */,
3269FC3C22B39E3A00969EDE /* QBImagePicker.framework in Frameworks */,
3269FC3A22B39E2D00969EDE /* RSKImageCropper.framework in Frameworks */,
3269FC3822B39E1A00969EDE /* libimageCropPicker.a in Frameworks */,
......@@ -885,6 +912,23 @@
name = Products;
sourceTree = "<group>";
};
60D59B1522D1418D00FBD174 /* Products */ = {
isa = PBXGroup;
children = (
60D59B2122D1418D00FBD174 /* libRCTCameraRoll.a */,
);
name = Products;
sourceTree = "<group>";
};
60D59B5122D1446200FBD174 /* Products */ = {
isa = PBXGroup;
children = (
60D59B5622D1446200FBD174 /* libRNFS.a */,
60D59B5822D1446200FBD174 /* libRNFS.a */,
);
name = Products;
sourceTree = "<group>";
};
78C398B11ACF4ADC00677621 /* Products */ = {
isa = PBXGroup;
children = (
......@@ -897,6 +941,8 @@
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
60D59B5022D1446200FBD174 /* RNFS.xcodeproj */,
60D59B1422D1418D00FBD174 /* RCTCameraRoll.xcodeproj */,
3269FC3022B39E0F00969EDE /* imageCropPicker.xcodeproj */,
32472E7822A00FB4002A0BC5 /* RNCStatusBar.xcodeproj */,
32472E3B22A00BB1002A0BC5 /* RNCAsyncStorage.xcodeproj */,
......@@ -1063,6 +1109,11 @@
};
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = FRD6Y7E88Y;
SystemCapabilities = {
com.apple.BackgroundModes = {
enabled = 1;
};
};
};
2D02E47A1E0B4A5D006451C7 = {
CreatedOnToolsVersion = 8.2.1;
......@@ -1108,6 +1159,10 @@
ProductGroup = ADBDB9201DFEBF0600ED6528 /* Products */;
ProjectRef = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */;
},
{
ProductGroup = 60D59B1522D1418D00FBD174 /* Products */;
ProjectRef = 60D59B1422D1418D00FBD174 /* RCTCameraRoll.xcodeproj */;
},
{
ProductGroup = 00C302BC1ABCB91800DB3ED1 /* Products */;
ProjectRef = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */;
......@@ -1156,6 +1211,10 @@
ProductGroup = 327DC494229EB42900DE29E9 /* Products */;
ProjectRef = 13BDFD6446B9442B9FDACCF9 /* RNFirebase.xcodeproj */;
},
{
ProductGroup = 60D59B5122D1446200FBD174 /* Products */;
ProjectRef = 60D59B5022D1446200FBD174 /* RNFS.xcodeproj */;
},
{
ProductGroup = 327DC543229EB76A00DE29E9 /* Products */;
ProjectRef = 327DC542229EB76A00DE29E9 /* RNGestureHandler.xcodeproj */;
......@@ -1542,6 +1601,27 @@
remoteRef = 5E9157341DD0AC6500FF2AA8 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
60D59B2122D1418D00FBD174 /* libRCTCameraRoll.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libRCTCameraRoll.a;
remoteRef = 60D59B2022D1418D00FBD174 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
60D59B5622D1446200FBD174 /* libRNFS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libRNFS.a;
remoteRef = 60D59B5522D1446200FBD174 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
60D59B5822D1446200FBD174 /* libRNFS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libRNFS.a;
remoteRef = 60D59B5722D1446200FBD174 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
......
This diff is collapsed.
......@@ -2391,6 +2391,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
base-64@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
integrity sha1-eAqZyE59YAJgNhURxId2E78k9rs=
base64-js@^1.0.2, base64-js@^1.1.2, base64-js@^1.2.3:
version "1.3.0"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
......@@ -7794,6 +7799,14 @@ react-native-firebase@5.5.6:
opencollective-postinstall "^2.0.0"
prop-types "^15.7.2"
react-native-fs@^2.14.1:
version "2.14.1"
resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.14.1.tgz#61c70a865b5b5bcb020dd4e4befd60a2c20c836f"
integrity sha512-ZcfiwNP+FBgvv2eRk0B62/NI58mbjszjjYvQlP352HLkUqVsK4Ld6X8fdBO1lZAz6SgitUk8WEc9NEciRIt31g==
dependencies:
base-64 "^0.1.0"
utf8 "^2.1.1"
react-native-gesture-handler@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-1.3.0.tgz#d0386f565928ccc1849537f03f2e37fd5f6ad43f"
......@@ -9392,6 +9405,11 @@ use@^3.1.0:
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
utf8@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/utf8/-/utf8-2.1.2.tgz#1fa0d9270e9be850d9b05027f63519bf46457d96"
integrity sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY=
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment