Browse Source

Add edit information modal

develop
Apostolos Fanakis 4 years ago
parent
commit
621e539e4a
  1. 20
      packages/concordia-app/public/locales/en/translation.json
  2. 104
      packages/concordia-app/src/components/UsernameSelector.jsx
  3. 235
      packages/concordia-app/src/views/Profile/GeneralTab/EditInformationModal/index.jsx
  4. 202
      packages/concordia-app/src/views/Profile/GeneralTab/index.jsx
  5. 5
      packages/concordia-app/src/views/Profile/index.jsx
  6. 82
      packages/concordia-app/src/views/Register/SignUpStep/index.jsx

20
packages/concordia-app/public/locales/en/translation.json

@ -3,17 +3,28 @@
"board.sub.header.no.topics.guest": "Sign up and be the first to post.", "board.sub.header.no.topics.guest": "Sign up and be the first to post.",
"board.sub.header.no.topics.user": "Be the first to post.", "board.sub.header.no.topics.user": "Be the first to post.",
"custom.loading.tab.pane.default.generic.message": "Magic in the background", "custom.loading.tab.pane.default.generic.message": "Magic in the background",
"edit.information.modal.form.cancel.button": "Cancel",
"edit.information.modal.form.error.invalid.profile.picture.url.message": "The profile picture URL provided is not valid.",
"edit.information.modal.form.error.message.header": "Form contains errors",
"edit.information.modal.form.location.field.label": "Location",
"edit.information.modal.form.location.field.placeholder": "Location",
"edit.information.modal.form.profile.picture.field.label": "Profile picture URL",
"edit.information.modal.form.profile.picture.field.placeholder": "URL",
"edit.information.modal.form.submit.button": "Submit",
"edit.information.modal.title": "Edit profile information",
"post.create.form.send.button": "Post", "post.create.form.send.button": "Post",
"post.form.content.field.placeholder": "Message", "post.form.content.field.placeholder": "Message",
"post.form.subject.field.placeholder": "Subject", "post.form.subject.field.placeholder": "Subject",
"post.list.row.post.id": "#{{id}}", "post.list.row.post.id": "#{{id}}",
"profile.general.tab.address.row.title": "Account address:", "profile.general.tab.address.row.title": "Account address:",
"profile.general.tab.edit.info.button.title": "Edit information",
"profile.general.tab.location.row.not.set": "Not set", "profile.general.tab.location.row.not.set": "Not set",
"profile.general.tab.location.row.title": "Location:", "profile.general.tab.location.row.title": "Location:",
"profile.general.tab.number.of.posts.row.title": "Number of posts:", "profile.general.tab.number.of.posts.row.title": "Number of posts:",
"profile.general.tab.number.of.topics.row.title": "Number of topics created:", "profile.general.tab.number.of.topics.row.title": "Number of topics created:",
"profile.general.tab.posts.db.address.row.title": "PostsDB:", "profile.general.tab.posts.db.address.row.title": "PostsDB:",
"profile.general.tab.registration.date.row.title": "Member since:", "profile.general.tab.registration.date.row.title": "Member since:",
"profile.general.tab.save.info.button.title": "Save information",
"profile.general.tab.title": "General", "profile.general.tab.title": "General",
"profile.general.tab.topics.db.address.row.title": "TopicsDB:", "profile.general.tab.topics.db.address.row.title": "TopicsDB:",
"profile.general.tab.user.db.address.row.title": "UserDB:", "profile.general.tab.user.db.address.row.title": "UserDB:",
@ -39,10 +50,7 @@
"register.form.sign.up.step.button.submit": "Sign Up", "register.form.sign.up.step.button.submit": "Sign Up",
"register.form.sign.up.step.description": "Create a Concordia account", "register.form.sign.up.step.description": "Create a Concordia account",
"register.form.sign.up.step.error.message.header": "Form contains errors", "register.form.sign.up.step.error.message.header": "Form contains errors",
"register.form.sign.up.step.error.username.taken.message": "The username {{username}} is already taken.",
"register.form.sign.up.step.title": "Sign Up", "register.form.sign.up.step.title": "Sign Up",
"register.form.sign.up.step.username.field.label": "Username",
"register.form.sign.up.step.username.field.placeholder": "Username",
"register.p.account.address": "Account address:", "register.p.account.address": "Account address:",
"topbar.button.create.topic": "Create topic", "topbar.button.create.topic": "Create topic",
"topbar.button.profile": "Profile", "topbar.button.profile": "Profile",
@ -54,5 +62,9 @@
"topic.create.form.subject.field.placeholder": "Subject", "topic.create.form.subject.field.placeholder": "Subject",
"topic.list.row.author": "by {{author}}", "topic.list.row.author": "by {{author}}",
"topic.list.row.number.of.replies": "{{numberOfReplies}} replies", "topic.list.row.number.of.replies": "{{numberOfReplies}} replies",
"topic.list.row.topic.id": "#{{id}}" "topic.list.row.topic.id": "#{{id}}",
"username.selector.error.username.empty.message": "Username is required",
"username.selector.error.username.taken.message": "The username {{username}} is already taken.",
"username.selector.username.field.label": "Username",
"username.selector.username.field.placeholder": "Username"
} }

