Browse Source

Merge branch 'implement-ui' into purge-button

develop
Apostolos Fanakis 4 years ago
parent
commit
278987c636
  1. 20
      packages/concordia-app/public/locales/en/translation.json
  2. 1
      packages/concordia-app/src/assets/images/metamask_logo.svg
  3. 6
      packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx
  4. 4
      packages/concordia-app/src/components/InitializationScreen/index.jsx
  5. 104
      packages/concordia-app/src/components/UsernameSelector.jsx
  6. 235
      packages/concordia-app/src/views/Profile/GeneralTab/EditInformationModal/index.jsx
  7. 202
      packages/concordia-app/src/views/Profile/GeneralTab/index.jsx
  8. 5
      packages/concordia-app/src/views/Profile/index.jsx
  9. 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"
} }

1
packages/concordia-app/src/assets/images/metamask_logo.svg

@ -0,0 +1 @@
<svg fill="none" height="33" viewBox="0 0 35 33" width="35" xmlns="http://www.w3.org/2000/svg"><g stroke-linecap="round" stroke-linejoin="round" stroke-width=".25"><path d="m32.9582 1-13.1341 9.7183 2.4424-5.72731z" fill="#e17726" stroke="#e17726"/><g fill="#e27625" stroke="#e27625"><path d="m2.66296 1 13.01714 9.809-2.3254-5.81802z"/><path d="m28.2295 23.5335-3.4947 5.3386 7.4829 2.0603 2.1436-7.2823z"/><path d="m1.27281 23.6501 2.13055 7.2823 7.46994-2.0603-3.48166-5.3386z"/><path d="m10.4706 14.5149-2.0786 3.1358 7.405.3369-.2469-7.969z"/><path d="m25.1505 14.5149-5.1575-4.58704-.1688 8.05974 7.4049-.3369z"/><path d="m10.8733 28.8721 4.4819-2.1639-3.8583-3.0062z"/><path d="m20.2659 26.7082 4.4689 2.1639-.6105-5.1701z"/></g><path d="m24.7348 28.8721-4.469-2.1639.3638 2.9025-.039 1.231z" fill="#d5bfb2" stroke="#d5bfb2"/><path d="m10.8732 28.8721 4.1572 1.9696-.026-1.231.3508-2.9025z" fill="#d5bfb2" stroke="#d5bfb2"/><path d="m15.1084 21.7842-3.7155-1.0884 2.6243-1.2051z" fill="#233447" stroke="#233447"/><path d="m20.5126 21.7842 1.0913-2.2935 2.6372 1.2051z" fill="#233447" stroke="#233447"/><path d="m10.8733 28.8721.6495-5.3386-4.13117.1167z" fill="#cc6228" stroke="#cc6228"/><path d="m24.0982 23.5335.6366 5.3386 3.4946-5.2219z" fill="#cc6228" stroke="#cc6228"/><path d="m27.2291 17.6507-7.405.3369.6885 3.7966 1.0913-2.2935 2.6372 1.2051z" fill="#cc6228" stroke="#cc6228"/><path d="m11.3929 20.6958 2.6242-1.2051 1.0913 2.2935.6885-3.7966-7.40495-.3369z" fill="#cc6228" stroke="#cc6228"/><path d="m8.392 17.6507 3.1049 6.0513-.1039-3.0062z" fill="#e27525" stroke="#e27525"/><path d="m24.2412 20.6958-.1169 3.0062 3.1049-6.0513z" fill="#e27525" stroke="#e27525"/><path d="m15.797 17.9876-.6886 3.7967.8704 4.4833.1949-5.9087z" fill="#e27525" stroke="#e27525"/><path d="m19.8242 17.9876-.3638 2.3584.1819 5.9216.8704-4.4833z" fill="#e27525" stroke="#e27525"/><path d="m20.5127 21.7842-.8704 4.4834.6236.4406 3.8584-3.0062.1169-3.0062z" fill="#f5841f" stroke="#f5841f"/><path d="m11.3929 20.6958.104 3.0062 3.8583 3.0062.6236-.4406-.8704-4.4834z" fill="#f5841f" stroke="#f5841f"/><path d="m20.5906 30.8417.039-1.231-.3378-.2851h-4.9626l-.3248.2851.026 1.231-4.1572-1.9696 1.4551 1.1921 2.9489 2.0344h5.0536l2.962-2.0344 1.442-1.1921z" fill="#c0ac9d" stroke="#c0ac9d"/><path d="m20.2659 26.7082-.6236-.4406h-3.6635l-.6236.4406-.3508 2.9025.3248-.2851h4.9626l.3378.2851z" fill="#161616" stroke="#161616"/><path d="m33.5168 11.3532 1.1043-5.36447-1.6629-4.98873-12.6923 9.3944 4.8846 4.1205 6.8983 2.0085 1.52-1.7752-.6626-.4795 1.0523-.9588-.8054-.622 1.0523-.8034z" fill="#763e1a" stroke="#763e1a"/><path d="m1 5.98873 1.11724 5.36447-.71451.5313 1.06527.8034-.80545.622 1.05228.9588-.66255.4795 1.51997 1.7752 6.89835-2.0085 4.8846-4.1205-12.69233-9.3944z" fill="#763e1a" stroke="#763e1a"/><path d="m32.0489 16.5234-6.8983-2.0085 2.0786 3.1358-3.1049 6.0513 4.1052-.0519h6.1318z" fill="#f5841f" stroke="#f5841f"/><path d="m10.4705 14.5149-6.89828 2.0085-2.29944 7.1267h6.11883l4.10519.0519-3.10487-6.0513z" fill="#f5841f" stroke="#f5841f"/><path d="m19.8241 17.9876.4417-7.5932 2.0007-5.4034h-8.9119l2.0006 5.4034.4417 7.5932.1689 2.3842.013 5.8958h3.6635l.013-5.8958z" fill="#f5841f" stroke="#f5841f"/></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

