mirror of https://gitlab.com/ecentrics/concordia
Apostolos Fanakis
4 years ago
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