Verified Commit c6d1ef2f authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg
Browse files

Turn gallery into separate screen

parent 320150f1
...@@ -7,6 +7,7 @@ export const PHOTOS_ALBUM_SUCCESS = 'PHOTOS_ALBUM_SUCCESS'; ...@@ -7,6 +7,7 @@ export const PHOTOS_ALBUM_SUCCESS = 'PHOTOS_ALBUM_SUCCESS';
export const PHOTOS_ALBUM_FAILURE = 'PHOTOS_ALBUM_FAILURE'; export const PHOTOS_ALBUM_FAILURE = 'PHOTOS_ALBUM_FAILURE';
export const PHOTOS_ALBUM_FETCHING = 'PHOTOS_ALBUM_FETCHING'; export const PHOTOS_ALBUM_FETCHING = 'PHOTOS_ALBUM_FETCHING';
export const PHOTOS_PHOTO_SHOW = 'PHOTOS_PHOTO_SHOW'; export const PHOTOS_PHOTO_SHOW = 'PHOTOS_PHOTO_SHOW';
export const PHOTOS_GALLERY_OPEN = 'PHOTOS_GALLERY_OPEN';
export function openAlbums() { export function openAlbums() {
return { return {
...@@ -31,6 +32,13 @@ export function fetchingAlbums() { ...@@ -31,6 +32,13 @@ export function fetchingAlbums() {
}; };
} }
export function openGallery(selection) {
return {
type: PHOTOS_GALLERY_OPEN,
payload: { selection },
};
}
export function openAlbum(pk) { export function openAlbum(pk) {
return { return {
type: PHOTOS_ALBUM_OPEN, type: PHOTOS_ALBUM_OPEN,
......
...@@ -15,8 +15,9 @@ import Profile from './ui/screens/profile/ProfileScreenConnector'; ...@@ -15,8 +15,9 @@ import Profile from './ui/screens/profile/ProfileScreenConnector';
import Pizza from './ui/screens/pizza/PizzaScreenConnector'; import Pizza from './ui/screens/pizza/PizzaScreenConnector';
import Registration from './ui/screens/events/RegistrationScreenConnector'; import Registration from './ui/screens/events/RegistrationScreenConnector';
import MemberList from './ui/screens/memberList/MemberListScreenConnector'; import MemberList from './ui/screens/memberList/MemberListScreenConnector';
import Photos from './ui/screens/photos/AlbumsOverviewScreenContainer'; import Photos from './ui/screens/photos/AlbumsOverviewScreenConnector';
import PhotoAlbum from './ui/screens/photos/AlbumDetailScreenContainer'; import PhotoAlbum from './ui/screens/photos/AlbumDetailScreenConnector';
import PhotoGallery from './ui/screens/photos/AlbumGalleryScreenConnector';
import SplashScreen from './ui/screens/splash/SplashScreen'; import SplashScreen from './ui/screens/splash/SplashScreen';
import Settings from './ui/screens/settings/SettingsScreenConnector'; import Settings from './ui/screens/settings/SettingsScreenConnector';
import EventAdmin from './ui/screens/events/EventAdminScreenConnector'; import EventAdmin from './ui/screens/events/EventAdminScreenConnector';
...@@ -38,6 +39,7 @@ const SignedInNavigator = createStackNavigator({ ...@@ -38,6 +39,7 @@ const SignedInNavigator = createStackNavigator({
Profile, Profile,
Pizza, Pizza,
PhotoAlbum, PhotoAlbum,
PhotoGallery,
Registration, Registration,
EventAdmin, EventAdmin,
}, { }, {
......
...@@ -69,6 +69,14 @@ export default function photos(state = initialState, action = {}) { ...@@ -69,6 +69,14 @@ export default function photos(state = initialState, action = {}) {
selection: undefined, selection: undefined,
}, },
}; };
case photosActions.PHOTOS_GALLERY_OPEN:
return {
...state,
album: {
...state.album,
selection: action.payload.selection,
},
};
default: default:
return state; return state;
} }
......
...@@ -10,6 +10,7 @@ import * as pizzaActions from '../actions/pizza'; ...@@ -10,6 +10,7 @@ import * as pizzaActions from '../actions/pizza';
import * as loginActions from '../actions/session'; import * as loginActions from '../actions/session';
import * as eventActions from '../actions/event'; import * as eventActions from '../actions/event';
import * as calendarActions from '../actions/calendar'; import * as calendarActions from '../actions/calendar';
import * as photosActions from '../actions/photos';
export const parseURL = (url) => { export const parseURL = (url) => {
const matches = new RegExp(`^${siteURL}(/[^?]+)(?:\\?(.+))?`).exec(url); const matches = new RegExp(`^${siteURL}(/[^?]+)(?:\\?(.+))?`).exec(url);
...@@ -65,6 +66,16 @@ const deepLink = function* deepLink(action) { ...@@ -65,6 +66,16 @@ const deepLink = function* deepLink(action) {
action: calendarActions.open, action: calendarActions.open,
args: [], args: [],
}, },
{
regexp: new RegExp('^/photos/([0-9]+)/$'),
action: photosActions.openAlbum,
args: [],
},
{
regexp: new RegExp('^/photos/$'),
action: photosActions.openAlbums,
args: [],
},
]; ];
for (let i = 0; i < patterns.length; i += 1) { for (let i = 0; i < patterns.length; i += 1) {
......
...@@ -45,6 +45,7 @@ export default function* () { ...@@ -45,6 +45,7 @@ export default function* () {
yield takeEvery(pizzaActions.PIZZA, navigate, 'Pizza'); yield takeEvery(pizzaActions.PIZZA, navigate, 'Pizza');
yield takeEvery(photosActions.PHOTOS_ALBUMS_OPEN, navigate, 'Photos'); yield takeEvery(photosActions.PHOTOS_ALBUMS_OPEN, navigate, 'Photos');
yield takeEvery(photosActions.PHOTOS_ALBUM_OPEN, navigate, 'PhotoAlbum'); yield takeEvery(photosActions.PHOTOS_ALBUM_OPEN, navigate, 'PhotoAlbum');
yield takeEvery(photosActions.PHOTOS_GALLERY_OPEN, navigate, 'PhotoGallery');
yield takeEvery(sessionActions.SIGNED_IN, navigate, 'SignedIn'); yield takeEvery(sessionActions.SIGNED_IN, navigate, 'SignedIn');
yield takeEvery([sessionActions.TOKEN_INVALID, sessionActions.SIGN_OUT], navigate, 'Auth'); yield takeEvery([sessionActions.TOKEN_INVALID, sessionActions.SIGN_OUT], navigate, 'Auth');
} }
...@@ -25,8 +25,10 @@ function* loadAlbums() { ...@@ -25,8 +25,10 @@ function* loadAlbums() {
try { try {
const response = yield call(apiRequest, 'photos/albums', data, params); const response = yield call(apiRequest, 'photos/albums', data, params);
yield put(photosActions.successAlbums(
yield put(photosActions.successAlbums(response.results, response.next)); response.results.filter(item => item.accessible && !item.hidden && item.cover != null),
response.next,
));
} catch (error) { } catch (error) {
yield put(photosActions.failureAlbums()); yield put(photosActions.failureAlbums());
} }
......
export const photosStore = state => state.photos;
export const albumsData = state => photosStore(state).albums.data;
export const albumsStatus = state => photosStore(state).albums.status;
export const isFetchingAlbums = state => photosStore(state).albums.fetching;
export const albumData = state => photosStore(state).album.data;
export const albumSelection = state => photosStore(state).album.selection;
export const albumStatus = state => photosStore(state).album.status;
export const isFetchingAlbum = state => photosStore(state).album.fetching;
import React, { Component } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import { Dimensions, FlatList, View } from 'react-native';
Dimensions, FlatList, TouchableOpacity, View,
} from 'react-native';
import ImageViewer from 'react-native-image-zoom-viewer';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { STATUS_FAILURE, STATUS_INITIAL } from '../../../reducers/photos'; import { STATUS_FAILURE, STATUS_INITIAL } from '../../../reducers/photos';
import LoadingScreen from '../../components/loadingScreen/LoadingScreen'; import LoadingScreen from '../../components/loadingScreen/LoadingScreen';
import ErrorScreen from '../../components/errorScreen/ErrorScreen'; import ErrorScreen from '../../components/errorScreen/ErrorScreen';
import styles from './style/AlbumDetailScreen'; import styles from './style/AlbumDetailScreen';
import PhotoListItem from './PhotoListItem'; import PhotoListItem from './PhotoListItem';
import PhotoViewContainer from './PhotoListItemContainer';
import Colors from '../../style/Colors';
import StandardHeader from '../../components/standardHeader/StandardHeader'; import StandardHeader from '../../components/standardHeader/StandardHeader';
const windowWidth = Dimensions.get('window').width; const windowWidth = Dimensions.get('window').width;
export const itemSize = (windowWidth - 48) / 3; export const itemSize = (windowWidth - 48) / 3;
class AlbumDetailScreen extends Component { const AlbumDetailScreen = ({
constructor(props) { t, fetching, status, photos, openGallery,
super(props); }) => {
let content = (
this.state = { <View style={styles.wrapper}>
gallery: [], <FlatList
selection: null, style={styles.flatList}
}; contentContainerStyle={styles.listContainer}
} data={photos.filter(p => !p.hidden)}
renderItem={
static getDerivedStateFromProps(nextProps, prevState) { data => (
const { token, photos } = nextProps; <PhotoListItem
const gallerySources = []; photo={data.item}
if (photos && photos.length > 0) { size={itemSize}
const keys = Object.keys(photos); style={styles.listItem}
for (let i = 0; i < keys.length; i += 1) { onPress={() => openGallery(data.index)}
const photo = photos[i]; />
if (!photo.hidden) { )
gallerySources.push({
url: photo.file.large,
props: {
headers: {
Authorization: `Token ${token}`,
},
},
});
} }
} keyExtractor={item => item.pk}
} numColumns={3}
/>
return { </View>
...prevState, );
gallery: gallerySources,
};
}
openGallery(index) {
this.setState({ selection: index });
}
closeGallery() {
this.setState({ selection: null });
}
render() {
const {
t, fetching, status, photos,
} = this.props;
let content = ( if (fetching && status === STATUS_INITIAL) {
content = (<LoadingScreen />);
} else if (!fetching && status === STATUS_FAILURE) {
content = (
<View style={styles.wrapper}> <View style={styles.wrapper}>
<FlatList <ErrorScreen message={t('Sorry! We couldn\'t load any data.')} />
style={styles.flatList}
contentContainerStyle={styles.listContainer}
data={photos.filter(p => !p.hidden)}
renderItem={
data => (
<PhotoViewContainer
photo={data.item}
size={itemSize}
style={styles.listItem}
onPress={() => this.openGallery(data.index)}
/>
)
}
keyExtractor={item => item.pk}
numColumns={3}
/>
</View>
);
if (fetching && status === STATUS_INITIAL) {
content = (<LoadingScreen />);
} else if (!fetching && status === STATUS_FAILURE) {
content = (
<View style={styles.wrapper}>
<ErrorScreen message={t('Sorry! We couldn\'t load any data.')} />
</View>
);
} else if (this.state.selection !== null) {
return (
<View style={styles.screenWrapper}>
<View style={styles.galleryWrapper}>
<ImageViewer
index={this.state.selection}
imageUrls={this.state.gallery}
/>
<TouchableOpacity
style={styles.closeGalleryTouchable}
onPress={() => this.closeGallery()}
>
<Icon
name="close"
style={styles.icon}
size={24}
color={Colors.white}
/>
</TouchableOpacity>
</View>
</View>
);
}
return (
<View style={styles.screenWrapper}>
<StandardHeader />
{content}
</View> </View>
); );
} }
}
return (
<View style={styles.screenWrapper}>
<StandardHeader />
{content}
</View>
);
};
AlbumDetailScreen.propTypes = { AlbumDetailScreen.propTypes = {
openGallery: PropTypes.func.isRequired,
fetching: PropTypes.bool.isRequired, fetching: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
photos: PropTypes.arrayOf(PhotoListItem.propTypes.photo), photos: PropTypes.arrayOf(PhotoListItem.propTypes.photo),
// eslint-disable-next-line react/no-unused-prop-types
token: PropTypes.string.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
}; };
......
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import AlbumDetailScreen from './AlbumDetailScreen';
import * as photosActions from '../../../actions/photos';
import { albumData, albumStatus, isFetchingAlbum } from '../../../selectors/photos';
const mapStateToProps = state => ({
photos: albumData(state).photos,
status: albumStatus(state),
fetching: isFetchingAlbum(state),
});
const mapDispatchToProps = {
openGallery: photosActions.openGallery,
};
export default connect(mapStateToProps, mapDispatchToProps)(withTranslation(['screens/photos/AlbumDetail'])(AlbumDetailScreen));
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import AlbumDetailScreen from './AlbumDetailScreen';
import { tokenSelector } from '../../../utils/url';
const mapStateToProps = state => ({
photos: state.photos.album.data.photos,
status: state.photos.album.status,
fetching: state.photos.album.fetching,
token: tokenSelector(state),
});
export default connect(mapStateToProps)(withTranslation(['screens/photos/AlbumDetail'])(AlbumDetailScreen));
import { TouchableOpacity, View } from 'react-native';
import ImageViewer from 'react-native-image-zoom-viewer';
import Icon from 'react-native-vector-icons/MaterialIcons';
import React from 'react';
import PropTypes from 'prop-types';
import Colors from '../../style/Colors';
import styles from './style/AlbumDetailScreen';
const AlbumGalleryScreen = ({ photos, goBack, selection }) => (
<View style={styles.screenWrapper}>
<View style={styles.galleryWrapper}>
<ImageViewer
index={selection}
imageUrls={photos}
/>
<TouchableOpacity
style={styles.closeGalleryTouchable}
onPress={goBack}
>
<Icon
name="close"
style={styles.icon}
size={24}
color={Colors.white}
/>
</TouchableOpacity>
</View>
</View>
);
AlbumGalleryScreen.propTypes = {
goBack: PropTypes.func.isRequired,
selection: PropTypes.number.isRequired,
photos: PropTypes.arrayOf(PropTypes.shape({
url: PropTypes.string.isRequired,
})).isRequired,
};
export default AlbumGalleryScreen;
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import AlbumGalleryScreen from './AlbumGalleryScreen';
import * as navigationActions from '../../../actions/navigation';
import { albumData, albumSelection } from '../../../selectors/photos';
const mapStateToProps = state => ({
photos: Object.values(albumData(state).photos)
.filter(photo => !photo.hidden)
.map(photo => ({
url: photo.file.large,
})),
selection: albumSelection(state),
});
const mapDispatchToProps = {
goBack: navigationActions.goBack,
};
export default connect(mapStateToProps, mapDispatchToProps)(withTranslation(['screens/photos/AlbumDetail'])(AlbumGalleryScreen));
...@@ -16,15 +16,12 @@ const AlbumListItem = (props) => { ...@@ -16,15 +16,12 @@ const AlbumListItem = (props) => {
<SquareView style={props.style} size={props.size}> <SquareView style={props.style} size={props.size}>
<TouchableHighlight <TouchableHighlight
style={styles.image} style={styles.image}
onPress={() => props.openAlbum(props.album.pk)} onPress={() => props.onPress(props.album.pk)}
> >
<ImageBackground <ImageBackground
style={styles.image} style={styles.image}
source={{ source={{
uri: props.album.cover.file.small, uri: props.album.cover.file.small,
headers: {
Authorization: `Token ${props.token}`,
},
}} }}
> >
<LinearGradient colors={['#55000000', '#000000']} style={styles.overlayGradient} /> <LinearGradient colors={['#55000000', '#000000']} style={styles.overlayGradient} />
...@@ -54,8 +51,7 @@ AlbumListItem.propTypes = { ...@@ -54,8 +51,7 @@ AlbumListItem.propTypes = {
hidden: PropTypes.bool.isRequired, hidden: PropTypes.bool.isRequired,
cover: PropTypes.shape(PhotoListItem.propTypes.photo), cover: PropTypes.shape(PhotoListItem.propTypes.photo),
}).isRequired, }).isRequired,
openAlbum: PropTypes.func.isRequired, onPress: PropTypes.func.isRequired,
token: PropTypes.string.isRequired,
}; };
AlbumListItem.defaultProps = { AlbumListItem.defaultProps = {
......
import { connect } from 'react-redux';
import AlbumListItem from './AlbumListItem';
import * as photosActions from '../../../actions/photos';
import { tokenSelector } from '../../../utils/url';
const mapStateToProps = state => ({
albums: state.photos.albums.data,
status: state.photos.albums.status,
fetching: state.photos.albums.fetching,
token: tokenSelector(state),
});
const mapDispatchToProps = dispatch => ({
openAlbum: pk => dispatch(photosActions.openAlbum(pk)),
});
export default connect(mapStateToProps, mapDispatchToProps)(AlbumListItem);
...@@ -6,7 +6,6 @@ import LoadingScreen from '../../components/loadingScreen/LoadingScreen'; ...@@ -6,7 +6,6 @@ import LoadingScreen from '../../components/loadingScreen/LoadingScreen';
import ErrorScreen from '../../components/errorScreen/ErrorScreen'; import ErrorScreen from '../../components/errorScreen/ErrorScreen';
import AlbumListItem from './AlbumListItem'; import AlbumListItem from './AlbumListItem';
import styles from './style/AlbumsOverviewScreen'; import styles from './style/AlbumsOverviewScreen';
import AlbumListItemContainer from './AlbumListItemContainer';
import { withStandardHeader } from '../../components/standardHeader/StandardHeader'; import { withStandardHeader } from '../../components/standardHeader/StandardHeader';
const windowWidth = Dimensions.get('window').width; const windowWidth = Dimensions.get('window').width;
...@@ -14,7 +13,9 @@ const numColumns = 2; ...@@ -14,7 +13,9 @@ const numColumns = 2;
export const albumSize = (windowWidth - 48) / numColumns; export const albumSize = (windowWidth - 48) / numColumns;
const AlbumsOverviewScreen = (props) => { const AlbumsOverviewScreen = (props) => {
const { t, fetching, status } = props; const {
t, fetching, status, openAlbum,
} = props;
if (fetching && status === STATUS_INITIAL) { if (fetching && status === STATUS_INITIAL) {
return <LoadingScreen />; return <LoadingScreen />;
} }
...@@ -34,13 +35,19 @@ const AlbumsOverviewScreen = (props) => { ...@@ -34,13 +35,19 @@ const AlbumsOverviewScreen = (props) => {
data={props.albums} data={props.albums}
renderItem={ renderItem={
({ item }) => ( ({ item }) => (
<AlbumListItemContainer album={item} size={albumSize} style={styles.listItem} /> <AlbumListItem
album={item}
size={albumSize}
style={styles.listItem}
onPress={openAlbum}
/>
) )
} }
keyExtractor={item => item.pk} keyExtractor={item => item.pk}
numColumns={numColumns} numColumns={numColumns}
/> />
</View>); </View>
);
}; };
...@@ -48,6 +55,7 @@ AlbumsOverviewScreen.propTypes = { ...@@ -48,6 +55,7 @@ AlbumsOverviewScreen.propTypes = {
fetching: PropTypes.bool.isRequired, fetching: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
albums: PropTypes.arrayOf(AlbumListItem.propTypes.album).isRequired, albums: PropTypes.arrayOf(AlbumListItem.propTypes.album).isRequired,
openAlbum: PropTypes.func.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
}; };
......
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import AlbumsOverview from './AlbumsOverviewScreen';
import { albumsData, albumsStatus, isFetchingAlbums } from '../../../selectors/photos';
import * as photosActions from '../../../actions/photos';
const mapStateToProps = state => ({
albums: albumsData(state),
status: albumsStatus(state),
fetching: isFetchingAlbums(state),
});