Merge pull request #1973 from ecency/nt/avatar-update-bug

Nt/avatar update bug
This commit is contained in:
Feruz M 2021-06-16 11:32:16 +03:00 committed by GitHub
commit 9a117c5e85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 311 additions and 175 deletions

View File

@ -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',

View File

@ -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,
}) => (
<LinearGradient
start={{ x: 0, y: 0 }}
@ -33,15 +34,25 @@ const AvatarHeader = ({
size={25}
/>
<View style={styles.wrapper}>
<UserAvatar key={avatarUrl || username} noAction size="xl" username={username} />
<IconButton
iconStyle={styles.addIcon}
style={styles.addButton}
iconType="MaterialCommunityIcons"
name="plus"
onPress={showImageUploadActions}
size={15}
/>
<TouchableOpacity onPress={showImageUploadActions}>
<UserAvatar
key={`${avatarUrl}-${username}`}
noAction
size="xl"
username={username}
avatarUrl={avatarUrl}
isLoading={isUploading}
/>
<IconButton
iconStyle={styles.addIcon}
style={styles.addButton}
iconType="MaterialCommunityIcons"
name="plus"
onPress={showImageUploadActions}
size={15}
/>
</TouchableOpacity>
<View style={styles.textWrapper}>
{!!name && <Text style={styles.name}>{name}</Text>}
<Text style={styles.username}>{`@${username} (${reputation})`}</Text>

View File

@ -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
}
});

View File

@ -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) => (
<View style={styles.container}>
<IconButton
iconStyle={styles.saveIcon}
style={styles.saveButton}
iconType="MaterialIcons"
name="save"
onPress={handleOnSubmit}
size={30}
isLoading={isLoading}
/>
<KeyboardAwareScrollView
enableAutoAutomaticScroll={Platform.OS === 'ios'}
contentContainerStyle={styles.contentContainer}
enableOnAndroid={true}
>
<TouchableOpacity style={styles.coverImgWrapper} onPress={showImageUploadActions}>
<Image
style={styles.coverImg}
source={{ uri: getResizedImage(coverUrl, 600) }}
defaultSource={isDarkTheme ? DARK_COVER_IMAGE : LIGHT_COVER_IMAGE}
/>
<FastImage
style={styles.coverImg}
source={
coverUrl
? { uri: getResizedImage(coverUrl, 600) }
: isDarkTheme
? DARK_COVER_IMAGE
: LIGHT_COVER_IMAGE
}
/>
{
isUploading && (
<ActivityIndicator
style={styles.activityIndicator}
color={EStyleSheet.value('$white')}
size='large'
/>
)
}
<IconButton
iconStyle={styles.addIcon}
@ -83,6 +111,22 @@ const ProfileEditFormView = ({
</View>
))}
</KeyboardAwareScrollView>
{saveEnabled && (
<AnimatedView style={styles.floatingContainer} animation="bounceInRight">
<MainButton
style={{ width: isLoading ? null : 120, marginBottom:24, alignSelf:'flex-end' }}
onPress={handleOnSubmit}
iconName="save"
iconType="MaterialIcons"
iconColor="white"
text="SAVE"
isLoading={isLoading}
/>
</AnimatedView>
)}
</View>
);

View File

@ -6,4 +6,10 @@ export default EStyleSheet.create({
borderColor: '$borderColor',
backgroundColor: '$pureWhite',
},
activityIndicator: {
position:'absolute',
alignSelf:'center',
top:0,
bottom:0
}
});

View File

@ -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 (
<TouchableOpacity disabled={noAction} onPress={() => this._handleOnAvatarPress(username)}>
<FastImage
style={[
styles.avatar,
style,
!disableSize && { width: _size, height: _size, borderRadius: _size / 2 },
]}
source={_avatar}
/>
</TouchableOpacity>
);
}
}
const mapStateToProps = (state) => ({
currentUsername: state.account.currentAccount,
});
export default connect(mapStateToProps)(UserAvatarView);

View File

@ -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 (
<TouchableOpacity disabled={noAction} onPress={() => _handleOnAvatarPress(username)}>
<FastImage
style={[
styles.avatar,
style,
!disableSize && { width: _size, height: _size, borderRadius: _size / 2 },
]}
source={_avatar}
/>
{
isLoading && (
<ActivityIndicator
style={styles.activityIndicator}
size='large'
color={EStyleSheet.value('$white')}
/>
)
}
</TouchableOpacity>
);
}
export default UserAvatarView;

View File

@ -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,
};

View File

@ -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
})

View File

@ -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';

View File

@ -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;
}

View File

@ -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) => {

View File

@ -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,
}) => (
<Fragment>
<StatusBar barStyle="light-content" />
<AvatarHeader
username={get(currentAccount, 'name')}
name={name}
reputation={get(currentAccount, 'reputation')}
avatarUrl={avatarUrl}
showImageUploadActions={() => this._showImageUploadActions('avatarUrl')}
isUploading={isUploading && selectedUploadAction === 'avatarUrl'}
/>
<ProfileEditForm
formData={formData}
@ -70,8 +76,11 @@ class ProfileEditScreen extends PureComponent {
showImageUploadActions={() => this._showImageUploadActions('coverUrl')}
handleOnItemChange={handleOnItemChange}
isLoading={isLoading}
isUploading={isUploading && selectedUploadAction === 'coverUrl'}
saveEnabled={saveEnabled}
handleOnSubmit={handleOnSubmit}
/>
<ActionSheet
ref={this.galleryRef}
options={[