Verified Commit f5f87ec0 authored by Bram's avatar Bram Committed by Sébastiaan Versteeg

Add basic profile picture updating functionality

parent a22ab52d
......@@ -79,6 +79,7 @@ dependencies {
implementation project(':@react-native-community_status-bar')
implementation project(':@react-native-community_async-storage')
implementation project(':react-native-device-info')
implementation project(':react-native-image-picker')
implementation project(':react-native-screens')
implementation project(':react-native-gesture-handler')
implementation project(':react-native-sentry')
......
......@@ -4,10 +4,11 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission tools:node="remove" android:name="android.permission.READ_PHONE_STATE" />
<uses-permission tools:node="remove" android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission tools:node="remove" android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name=".MainApplication"
......
......@@ -8,6 +8,7 @@ import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.swmansion.rnscreens.RNScreensPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.imagepicker.ImagePickerPackage;
import cl.json.ShareApplication;
import cl.json.RNSharePackage;
......@@ -44,6 +45,7 @@ public class MainApplication extends Application implements ShareApplication, Re
new RNDeviceInfo(),
new RNScreensPackage(),
new RNGestureHandlerPackage(),
new ImagePickerPackage(),
new RNSharePackage(),
new RNSentryPackage(),
new SnackbarPackage(),
......
......@@ -9,6 +9,8 @@ include ':react-native-screens'
project(':react-native-screens').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-screens/android')
include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
include ':react-native-image-picker'
project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android')
include ':react-native-share'
project(':react-native-share').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share/android')
include ':react-native-sentry'
......
......@@ -2,6 +2,9 @@ export const PROFILE = 'PROFILE_PROFILE';
export const FETCHING = 'PROFILE_FETCHING';
export const SUCCESS = 'PROFILE_SUCCESS';
export const FAILURE = 'PROFILE_FAILURE';
export const UPDATING = 'PROFILE_UPDATING';
export const UPDATE_SUCCESS = 'PROFILE_UPDATE_SUCCESS';
export const UPDATE_FAIL = 'PROFILE_UPDATE_FAIL';
export function profile(member = 'me') {
return {
......@@ -16,6 +19,25 @@ export function fetching() {
};
}
export function updating() {
return {
type: UPDATING,
};
}
export function updateSuccess(profileData) {
return {
type: UPDATE_SUCCESS,
payload: { profileData },
};
}
export function updateFail() {
return {
type: UPDATE_FAIL,
};
}
export function success(profileData) {
return {
type: SUCCESS,
......
......@@ -30,6 +30,6 @@ export function fetchUserInfo() {
return { type: FETCH_USER_INFO };
}
export function setUserInfo(displayName, photo) {
return { type: SET_USER_INFO, payload: { displayName, photo } };
export function setUserInfo(displayName, photo, pk) {
return { type: SET_USER_INFO, payload: { displayName, photo, pk } };
}
......@@ -24,6 +24,7 @@ const initialState = {
},
success: false,
hasLoaded: false,
updating: false,
};
export default function profile(state = initialState, action = {}) {
......@@ -46,6 +47,23 @@ export default function profile(state = initialState, action = {}) {
success: false,
hasLoaded: true,
};
case profileActions.UPDATING:
return {
...state,
updating: true,
};
case profileActions.UPDATE_SUCCESS:
return {
...state,
updating: false,
success: true,
};
case profileActions.UPDATE_FAIL:
return {
...state,
updating: false,
success: true,
};
default:
return state;
}
......
......@@ -11,6 +11,7 @@ const initialState = {
token: '',
username: '',
displayName: '',
pk: -1,
photo: defaultProfileImage,
};
......@@ -33,6 +34,7 @@ export default function session(state = initialState, action = {}) {
...state,
displayName: action.payload.displayName,
photo: action.payload.photo,
pk: action.payload.pk,
};
case sessionActions.TOKEN_INVALID:
case sessionActions.SIGN_OUT:
......
......@@ -7,7 +7,7 @@ import { apiRequest } from '../utils/url';
import * as profileActions from '../actions/profile';
import { tokenSelector } from '../selectors/session';
const profile = function* profile(action) {
function* profile(action) {
const { member } = action.payload;
const token = yield select(tokenSelector);
......@@ -29,10 +29,32 @@ const profile = function* profile(action) {
Sentry.captureException(error);
yield put(profileActions.failure());
}
};
}
const profileSaga = function* eventSaga() {
function* uploadProfilePicture(action) {
const { member } = action.payload;
const token = yield select(tokenSelector);
yield put(profileActions.updating());
const data = {
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
};
}
try {
const profileData = yield call(apiRequest, `members/${member}`, data);
yield put(profileActions.updateSuccess(profileData));
} catch (error) {
Sentry.captureException(error);
yield put(profileActions.updateFail());
}
}
function* eventSaga() {
yield takeEvery(profileActions.PROFILE, profile);
};
}
export default profileSaga;
......@@ -128,7 +128,7 @@ function* userInfo() {
[DISPLAYNAMEKEY, userProfile.display_name],
[PHOTOKEY, userProfile.avatar.medium],
]);
yield put(sessionActions.setUserInfo(userProfile.display_name, userProfile.avatar.medium));
yield put(sessionActions.setUserInfo(userProfile.display_name, userProfile.avatar.medium, userProfile.pk));
} catch (error) {
Sentry.captureException(error);
}
......
......@@ -6,12 +6,19 @@ import {
Platform,
ScrollView,
TouchableOpacity,
TouchableWithoutFeedback,
View,
Image,
Modal,
} from 'react-native';
import StatusBar from '@react-native-community/status-bar';
import { withTranslation } from 'react-i18next';
import LinearGradient from 'react-native-linear-gradient';
import Icon from 'react-native-vector-icons/MaterialIcons';
import Moment from 'moment';
import ImagePicker from 'react-native-image-picker';
import StandardHeader from '../../components/standardHeader/StandardHeader';
import LoadingScreen from '../../components/loadingScreen/LoadingScreen';
import ErrorScreen from '../../components/errorScreen/ErrorScreen';
import LoadingScreen from '../../components/loadingScreen/LoadingScreen';
......@@ -29,14 +36,27 @@ import styles, {
HEADER_SCROLL_DISTANCE,
} from './style/Profile';
class ProfileScreen extends Component {
constructor(props) {
super(props);
this.scrollY = new Animated.Value(0);
this.textHeight = Platform.OS === 'android' ? 27 : 22;
this.state = {
modalVisible: false,
image: this.props.profile.avatar.full,
};
}
setModalVisible = (visible) => {
this.setState({
modalVisible: visible,
});
};
isOwnProfilePage = () => {
return this.props.pk === this.props.profile.pk;
};
getAppbar = () => {
const headerHeight = this.scrollY.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE],
......@@ -76,8 +96,7 @@ class ProfileScreen extends Component {
fontSize: textSize,
bottom: textPosBottom,
};
let appBarBorderStyle = {
};
let appBarBorderStyle = {};
if (Platform.OS === 'android') {
textStyle = {
...textStyle,
......@@ -101,44 +120,75 @@ class ProfileScreen extends Component {
}
return (
<Animated.View style={[styles.header, { height: headerHeight }]}>
<Animated.View
style={[
styles.backgroundImage,
{
opacity: imageOpacity,
transform: [{ translateY: imageTranslate }],
},
]}
>
<ImageBackground
source={{ uri: this.props.profile.avatar.full }}
style={styles.backgroundImage}
resizeMode="cover"
>
<LinearGradient colors={['#55000000', '#000000']} style={styles.overlayGradient} />
</ImageBackground>
</Animated.View>
<Animated.View style={[styles.appBar, appBarBorderStyle]}>
<TouchableOpacity
onPress={this.props.goBack}
<TouchableWithoutFeedback
onPress={() => this.setModalVisible(true)}
>
<Animated.View style={[styles.header, { height: headerHeight }]}>
<Animated.View
style={[
styles.backgroundImage,
{
opacity: imageOpacity,
transform: [{ translateY: imageTranslate }],
},
]}
>
<Icon
name="arrow-back"
style={styles.icon}
size={24}
/>
</TouchableOpacity>
<Animated.Text
style={[styles.title, textStyle]}
>
{this.props.profile.display_name}
</Animated.Text>
<ImageBackground
source={{ uri: this.state.image }}
style={styles.backgroundImage}
resizeMode="cover"
>
<LinearGradient colors={['#55000000', '#000000']} style={styles.overlayGradient}/>
</ImageBackground>
</Animated.View>
<Animated.View style={[styles.appBar, appBarBorderStyle]}>
<TouchableOpacity
onPress={this.props.goBack}
>
<Icon
name="arrow-back"
style={styles.icon}
size={24}
/>
</TouchableOpacity>
<Animated.Text
style={[styles.title, textStyle]}
>
{this.props.profile.display_name}
</Animated.Text>
</Animated.View>
</Animated.View>
</Animated.View>
</TouchableWithoutFeedback>
);
};
handleEditPress = () => {
const options = {
title: 'Select Avatar',
storageOptions: {
skipBackup: true,
path: 'images',
},
};
ImagePicker.launchImageLibrary(options, (response) => {
console.log('Response = ', response);
if (response.didCancel) {
console.log('User cancelled image picker');
} else if (response.error) {
console.log('ImagePicker Error: ', response.error);
} else if (response.customButton) {
console.log('User tapped custom button: ', response.customButton);
} else {
const source = response.uri;
this.setState({
image: source,
});
}
});
};
render() {
const {
hasLoaded, profile, t, openUrl, success,
......@@ -147,8 +197,8 @@ class ProfileScreen extends Component {
if (!hasLoaded) {
return (
<View style={styles.container}>
<StandardHeader />
<LoadingScreen />
<StandardHeader/>
<LoadingScreen/>
</View>
);
} if (!success) {
......@@ -168,6 +218,44 @@ class ProfileScreen extends Component {
translucent
animated
/>
<Modal
visible={this.state.modalVisible}
transparent={true}
onRequestClose={() => this.setModalVisible(false)}
animationType={'fade'}
>
<View
style={{
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(52, 52, 52, 0.8)',
width: '100%',
height: '100%'
}}
>
<Image
source={{ uri: this.state.image }}
style={{ width: '90%', height: '60%' }}
resizeMode={'contain'}
/>
<View
style={{ justifyContent: 'space-around', flexDirection: 'row' }}
>
{this.isOwnProfilePage() && (
<Icon
name="edit"
style={{ flex: 1, fontSize: 24, color: 'white', textAlign: 'center' }}
onPress={this.handleEditPress}
/>
)}
<Icon
name="cancel"
style={{ flex: 1, fontSize: 24, color: 'white', textAlign: 'center' }}
onPress={() => this.setModalVisible(false)}
/>
</View>
</View>
</Modal>
<ScrollView
style={styles.container}
scrollEventThrottle={16}
......@@ -187,6 +275,7 @@ class ProfileScreen extends Component {
}
ProfileScreen.propTypes = {
pk: PropTypes.number.isRequired,
profile: PropTypes.shape({
pk: PropTypes.number.isRequired,
display_name: PropTypes.string.isRequired,
......
......@@ -6,6 +6,7 @@ const mapStateToProps = state => ({
profile: state.profile.profile,
success: state.profile.success,
hasLoaded: state.profile.hasLoaded,
pk: state.session.pk,
});
const mapDispatchToProps = {
......
......@@ -9,6 +9,8 @@ target 'ThaliApp' do
pod 'Firebase/Core', '~> 5.20.1'
pod 'Firebase/Messaging', '~> 5.20.1'
pod 'react-native-image-picker', :path => '../node_modules/react-native-image-picker'
target 'ThaliAppTests' do
inherit! :search_paths
# Pods for testing
......
......@@ -80,5 +80,13 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSPhotoLibraryUsageDescription</key>
<string>$(PRODUCT_NAME) would like access to your photo gallery</string>
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) would like to use your camera</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>$(PRODUCT_NAME) would like to save photos to your photo gallery</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) would like to your microphone (for videos)</string>
</dict>
</plist>
This diff is collapsed.
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