104
packages/concordia-app/src/components/UsernameSelector.jsx

@ -0,0 +1,104 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import {
Form, Input,
} from 'semantic-ui-react';
import throttle from 'lodash/throttle';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { drizzle } from '../redux/store';
import { FORUM_CONTRACT } from '../constants/contracts/ContractNames';
const { contracts: { [FORUM_CONTRACT]: { methods: { isUserNameTaken } } } } = drizzle;
const UsernameSelector = (props) => {
const {
initialUsername, username, onChangeCallback, onErrorChangeCallback,
} = props;
const isUserNameTakenResults = useSelector((state) => state.contracts[FORUM_CONTRACT].isUserNameTaken);
const { t } = useTranslation();
useEffect(() => {
if (username.length > 0) {
const checkedUsernames = Object
.values(isUserNameTakenResults)
.map((callCompleted) => ({
checkedUsername: callCompleted.args[0],
isTaken: callCompleted.value,
}));
const checkedUsername = checkedUsernames
.find((callCompleted) => callCompleted.checkedUsername === username);
if (checkedUsername && checkedUsername.isTaken && username !== initialUsername) {
onErrorChangeCallback({
usernameChecked: true,
error: true,
errorMessage: t('username.selector.error.username.taken.message', { username }),
});
} else {
onErrorChangeCallback({
usernameChecked: checkedUsername !== undefined,
error: false,
errorMessage: null,
});
}
return;
}
// Username input is empty
if (initialUsername && initialUsername !== '') {
onErrorChangeCallback({
usernameChecked: true,
error: true,
errorMessage: t('username.selector.error.username.empty.message'),
});
} else {
onErrorChangeCallback({
usernameChecked: true,
error: false,
errorMessage: null,
});
}
}, [initialUsername, isUserNameTakenResults, onErrorChangeCallback, t, username, username.length]);
const checkUsernameTaken = useMemo(() => throttle(
(usernameToCheck) => {
isUserNameTaken.cacheCall(usernameToCheck);
}, 200,
), []);
const handleInputChange = useCallback((event, { value }) => {
onChangeCallback(value);
if (value.length > 0) {
checkUsernameTaken(value);
}
}, [checkUsernameTaken, onChangeCallback]);
return (
<Form.Field required>
<label htmlFor="form-field-username-selector">
{t('username.selector.username.field.label')}
</label>
<Input
id="form-field-username-selector"
placeholder={t('username.selector.username.field.placeholder')}
name="usernameInput"
className="form-input"
value={username}
onChange={handleInputChange}
/>
</Form.Field>
);
};
UsernameSelector.propTypes = {
initialUsername: PropTypes.string,
username: PropTypes.string.isRequired,
onChangeCallback: PropTypes.func.isRequired,
onErrorChangeCallback: PropTypes.func.isRequired,
};
export default UsernameSelector;

235
packages/concordia-app/src/views/Profile/GeneralTab/EditInformationModal/index.jsx

