mirror of https://gitlab.com/ecentrics/concordia
				
				
			
				 7 changed files with 352 additions and 1 deletions
			
			
		| @ -0,0 +1,46 @@ | |||||
|  | import React, { useMemo } from 'react'; | ||||
|  | import PropTypes from 'prop-types'; | ||||
|  | import { | ||||
|  |   Dimmer, Loader, Placeholder, Segment, Tab, | ||||
|  | } from 'semantic-ui-react'; | ||||
|  | import { useTranslation } from 'react-i18next'; | ||||
|  | 
 | ||||
|  | const CustomLoadingTabPane = (props) => { | ||||
|  |   const { loading, loadingMessage, children } = props; | ||||
|  |   const { t } = useTranslation(); | ||||
|  | 
 | ||||
|  |   return useMemo(() => { | ||||
|  |     if (loading) { | ||||
|  |       return ( | ||||
|  |           <Tab.Pane> | ||||
|  |               <Dimmer active inverted> | ||||
|  |                   <Loader inverted> | ||||
|  |                       {loadingMessage !== undefined | ||||
|  |                         ? loadingMessage | ||||
|  |                         : t('custom.loading.tab.pane.default.generic.message')} | ||||
|  |                   </Loader> | ||||
|  |               </Dimmer> | ||||
|  |               <Placeholder fluid> | ||||
|  |                   <Placeholder.Line length="very long" /> | ||||
|  |                   <Placeholder.Line length="medium" /> | ||||
|  |                   <Placeholder.Line length="long" /> | ||||
|  |               </Placeholder> | ||||
|  |           </Tab.Pane> | ||||
|  |       ); | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     return ( | ||||
|  |         <Tab.Pane> | ||||
|  |             {children} | ||||
|  |         </Tab.Pane> | ||||
|  |     ); | ||||
|  |   }, [children, loading, loadingMessage, t]); | ||||
|  | }; | ||||
|  | 
 | ||||
|  | CustomLoadingTabPane.propTypes = { | ||||
|  |   loading: PropTypes.bool, | ||||
|  |   loadingMessage: PropTypes.string, | ||||
|  |   children: PropTypes.element.isRequired, | ||||
|  | }; | ||||
|  | 
 | ||||
|  | export default CustomLoadingTabPane; | ||||
| @ -0,0 +1,22 @@ | |||||
|  | export const GENERAL_TAB = { | ||||
|  |   id: 'general-tab', | ||||
|  |   intl_display_name_id: 'profile.general.tab.title', | ||||
|  | }; | ||||
|  | 
 | ||||
|  | export const TOPICS_TAB = { | ||||
|  |   id: 'topics-tab', | ||||
|  |   intl_display_name_id: 'profile.topics.tab.title', | ||||
|  | }; | ||||
|  | 
 | ||||
|  | export const POSTS_TAB = { | ||||
|  |   id: 'posts-tab', | ||||
|  |   intl_display_name_id: 'profile.posts.tab.title', | ||||
|  | }; | ||||
|  | 
 | ||||
|  | const profileTabs = [ | ||||
|  |   GENERAL_TAB, | ||||
|  |   TOPICS_TAB, | ||||
|  |   POSTS_TAB, | ||||
|  | ]; | ||||
|  | 
 | ||||
|  | export default profileTabs; | ||||
| @ -0,0 +1,155 @@ | |||||
|  | import React, { useEffect, useMemo, useState } from 'react'; | ||||
|  | import { | ||||
|  |   Icon, Image, Placeholder, Table, | ||||
|  | } from 'semantic-ui-react'; | ||||
|  | import PropTypes from 'prop-types'; | ||||
|  | import moment from 'moment'; | ||||
|  | import { useDispatch, useSelector } from 'react-redux'; | ||||
|  | import determineKVAddress from '../../../utils/orbitUtils'; | ||||
|  | import databases, { USER_DATABASE } from '../../../constants/OrbitDatabases'; | ||||
|  | import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions'; | ||||
|  | import { breeze } from '../../../redux/store'; | ||||
|  | import { USER_LOCATION, USER_PROFILE_PICTURE } from '../../../constants/UserDatabaseKeys'; | ||||
|  | import './styles.css'; | ||||
|  | 
 | ||||
|  | const { orbit } = breeze; | ||||
|  | 
 | ||||
|  | const GeneralTab = (props) => { | ||||
|  |   const { | ||||
|  |     profileAddress, username, numberOfTopics, numberOfPosts, userRegistrationTimestamp, | ||||
|  |   } = props; | ||||
|  |   const [userInfoOrbitAddress, setUserInfoOrbitAddress] = useState(null); | ||||
|  |   const [userTopicsOrbitAddress, setUserTopicsOrbitAddress] = useState(null); | ||||
|  |   const [userPostsOrbitAddress, setUserPostsOrbitAddress] = useState(null); | ||||
|  |   const [profileMeta, setProfileMeta] = useState(null); | ||||
|  |   const users = useSelector((state) => state.orbitData.users); | ||||
|  |   const dispatch = useDispatch(); | ||||
|  | 
 | ||||
|  |   useEffect(() => { | ||||
|  |     if (profileAddress) { | ||||
|  |       Promise | ||||
|  |         .all(databases | ||||
|  |           .map((database) => determineKVAddress({ | ||||
|  |             orbit, | ||||
|  |             dbName: database.address, | ||||
|  |             userAddress: profileAddress, | ||||
|  |           }))) | ||||
|  |         .then((values) => { | ||||
|  |           const [userOrbitAddress, topicsOrbitAddress, postsOrbitAddress] = values; | ||||
|  |           setUserInfoOrbitAddress(userOrbitAddress); | ||||
|  |           setUserTopicsOrbitAddress(topicsOrbitAddress); | ||||
|  |           setUserPostsOrbitAddress(postsOrbitAddress); | ||||
|  | 
 | ||||
|  |           const userFound = users | ||||
|  |             .find((user) => user.id === userOrbitAddress); | ||||
|  | 
 | ||||
|  |           if (userFound) { | ||||
|  |             setProfileMeta(userFound); | ||||
|  |           } else { | ||||
|  |             dispatch({ | ||||
|  |               type: FETCH_USER_DATABASE, | ||||
|  |               orbit, | ||||
|  |               dbName: USER_DATABASE, | ||||
|  |               userAddress: userOrbitAddress, | ||||
|  |             }); | ||||
|  |           } | ||||
|  |         }).catch((error) => { | ||||
|  |           console.error('Error during determination of key-value DB address:', error); | ||||
|  |         }); | ||||
|  |     } | ||||
|  |   }, [dispatch, profileAddress, users]); | ||||
|  | 
 | ||||
|  |   const authorAvatar = useMemo(() => (profileMeta !== null && profileMeta[USER_PROFILE_PICTURE] | ||||
|  |     ? ( | ||||
|  |         <Image | ||||
|  |           className="general-tab-profile-picture" | ||||
|  |           centered | ||||
|  |           size="tiny" | ||||
|  |           src={profileMeta[USER_PROFILE_PICTURE]} | ||||
|  |         /> | ||||
|  |     ) | ||||
|  |     : ( | ||||
|  |         <Icon | ||||
|  |           name="user circle" | ||||
|  |           size="massive" | ||||
|  |           inverted | ||||
|  |           color="black" | ||||
|  |           verticalAlign="middle" | ||||
|  |         /> | ||||
|  |     )), [profileMeta]); | ||||
|  | 
 | ||||
|  |   return useMemo(() => ( | ||||
|  |       <Table basic="very" singleLine> | ||||
|  |           <Table.Body> | ||||
|  |               <Table.Row textAlign="center"> | ||||
|  |                   <Table.Cell colSpan="3">{authorAvatar}</Table.Cell> | ||||
|  |               </Table.Row> | ||||
|  |               <Table.Row> | ||||
|  |                   <Table.Cell><strong>Username:</strong></Table.Cell> | ||||
|  |                   <Table.Cell>{username}</Table.Cell> | ||||
|  |               </Table.Row> | ||||
|  |               <Table.Row> | ||||
|  |                   <Table.Cell><strong>Account address:</strong></Table.Cell> | ||||
|  |                   <Table.Cell>{profileAddress}</Table.Cell> | ||||
|  |               </Table.Row> | ||||
|  |               <Table.Row> | ||||
|  |                   <Table.Cell><strong>UserDB:</strong></Table.Cell> | ||||
|  |                   <Table.Cell> | ||||
|  |                       {userInfoOrbitAddress || (<Placeholder><Placeholder.Line /></Placeholder>)} | ||||
|  |                   </Table.Cell> | ||||
|  |               </Table.Row> | ||||
|  |               <Table.Row> | ||||
|  |                   <Table.Cell><strong>TopicsDB:</strong></Table.Cell> | ||||
|  |                   <Table.Cell> | ||||
|  |                       {userTopicsOrbitAddress || (<Placeholder><Placeholder.Line /></Placeholder>)} | ||||
|  |                   </Table.Cell> | ||||
|  |               </Table.Row> | ||||
|  |               <Table.Row> | ||||
|  |                   <Table.Cell><strong>PostsDB:</strong></Table.Cell> | ||||
|  |                   <Table.Cell> | ||||
|  |                       {userPostsOrbitAddress || (<Placeholder><Placeholder.Line /></Placeholder>)} | ||||
|  |                   </Table.Cell> | ||||
|  |               </Table.Row> | ||||
|  |               <Table.Row> | ||||
|  |                   <Table.Cell><strong>Number of topics created:</strong></Table.Cell> | ||||
|  |                   <Table.Cell> | ||||
|  |                       {numberOfTopics} | ||||
|  |                   </Table.Cell> | ||||
|  |               </Table.Row> | ||||
|  |               <Table.Row> | ||||
|  |                   <Table.Cell><strong>Number of posts:</strong></Table.Cell> | ||||
|  |                   <Table.Cell> | ||||
|  |                       {numberOfPosts} | ||||
|  |                   </Table.Cell> | ||||
|  |               </Table.Row> | ||||
|  |               <Table.Row> | ||||
|  |                   <Table.Cell><strong>Number of posts:</strong></Table.Cell> | ||||
|  |                   <Table.Cell> | ||||
|  |                       {profileMeta !== null && profileMeta[USER_LOCATION] | ||||
|  |                         ? profileMeta[USER_LOCATION] | ||||
|  |                         : <Placeholder><Placeholder.Line length="medium" /></Placeholder>} | ||||
|  |                   </Table.Cell> | ||||
|  |               </Table.Row> | ||||
|  |               <Table.Row> | ||||
|  |                   <Table.Cell><strong>Member since:</strong></Table.Cell> | ||||
|  |                   <Table.Cell> | ||||
|  |                       {moment(userRegistrationTimestamp * 1000).format('dddd, MMMM Do YYYY, h:mm:ss A')} | ||||
|  |                   </Table.Cell> | ||||
|  |               </Table.Row> | ||||
|  |           </Table.Body> | ||||
|  |       </Table> | ||||
|  |   ), [ | ||||
|  |     authorAvatar, numberOfPosts, numberOfTopics, profileAddress, profileMeta, userInfoOrbitAddress, | ||||
|  |     userPostsOrbitAddress, userRegistrationTimestamp, userTopicsOrbitAddress, username, | ||||
|  |   ]); | ||||
|  | }; | ||||
|  | 
 | ||||
|  | GeneralTab.propTypes = { | ||||
|  |   profileAddress: PropTypes.string.isRequired, | ||||
|  |   username: PropTypes.string.isRequired, | ||||
|  |   numberOfTopics: PropTypes.number.isRequired, | ||||
|  |   numberOfPosts: PropTypes.number.isRequired, | ||||
|  |   userRegistrationTimestamp: PropTypes.string.isRequired, | ||||
|  | }; | ||||
|  | 
 | ||||
|  | export default GeneralTab; | ||||
| @ -0,0 +1,6 @@ | |||||
|  | .general-tab-profile-picture { | ||||
|  |     max-width: 112px; | ||||
|  |     max-height: 112px; | ||||
|  |     margin: 0; | ||||
|  |     vertical-align: middle; | ||||
|  | } | ||||
| @ -0,0 +1,111 @@ | |||||
|  | import React, { | ||||
|  |   memo, useEffect, useMemo, useState, | ||||
|  | } from 'react'; | ||||
|  | import { Container, Header, Tab } from 'semantic-ui-react'; | ||||
|  | import { useSelector } from 'react-redux'; | ||||
|  | import { useHistory, useRouteMatch } from 'react-router'; | ||||
|  | import { useTranslation } from 'react-i18next'; | ||||
|  | import { drizzle } from '../../redux/store'; | ||||
|  | import { FORUM_CONTRACT } from '../../constants/ContractNames'; | ||||
|  | import CustomLoadingTabPane from '../../components/CustomLoadingTabPane'; | ||||
|  | import TopicList from '../../components/TopicList'; | ||||
|  | import PostList from '../../components/PostList'; | ||||
|  | import GeneralTab from './GeneralTab'; | ||||
|  | import { GENERAL_TAB, POSTS_TAB, TOPICS_TAB } from '../../constants/ProfileTabs'; | ||||
|  | 
 | ||||
|  | const { contracts: { [FORUM_CONTRACT]: { methods: { getUser } } } } = drizzle; | ||||
|  | 
 | ||||
|  | const Profile = () => { | ||||
|  |   const [userCallHash, setUserCallHash] = useState(''); | ||||
|  |   const getUserResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getUser); | ||||
|  |   const [profileAddress, setProfileAddress] = useState(); | ||||
|  |   const [username, setUsername] = useState(null); | ||||
|  |   const [userTopicIds, setUserTopicIds] = useState([]); | ||||
|  |   const [userPostIds, setUserPostIds] = useState([]); | ||||
|  |   const [userRegistrationTimestamp, setUserRegistrationTimestamp] = useState(null); | ||||
|  |   const [loading, setLoading] = useState(true); | ||||
|  |   const self = useSelector((state) => state.user); | ||||
|  |   const { t } = useTranslation(); | ||||
|  |   const match = useRouteMatch(); | ||||
|  |   const history = useHistory(); | ||||
|  | 
 | ||||
|  |   useEffect(() => { | ||||
|  |     if (history.location.pathname === '/profile') { | ||||
|  |       if (self.hasSignedUp) { | ||||
|  |         setProfileAddress(self.address); | ||||
|  |       } else { | ||||
|  |         history.push('/'); | ||||
|  |       } | ||||
|  |     } else { | ||||
|  |       const { id: userAddress } = match.params; | ||||
|  | 
 | ||||
|  |       setProfileAddress(userAddress); | ||||
|  |     } | ||||
|  |   }, [history, match.params, self.address, self.hasSignedUp]); | ||||
|  | 
 | ||||
|  |   useEffect(() => { | ||||
|  |     if (profileAddress) { | ||||
|  |       setUserCallHash(getUser.cacheCall(profileAddress)); | ||||
|  |     } | ||||
|  |   }, [profileAddress]); | ||||
|  | 
 | ||||
|  |   useEffect(() => { | ||||
|  |     if (getUserResults[userCallHash] !== undefined && getUserResults[userCallHash].value) { | ||||
|  |       const [lUsername, topicIds, postIds, registrationTimestamp] = getUserResults[userCallHash].value; | ||||
|  |       setUsername(lUsername); | ||||
|  |       setUserTopicIds(topicIds.map((userTopicId) => parseInt(userTopicId, 10))); | ||||
|  |       setUserPostIds(postIds.map((userPostId) => parseInt(userPostId, 10))); | ||||
|  |       setUserRegistrationTimestamp(registrationTimestamp); | ||||
|  |       setLoading(false); | ||||
|  |     } | ||||
|  |   }, [getUserResults, userCallHash]); | ||||
|  | 
 | ||||
|  |   const generalTab = useMemo(() => (loading | ||||
|  |     ? null | ||||
|  |     : ( | ||||
|  |         <GeneralTab | ||||
|  |           profileAddress={profileAddress} | ||||
|  |           username={username} | ||||
|  |           numberOfTopics={userTopicIds.length} | ||||
|  |           numberOfPosts={userPostIds.length} | ||||
|  |           userRegistrationTimestamp={userRegistrationTimestamp} | ||||
|  |         /> | ||||
|  |     )), [loading, profileAddress, userPostIds.length, userRegistrationTimestamp, userTopicIds.length, username]); | ||||
|  | 
 | ||||
|  |   const topicsTab = useMemo(() => (userTopicIds.length > 0 | ||||
|  |     ? (<TopicList topicIds={userTopicIds} />) | ||||
|  |     : ( | ||||
|  |         <Header textAlign="center" as="h2"> | ||||
|  |             {t('profile.user.has.no.topics.header.message', { user: username })} | ||||
|  |         </Header> | ||||
|  |     ) | ||||
|  |   ), [t, userTopicIds, username]); | ||||
|  | 
 | ||||
|  |   const postsTab = useMemo(() => (userPostIds.length > 0 | ||||
|  |     ? (<PostList postIds={userPostIds} />) | ||||
|  |     : ( | ||||
|  |         <Header textAlign="center" as="h2"> | ||||
|  |             {t('profile.user.has.no.posts.header.message', { user: username })} | ||||
|  |         </Header> | ||||
|  |     )), [t, userPostIds, username]); | ||||
|  | 
 | ||||
|  |   const panes = useMemo(() => { | ||||
|  |     const generalTabPane = (<CustomLoadingTabPane loading={loading}>{generalTab}</CustomLoadingTabPane>); | ||||
|  |     const topicsTabPane = (<CustomLoadingTabPane loading={loading}>{topicsTab}</CustomLoadingTabPane>); | ||||
|  |     const postsTabPane = (<CustomLoadingTabPane loading={loading}>{postsTab}</CustomLoadingTabPane>); | ||||
|  | 
 | ||||
|  |     return ([ | ||||
|  |       { menuItem: t(GENERAL_TAB.intl_display_name_id), render: () => generalTabPane }, | ||||
|  |       { menuItem: t(TOPICS_TAB.intl_display_name_id), render: () => topicsTabPane }, | ||||
|  |       { menuItem: t(POSTS_TAB.intl_display_name_id), render: () => postsTabPane }, | ||||
|  |     ]); | ||||
|  |   }, [generalTab, loading, postsTab, t, topicsTab]); | ||||
|  | 
 | ||||
|  |   return useMemo(() => ( | ||||
|  |       <Container id="home-container" textAlign="center"> | ||||
|  |           <Tab panes={panes} /> | ||||
|  |       </Container> | ||||
|  |   ), [panes]); | ||||
|  | }; | ||||
|  | 
 | ||||
|  | export default memo(Profile); | ||||
					Loading…
					
					
				
		Reference in new issue