Skip to content

Commit

Permalink
Add account data deletion functionality (#738)
Browse files Browse the repository at this point in the history
Includes refactoring of Avatar Upload functionality to unionize it.
* Add account data deletion functionality with deletion modal
* Add profile deletion notification
* Make remove data component into button and change Settings view
* Reroute to dashboard after delete profile is complete
* Separate component DialogContentUploader from EditProfile
* Break out DialogAvatarUpload from EditProfile
* Make AvatarUploader the universal uploader
* Unionize setAvatarUploadUrl for camera upload and file upload
* Change Dialog component and use it in EditProfile
* Use latest core version with delete profile data functionality
* Add core methods for upload correctly and use in Upload
* Pass avatarUploadUrl to upload components
* Delete old avatar on save new profile
* Delete old unsaved upload upon new unsaved upload in onboarding and edit
* Delete avatar image when deleting user entry in delete profile
* Make upload onboarding continue possible with new component
* Show avatar in next steps after uploading it in onboarding
* Make avatar upload persist when going backwards in onboarding
* Fix loading state for file upload
* Add option to edit profile in profile deletion modal from settings

Other:
* Fix count of unread news in unread activities on dashboard
* Delete old todo note in TabNavigationAction (fixed)
* Skip blue border on avatar hover by default

---------

Co-authored-by: Louise Linné <linne.louise@gmail.com>
  • Loading branch information
mikozet and louilinn authored Nov 23, 2023
1 parent a61ddd9 commit 97ebad1
Show file tree
Hide file tree
Showing 20 changed files with 4,388 additions and 1,049 deletions.
16 changes: 15 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
"optionCamera": "Camera",
"optionUpload": "Upload",
"optionFile": "Gallery",
"titleCancel": "Do you want to save changes to your profile?"
"titleCancel": "Do you want to save your changes to your profile?"
},
"ErrorCodes": {
"CoreErrorInsufficientFunds": "Please send some Circles to this account if you want to perform any transaction",
Expand Down Expand Up @@ -343,6 +343,20 @@
},
"bodyUndeployedToken": "Account not verified"
},
"ButtonDeleteProfile": {
"titleText": "Are you sure you want to delete your profile data?",
"bodyText": "This action will delete your profile picture, username and email from our databases.",
"bodyText2": "However, this action will not delete historical transaction data or trust interactions.",
"bodyText3": "After deletion you can chose to end session through Settings. If you do not log in within 90 days, your UBI payouts will be stopped forever.",
"bodyText4": "Please note that data deletion will not happen across wallets. You will have to do profile data deletion your personal wallet and any shared wallets separately before ending session.",
"btnText": "Delete Profile Data",
"confirmationDelete": "Delete",
"confirmationCancel": "Cancel",
"linkEditProfile": "Edit Profile Instead",
"notificationError": "Oops, something went wrong. Try again!",
"notificationSuccess": "The profile data of this wallet has successfully been deleted. You can end session in Settings.",
"readMore": "Read more"
},
"QRCodeScanner": {
"notificationError": "Could not open QR code scanner: {error}."
},
Expand Down
4,386 changes: 3,725 additions & 661 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"webpack-dev-server": "^3.11.3"
},
"dependencies": {
"@circles/core": "^4.8.0",
"@circles/core": "^4.9.1",
"@circles/timecircles": "^1.0.6",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
Expand Down
25 changes: 15 additions & 10 deletions src/components/ActivityIcon.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,25 @@ const DashboardActivityIcon = () => {
});
});

// Count how many activities we haven't seen yet
const count = CATEGORIES.reduce((acc, category) => {
return (
acc +
categories[category].activities.reduce((itemAcc, activity) => {
return activity.createdAt > lastSeenAt ? itemAcc + 1 : itemAcc;
}, 0)
);
}, 0);

const countNews = news.activities.reduce((itemAcc, activity) => {
return activity.createdAt > lastSeenAt ? itemAcc + 1 : itemAcc;
}, 0);

// Count how many activities we haven't seen yet
const count =
CATEGORIES.reduce((acc, category) => {
/* eslint-disable no-console */
console.log(category);
/* eslint-enable no-console */

return (
acc +
categories[category].activities.reduce((itemAcc, activity) => {
return activity.createdAt > lastSeenAt ? itemAcc + 1 : itemAcc;
}, 0)
);
}, 0) + countNews;

