Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add avatar preview feature #22145

Merged
merged 16 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 78 additions & 51 deletions src/components/AvatarWithImagePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import stylePropTypes from '../styles/stylePropTypes';
import * as FileUtils from '../libs/fileDownload/FileUtils';
import getImageResolution from '../libs/fileDownload/getImageResolution';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import AttachmentModal from './AttachmentModal';
import DotIndicatorMessage from './DotIndicatorMessage';
import * as Browser from '../libs/Browser';

Expand Down Expand Up @@ -79,6 +80,15 @@ const propTypes = {
// eslint-disable-next-line react/forbid-prop-types
errors: PropTypes.object,

/** Title for avatar preview modal */
headerTitle: PropTypes.string,

/** Avatar source for avatar preview modal */
previewSource: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),

/** File name of the avatar */
originalFileName: PropTypes.string,

...withLocalizePropTypes,
};

Expand All @@ -98,6 +108,9 @@ const defaultProps = {
onErrorClose: () => {},
pendingAction: null,
errors: null,
headerTitle: '',
previewSource: '',
originalFileName: '',
};

class AvatarWithImagePicker extends React.Component {
Expand Down Expand Up @@ -273,58 +286,72 @@ class AvatarWithImagePicker extends React.Component {
</PressableWithoutFeedback>
</Tooltip>
</OfflineWithFeedback>
<AttachmentPicker type={CONST.ATTACHMENT_PICKER_TYPE.IMAGE}>
{({openPicker}) => {
const menuItems = [
{
icon: Expensicons.Upload,
text: this.props.translate('avatarWithImagePicker.uploadPhoto'),
onSelected: () => {
if (Browser.isSafari()) {
return;
}
openPicker({
onPicked: this.showAvatarCropModal,
<AttachmentModal
headerTitle={this.props.headerTitle}
source={this.props.previewSource}
originalFileName={this.props.originalFileName}
>
{({show}) => (
<AttachmentPicker type={CONST.ATTACHMENT_PICKER_TYPE.IMAGE}>
{({openPicker}) => {
const menuItems = [
{
icon: Expensicons.Upload,
text: this.props.translate('avatarWithImagePicker.uploadPhoto'),
onSelected: () => {
if (Browser.isSafari()) {
return;
}
openPicker({
onPicked: this.showAvatarCropModal,
});
},
},
];

// If current avatar isn't a default avatar, allow Remove Photo option
if (!this.props.isUsingDefaultAvatar) {
menuItems.push({
icon: Expensicons.Trashcan,
text: this.props.translate('avatarWithImagePicker.removePhoto'),
onSelected: () => {
this.setError(null, {});
this.props.onImageRemoved();
},
});
},
},
];

// If current avatar isn't a default avatar, allow Remove Photo option
if (!this.props.isUsingDefaultAvatar) {
menuItems.push({
icon: Expensicons.Trashcan,
text: this.props.translate('avatarWithImagePicker.removePhoto'),
onSelected: () => {
this.setError(null, {});
this.props.onImageRemoved();
},
});
}
return (
<PopoverMenu
isVisible={this.state.isMenuVisible}
onClose={() => this.setState({isMenuVisible: false})}
onItemSelected={(item, index) => {
this.setState({isMenuVisible: false});
// In order for the file picker to open dynamically, the click
// function must be called from within a event handler that was initiated
// by the user on Safari.
if (index === 0 && Browser.isSafari()) {
openPicker({
onPicked: this.showAvatarCropModal,
});
}
}}
menuItems={menuItems}
anchorPosition={this.props.anchorPosition}
withoutOverlay
anchorRef={this.anchorRef}
anchorAlignment={this.props.anchorAlignment}
/>
);
}}
</AttachmentPicker>

menuItems.push({
icon: Expensicons.Eye,
text: this.props.translate('avatarWithImagePicker.viewPhoto'),
onSelected: () => show(),
});
}
return (
<PopoverMenu
isVisible={this.state.isMenuVisible}
onClose={() => this.setState({isMenuVisible: false})}
onItemSelected={(item, index) => {
this.setState({isMenuVisible: false});
// In order for the file picker to open dynamically, the click
// function must be called from within a event handler that was initiated
// by the user on Safari.
if (index === 0 && Browser.isSafari()) {
openPicker({
onPicked: this.showAvatarCropModal,
});
}
}}
menuItems={menuItems}
anchorPosition={this.props.anchorPosition}
withoutOverlay
anchorRef={this.anchorRef}
anchorAlignment={this.props.anchorAlignment}
/>
);
}}
</AttachmentPicker>
)}
</AttachmentModal>
</View>
{this.state.validationError && (
<DotIndicatorMessage
Expand Down
4 changes: 4 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,8 @@ export default {
uploadPhoto: 'Upload photo',
removePhoto: 'Remove photo',
editImage: 'Edit photo',
viewPhoto: 'View photo',
imageUploadFailed: 'Image upload failed',
deleteWorkspaceError: 'Sorry, there was an unexpected problem deleting your workspace avatar.',
sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `The selected image exceeds the maximum upload size of ${maxUploadSizeInMB}MB.`,
resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}: ResolutionConstraintsParams) =>
Expand All @@ -592,6 +594,7 @@ export default {
online: 'Online',
offline: 'Offline',
syncing: 'Syncing',
profileAvatar: 'Profile avatar',
},
loungeAccessPage: {
loungeAccess: 'Lounge access',
Expand Down Expand Up @@ -1312,6 +1315,7 @@ export default {
memberNotFound: 'Member not found. To invite a new member to the workspace, please use the Invite button above.',
notAuthorized: `You do not have access to this page. Are you trying to join the workspace? Please reach out to the owner of this workspace so they can add you as a member! Something else? Reach out to ${CONST.EMAIL.CONCIERGE}`,
goToRoom: ({roomName}: GoToRoomParams) => `Go to ${roomName} room`,
workspaceAvatar: 'Workspace avatar',
},
emptyWorkspace: {
title: 'Create a new workspace',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,8 @@ export default {
uploadPhoto: 'Subir foto',
removePhoto: 'Eliminar foto',
editImage: 'Editar foto',
viewPhoto: 'Ver foto',
imageUploadFailed: 'Error al cargar la imagen',
deleteWorkspaceError: 'Lo sentimos, hubo un problema eliminando el avatar de su espacio de trabajo.',
sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `La imagen supera el tamaño máximo de ${maxUploadSizeInMB}MB.`,
resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}: ResolutionConstraintsParams) =>
Expand All @@ -585,6 +587,7 @@ export default {
online: 'En línea',
offline: 'Desconectado',
syncing: 'Sincronizando',
profileAvatar: 'Perfil avatar',
},
loungeAccessPage: {
loungeAccess: 'Acceso a la sala vip',
Expand Down Expand Up @@ -1331,6 +1334,7 @@ export default {
memberNotFound: 'Miembro no encontrado. Para invitar a un nuevo miembro al espacio de trabajo, por favor, utiliza el botón Invitar que está arriba.',
notAuthorized: `No tienes acceso a esta página. ¿Estás tratando de unirte al espacio de trabajo? Comunícate con el propietario de este espacio de trabajo para que pueda añadirte como miembro. ¿Necesitas algo más? Comunícate con ${CONST.EMAIL.CONCIERGE}`,
goToRoom: ({roomName}: GoToRoomParams) => `Ir a la sala ${roomName}`,
workspaceAvatar: 'Espacio de trabajo avatar',
},
emptyWorkspace: {
title: 'Crear un nuevo espacio de trabajo',
Expand Down
1 change: 1 addition & 0 deletions src/libs/actions/Policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ function updateWorkspaceAvatar(policyID, file) {
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
avatar: file.uri,
originalFileName: file.name,
errorFields: {
avatar: null,
},
Expand Down
7 changes: 6 additions & 1 deletion src/pages/settings/Profile/ProfilePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ function ProfilePage(props) {
};
const currentUserDetails = props.currentUserPersonalDetails || {};
const contactMethodBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(props.loginList);
const avatarURL = lodashGet(currentUserDetails, 'avatar', '');
const accountID = lodashGet(currentUserDetails, 'accountID', '');
const emojiCode = lodashGet(props, 'currentUserPersonalDetails.status.emojiCode', '');

const profileSettingsOptions = [
Expand Down Expand Up @@ -113,7 +115,7 @@ function ProfilePage(props) {
<ScrollView>
<AvatarWithImagePicker
isUsingDefaultAvatar={UserUtils.isDefaultAvatar(lodashGet(currentUserDetails, 'avatar', ''))}
source={UserUtils.getAvatar(lodashGet(currentUserDetails, 'avatar', ''), lodashGet(currentUserDetails, 'accountID', ''))}
source={UserUtils.getAvatar(avatarURL, accountID)}
onImageSelected={PersonalDetails.updateAvatar}
onImageRemoved={PersonalDetails.deleteAvatar}
anchorPosition={styles.createMenuPositionProfile(props.windowWidth)}
Expand All @@ -123,6 +125,9 @@ function ProfilePage(props) {
errors={lodashGet(props.currentUserPersonalDetails, 'errorFields.avatar', null)}
errorRowStyles={[styles.mt6]}
onErrorClose={PersonalDetails.clearAvatarErrors}
previewSource={UserUtils.getFullSizeAvatar(avatarURL, accountID)}
originalFileName={currentUserDetails.originalFileName}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Puneet-here, @rushatgabhane This line is confusing, does the personal details have an originalFileName prop? the server does not seem to return this prop and it's causing this bug #29647 .

headerTitle={props.translate('profilePage.profileAvatar')}
style={[styles.mh5]}
/>
<View style={[styles.mt4]}>
Expand Down
4 changes: 4 additions & 0 deletions src/pages/workspace/WorkspaceSettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {withNetwork} from '../../components/OnyxProvider';
import OfflineWithFeedback from '../../components/OfflineWithFeedback';
import Form from '../../components/Form';
import * as ReportUtils from '../../libs/ReportUtils';
import * as UserUtils from '../../libs/UserUtils';
import Avatar from '../../components/Avatar';
import Navigation from '../../libs/Navigation/Navigation';
import ROUTES from '../../ROUTES';
Expand Down Expand Up @@ -125,6 +126,9 @@ function WorkspaceSettingsPage(props) {
pendingAction={lodashGet(props.policy, 'pendingFields.avatar', null)}
errors={lodashGet(props.policy, 'errorFields.avatar', null)}
onErrorClose={() => Policy.clearAvatarErrors(props.policy.id)}
previewSource={UserUtils.getFullSizeAvatar(props.policy.avatar, '')}
headerTitle={props.translate('workspace.common.workspaceAvatar')}
originalFileName={props.policy.originalFileName}
/>
<OfflineWithFeedback pendingAction={lodashGet(props.policy, 'pendingFields.generalSettings')}>
<TextInput
Expand Down
Loading