diff --git a/src/components/avatarHeader/avatarHeaderStyles.js b/src/components/avatarHeader/avatarHeaderStyles.js index 1840141d8..9acafbef3 100644 --- a/src/components/avatarHeader/avatarHeaderStyles.js +++ b/src/components/avatarHeader/avatarHeaderStyles.js @@ -2,9 +2,11 @@ import EStyleSheet from 'react-native-extended-stylesheet'; export default EStyleSheet.create({ headerContainer: { - height: 100, flexDirection: 'row', - padding: 21, + paddingTop: 8, + paddingHorizontal: 24, + paddingBottom: 24, + alignItems: 'center', }, backIcon: { color: '$white', @@ -16,7 +18,7 @@ export default EStyleSheet.create({ alignItems: 'center', }, textWrapper: { - marginLeft: 16, + marginLeft: 24, }, name: { color: '$white', diff --git a/src/components/avatarHeader/avatarHeaderView.js b/src/components/avatarHeader/avatarHeaderView.js index c1f0111f2..32959effd 100644 --- a/src/components/avatarHeader/avatarHeaderView.js +++ b/src/components/avatarHeader/avatarHeaderView.js @@ -1,6 +1,6 @@ import React from 'react'; import { withNavigation } from 'react-navigation'; -import { View, Text, SafeAreaView } from 'react-native'; +import { View, Text, SafeAreaView, TouchableOpacity } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import { UserAvatar } from '../userAvatar'; @@ -16,6 +16,7 @@ const AvatarHeader = ({ navigation, avatarUrl, showImageUploadActions, + isUploading, }) => ( - - + + + + + {!!name && {name}} {`@${username} (${reputation})`} diff --git a/src/components/profileEditForm/profileEditFormStyles.js b/src/components/profileEditForm/profileEditFormStyles.ts similarity index 84% rename from src/components/profileEditForm/profileEditFormStyles.js rename to src/components/profileEditForm/profileEditFormStyles.ts index f25d66add..ccd3b45b2 100644 --- a/src/components/profileEditForm/profileEditFormStyles.js +++ b/src/components/profileEditForm/profileEditFormStyles.ts @@ -13,6 +13,7 @@ export default EStyleSheet.create({ marginTop: 8, }, label: { + marginTop:8, fontSize: 14, color: '$primaryDarkText', fontWeight: '500', @@ -25,7 +26,7 @@ export default EStyleSheet.create({ height: 60, marginBottom: 12, alignSelf: 'stretch', - backgroundColor: '#296CC0', + backgroundColor: '$primaryGray', }, coverImageWrapper: {}, addIcon: { @@ -62,13 +63,19 @@ export default EStyleSheet.create({ }, input: { - fontSize: 14, - color: '$primaryDarkText', + fontSize: 20, + color: '$primaryBlack', alignSelf: 'flex-start', width: '100%', - height: 40, + paddingBottom:10, }, contentContainer: { flexGrow: 1, }, + activityIndicator: { + position:'absolute', + alignSelf:'center', + top:0, + bottom:8 + } }); diff --git a/src/components/profileEditForm/profileEditFormView.js b/src/components/profileEditForm/profileEditFormView.tsx similarity index 55% rename from src/components/profileEditForm/profileEditFormView.js rename to src/components/profileEditForm/profileEditFormView.tsx index 982973e93..41d43a9a6 100644 --- a/src/components/profileEditForm/profileEditFormView.js +++ b/src/components/profileEditForm/profileEditFormView.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { withNavigation } from 'react-navigation'; -import { View, TouchableOpacity, Image, Text, Platform } from 'react-native'; +import { View, TouchableOpacity, Text, Platform, ActivityIndicator } from 'react-native'; +import { View as AnimatedView } from 'react-native-animatable'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { injectIntl } from 'react-intl'; @@ -17,9 +18,25 @@ import { getResizedImage } from '../../utils/image'; // Styles import styles from './profileEditFormStyles'; +import FastImage from 'react-native-fast-image'; +import EStyleSheet from 'react-native-extended-stylesheet'; +import { MainButton } from '../mainButton'; + + +interface ProfileEditFormProps { + coverUrl:string; + formData:any; + handleOnItemChange:()=>void; + handleOnSubmit:()=>void; + intl:any, + isDarkTheme:boolean, + isLoading:boolean, + isUploading:boolean, + showImageUploadActions:boolean, + saveEnabled:boolean, +} const ProfileEditFormView = ({ - avatarUrl, coverUrl, formData, handleOnItemChange, @@ -27,30 +44,41 @@ const ProfileEditFormView = ({ intl, isDarkTheme, isLoading, + isUploading, showImageUploadActions, + saveEnabled, ...props -}) => ( +}:ProfileEditFormProps) => ( + - + - + + + { + isUploading && ( + + ) + } + ))} + + {saveEnabled && ( + + + + )} + + ); diff --git a/src/components/userAvatar/view/userAvatarStyles.js b/src/components/userAvatar/view/userAvatarStyles.ts similarity index 66% rename from src/components/userAvatar/view/userAvatarStyles.js rename to src/components/userAvatar/view/userAvatarStyles.ts index 0b6c0ae56..b8578471d 100644 --- a/src/components/userAvatar/view/userAvatarStyles.js +++ b/src/components/userAvatar/view/userAvatarStyles.ts @@ -6,4 +6,10 @@ export default EStyleSheet.create({ borderColor: '$borderColor', backgroundColor: '$pureWhite', }, + activityIndicator: { + position:'absolute', + alignSelf:'center', + top:0, + bottom:0 + } }); diff --git a/src/components/userAvatar/view/userAvatarView.js b/src/components/userAvatar/view/userAvatarView.js deleted file mode 100644 index d761c1d9a..000000000 --- a/src/components/userAvatar/view/userAvatarView.js +++ /dev/null @@ -1,91 +0,0 @@ -import React, { Component } from 'react'; -import { TouchableOpacity } from 'react-native'; -import { connect } from 'react-redux'; - -import FastImage from 'react-native-fast-image'; -import styles from './userAvatarStyles'; -import { navigate } from '../../../navigation/service'; - -// Constants -import ROUTES from '../../../constants/routeNames'; - -// Utils -import { getResizedAvatar } from '../../../utils/image'; - -const DEFAULT_IMAGE = require('../../../assets/avatar_default.png'); - -/* Props - * ------------------------------------------------ - * @prop { type } name - Description.... - */ - -class UserAvatarView extends Component { - // Component Life Cycles - shouldComponentUpdate(nextProps) { - const { username } = this.props; - - return nextProps.username !== username; - } - - // Component Functions - _handleOnAvatarPress = (username) => { - const { - currentUsername: { name }, - } = this.props; - - const routeName = name === username ? ROUTES.TABBAR.PROFILE : ROUTES.SCREENS.PROFILE; - - navigate({ - routeName, - params: { - username, - }, - key: username, - }); - }; - - render() { - const { - username, - size, - style, - disableSize, - noAction, - currentUsername: { name, avatar }, - } = this.props; - const imageSize = 'large'; - let _size; - const _avatar = username - ? { - uri: getResizedAvatar(username, imageSize), - } - : DEFAULT_IMAGE; - - if (!disableSize) { - _size = 32; - - if (size === 'xl') { - _size = 64; - } - } - - return ( - this._handleOnAvatarPress(username)}> - - - ); - } -} - -const mapStateToProps = (state) => ({ - currentUsername: state.account.currentAccount, -}); - -export default connect(mapStateToProps)(UserAvatarView); diff --git a/src/components/userAvatar/view/userAvatarView.tsx b/src/components/userAvatar/view/userAvatarView.tsx new file mode 100644 index 000000000..d382ff5d6 --- /dev/null +++ b/src/components/userAvatar/view/userAvatarView.tsx @@ -0,0 +1,98 @@ +import React, { Component } from 'react'; +import { ActivityIndicator, TouchableOpacity, ViewStyle } from 'react-native'; +import { connect } from 'react-redux'; + +import FastImage from 'react-native-fast-image'; +import styles from './userAvatarStyles'; +import { navigate } from '../../../navigation/service'; + +// Constants +import ROUTES from '../../../constants/routeNames'; + +// Utils +import { getResizedAvatar } from '../../../utils/image'; +import { useAppSelector } from '../../../hooks'; +import EStyleSheet from 'react-native-extended-stylesheet'; + +const DEFAULT_IMAGE = require('../../../assets/avatar_default.png'); + +/* Props + * ------------------------------------------------ + * @prop { type } name - Description.... + */ + +interface UserAvatarProps { + username:string; + avatarUrl?:string; + size?:'xl'; + style?:ViewStyle; + disableSize?:boolean; + noAction?:boolean; + isLoading?:boolean; +} + +const UserAvatarView = ({ + username, + avatarUrl, + size, + style, + disableSize, + noAction, + isLoading +}:UserAvatarProps) => { + + const curUsername = useAppSelector(state=>state.account.currentAccount.name); + const avatarCacheStamp = useAppSelector(state=>state.ui.avatarCacheStamp); + + // Component Functions + const _handleOnAvatarPress = (username:string) => { + const routeName = curUsername === username ? ROUTES.TABBAR.PROFILE : ROUTES.SCREENS.PROFILE; + navigate({ + routeName, + params: { + username, + }, + key: username, + }); + }; + + + const uri = avatarUrl ? avatarUrl : getResizedAvatar(username, 'large'); + + const _avatar = username + ? { uri : `${uri}?stamp=${avatarCacheStamp}` } + : DEFAULT_IMAGE; + + let _size:number; + if (!disableSize) { + _size = 32; + if (size === 'xl') { + _size = 64; + } + } + + + return ( + _handleOnAvatarPress(username)}> + + { + isLoading && ( + + ) + } + + ); +} + +export default UserAvatarView; diff --git a/src/containers/profileEditContainer.js b/src/containers/profileEditContainer.js index 2b4c55d7f..41602d372 100644 --- a/src/containers/profileEditContainer.js +++ b/src/containers/profileEditContainer.js @@ -10,6 +10,7 @@ import { uploadImage } from '../providers/ecency/ecency'; import { profileUpdate, signImage } from '../providers/hive/dhive'; import { updateCurrentAccount } from '../redux/actions/accountAction'; +import { setAvatarCacheStamp } from '../redux/actions/uiAction'; // import ROUTES from '../constants/routeNames'; @@ -50,6 +51,8 @@ class ProfileEditContainer extends Component { super(props); this.state = { isLoading: false, + isUploading: false, + saveEnabled: false, about: get(props.currentAccount, 'about.profile.about'), name: get(props.currentAccount, 'about.profile.name'), location: get(props.currentAccount, 'about.profile.location'), @@ -64,20 +67,20 @@ class ProfileEditContainer extends Component { // Component Functions _handleOnItemChange = (val, item) => { - this.setState({ [item]: val }); + this.setState({ [item]: val, saveEnabled: true }); }; _uploadImage = async (media, action) => { const { intl, currentAccount, pinCode } = this.props; - this.setState({ isLoading: true }); + this.setState({ isUploading: true }); let sign = await signImage(media, currentAccount, pinCode); uploadImage(media, currentAccount.name, sign) .then((res) => { if (res.data && res.data.url) { - this.setState({ [action]: res.data.url, isLoading: false }); + this.setState({ [action]: res.data.url, isUploading: false, saveEnabled: true }); } }) .catch((error) => { @@ -89,7 +92,7 @@ class ProfileEditContainer extends Component { error.message || error.toString(), ); } - this.setState({ isLoading: false }); + this.setState({ isUploading: false }); }); }; @@ -102,11 +105,11 @@ class ProfileEditContainer extends Component { }; _handleOpenImagePicker = (action) => { - ImagePicker.openPicker({ - includeBase64: true, - }) - .then((image) => { - this._handleMediaOnSelected(image, action); + ImagePicker.openPicker( + action == 'avatarUrl' ? IMAGE_PICKER_AVATAR_OPTIONS : IMAGE_PICKER_COVER_OPTIONS, + ) + .then((media) => { + this._uploadImage(media, action); }) .catch((e) => { this._handleMediaOnSelectFailure(e); @@ -114,23 +117,17 @@ class ProfileEditContainer extends Component { }; _handleOpenCamera = (action) => { - ImagePicker.openCamera({ - includeBase64: true, - }) - .then((image) => { - this._handleMediaOnSelected(image, action); + ImagePicker.openCamera( + action == 'avatarUrl' ? IMAGE_PICKER_AVATAR_OPTIONS : IMAGE_PICKER_COVER_OPTIONS, + ) + .then((media) => { + this._uploadImage(media, action); }) .catch((e) => { this._handleMediaOnSelectFailure(e); }); }; - _handleMediaOnSelected = (media, action) => { - this.setState({ isLoading: true }, () => { - this._uploadImage(media, action); - }); - }; - _handleMediaOnSelectFailure = (error) => { const { intl } = this.props; @@ -150,7 +147,7 @@ class ProfileEditContainer extends Component { const { currentAccount, pinCode, dispatch, navigation, intl } = this.props; const { name, location, website, about, coverUrl, avatarUrl } = this.state; - await this.setState({ isLoading: true }); + this.setState({ isLoading: true }); const params = { profile_image: avatarUrl, @@ -161,31 +158,42 @@ class ProfileEditContainer extends Component { location, version: 2, }; - await profileUpdate(params, pinCode, currentAccount) - .then(async () => { - const _currentAccount = { ...currentAccount, display_name: name, avatar: avatarUrl }; - _currentAccount.about.profile = { ...params }; - dispatch(updateCurrentAccount(_currentAccount)); + try { + await profileUpdate(params, pinCode, currentAccount); - navigation.state.params.fetchUser(); - navigation.goBack(); - }) - .catch((error) => { - Alert.alert( - intl.formatMessage({ - id: 'alert.fail', - }), - get(error, 'message', error.toString()), - ); - }); + const _currentAccount = { ...currentAccount, display_name: name, avatar: avatarUrl }; + _currentAccount.about.profile = { ...params }; - this.setState({ isLoading: false }); + dispatch(updateCurrentAccount(_currentAccount)); + dispatch(setAvatarCacheStamp(new Date().getTime())); + this.setState({ isLoading: false }); + navigation.state.params.fetchUser(); + navigation.goBack(); + } catch (err) { + Alert.alert( + intl.formatMessage({ + id: 'alert.fail', + }), + get(error, 'message', error.toString()), + ); + this.setState({ isLoading: false }); + } }; render() { const { children, currentAccount, isDarkTheme } = this.props; - const { isLoading, name, location, website, about, coverUrl, avatarUrl } = this.state; + const { + isLoading, + isUploading, + name, + location, + website, + about, + coverUrl, + avatarUrl, + saveEnabled, + } = this.state; return ( children && @@ -200,9 +208,11 @@ class ProfileEditContainer extends Component { handleOnSubmit: this._handleOnSubmit, isDarkTheme, isLoading, + isUploading, location, name, website, + saveEnabled, }) ); } @@ -215,3 +225,14 @@ const mapStateToProps = (state) => ({ }); export default connect(mapStateToProps)(injectIntl(withNavigation(ProfileEditContainer))); + +const IMAGE_PICKER_AVATAR_OPTIONS = { + includeBase64: true, + cropping: true, + width: 512, + height: 512, +}; + +const IMAGE_PICKER_COVER_OPTIONS = { + includeBase64: true, +}; diff --git a/src/redux/actions/uiAction.js b/src/redux/actions/uiAction.ts similarity index 53% rename from src/redux/actions/uiAction.js rename to src/redux/actions/uiAction.ts index 40314d795..b2a5aa265 100644 --- a/src/redux/actions/uiAction.js +++ b/src/redux/actions/uiAction.ts @@ -1,3 +1,4 @@ +import { ButtonProps } from 'react-native'; import { TOAST_NOTIFICATION, UPDATE_ACTIVE_BOTTOM_TAB, @@ -6,19 +7,20 @@ import { TOGGLE_ACCOUNTS_BOTTOM_SHEET, SHOW_ACTION_MODAL, HIDE_ACTION_MODAL, + SET_AVATAR_CACHE_STAMP, } from '../constants/constants'; -export const updateActiveBottomTab = (payload) => ({ +export const updateActiveBottomTab = (payload:string) => ({ payload, type: UPDATE_ACTIVE_BOTTOM_TAB, }); -export const toastNotification = (payload) => ({ +export const toastNotification = (payload:string) => ({ payload, type: TOAST_NOTIFICATION, }); -export const showActionModal = (title, body, buttons, headerImage, onClosed) => ({ +export const showActionModal = (title:string, body:string, buttons:ButtonProps[], headerImage:any, onClosed:()=>void) => ({ payload: { actionModalVisible: true, actionModalData: { @@ -36,17 +38,22 @@ export const hideActionModal = () => ({ type: HIDE_ACTION_MODAL, }); -export const setRcOffer = (payload) => ({ +export const setRcOffer = (payload:boolean) => ({ payload, type: RC_OFFER, }); -export const hidePostsThumbnails = (payload) => ({ +export const hidePostsThumbnails = (payload:boolean) => ({ payload, type: HIDE_POSTS_THUMBNAILS, }); -export const toggleAccountsBottomSheet = (payload) => ({ +export const toggleAccountsBottomSheet = (payload:boolean) => ({ payload, type: TOGGLE_ACCOUNTS_BOTTOM_SHEET, }); + +export const setAvatarCacheStamp = (payload:number) => ({ + payload, + type:SET_AVATAR_CACHE_STAMP +}) diff --git a/src/redux/constants/constants.js b/src/redux/constants/constants.js index d22cf9a92..8c5de7830 100644 --- a/src/redux/constants/constants.js +++ b/src/redux/constants/constants.js @@ -54,6 +54,7 @@ export const RC_OFFER = 'RC_OFFER'; export const TOGGLE_ACCOUNTS_BOTTOM_SHEET = 'TOGGLE_ACCOUNTS_BOTTOM_SHEET'; export const SHOW_ACTION_MODAL = 'SHOW_ACTION_MODAL'; export const HIDE_ACTION_MODAL = 'HIDE_ACTION_MODAL'; +export const SET_AVATAR_CACHE_STAMP = 'SET_AVATAR_CACHE_STAMP'; // POSTS export const SET_FEED_POSTS = 'SET_FEED_POSTS'; diff --git a/src/redux/reducers/uiReducer.js b/src/redux/reducers/uiReducer.ts similarity index 76% rename from src/redux/reducers/uiReducer.js rename to src/redux/reducers/uiReducer.ts index 783bcdb62..b792ac16d 100644 --- a/src/redux/reducers/uiReducer.js +++ b/src/redux/reducers/uiReducer.ts @@ -6,9 +6,21 @@ import { TOGGLE_ACCOUNTS_BOTTOM_SHEET, SHOW_ACTION_MODAL, HIDE_ACTION_MODAL, + SET_AVATAR_CACHE_STAMP, } from '../constants/constants'; -const initialState = { +interface UiState { + activeBottomTab:string; + toastNotification:string; + hidePostsThumbnails:boolean; + rcOffer:boolean; + isVisibleAccountsBottomSheet:boolean; + actionModalVisible:boolean; + actionModalData:any; + avatarCacheStamp:number +} + +const initialState:UiState = { activeBottomTab: 'HomeTabbar', toastNotification: '', hidePostsThumbnails: false, @@ -16,6 +28,7 @@ const initialState = { isVisibleAccountsBottomSheet: false, actionModalVisible: false, actionModalData: null, + avatarCacheStamp: 0 }; export default function (state = initialState, action) { @@ -65,6 +78,11 @@ export default function (state = initialState, action) { ...state, isVisibleAccountsBottomSheet: action.payload, }; + case SET_AVATAR_CACHE_STAMP: + return { + ...state, + avatarCacheStamp: action.payload + } default: return state; } diff --git a/src/screens/application/container/applicationContainer.js b/src/screens/application/container/applicationContainer.js index 7b745c082..04ecebfc7 100644 --- a/src/screens/application/container/applicationContainer.js +++ b/src/screens/application/container/applicationContainer.js @@ -77,6 +77,7 @@ import { } from '../../../redux/actions/applicationActions'; import { hideActionModal, + setAvatarCacheStamp, setRcOffer, toastNotification, updateActiveBottomTab, @@ -126,7 +127,6 @@ class ApplicationContainer extends Component { this._setNetworkListener(); Linking.addEventListener('url', this._handleOpenURL); - Linking.getInitialURL().then((url) => { this._handleDeepLink(url); }); @@ -145,6 +145,9 @@ class ApplicationContainer extends Component { if (!isIos) BackHandler.addEventListener('hardwareBackPress', this._onBackPress); + //set avatar cache stamp to invalidate previous session avatars + dispatch(setAvatarCacheStamp(new Date().getTime())); + getVersionForWelcomeModal().then((version) => { if (version < parseVersionNumber(appVersion)) { getUserData().then((accounts) => { diff --git a/src/screens/profileEdit/screen/profileEditScreen.js b/src/screens/profileEdit/screen/profileEditScreen.js index 1971537ca..fa71e0702 100644 --- a/src/screens/profileEdit/screen/profileEditScreen.js +++ b/src/screens/profileEdit/screen/profileEditScreen.js @@ -1,4 +1,5 @@ import React, { PureComponent, Fragment } from 'react'; +import { StatusBar } from 'react-native'; import { injectIntl } from 'react-intl'; import get from 'lodash/get'; import ActionSheet from 'react-native-actionsheet'; @@ -25,9 +26,10 @@ class ProfileEditScreen extends PureComponent { // Component Life Cycles // Component Functions - _showImageUploadActions = async (action) => { - await this.setState({ selectedUploadAction: action }); - this.galleryRef.current.show(); + _showImageUploadActions = (action) => { + this.setState({ selectedUploadAction: action }, () => { + this.galleryRef.current.show(); + }); }; render() { @@ -49,15 +51,19 @@ class ProfileEditScreen extends PureComponent { avatarUrl, coverUrl, isLoading, + isUploading, + saveEnabled, handleOnSubmit, }) => ( + this._showImageUploadActions('avatarUrl')} + isUploading={isUploading && selectedUploadAction === 'avatarUrl'} /> this._showImageUploadActions('coverUrl')} handleOnItemChange={handleOnItemChange} isLoading={isLoading} + isUploading={isUploading && selectedUploadAction === 'coverUrl'} + saveEnabled={saveEnabled} handleOnSubmit={handleOnSubmit} /> +