return (
<IconButton
aria-label="Activities"
Expand Down
26 changes: 21 additions & 5 deletions src/components/Avatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const useStyles = makeStyles((theme) => ({
},
circleGrey: {
background: theme.custom.colors.greyHover,
border: `2px solid ${theme.custom.colors.blue100}`,
position: 'absolute',
left: '-1px',
top: '-1px',
Expand All @@ -45,6 +44,9 @@ const useStyles = makeStyles((theme) => ({
opacity: 0,
transition: 'opacity 0.1s ease-in-out',
},
circleBorder: {
border: `2px solid ${theme.custom.colors.blue100}`,
},
isHovered: {
opacity: 1,
},
Expand All @@ -55,6 +57,8 @@ const useStyles = makeStyles((theme) => ({

const Avatar = ({
address,
children,
showIndicatorRing,
size = 'small',
url,
useCache,
Expand All @@ -78,13 +82,23 @@ const Avatar = ({
const sizePixelRing = sizePixelAvatar * ORGANIZATION_RING_MULTIPLIER;
const initials = username.slice(0, 2) === '0x' ? null : username.slice(0, 2);

const backupImage = () => {
if (avatarUrl && initials) {
return initials.toUpperCase();
}
if (address) {
return <Jazzicon address={address} size={sizePixelAvatar} />;
}
return children;
};

return (
<Box
className={classes.avatarContainer}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{isOrganization && (
{(isOrganization || showIndicatorRing) && (
<Box className={classes.organizationIndicator}>
<GroupWalletCircleSVG width={sizePixelRing} />
</Box>
Expand All @@ -94,6 +108,8 @@ const Avatar = ({
<Box
className={clsx(classes.circleGrey, {
[classes.isHovered]: isHovered || withClickEffect,
[classes.circleBorder]:
(isHovered || withClickEffect) && !hidePlusIcon,
})}
style={{
width: sizePixelAvatar + 2,
Expand All @@ -118,17 +134,17 @@ const Avatar = ({
}}
{...props}
>
{avatarUrl && initials
? initials.toUpperCase()
: address && <Jazzicon address={address} size={sizePixelAvatar} />}
{backupImage()}
</MuiAvatar>
</Box>
);
};

Avatar.propTypes = {
address: PropTypes.string,
children: PropTypes.node,
hidePlusIcon: PropTypes.bool,
showIndicatorRing: PropTypes.bool,
size: PropTypes.string,
url: PropTypes.string,
useCache: PropTypes.bool,
Expand Down
3 changes: 3 additions & 0 deletions src/components/AvatarHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const useStyles = makeStyles(() => ({
const AvatarHeader = ({
hideImage,
hidePlusIcon,
url,
username,
useCache = true,
withClickEffect,
Expand Down Expand Up @@ -61,6 +62,7 @@ const AvatarHeader = ({
className={classes.avatarContainer}
hidePlusIcon={hidePlusIcon}
size={'smallXl'}
url={url}
useCache={useCache}
withClickEffect={withClickEffect}
withHoverEffect={withHoverEffect}
Expand All @@ -80,6 +82,7 @@ const AvatarHeader = ({
AvatarHeader.propTypes = {
hideImage: PropTypes.bool,
hidePlusIcon: PropTypes.bool,
url: PropTypes.string,
useCache: PropTypes.bool,
username: PropTypes.string,
withClickEffect: PropTypes.bool,
Expand Down
142 changes: 44 additions & 98 deletions src/components/AvatarUploader.js
Original file line number Diff line number Diff line change
@@ -1,137 +1,83 @@
import { Avatar, Box, CircularProgress, Typography } from '@mui/material';
import { Box, CircularProgress } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import mime from 'mime/lite';
import PropTypes from 'prop-types';
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import React, { Fragment, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';

import GroupWalletCircleSVG from '%/images/organization-indicator.svg';
import core from '~/services/core';
import translate from '~/services/locale';
import notify, { NotificationsTypes } from '~/store/notifications/actions';

const IMAGE_FILE_TYPES = ['jpg', 'jpeg', 'png'];
import Avatar from '~/components/Avatar';
import DialogAvatarUpload from '~/components/DialogAvatarUpload';
import { IconPlus } from '~/styles/icons';

const useStyles = makeStyles((theme) => ({
avatarUpload: {
margin: '0 auto',
width: theme.custom.components.avatarUploader,
height: theme.custom.components.avatarUploader,
color: theme.palette.text.primary,
fontSize: '30px',
fontWeight: theme.typography.fontWeightMedium,
backgroundColor: theme.custom.colors.white,
border: `1px solid ${theme.palette.text.primary}`,
cursor: 'pointer',
position: 'relative',
zIndex: theme.zIndex.layer1,
},
indicatorContainer: {
position: 'absolute',
zIndex: theme.zIndex.layer1,
top: '-2px',
left: 0,
border: `1px solid ${theme.palette.secondary.main}`,
},
avatarUploaderContainer: {
width: theme.custom.components.avatarUploader,
height: theme.custom.components.avatarUploader,
margin: '0 auto',
display: 'inline-flex',
position: 'relative',
flexShrink: 0,
verticalAlign: 'middle',
},
plusIcon: {
fontSize: '16px',
zIndex: theme.zIndex.layer2,
},
}));

const AvatarUploader = ({
handleUpload,
onLoadingChange,
onUpload,
value,
shouldHaveIndicator,
showIndicatorRing,
presetUrl,
}) => {
const classes = useStyles();

const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState(false);
const fileInputElem = useRef();

const handleUploadClick = (event) => {
event.preventDefault();
fileInputElem.current.click();
};
const [isOpenDialogUploadInfo, setIsOpenDialogUploadInfo] = useState(false);
const [avatarUploadUrl, setAvatarUploadUrl] = useState(presetUrl);
const isLoading = useSelector((state) => state.isLoading);

useEffect(() => {
onLoadingChange(isLoading);
}, [isLoading, onLoadingChange]);

const handleChange = async (event) => {
setIsLoading(true);

const { files } = event.target;
if (files.length === 0) {
return;
}

try {
const result = await core.utils.requestAPI({
path: ['uploads', 'avatar'],
method: 'POST',
data: [...files].reduce((acc, file) => {
acc.append('files', file, file.name);
return acc;
}, new FormData()),
});

onUpload(result.data.url);
} catch (error) {
dispatch(
notify({
text: (
<Typography classes={{ root: 'body4_white' }} variant="body4">
{translate('AvatarUploader.errorAvatarUpload')}
</Typography>
),
type: NotificationsTypes.ERROR,
}),
);
}

setIsLoading(false);
};

const fileTypesStr = IMAGE_FILE_TYPES.map((ext) => {
return mime.getType(ext);
}).join(',');

return (
<Fragment>
<Box className={classes.avatarUploaderContainer}>
{shouldHaveIndicator && (
<Box className={classes.indicatorContainer}>
<GroupWalletCircleSVG width={94} />
</Box>
)}
<DialogAvatarUpload
avatarUploadUrl={avatarUploadUrl}
handleClose={() => setIsOpenDialogUploadInfo(false)}
handleUpload={handleUpload}
isOpen={isOpenDialogUploadInfo}
setAvatarUploadUrl={setAvatarUploadUrl}
/>
<Box
className={classes.avatarUploaderContainer}
onClick={() => setIsOpenDialogUploadInfo(true)}
>
<Avatar
className={classes.avatarUpload}
src={isLoading ? null : value}
onClick={handleUploadClick}
showIndicatorRing={showIndicatorRing}
size="medium"
url={avatarUploadUrl}
withClickEffect={isOpenDialogUploadInfo}
withHoverEffect
>
{isLoading ? <CircularProgress /> : '+'}
{isLoading ? (
<CircularProgress />
) : (
<IconPlus className={classes.plusIcon} />
)}
</Avatar>
<input
accept={fileTypesStr}
ref={fileInputElem}
style={{ display: 'none' }}
type="file"
onChange={handleChange}
/>
</Box>
</Fragment>
);
};

AvatarUploader.propTypes = {
handleUpload: PropTypes.func,
onLoadingChange: PropTypes.func.isRequired,
onUpload: PropTypes.func.isRequired,
shouldHaveIndicator: PropTypes.bool,
value: PropTypes.string,
presetUrl: PropTypes.string,
showIndicatorRing: PropTypes.bool,
};

export default AvatarUploader;
Loading

0 comments on commit 97ebad1

Please sign in to comment.