@ -0,0 +1,235 @@
import React, {
useCallback, useEffect, useMemo, useState,
} from 'react';
import {
Button, Form, Icon, Image, Input, Message, Modal,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import checkUrlValid from '../../../../utils/urlUtils';
import { USER_LOCATION, USER_PROFILE_PICTURE } from '../../../../constants/orbit/UserDatabaseKeys';
import { USER_DATABASE } from '../../../../constants/orbit/OrbitDatabases';
import { breeze, drizzle } from '../../../../redux/store';
import UsernameSelector from '../../../../components/UsernameSelector';
import { FORUM_CONTRACT } from '../../../../constants/contracts/ContractNames';
const { orbit: { stores } } = breeze;
const { contracts: { [FORUM_CONTRACT]: { methods: { updateUsername } } } } = drizzle;
const EditInformationModal = (props) => {
const {
initialUsername, initialAuthorAvatar, initialUserLocation, open, onSubmit, onCancel,
} = props;
const [usernameInput, setUsernameInput] = useState(initialUsername);
const [usernameChecked, setUsernameChecked] = useState(true);
const [profilePictureInput, setProfilePictureInput] = useState('');
const [profilePictureUrlValid, setProfilePictureUrlValid] = useState(true);
const [locationInput, setLocationInput] = useState('');
const [error, setError] = useState(false);
const [errorMessages, setErrorMessages] = useState([]);
const [usernameError, setUsernameError] = useState(false);
const [usernameErrorMessage, setUsernameErrorMessage] = useState('');
const { t } = useTranslation();
useEffect(() => {
setLocationInput(initialUserLocation || '');
}, [initialUserLocation]);
useEffect(() => {
setProfilePictureInput(initialAuthorAvatar || '');
setProfilePictureUrlValid(initialAuthorAvatar ? checkUrlValid(initialAuthorAvatar) : true);
}, [initialAuthorAvatar]);
useEffect(() => {
let formHasError = false;
const formErrors = [];
if (!profilePictureUrlValid) {
formHasError = true;
formErrors.push(t('edit.information.modal.form.error.invalid.profile.picture.url.message'));
}
setError(formHasError);
setErrorMessages(formErrors);
}, [profilePictureUrlValid, t]);
const handleUsernameChange = (modifiedUsername) => {
setUsernameInput(modifiedUsername);
};
const handleUsernameErrorChange = useCallback(({
usernameChecked: isUsernameChecked,
error: hasUsernameError,
errorMessage,
}) => {
setUsernameChecked(isUsernameChecked);
if (hasUsernameError) {
setUsernameError(true);
setUsernameErrorMessage(errorMessage);
} else {
setUsernameError(false);
}
}, []);
const handleInputChange = useCallback((event, { name, value }) => {
if (name === 'profilePictureInput') {
setProfilePictureInput(value);
if (value.length > 0) {
setProfilePictureUrlValid(checkUrlValid(value));
} else {
setProfilePictureUrlValid(true);
}
}
if (name === 'locationInput') {
setLocationInput(value);
}
}, []);
const profilePicture = useMemo(() => (profilePictureInput.length > 0 && profilePictureUrlValid
? (<Image size="medium" src={profilePictureInput} wrapped />)
: (<Icon name="user circle" size="massive" inverted color="black" />)
), [profilePictureInput, profilePictureUrlValid]);
const handleSubmit = useCallback(() => {
const keyValuesToStore = [];
keyValuesToStore.push({
key: USER_PROFILE_PICTURE,
value: profilePictureInput,
});
keyValuesToStore.push({
key: USER_LOCATION,
value: locationInput,
});
const userDb = Object.values(stores).find((store) => store.dbname === USER_DATABASE);
const promiseArray = keyValuesToStore
.map((keyValueToStore) => {
if (keyValueToStore.value !== '') {
return userDb
.put(keyValueToStore.key, keyValueToStore.value, { pin: true });
}
return userDb.del(keyValueToStore.key);
});
Promise
.all(promiseArray)
.then(() => {
// TODO: display a message
})
.catch((reason) => {
console.log(reason);
});
if (usernameInput !== initialUsername) {
updateUsername.cacheSend(usernameInput);
}
onSubmit();
}, [initialUsername, locationInput, onSubmit, profilePictureInput, usernameInput]);
return useMemo(() => (
<Modal
onClose={onCancel}
open={open}
>
<Modal.Header>{t('edit.information.modal.title')}</Modal.Header>
<Modal.Content image>
{profilePicture}
<Modal.Description>
<Form>
<UsernameSelector
initialUsername={initialUsername}
username={usernameInput}
onChangeCallback={handleUsernameChange}
onErrorChangeCallback={handleUsernameErrorChange}
/>
<Form.Field>
<label htmlFor="form-edit-information-field-profile-picture">
{t('edit.information.modal.form.profile.picture.field.label')}
</label>
<Input
id="form-edit-information-field-profile-picture"
placeholder={t('edit.information.modal.form.profile.picture.field.placeholder')}
name="profilePictureInput"
className="form-input"
value={profilePictureInput}
onChange={handleInputChange}
/>
</Form.Field>
<Form.Field>
<label htmlFor="form-edit-information-field-location">
{t('edit.information.modal.form.location.field.label')}
</label>
<Input
id="form-edit-information-field-location"
placeholder={t('edit.information.modal.form.location.field.placeholder')}
name="locationInput"
className="form-input"
value={locationInput}
onChange={handleInputChange}
/>
</Form.Field>
</Form>
{error === true && (
errorMessages
.map((errorMessage) => (
<Message
error
header={t('edit.information.modal.form.error.message.header')}
content={errorMessage}
/>
))
)}
{usernameError === true && (
<Message
error
header={t('edit.information.modal.form.error.message.header')}
content={usernameErrorMessage}
/>
)}
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button color="black" onClick={onCancel}>
{t('edit.information.modal.form.cancel.button')}
</Button>
<Button
content={t('edit.information.modal.form.submit.button')}
labelPosition="right"
icon="checkmark"
onClick={handleSubmit}
positive
loading={!usernameChecked}
disabled={!usernameChecked || error || usernameError}
/>
</Modal.Actions>
</Modal>
), [
error, errorMessages, handleInputChange, handleSubmit, handleUsernameErrorChange, initialUsername, locationInput,
onCancel, open, profilePicture, profilePictureInput, t, usernameChecked, usernameError, usernameErrorMessage,
usernameInput,
]);
};
EditInformationModal.defaultProps = {
open: false,
};
EditInformationModal.propTypes = {
profileAddress: PropTypes.string.isRequired,
initialUsername: PropTypes.string.isRequired,
initialAuthorAvatar: PropTypes.string,
initialUserLocation: PropTypes.string,
open: PropTypes.bool,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
};
export default EditInformationModal;

202
packages/concordia-app/src/views/Profile/GeneralTab/index.jsx

@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { import {
Icon, Image, Placeholder, Table, Button, Icon, Image, Placeholder, Table,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@ -11,17 +11,21 @@ import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationAct
import { breeze } from '../../../redux/store'; import { breeze } from '../../../redux/store';
import { USER_LOCATION, USER_PROFILE_PICTURE } from '../../../constants/orbit/UserDatabaseKeys'; import { USER_LOCATION, USER_PROFILE_PICTURE } from '../../../constants/orbit/UserDatabaseKeys';
import './styles.css'; import './styles.css';
import EditInformationModal from './EditInformationModal';
const { orbit } = breeze; const { orbit } = breeze;
const GeneralTab = (props) => { const GeneralTab = (props) => {
const { const {
profileAddress, username, numberOfTopics, numberOfPosts, userRegistrationTimestamp, profileAddress, username, numberOfTopics, numberOfPosts, userRegistrationTimestamp, isSelf,
} = props; } = props;
const [userInfoOrbitAddress, setUserInfoOrbitAddress] = useState(null); const [userInfoOrbitAddress, setUserInfoOrbitAddress] = useState(null);
const [userTopicsOrbitAddress, setUserTopicsOrbitAddress] = useState(null); const [userTopicsOrbitAddress, setUserTopicsOrbitAddress] = useState(null);
const [userPostsOrbitAddress, setUserPostsOrbitAddress] = useState(null); const [userPostsOrbitAddress, setUserPostsOrbitAddress] = useState(null);
const [profileMeta, setProfileMeta] = useState(null); const [profileMetadataFetched, setProfileMetadataFetched] = useState(false);
const [userAvatarUrl, setUserAvatarUrl] = useState(null);
const [userLocation, setUserLocation] = useState(null);
const [editingProfileInformation, setEditingProfileInformation] = useState(false);
const users = useSelector((state) => state.orbitData.users); const users = useSelector((state) => state.orbitData.users);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@ -45,7 +49,9 @@ const GeneralTab = (props) => {
.find((user) => user.id === userOrbitAddress); .find((user) => user.id === userOrbitAddress);
if (userFound) { if (userFound) {
setProfileMeta(userFound); setProfileMetadataFetched(true);
setUserAvatarUrl(userFound[USER_PROFILE_PICTURE]);
setUserLocation(userFound[USER_LOCATION]);
} else { } else {
dispatch({ dispatch({
type: FETCH_USER_DATABASE, type: FETCH_USER_DATABASE,
@ -60,13 +66,13 @@ const GeneralTab = (props) => {
} }
}, [dispatch, profileAddress, users]); }, [dispatch, profileAddress, users]);
const authorAvatar = useMemo(() => (profileMeta !== null && profileMeta[USER_PROFILE_PICTURE] const authorAvatar = useMemo(() => (profileMetadataFetched && userAvatarUrl !== null
? ( ? (
<Image <Image
className="general-tab-profile-picture" className="general-tab-profile-picture"
centered centered
size="tiny" size="tiny"
src={profileMeta[USER_PROFILE_PICTURE]} src={userAvatarUrl}
/> />
) )
: ( : (
@ -75,91 +81,143 @@ const GeneralTab = (props) => {
size="massive" size="massive"
inverted inverted
color="black" color="black"
verticalAlign="middle"
/> />
)), [profileMeta]); )), [profileMetadataFetched, userAvatarUrl]);
const userLocation = useMemo(() => { const userLocationCell = useMemo(() => {
if (profileMeta === null) { if (!profileMetadataFetched) {
return ( return (
<Placeholder><Placeholder.Line length="medium" /></Placeholder> <Placeholder><Placeholder.Line length="medium" /></Placeholder>
); );
} if (profileMeta[USER_LOCATION] === undefined) { }
if (!userLocation) {
return <span className="text-secondary">{t('profile.general.tab.location.row.not.set')}</span>; return <span className="text-secondary">{t('profile.general.tab.location.row.not.set')}</span>;
} }
return profileMeta[USER_LOCATION];
}, [profileMeta, t]); return userLocation;
}, [profileMetadataFetched, t, userLocation]);
const handleEditInfoClick = () => {
setEditingProfileInformation(true);
};
const closeEditInformationModal = () => {
setEditingProfileInformation(false);
};
const editInformationModal = useMemo(() => profileMetadataFetched && (
<EditInformationModal
profileAddress={profileAddress}
initialUsername={username}
initialAuthorAvatar={userAvatarUrl}
initialUserLocation={userLocation}
open={editingProfileInformation}
onCancel={closeEditInformationModal}
onSubmit={closeEditInformationModal}
/>
), [editingProfileInformation, profileAddress, profileMetadataFetched, userAvatarUrl, userLocation, username]);
return useMemo(() => ( return useMemo(() => (
<Table basic="very" singleLine> <>
<Table.Body> <Table basic="very" singleLine>
<Table.Row textAlign="center"> <Table.Body>
<Table.Cell colSpan="3">{authorAvatar}</Table.Cell> <Table.Row textAlign="center">
</Table.Row> <Table.Cell colSpan="3">{authorAvatar}</Table.Cell>
<Table.Row> </Table.Row>
<Table.Cell><strong>{t('profile.general.tab.username.row.title')}</strong></Table.Cell> <Table.Row>
<Table.Cell>{username}</Table.Cell> <Table.Cell><strong>{t('profile.general.tab.username.row.title')}</strong></Table.Cell>
</Table.Row> <Table.Cell>{username}</Table.Cell>
<Table.Row> </Table.Row>
<Table.Cell><strong>{t('profile.general.tab.address.row.title')}</strong></Table.Cell> <Table.Row>
<Table.Cell>{profileAddress}</Table.Cell> <Table.Cell><strong>{t('profile.general.tab.address.row.title')}</strong></Table.Cell>
</Table.Row> <Table.Cell>{profileAddress}</Table.Cell>
<Table.Row> </Table.Row>
<Table.Cell><strong>{t('profile.general.tab.user.db.address.row.title')}</strong></Table.Cell> <Table.Row>
<Table.Cell> <Table.Cell><strong>{t('profile.general.tab.user.db.address.row.title')}</strong></Table.Cell>
{userInfoOrbitAddress || (<Placeholder><Placeholder.Line /></Placeholder>)} <Table.Cell>
</Table.Cell> {userInfoOrbitAddress || (<Placeholder><Placeholder.Line /></Placeholder>)}
</Table.Row> </Table.Cell>
<Table.Row> </Table.Row>
<Table.Cell><strong>{t('profile.general.tab.topics.db.address.row.title')}</strong></Table.Cell> <Table.Row>
<Table.Cell> <Table.Cell><strong>{t('profile.general.tab.topics.db.address.row.title')}</strong></Table.Cell>
{userTopicsOrbitAddress || (<Placeholder><Placeholder.Line /></Placeholder>)} <Table.Cell>
</Table.Cell> {userTopicsOrbitAddress || (<Placeholder><Placeholder.Line /></Placeholder>)}
</Table.Row> </Table.Cell>
<Table.Row> </Table.Row>
<Table.Cell><strong>{t('profile.general.tab.posts.db.address.row.title')}</strong></Table.Cell> <Table.Row>
<Table.Cell> <Table.Cell><strong>{t('profile.general.tab.posts.db.address.row.title')}</strong></Table.Cell>
{userPostsOrbitAddress || (<Placeholder><Placeholder.Line /></Placeholder>)} <Table.Cell>
</Table.Cell> {userPostsOrbitAddress || (<Placeholder><Placeholder.Line /></Placeholder>)}
</Table.Row> </Table.Cell>
<Table.Row> </Table.Row>
<Table.Cell><strong>{t('profile.general.tab.number.of.topics.row.title')}</strong></Table.Cell> <Table.Row>
<Table.Cell> <Table.Cell><strong>{t('profile.general.tab.number.of.topics.row.title')}</strong></Table.Cell>
{numberOfTopics} <Table.Cell>
</Table.Cell> {numberOfTopics}
</Table.Row> </Table.Cell>
<Table.Row> </Table.Row>
<Table.Cell><strong>{t('profile.general.tab.number.of.posts.row.title')}</strong></Table.Cell> <Table.Row>
<Table.Cell> <Table.Cell><strong>{t('profile.general.tab.number.of.posts.row.title')}</strong></Table.Cell>
{numberOfPosts} <Table.Cell>
</Table.Cell> {numberOfPosts}
</Table.Row> </Table.Cell>
<Table.Row> </Table.Row>
<Table.Cell><strong>{t('profile.general.tab.location.row.title')}</strong></Table.Cell> <Table.Row>
<Table.Cell> <Table.Cell><strong>{t('profile.general.tab.location.row.title')}</strong></Table.Cell>
{userLocation} <Table.Cell>
</Table.Cell> {userLocationCell}
</Table.Row> </Table.Cell>
<Table.Row> </Table.Row>
<Table.Cell><strong>{t('profile.general.tab.registration.date.row.title')}</strong></Table.Cell> <Table.Row>
<Table.Cell> <Table.Cell><strong>{t('profile.general.tab.registration.date.row.title')}</strong></Table.Cell>
{new Date(userRegistrationTimestamp * 1000).toLocaleString()} <Table.Cell>
</Table.Cell> {new Date(userRegistrationTimestamp * 1000).toLocaleString()}
</Table.Row> </Table.Cell>
</Table.Body> </Table.Row>
</Table> </Table.Body>
{isSelf && (
<Table.Footer fullWidth>
<Table.Row>
<Table.HeaderCell colSpan="2">
<Button
floated="right"
icon
labelPosition="left"
primary
disabled={!profileMetadataFetched}
size="small"
onClick={handleEditInfoClick}
>
<Icon name="edit" />
{t('profile.general.tab.edit.info.button.title')}
</Button>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
)}
</Table>
{isSelf && editInformationModal}
</>
), [ ), [
authorAvatar, numberOfPosts, numberOfTopics, profileAddress, profileMeta, t, userInfoOrbitAddress, authorAvatar, editInformationModal, isSelf, numberOfPosts, numberOfTopics, profileAddress, profileMetadataFetched,
userPostsOrbitAddress, userRegistrationTimestamp, userTopicsOrbitAddress, username, t, userInfoOrbitAddress, userLocationCell, userPostsOrbitAddress, userRegistrationTimestamp, userTopicsOrbitAddress,
username,
]); ]);
}; };
GeneralTab.defaultProps = {
isSelf: false,
};
GeneralTab.propTypes = { GeneralTab.propTypes = {
profileAddress: PropTypes.string.isRequired, profileAddress: PropTypes.string.isRequired,
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
numberOfTopics: PropTypes.number.isRequired, numberOfTopics: PropTypes.number.isRequired,
numberOfPosts: PropTypes.number.isRequired, numberOfPosts: PropTypes.number.isRequired,
userRegistrationTimestamp: PropTypes.string.isRequired, userRegistrationTimestamp: PropTypes.string.isRequired,
isSelf: PropTypes.bool,
}; };
export default GeneralTab; export default GeneralTab;

5
packages/concordia-app/src/views/Profile/index.jsx

@ -69,8 +69,11 @@ const Profile = () => {
numberOfTopics={userTopicIds.length} numberOfTopics={userTopicIds.length}
numberOfPosts={userPostIds.length} numberOfPosts={userPostIds.length}
userRegistrationTimestamp={userRegistrationTimestamp} userRegistrationTimestamp={userRegistrationTimestamp}
isSelf={profileAddress === self.address}
/> />
)), [loading, profileAddress, userPostIds.length, userRegistrationTimestamp, userTopicIds.length, username]); )), [
loading, profileAddress, self.address, userPostIds.length, userRegistrationTimestamp, userTopicIds.length, username,
]);
const topicsTab = useMemo(() => (userTopicIds.length > 0 const topicsTab = useMemo(() => (userTopicIds.length > 0
? (<TopicList topicIds={userTopicIds} />) ? (<TopicList topicIds={userTopicIds} />)

82
packages/concordia-app/src/views/Register/SignUpStep/index.jsx

@ -1,10 +1,7 @@
import React, { import React, { useCallback, useEffect, useState } from 'react';
useCallback, useEffect, useMemo, useState,
} from 'react';
import { import {
Button, Card, Form, Input, Message, Button, Card, Form, Message,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import throttle from 'lodash/throttle';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
@ -12,18 +9,17 @@ import PropTypes from 'prop-types';
import { drizzle } from '../../../redux/store'; import { drizzle } from '../../../redux/store';
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../../constants/TransactionStatus'; import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../../constants/TransactionStatus';
import { FORUM_CONTRACT } from '../../../constants/contracts/ContractNames'; import { FORUM_CONTRACT } from '../../../constants/contracts/ContractNames';
import UsernameSelector from '../../../components/UsernameSelector';
const { contracts: { [FORUM_CONTRACT]: { methods: { isUserNameTaken, signUp } } } } = drizzle; const { contracts: { [FORUM_CONTRACT]: { methods: { signUp } } } } = drizzle;
const SignUpStep = (props) => { const SignUpStep = (props) => {
const { pushNextStep, account } = props; const { pushNextStep, account } = props;
const user = useSelector((state) => state.user); const user = useSelector((state) => state.user);
const isUserNameTakenResults = useSelector((state) => state.contracts[FORUM_CONTRACT].isUserNameTaken);
const transactionStack = useSelector((state) => state.transactionStack); const transactionStack = useSelector((state) => state.transactionStack);
const transactions = useSelector((state) => state.transactions); const transactions = useSelector((state) => state.transactions);
const [usernameInput, setUsernameInput] = useState(''); const [usernameInput, setUsernameInput] = useState('');
const [usernameIsChecked, setUsernameIsChecked] = useState(true); const [usernameIsChecked, setUsernameIsChecked] = useState(true);
const [usernameIsTaken, setUsernameIsTaken] = useState(true);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const [signingUp, setSigningUp] = useState(false); const [signingUp, setSigningUp] = useState(false);
@ -32,31 +28,6 @@ const SignUpStep = (props) => {
const history = useHistory(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => {
if (usernameInput.length > 0) {
const checkedUsernames = Object
.values(isUserNameTakenResults)
.map((callCompleted) => ({
checkedUsername: callCompleted.args[0],
isTaken: callCompleted.value,
}));
const checkedUsername = checkedUsernames
.find((callCompleted) => callCompleted.checkedUsername === usernameInput);
setUsernameIsChecked(checkedUsername !== undefined);
if (checkedUsername && checkedUsername.isTaken) {
setUsernameIsTaken(true);
setError(true);
setErrorMessage(t('register.form.sign.up.step.error.username.taken.message', { username: usernameInput }));
} else {
setUsernameIsTaken(false);
setError(false);
}
}
}, [isUserNameTakenResults, t, usernameInput]);
useEffect(() => { useEffect(() => {
if (signingUp && transactionStack && transactionStack[registerCacheSendStackId] if (signingUp && transactionStack && transactionStack[registerCacheSendStackId]
&& transactions[transactionStack[registerCacheSendStackId]]) { && transactions[transactionStack[registerCacheSendStackId]]) {
@ -69,19 +40,24 @@ const SignUpStep = (props) => {
} }
}, [pushNextStep, registerCacheSendStackId, signingUp, transactionStack, transactions]); }, [pushNextStep, registerCacheSendStackId, signingUp, transactionStack, transactions]);
const checkUsernameTaken = useMemo(() => throttle( const handleUsernameChange = useCallback((modifiedUsername) => {
(username) => { setUsernameInput(modifiedUsername);
isUserNameTaken.cacheCall(username); }, []);
}, 200,
), []);
const handleInputChange = useCallback((event, { value }) => { const handleUsernameErrorChange = useCallback(({
setUsernameInput(value); usernameChecked: isUsernameChecked,
error: hasUsernameError,
errorMessage: usernameErrorMessage,
}) => {
setUsernameIsChecked(isUsernameChecked);
if (value.length > 0) { if (hasUsernameError) {
checkUsernameTaken(value); setError(true);
setErrorMessage(usernameErrorMessage);
} else {
setError(false);
} }
}, [checkUsernameTaken]); }, []);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
if (user.hasSignedUp) { if (user.hasSignedUp) {
@ -99,19 +75,11 @@ const SignUpStep = (props) => {
<Card.Content> <Card.Content>
<Card.Description> <Card.Description>
<Form loading={signingUp}> <Form loading={signingUp}>
<Form.Field required> <UsernameSelector
<label htmlFor="form-register-field-username"> username={usernameInput}
{t('register.form.sign.up.step.username.field.label')} onChangeCallback={handleUsernameChange}
</label> onErrorChangeCallback={handleUsernameErrorChange}
<Input />
id="form-register-field-username"
placeholder={t('register.form.sign.up.step.username.field.placeholder')}
name="usernameInput"
className="form-input"
value={usernameInput}
onChange={handleInputChange}
/>
</Form.Field>
</Form> </Form>
</Card.Description> </Card.Description>
</Card.Content> </Card.Content>
@ -130,7 +98,7 @@ const SignUpStep = (props) => {
floated="right" floated="right"
content={t('register.form.sign.up.step.button.submit')} content={t('register.form.sign.up.step.button.submit')}
onClick={handleSubmit} onClick={handleSubmit}
disabled={usernameIsTaken || signingUp} disabled={error || signingUp || usernameInput.length === 0}
loading={!usernameIsChecked} loading={!usernameIsChecked}
/> />
<Button <Button

Loading…
Cancel
Save