6
packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx

@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { Container, Progress } from 'semantic-ui-react'; import { Container, Progress } from 'semantic-ui-react';
// Images // Images
import metamaskLogo from '../../../assets/images/metamask_logo.svg';
import ethereumLogo from '../../../assets/images/ethereum_logo.svg'; import ethereumLogo from '../../../assets/images/ethereum_logo.svg';
import ipfsLogo from '../../../assets/images/ipfs_logo.svg'; import ipfsLogo from '../../../assets/images/ipfs_logo.svg';
import orbitdbLogo from '../../../assets/images/orbitdb_logo.svg'; import orbitdbLogo from '../../../assets/images/orbitdb_logo.svg';
@ -20,7 +21,10 @@ const LoadingComponent = (props) => {
let imageSrc; let imageAlt; let listItems; let indicating; let let imageSrc; let imageAlt; let listItems; let indicating; let
error; error;
if (imageType === 'ethereum') { if (imageType === 'metamask') {
imageSrc = metamaskLogo;
imageAlt = 'metamask_logo';
} else if (imageType === 'ethereum') {
imageSrc = ethereumLogo; imageSrc = ethereumLogo;
imageAlt = 'ethereum_logo'; imageAlt = 'ethereum_logo';
} else if (imageType === 'ipfs') { } else if (imageType === 'ipfs') {

4
packages/concordia-app/src/components/InitializationScreen/index.jsx

@ -25,9 +25,9 @@ const InitializationLoader = ({ children }) => {
<CustomLoader <CustomLoader
title="Couldn't detect MetaMask!" title="Couldn't detect MetaMask!"
message={['Please make sure to install ', <a href="https://metamask.io/">MetaMask</a>, ' first.']} message={['Please make sure to install ', <a href="https://metamask.io/">MetaMask</a>, ' first.']}
imageType="ethereum" imageType="metamask"
progress={10} progress={10}
progressType="indicating" progressType="error"
/> />
); );
} }

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