Commit 8e5841ff authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg
Browse files

Merge branch 'feature/photos-3' into 'master'

In-app photo gallery

Closes #100

See merge request !299
parents 798b52c1 e13eda68
...@@ -5,7 +5,7 @@ exports[`LoadingScreen component renders correctly 1`] = ` ...@@ -5,7 +5,7 @@ exports[`LoadingScreen component renders correctly 1`] = `
style={ style={
Object { Object {
"alignItems": "center", "alignItems": "center",
"backgroundColor": "#FFFFFF", "backgroundColor": "#FAFAFA",
"flex": 1, "flex": 1,
"justifyContent": "center", "justifyContent": "center",
} }
......
...@@ -402,6 +402,97 @@ exports[`Sidebar component renders correctly 1`] = ` ...@@ -402,6 +402,97 @@ exports[`Sidebar component renders correctly 1`] = `
</Text> </Text>
</View> </View>
</View> </View>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"overflow": "hidden",
},
Object {
"backgroundColor": "#FFFFFF",
"borderRadius": 0,
},
]
}
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"justifyContent": "flex-start",
"padding": 8,
},
Object {
"backgroundColor": "#FFFFFF",
"borderRadius": 0,
},
Array [
Object {
"padding": 16,
},
Object {},
],
]
}
>
<Text
allowFontScaling={false}
style={
Array [
Object {
"color": "#313131",
"fontSize": 24,
},
Array [
Object {
"marginRight": 10,
},
Object {
"marginRight": 30,
"textAlign": "center",
"width": 28,
},
],
Object {
"fontFamily": "Material Icons",
"fontStyle": "normal",
"fontWeight": "normal",
},
Object {},
]
}
>
</Text>
<Text
style={
Array [
Object {
"color": "#313131",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "600",
},
Object {
"color": "#313131",
},
]
}
>
Photos
</Text>
</View>
</View>
<View <View
accessible={true} accessible={true}
isTVSelectable={true} isTVSelectable={true}
......
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
<data android:host="thalia.nu" /> <data android:host="thalia.nu" />
<data android:path="/pizzas/" /> <data android:path="/pizzas/" />
<data android:pathPattern="/events/.*/" /> <data android:pathPattern="/events/.*/" />
<data android:pathPattern="/photos/.*/" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/> <activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
......
export const PHOTOS_ALBUMS_OPEN = 'PHOTOS_ALBUMS_OPEN';
export const PHOTOS_ALBUMS_SUCCESS = 'PHOTOS_ALBUMS_SUCCESS';
export const PHOTOS_ALBUMS_FAILURE = 'PHOTOS_ALBUMS_FAILURE';
export const PHOTOS_ALBUMS_FETCHING = 'PHOTOS_ALBUMS_FETCHING';
export const PHOTOS_ALBUM_OPEN = 'PHOTOS_ALBUM_OPEN';
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_PHOTO_SHOW = 'PHOTOS_PHOTO_SHOW';
export const PHOTOS_GALLERY_OPEN = 'PHOTOS_GALLERY_OPEN';
export function openAlbums() {
return {
type: PHOTOS_ALBUMS_OPEN,
};
}
export function successAlbums(albums, more) {
return {
type: PHOTOS_ALBUMS_SUCCESS,
payload: { albums, more },
};
}
export function failureAlbums() {
return { type: PHOTOS_ALBUMS_FAILURE };
}
export function fetchingAlbums() {
return {
type: PHOTOS_ALBUMS_FETCHING,
};
}
export function openGallery(selection) {
return {
type: PHOTOS_GALLERY_OPEN,
payload: { selection },
};
}
export function openAlbum(pk) {
return {
type: PHOTOS_ALBUM_OPEN,
payload: { pk },
};
}
export function openAlbumWithSlug(slug) {
return {
type: PHOTOS_ALBUM_OPEN,
payload: { slug },
};
}
export function successAlbum(album) {
return {
type: PHOTOS_ALBUM_SUCCESS,
payload: album,
};
}
export function failureAlbum() {
return { type: PHOTOS_ALBUM_FAILURE };
}
export function fetchingAlbum() {
return {
type: PHOTOS_ALBUM_FETCHING,
};
}
export function showPhoto(photo) {
return {
type: PHOTOS_PHOTO_SHOW,
payload: photo,
};
}
import React, { Component } from 'react'; import React, { Component } from 'react';
import { import {
Linking, Platform, NativeModules, Alert, Alert, Linking, NativeModules, Platform,
} from 'react-native'; } from 'react-native';
import { applyMiddleware, createStore } from 'redux'; import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { I18nextProvider, withTranslation } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import createSagaMiddleware from 'redux-saga'; import createSagaMiddleware from 'redux-saga';
import firebase from 'react-native-firebase'; import firebase from 'react-native-firebase';
import DeviceInfo from 'react-native-device-info'; import DeviceInfo from 'react-native-device-info';
import Moment from 'moment'; import Moment from 'moment';
import 'moment/locale/nl'; import 'moment/locale/nl';
import PropTypes from 'prop-types';
import { i18n as i18next } from 'i18next';
import reducers from './reducers'; import reducers from './reducers';
import i18n from './utils/i18n'; import i18n from './utils/i18n';
...@@ -74,12 +74,13 @@ class Main extends Component { ...@@ -74,12 +74,13 @@ class Main extends Component {
}; };
showNotification = (notification) => { showNotification = (notification) => {
const t = i18next.getFixedT(undefined, 'app');
let buttons; let buttons;
if (notification.data.url) { if (notification.data.url) {
buttons = [ buttons = [
{ text: this.props.t('Dismiss') }, { text: t('Dismiss') },
{ {
text: this.props.t('Open'), text: t('Open'),
onPress: () => store.dispatch( onPress: () => store.dispatch(
deepLinkingActions.deepLink(notification.data.url, false), deepLinkingActions.deepLink(notification.data.url, false),
), ),
...@@ -128,8 +129,4 @@ class Main extends Component { ...@@ -128,8 +129,4 @@ class Main extends Component {
} }
} }
Main.propTypes = { export default Main;
t: PropTypes.func.isRequired,
};
export default withTranslation('app')(Main);
...@@ -15,6 +15,9 @@ import Profile from './ui/screens/profile/ProfileScreenConnector'; ...@@ -15,6 +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/AlbumsOverviewScreenConnector';
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';
...@@ -24,6 +27,7 @@ const MainNavigator = createDrawerNavigator({ ...@@ -24,6 +27,7 @@ const MainNavigator = createDrawerNavigator({
Welcome, Welcome,
Calendar, Calendar,
MemberList, MemberList,
Photos,
Settings, Settings,
}, { }, {
contentComponent: Sidebar, contentComponent: Sidebar,
...@@ -34,6 +38,8 @@ const SignedInNavigator = createStackNavigator({ ...@@ -34,6 +38,8 @@ const SignedInNavigator = createStackNavigator({
Event, Event,
Profile, Profile,
Pizza, Pizza,
PhotoAlbum,
PhotoGallery,
Registration, Registration,
EventAdmin, EventAdmin,
}, { }, {
......
...@@ -8,6 +8,7 @@ import pizza from './pizza'; ...@@ -8,6 +8,7 @@ import pizza from './pizza';
import registration from './registration'; import registration from './registration';
import members from './members'; import members from './members';
import settings from './settings'; import settings from './settings';
import photos from './photos';
export default combineReducers({ export default combineReducers({
session, session,
...@@ -19,4 +20,5 @@ export default combineReducers({ ...@@ -19,4 +20,5 @@ export default combineReducers({
registration, registration,
members, members,
settings, settings,
photos,
}); });
import * as photosActions from '../actions/photos';
export const STATUS_INITIAL = 'initial';
export const STATUS_SUCCESS = 'success';
export const STATUS_FAILURE = 'failure';
const initialState = {
albums: {
status: STATUS_INITIAL,
fetching: true,
data: [],
},
album: {
status: STATUS_INITIAL,
fetching: true,
data: {},
selection: undefined,
},
};
export default function photos(state = initialState, action = {}) {
switch (action.type) {
case photosActions.PHOTOS_ALBUMS_FETCHING:
return {
...state,
albums: initialState.albums,
};
case photosActions.PHOTOS_ALBUMS_SUCCESS:
return {
...state,
albums: {
status: STATUS_SUCCESS,
fetching: false,
data: action.payload.albums,
more: action.payload.more,
},
};
case photosActions.PHOTOS_ALBUMS_FAILURE:
return {
...state,
albums: {
status: STATUS_FAILURE,
fetching: false,
data: [],
},
};
case photosActions.PHOTOS_ALBUM_FETCHING:
return {
...state,
album: initialState.album,
};
case photosActions.PHOTOS_ALBUM_SUCCESS:
return {
...state,
album: {
status: STATUS_SUCCESS,
fetching: false,
data: action.payload,
selection: undefined,
},
};
case photosActions.PHOTOS_ALBUM_FAILURE:
return {
...state,
album: {
status: STATUS_FAILURE,
fetching: false,
data: {},
selection: undefined,
},
};
case photosActions.PHOTOS_GALLERY_OPEN:
return {
...state,
album: {
...state.album,
selection: action.payload.selection,
},
};
default:
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/([-\\w]+)/$'),
action: photosActions.openAlbumWithSlug,
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) {
......
...@@ -12,6 +12,7 @@ import registrationSaga from './registration'; ...@@ -12,6 +12,7 @@ import registrationSaga from './registration';
import deepLinkingSaga from './deepLinking'; import deepLinkingSaga from './deepLinking';
import membersSaga from './members'; import membersSaga from './members';
import settingsSaga from './settings'; import settingsSaga from './settings';
import photosSaga from './photos';
export default function* () { export default function* () {
yield all([ yield all([
...@@ -23,6 +24,7 @@ export default function* () { ...@@ -23,6 +24,7 @@ export default function* () {
fork(calendarSaga), fork(calendarSaga),
fork(pushNotificationsSaga), fork(pushNotificationsSaga),
fork(pizzaSaga), fork(pizzaSaga),
fork(photosSaga),
fork(registrationSaga), fork(registrationSaga),
fork(deepLinkingSaga), fork(deepLinkingSaga),
fork(membersSaga), fork(membersSaga),
......
...@@ -11,6 +11,7 @@ import * as membersActions from '../actions/members'; ...@@ -11,6 +11,7 @@ import * as membersActions from '../actions/members';
import * as welcomeActions from '../actions/welcome'; import * as welcomeActions from '../actions/welcome';
import { settingsActions } from '../actions/settings'; import { settingsActions } from '../actions/settings';
import NavigationService from '../navigation'; import NavigationService from '../navigation';
import * as photosActions from '../actions/photos';
function* navigate(routeName) { function* navigate(routeName) {
yield call(NavigationService.navigate, routeName); yield call(NavigationService.navigate, routeName);
...@@ -42,6 +43,9 @@ export default function* () { ...@@ -42,6 +43,9 @@ export default function* () {
yield takeEvery(registrationActions.FIELDS, navigate, 'Registration'); yield takeEvery(registrationActions.FIELDS, navigate, 'Registration');
yield takeEvery(registrationActions.SUCCESS, back); yield takeEvery(registrationActions.SUCCESS, back);
yield takeEvery(pizzaActions.PIZZA, navigate, 'Pizza'); yield takeEvery(pizzaActions.PIZZA, navigate, 'Pizza');
yield takeEvery(photosActions.PHOTOS_ALBUMS_OPEN, navigate, 'Photos');
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');
} }
import {
call, put, select, takeEvery,
} from 'redux-saga/effects';
import { apiRequest, tokenSelector } from '../utils/url';
import * as photosActions from '../actions/photos';
function* loadAlbums() {
const token = yield select(tokenSelector);
yield put(photosActions.fetchingAlbums());
const data = {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
},
};
const params = {
limit: 12,
};
try {
const response = yield call(apiRequest, 'photos/albums', data, params);
yield put(photosActions.successAlbums(
response.results.filter(item => item.accessible && !item.hidden && item.cover != null),
response.next,
));
} catch (error) {
yield put(photosActions.failureAlbums());
}
}
function* loadAlbum(action) {
const { pk, slug } = action.payload;
const token = yield select(tokenSelector);
yield put(photosActions.fetchingAlbum());
const data = {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
},
};
try {
let album;
if (pk !== undefined) {
album = yield call(apiRequest, `photos/albums/${pk}`, data);
} else {
const albums = yield call(apiRequest, 'photos/albums', data, {
search: slug,
});
album = yield call(apiRequest, `photos/albums/${albums[0].pk}`, data);
}
if (album !== undefined) {
yield put(photosActions.successAlbum(album));
} else {
yield put(photosActions.failureAlbum());
}
} catch (error) {
yield put(photosActions.failureAlbum());
}
}
export default function* photosSaga() {
yield takeEvery(photosActions.PHOTOS_ALBUMS_OPEN, loadAlbums);
yield takeEvery(photosActions.PHOTOS_ALBUM_OPEN, loadAlbum);
}
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 { StyleSheet } from 'react-native';
import Colors from '../../../style/Colors'; import Colors from '../../../style/Colors';
import StyleSheet from '../../../style/StyleSheet';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
indicatorView: { indicatorView: {
justifyContent: 'center', justifyContent: 'center',