Browse Source

Merge branch 'feature/pagination' into develop

develop
Ezerous 4 years ago
parent
commit
a33fe9e2d6
  1. 22
      packages/concordia-app/src/ErrorBoundary.jsx
  2. 2
      packages/concordia-app/src/components/CustomLoadingTabPane.jsx
  3. 4
      packages/concordia-app/src/components/Pagination/PaginatedTopicList/styles.css
  4. 0
      packages/concordia-app/src/components/PaginationComponent.jsx
  5. 6
      packages/concordia-app/src/components/PostList/PostListRow/index.jsx
  6. 48
      packages/concordia-app/src/components/PostList/index.jsx
  7. 4
      packages/concordia-app/src/components/PostList/styles.css
  8. 18
      packages/concordia-app/src/components/ProfileImage.jsx
  9. 6
      packages/concordia-app/src/components/TopicList/TopicListRow/index.jsx
  10. 15
      packages/concordia-app/src/components/TopicList/index.jsx
  11. 5
      packages/concordia-app/src/index.jsx
  12. 3
      packages/concordia-app/src/layouts/RegisterLayout/styles.css
  13. 67
      packages/concordia-app/src/views/Home/Board/index.jsx
  14. 15
      packages/concordia-app/src/views/Home/Board/styles.css
  15. 39
      packages/concordia-app/src/views/Home/HomeTopicList/index.jsx
  16. 55
      packages/concordia-app/src/views/Home/index.jsx
  17. 16
      packages/concordia-app/src/views/Home/styles.css
  18. 4
      packages/concordia-app/src/views/Profile/GeneralTab/index.jsx
  19. 89
      packages/concordia-app/src/views/Profile/ProfilePostList/index.jsx
  20. 89
      packages/concordia-app/src/views/Profile/ProfileTopicList/index.jsx
  21. 27
      packages/concordia-app/src/views/Profile/index.jsx
  22. 82
      packages/concordia-app/src/views/Topic/TopicView/TopicPostList/index.jsx
  23. 12
      packages/concordia-app/src/views/Topic/TopicView/index.jsx
  24. 1
      packages/concordia-app/src/views/Topic/TopicView/styles.css
  25. 52
      packages/concordia-contracts/contracts/Forum.sol

22
packages/concordia-app/src/ErrorBoundary.jsx

@ -0,0 +1,22 @@
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
const { props: { children }, state: { hasError } } = this;
if (hasError) {
return <h1>Something went wrong.</h1>; // TODO: Make a better "Something went wrong" screen
}
return children;
}
}
export default ErrorBoundary;

2
packages/concordia-app/src/components/CustomLoadingTabPane.jsx

@ -40,7 +40,7 @@ const CustomLoadingTabPane = (props) => {
CustomLoadingTabPane.propTypes = { CustomLoadingTabPane.propTypes = {
loading: PropTypes.bool, loading: PropTypes.bool,
loadingMessage: PropTypes.string, loadingMessage: PropTypes.string,
children: PropTypes.element.isRequired, children: PropTypes.element,
}; };
export default CustomLoadingTabPane; export default CustomLoadingTabPane;

4
packages/concordia-app/src/components/Pagination/PaginatedTopicList/styles.css

@ -1,4 +0,0 @@
#topic-list{
height: auto;
clear: both;
}

0
packages/concordia-app/src/components/Pagination/index.jsx → packages/concordia-app/src/components/PaginationComponent.jsx

6
packages/concordia-app/src/components/PostList/PostListRow/index.jsx

@ -108,9 +108,9 @@ const PostListRow = (props) => {
<Ref innerRef={focusRef}> <Ref innerRef={focusRef}>
<Feed.Label className="post-profile-picture"> <Feed.Label className="post-profile-picture">
<ProfileImage <ProfileImage
topicAuthorAddress={postAuthorAddress} profileAddress={postAuthorAddress}
topicAuthor={postAuthor} profileUsername={postAuthor}
topicAuthorMeta={postAuthorMeta} profileUserMeta={postAuthorMeta}
size="42" size="42"
link link
/> />

48
packages/concordia-app/src/components/PostList/index.jsx

@ -2,41 +2,34 @@ import React, {
useEffect, useMemo, useState, useEffect, useMemo, useState,
} from 'react'; } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { import {
Dimmer, Feed, Loader, Dimmer, Divider, Feed, Loader,
} from 'semantic-ui-react'; } from 'semantic-ui-react';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import PostListRow from './PostListRow';
import { drizzle } from '../../redux/store'; import { drizzle } from '../../redux/store';
import PostListRow from './PostListRow';
import PaginationComponent, { ITEMS_PER_PAGE } from '../PaginationComponent';
import './styles.css';
const { contracts: { [FORUM_CONTRACT]: { methods: { getPost: { cacheCall: getPostChainData } } } } } = drizzle; const { contracts: { [FORUM_CONTRACT]: { methods: { getPost: { cacheCall: getPostChainData } } } } } = drizzle;
const PostList = (props) => { const PostList = (props) => {
const { postIds, loading, focusOnPost } = props; const {
postIds, numberOfItems, onPageChange, loading, focusOnPost,
} = props;
const [pageNumber, setPageNumber] = useState(1);
const [getPostCallHashes, setGetPostCallHashes] = useState([]); const [getPostCallHashes, setGetPostCallHashes] = useState([]);
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
useEffect(() => { useEffect(() => {
if (drizzleInitialized && !drizzleInitializationFailed && !loading) { if (!loading) {
const newPostsFound = postIds setGetPostCallHashes(
.filter((postId) => !getPostCallHashes postIds.map((postId) => ({
.map((getPostCallHash) => getPostCallHash.id)
.includes(postId));
if (newPostsFound.length > 0) {
setGetPostCallHashes([
...getPostCallHashes,
...newPostsFound
.map((postId) => ({
id: postId, id: postId,
hash: getPostChainData(postId), hash: getPostChainData(postId),
})), })),
]); );
}
} }
}, [drizzleInitializationFailed, drizzleInitialized, getPostCallHashes, loading, postIds]); }, [loading, postIds]);
const posts = useMemo(() => { const posts = useMemo(() => {
if (loading) { if (loading) {
@ -49,7 +42,7 @@ const PostList = (props) => {
return ( return (
<PostListRow <PostListRow
id={postId} id={postId}
postIndex={index + 1} postIndex={ITEMS_PER_PAGE * (pageNumber - 1) + index}
key={postId} key={postId}
postCallHash={postHash && postHash.hash} postCallHash={postHash && postHash.hash}
loading={postHash === undefined} loading={postHash === undefined}
@ -57,13 +50,24 @@ const PostList = (props) => {
/> />
); );
}); });
}, [focusOnPost, getPostCallHashes, loading, postIds]); }, [focusOnPost, getPostCallHashes, loading, pageNumber, postIds]);
const handlePageChange = (event, data) => {
setPageNumber(data.activePage);
onPageChange(event, data);
};
return ( return (
<>
<Dimmer.Dimmable as={Feed} blurring dimmed={loading} id="post-list" size="large"> <Dimmer.Dimmable as={Feed} blurring dimmed={loading} id="post-list" size="large">
<Loader active={loading} /> <Loader active={loading} />
{posts} {posts}
</Dimmer.Dimmable> </Dimmer.Dimmable>
<Divider />
<div id="post-list-pagination">
<PaginationComponent onPageChange={handlePageChange} numberOfItems={numberOfItems} />
</div>
</>
); );
}; };

4
packages/concordia-app/src/components/PostList/styles.css

@ -1,3 +1,7 @@
#post-list{ #post-list{
height: 100%; height: 100%;
} }
#post-list-pagination{
padding: 1rem 0;
}

18
packages/concordia-app/src/components/ProfileImage.jsx

@ -6,7 +6,7 @@ import { USER_PROFILE_PICTURE } from '../constants/orbit/UserDatabaseKeys';
const ProfileImage = (props) => { const ProfileImage = (props) => {
const { const {
topicAuthorAddress, topicAuthor, topicAuthorMeta, avatarUrl, size, link, profileAddress, profileUsername, topicAuthorMeta, avatarUrl, size, link,
} = props; } = props;
const stopClickPropagation = (event) => { const stopClickPropagation = (event) => {
@ -20,30 +20,30 @@ const ProfileImage = (props) => {
return ( return (
<Avatar <Avatar
name={topicAuthor} name={profileUsername}
size={size} size={size}
round round
src={profileImageUrl} src={profileImageUrl}
/> />
); );
}, [avatarUrl, size, topicAuthor, topicAuthorMeta]); }, [avatarUrl, size, profileUsername, topicAuthorMeta]);
return useMemo(() => { return useMemo(() => {
if (link && topicAuthorAddress) { if (link && profileAddress) {
return ( return (
<Link to={`/users/${topicAuthorAddress}`} onClick={stopClickPropagation}> <Link to={`/users/${profileAddress}`} onClick={stopClickPropagation}>
{authorAvatar} {authorAvatar}
</Link> </Link>
); );
} }
return authorAvatar; return authorAvatar;
}, [authorAvatar, link, topicAuthorAddress]); }, [authorAvatar, link, profileAddress]);
}; };
ProfileImage.propTypes = { ProfileImage.propTypes = {
topicAuthorAddress: PropTypes.string, profileAddress: PropTypes.string,
topicAuthor: PropTypes.string, profileUsername: PropTypes.string,
topicAuthorMeta: PropTypes.shape({ id: PropTypes.string, profile_picture: PropTypes.string }), profileUserMeta: PropTypes.shape({ id: PropTypes.string, profile_picture: PropTypes.string }),
avatarUrl: PropTypes.string, avatarUrl: PropTypes.string,
size: PropTypes.string, size: PropTypes.string,
link: PropTypes.bool, link: PropTypes.bool,

6
packages/concordia-app/src/components/TopicList/TopicListRow/index.jsx

@ -105,9 +105,9 @@ const TopicListRow = (props) => {
<Grid.Column width={1} className="topic-row-avatar"> <Grid.Column width={1} className="topic-row-avatar">
<Item> <Item>
<ProfileImage <ProfileImage
topicAuthorAddress={topicAuthorAddress} profileAddress={topicAuthorAddress}
topicAuthor={topicAuthor} profileUsername={topicAuthor}
topicAuthorMeta={topicAuthorMeta} profileUserMeta={topicAuthorMeta}
size="65" size="65"
link link
/> />

15
packages/concordia-app/src/components/TopicList/index.jsx

@ -2,23 +2,20 @@ import React, {
useEffect, useMemo, useState, useEffect, useMemo, useState,
} from 'react'; } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { List } from 'semantic-ui-react'; import { List } from 'semantic-ui-react';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import TopicListRow from './TopicListRow'; import TopicListRow from './TopicListRow';
import PaginationComponent from '../PaginationComponent';
import { drizzle } from '../../redux/store'; import { drizzle } from '../../redux/store';
import './styles.css'; import './styles.css';
const { contracts: { [FORUM_CONTRACT]: { methods: { getTopic: { cacheCall: getTopicChainData } } } } } = drizzle; const { contracts: { [FORUM_CONTRACT]: { methods: { getTopic: { cacheCall: getTopicChainData } } } } } = drizzle;
const TopicList = (props) => { const TopicList = (props) => {
const { topicIds } = props; const { topicIds, numberOfItems, onPageChange } = props;
const [getTopicCallHashes, setGetTopicCallHashes] = useState([]); const [getTopicCallHashes, setGetTopicCallHashes] = useState([]);
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
useEffect(() => { useEffect(() => {
if (drizzleInitialized && !drizzleInitializationFailed) {
setGetTopicCallHashes( setGetTopicCallHashes(
topicIds topicIds
.map((topicId) => ({ .map((topicId) => ({
@ -26,8 +23,7 @@ const TopicList = (props) => {
hash: getTopicChainData(topicId), hash: getTopicChainData(topicId),
})), })),
); );
} }, [topicIds]);
}, [drizzleInitializationFailed, drizzleInitialized, topicIds]);
const topics = useMemo(() => topicIds const topics = useMemo(() => topicIds
.map((topicId) => { .map((topicId) => {
@ -44,14 +40,19 @@ const TopicList = (props) => {
}), [getTopicCallHashes, topicIds]); }), [getTopicCallHashes, topicIds]);
return ( return (
<div>
<List id="topic-list" size="big"> <List id="topic-list" size="big">
{topics} {topics}
</List> </List>
<PaginationComponent onPageChange={onPageChange} numberOfItems={numberOfItems} />
</div>
); );
}; };
TopicList.propTypes = { TopicList.propTypes = {
topicIds: PropTypes.arrayOf(PropTypes.number).isRequired, topicIds: PropTypes.arrayOf(PropTypes.number).isRequired,
numberOfItems: PropTypes.number.isRequired,
onPageChange: PropTypes.func,
}; };
export default TopicList; export default TopicList;

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

@ -3,15 +3,18 @@ import './utils/wdyr';
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import App from './App'; import App from './App';
import ErrorBoundary from './ErrorBoundary';
import store from './redux/store'; import store from './redux/store';
import * as serviceWorker from './utils/serviceWorker'; import * as serviceWorker from './utils/serviceWorker';
import LoadingScreen from './components/LoadingScreen'; import LoadingScreen from './components/LoadingScreen';
import './assets/css/index.css'; import './assets/css/index.css';
render( render(
<ErrorBoundary>
<Suspense fallback={<LoadingScreen />}> <Suspense fallback={<LoadingScreen />}>
<App store={store} /> <App store={store} />
</Suspense>, </Suspense>
</ErrorBoundary>,
document.getElementById('root'), document.getElementById('root'),
); );

3
packages/concordia-app/src/layouts/RegisterLayout/styles.css

@ -1,7 +1,8 @@
.particles { .particles {
position: fixed; position: fixed;
right: 0; top: 0;
bottom: 0; bottom: 0;
right: 0;
left: 0; left: 0;
background: var(--secondary-color); background: var(--secondary-color);
} }

67
packages/concordia-app/src/views/Home/Board/index.jsx

@ -1,67 +0,0 @@
import React, { useMemo } from 'react';
import { Button, Header } from 'semantic-ui-react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import PaginatedTopicList from '../../../components/Pagination/PaginatedTopicList';
import './styles.css';
const Board = (props) => {
const { numberOfTopics } = props;
const hasSignedUp = useSelector((state) => state.user.hasSignedUp);
const history = useHistory();
const { t } = useTranslation();
const boardContents = useMemo(() => (
<>
{hasSignedUp
? (
<Button
id="new-topic-button"
className="primary-button"
content="New Topic"
icon="plus"
labelPosition="left"
positive
onClick={() => history.push('/topics/new')}
/>
)
: null}
{/* eslint-disable-next-line no-nested-ternary */}
{numberOfTopics > 0
// eslint-disable-next-line react/jsx-no-undef
? (<PaginatedTopicList />)
: (!hasSignedUp
? (
<div id="no-topic-message" className="vertical-center-in-parent unselectable">
<Header textAlign="center" as="h2">
{t('board.header.no.topics.message')}
</Header>
<Header textAlign="center" as="h3">
{t('board.sub.header.no.topics.guest')}
</Header>
</div>
)
: (
<div id="no-topic-message" className="vertical-center-in-parent unselectable">
<Header textAlign="center" as="h2">
{t('board.header.no.topics.message')}
</Header>
<Header textAlign="center" as="h3">
{t('board.sub.header.no.topics.user')}
</Header>
</div>
))}
</>
), [numberOfTopics, hasSignedUp, t, history]);
return (boardContents);
};
Board.propTypes = {
numberOfTopics: PropTypes.number.isRequired,
};
export default Board;

15
packages/concordia-app/src/views/Home/Board/styles.css

@ -1,15 +0,0 @@
#no-topic-message {
padding-top: 22rem;
clear: both;
}
#no-topic-message > .header {
color: white;
}
#new-topic-button{
float:right;
margin-top: 1px;
margin-bottom: 2em;
margin-right: 0;
}

39
packages/concordia-app/src/components/Pagination/PaginatedTopicList/index.jsx → packages/concordia-app/src/views/Home/HomeTopicList/index.jsx

@ -5,9 +5,8 @@ import { useSelector } from 'react-redux';
import _ from 'lodash'; import _ from 'lodash';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { drizzle } from '../../../redux/store'; import { drizzle } from '../../../redux/store';
import TopicList from '../../TopicList'; import { ITEMS_PER_PAGE } from '../../../components/PaginationComponent';
import PaginationComponent, { ITEMS_PER_PAGE } from '../index'; import TopicList from '../../../components/TopicList';
import './styles.css';
const { const {
contracts: { contracts: {
@ -19,9 +18,7 @@ const {
}, },
} = drizzle; } = drizzle;
const PaginatedTopicList = () => { const HomeTopicList = () => {
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
const [pageNumber, setPageNumber] = useState(1); const [pageNumber, setPageNumber] = useState(1);
const [numTopics, setNumTopics] = useState(null); const [numTopics, setNumTopics] = useState(null);
const [topicIds, setTopicIds] = useState([]); const [topicIds, setTopicIds] = useState([]);
@ -30,21 +27,21 @@ const PaginatedTopicList = () => {
const numTopicsResult = useSelector((state) => state.contracts[FORUM_CONTRACT].numTopics[numTopicsCallHash]); const numTopicsResult = useSelector((state) => state.contracts[FORUM_CONTRACT].numTopics[numTopicsCallHash]);
useEffect(() => { useEffect(() => {
if (drizzleInitialized && !drizzleInitializationFailed && numTopicsCallHash === null) { if (numTopicsCallHash === null) {
setNumTopicsCallHash(numTopicsChainData()); setNumTopicsCallHash(numTopicsChainData());
} }
}, [drizzleInitializationFailed, drizzleInitialized, numTopicsCallHash]); }, [numTopicsCallHash]);
useEffect(() => { useEffect(() => {
if (drizzleInitialized && !drizzleInitializationFailed && numTopics !== null) { if (numTopics !== null) {
setTopicIds(_.rangeRight(Math.max(numTopics - ITEMS_PER_PAGE * pageNumber, 0), setTopicIds(_.rangeRight(Math.max(numTopics - ITEMS_PER_PAGE * pageNumber, 0),
numTopics - ITEMS_PER_PAGE * (pageNumber - 1))); numTopics - ITEMS_PER_PAGE * (pageNumber - 1)));
} }
}, [pageNumber, drizzleInitializationFailed, drizzleInitialized, numTopics]); }, [numTopics, pageNumber]);
useEffect(() => { useEffect(() => {
if (numTopicsResult) { if (numTopicsResult) {
setNumTopics(numTopicsResult.value); setNumTopics(parseInt(numTopicsResult.value, 10));
} }
}, [numTopicsResult]); }, [numTopicsResult]);
@ -52,12 +49,18 @@ const PaginatedTopicList = () => {
setPageNumber(data.activePage); setPageNumber(data.activePage);
}; };
return useMemo(() => ( return useMemo(() => {
<div id="paginated-topic-list"> if (numTopics !== null) {
<TopicList topicIds={topicIds} /> return (
<PaginationComponent onPageChange={handlePageChange} numberOfItems={numTopics} /> <TopicList
</div> topicIds={topicIds}
), [numTopics, topicIds]); numberOfItems={numTopics}
onPageChange={handlePageChange}
/>
);
}
return null;
}, [numTopics, topicIds]);
}; };
export default PaginatedTopicList; export default HomeTopicList;

55
packages/concordia-app/src/views/Home/index.jsx

@ -1,10 +1,12 @@
import React, { import React, {
memo, useEffect, useMemo, useState, memo, useEffect, useMemo, useState,
} from 'react'; } from 'react';
import { Container } from 'semantic-ui-react'; import { useHistory } from 'react-router';
import { useTranslation } from 'react-i18next';
import { Button, Container, Header } from 'semantic-ui-react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import Board from './Board'; import HomeScreenTopicList from './HomeTopicList';
import './styles.css'; import './styles.css';
import { drizzle } from '../../redux/store'; import { drizzle } from '../../redux/store';
@ -13,6 +15,9 @@ const { contracts: { [FORUM_CONTRACT]: { methods: { numTopics } } } } = drizzle;
const Home = () => { const Home = () => {
const [numberOfTopicsCallHash, setNumberOfTopicsCallHash] = useState(''); const [numberOfTopicsCallHash, setNumberOfTopicsCallHash] = useState('');
const numTopicsResults = useSelector((state) => state.contracts[FORUM_CONTRACT].numTopics); const numTopicsResults = useSelector((state) => state.contracts[FORUM_CONTRACT].numTopics);
const hasSignedUp = useSelector((state) => state.user.hasSignedUp);
const history = useHistory();
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
setNumberOfTopicsCallHash(numTopics.cacheCall()); setNumberOfTopicsCallHash(numTopics.cacheCall());
@ -25,9 +30,51 @@ const Home = () => {
return useMemo(() => ( return useMemo(() => (
<Container id="home-container" textAlign="center"> <Container id="home-container" textAlign="center">
{numberOfTopics !== null && <Board numberOfTopics={numberOfTopics} />} {numberOfTopics !== null && (
<>
{hasSignedUp
? (
<Button
id="new-topic-button"
className="primary-button"
content="New Topic"
icon="plus"
labelPosition="left"
positive
onClick={() => history.push('/topics/new')}
/>
)
: null}
{/* eslint-disable-next-line no-nested-ternary */}
{numberOfTopics > 0
// eslint-disable-next-line react/jsx-no-undef
? (<HomeScreenTopicList />)
: (!hasSignedUp
? (
<div id="no-topic-message" className="vertical-center-in-parent unselectable">
<Header textAlign="center" as="h2">
{t('board.header.no.topics.message')}
</Header>
<Header textAlign="center" as="h3">
{t('board.sub.header.no.topics.guest')}
</Header>
</div>
)
: (
<div id="no-topic-message" className="vertical-center-in-parent unselectable">
<Header textAlign="center" as="h2">
{t('board.header.no.topics.message')}
</Header>
<Header textAlign="center" as="h3">
{t('board.sub.header.no.topics.user')}
</Header>
</div>
))}
</>
)}
</Container> </Container>
), [numberOfTopics]); ), [numberOfTopics, hasSignedUp, t, history]);
}; };
export default memo(Home); export default memo(Home);

16
packages/concordia-app/src/views/Home/styles.css

@ -1,3 +1,19 @@
#home-container { #home-container {
height: 100%; height: 100%;
} }
#no-topic-message {
padding-top: 22rem;
clear: both;
}
#no-topic-message > .header {
color: white;
}
#new-topic-button{
float:right;
margin-top: 1px;
margin-bottom: 2em;
margin-right: 0;
}

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

@ -108,8 +108,8 @@ const GeneralTab = (props) => {
<Table.Row textAlign="center"> <Table.Row textAlign="center">
<Table.Cell colSpan="3" className="profile-image"> <Table.Cell colSpan="3" className="profile-image">
<ProfileImage <ProfileImage
topicAuthorAddress={profileAddress} profileAddress={profileAddress}
topicAuthor={username} profileUsername={username}
avatarUrl={userAvatarUrl} avatarUrl={userAvatarUrl}
size="160" size="160"
/> />

89
packages/concordia-app/src/views/Profile/ProfilePostList/index.jsx

@ -0,0 +1,89 @@
import React, {
useEffect, useMemo, useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { Header } from 'semantic-ui-react';
import { drizzle } from '../../../redux/store';
import { ITEMS_PER_PAGE } from '../../../components/PaginationComponent';
import PostList from '../../../components/PostList';
const {
contracts: {
[FORUM_CONTRACT]: {
methods: {
getUserPostCount: { cacheCall: getUserPostCountChainData },
getUserPosts: { cacheCall: getUserPostsChainData },
},
},
},
} = drizzle;
const ProfilePostList = (props) => {
const { username, profileAddress } = props;
const [pageNumber, setPageNumber] = useState(1);
const [userPostCount, setUserPostCount] = useState(null);
const [postIds, setPostIds] = useState([]);
const [getUserPostCountCallHash, setGetUserPostCountCallHash] = useState(null);
const [getUserPostsCallHash, setGetUserPostsCallHash] = useState(null);
const getUserPostCountResult = useSelector((state) => state.contracts[FORUM_CONTRACT].getUserPostCount[getUserPostCountCallHash]);
const getUserPostsResult = useSelector((state) => state.contracts[FORUM_CONTRACT].getUserPosts[getUserPostsCallHash]);
const { t } = useTranslation();
useEffect(() => {
if (getUserPostCountCallHash === null) {
setGetUserPostCountCallHash(getUserPostCountChainData(profileAddress));
}
}, [getUserPostCountCallHash, profileAddress]);
useEffect(() => {
if (userPostCount !== null && userPostCount !== 0) {
const startIndex = Math.max(userPostCount - ITEMS_PER_PAGE * pageNumber, 0);
const endIndex = userPostCount - ITEMS_PER_PAGE * (pageNumber - 1) - 1;
setGetUserPostsCallHash(getUserPostsChainData(profileAddress, startIndex, endIndex));
}
}, [pageNumber, profileAddress, userPostCount]);
useEffect(() => {
if (getUserPostCountResult) {
setUserPostCount(parseInt(getUserPostCountResult.value, 10));
}
}, [getUserPostCountResult, userPostCount]);
useEffect(() => {
if (getUserPostsResult) {
setPostIds(getUserPostsResult.value.slice().reverse().map(Number));
}
}, [getUserPostsResult, userPostCount]);
const handlePageChange = (event, data) => {
setPageNumber(data.activePage);
};
return useMemo(() => {
if (postIds.length && postIds.length !== 0) {
return (
<PostList
postIds={postIds}
numberOfItems={userPostCount}
onPageChange={handlePageChange}
/>
);
}
if (userPostCount === 0) {
return (
<Header textAlign="center" as="h2">
{t('profile.user.has.no.posts.header.message', { user: username })}
</Header>
);
}
return null;
}, [t, postIds, userPostCount, username]);
};
export default ProfilePostList;

89
packages/concordia-app/src/views/Profile/ProfileTopicList/index.jsx

@ -0,0 +1,89 @@
import React, {
useEffect, useMemo, useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { Header } from 'semantic-ui-react';
import { drizzle } from '../../../redux/store';
import { ITEMS_PER_PAGE } from '../../../components/PaginationComponent';
import TopicList from '../../../components/TopicList';
const {
contracts: {
[FORUM_CONTRACT]: {
methods: {
getUserTopicCount: { cacheCall: getUserTopicCountChainData },
getUserTopics: { cacheCall: getUserTopicsChainData },
},
},
},
} = drizzle;
const ProfileTopicList = (props) => {
const { username, profileAddress } = props;
const [pageNumber, setPageNumber] = useState(1);
const [userTopicCount, setUserTopicCount] = useState(null);
const [topicIds, setTopicIds] = useState([]);
const [getUserTopicCountCallHash, setGetUserTopicCountCallHash] = useState(null);
const [getUserTopicsCallHash, setGetUserTopicsCallHash] = useState(null);
const getUserTopicCountResult = useSelector((state) => state.contracts[FORUM_CONTRACT].getUserTopicCount[getUserTopicCountCallHash]);
const getUserTopicsResult = useSelector((state) => state.contracts[FORUM_CONTRACT].getUserTopics[getUserTopicsCallHash]);
const { t } = useTranslation();
useEffect(() => {
if (getUserTopicCountCallHash === null) {
setGetUserTopicCountCallHash(getUserTopicCountChainData(profileAddress));
}
}, [getUserTopicCountCallHash, profileAddress]);
useEffect(() => {
if (userTopicCount !== null && userTopicCount !== 0) {
const startIndex = Math.max(userTopicCount - ITEMS_PER_PAGE * pageNumber, 0);
const endIndex = userTopicCount - ITEMS_PER_PAGE * (pageNumber - 1) - 1;
setGetUserTopicsCallHash(getUserTopicsChainData(profileAddress, startIndex, endIndex));
}
}, [pageNumber, profileAddress, userTopicCount]);
useEffect(() => {
if (getUserTopicCountResult) {
setUserTopicCount(parseInt(getUserTopicCountResult.value, 10));
}
}, [getUserTopicCountResult, userTopicCount]);
useEffect(() => {
if (getUserTopicsResult) {
setTopicIds(getUserTopicsResult.value.slice().reverse().map(Number));
}
}, [getUserTopicsResult, userTopicCount]);
const handlePageChange = (event, data) => {
setPageNumber(data.activePage);
};
return useMemo(() => {
if (topicIds.length && topicIds.length !== 0) {
return (
<TopicList
topicIds={topicIds}
numberOfItems={userTopicCount}
onPageChange={handlePageChange}
/>
);
}
if (userTopicCount === 0) {
return (
<Header textAlign="center" as="h2">
{t('profile.user.has.no.topics.header.message', { user: username })}
</Header>
);
}
return null;
}, [t, topicIds, userTopicCount, username]);
};
export default ProfileTopicList;

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

@ -1,15 +1,15 @@
import React, { import React, {
memo, useEffect, useMemo, useState, memo, useEffect, useMemo, useState,
} from 'react'; } from 'react';
import { Container, Header, Tab } from 'semantic-ui-react'; import { Container, Tab } from 'semantic-ui-react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useHistory, useRouteMatch } from 'react-router'; import { useHistory, useRouteMatch } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { drizzle } from '../../redux/store'; import { drizzle } from '../../redux/store';
import CustomLoadingTabPane from '../../components/CustomLoadingTabPane'; import CustomLoadingTabPane from '../../components/CustomLoadingTabPane';
import TopicList from '../../components/TopicList'; import ProfileTopicList from './ProfileTopicList';
import PostList from '../../components/PostList'; import ProfilePostList from './ProfilePostList';
import GeneralTab from './GeneralTab'; import GeneralTab from './GeneralTab';
import { GENERAL_TAB, POSTS_TAB, TOPICS_TAB } from '../../constants/ProfileTabs'; import { GENERAL_TAB, POSTS_TAB, TOPICS_TAB } from '../../constants/ProfileTabs';
import './styles.css'; import './styles.css';
@ -76,22 +76,13 @@ const Profile = () => {
loading, profileAddress, self.address, 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(() => (
? (<TopicList topicIds={userTopicIds} />) <ProfileTopicList username={username} profileAddress={profileAddress} />
: ( ), [profileAddress, username]);
<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 const postsTab = useMemo(() => (
? (<PostList postIds={userPostIds} />) <ProfilePostList username={username} profileAddress={profileAddress} />
: ( ), [profileAddress, username]);
<Header textAlign="center" as="h2">
{t('profile.user.has.no.posts.header.message', { user: username })}
</Header>
)), [t, userPostIds, username]);
const panes = useMemo(() => { const panes = useMemo(() => {
const generalTabPane = (<CustomLoadingTabPane loading={loading}>{generalTab}</CustomLoadingTabPane>); const generalTabPane = (<CustomLoadingTabPane loading={loading}>{generalTab}</CustomLoadingTabPane>);

82
packages/concordia-app/src/views/Topic/TopicView/TopicPostList/index.jsx

@ -0,0 +1,82 @@
import React, {
useEffect, useMemo, useState,
} from 'react';
import { useSelector } from 'react-redux';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import PostList from '../../../../components/PostList';
import { ITEMS_PER_PAGE } from '../../../../components/PaginationComponent';
import { drizzle } from '../../../../redux/store';
const {
contracts: {
[FORUM_CONTRACT]: {
methods: {
getTopicPostCount: { cacheCall: getTopicPostCountChainData },
getTopicPosts: { cacheCall: getTopicPostsChainData },
},
},
},
} = drizzle;
const TopicPostList = (props) => {
const {
topicId, loading, focusOnPost,
} = props;
const [pageNumber, setPageNumber] = useState(1);
const [topicPostCount, setTopicPostCount] = useState(null);
const [postIds, setPostIds] = useState([]);
const [getTopicPostCountCallHash, setGetTopicPostCountCallHash] = useState(null);
const [getTopicPostsCallHash, setGetTopicPostsCallHash] = useState(null);
const getTopicPostCountResult = useSelector((state) => state.contracts[FORUM_CONTRACT].getTopicPostCount[getTopicPostCountCallHash]);
const getTopicPostsResult = useSelector((state) => state.contracts[FORUM_CONTRACT].getTopicPosts[getTopicPostsCallHash]);
useEffect(() => {
if (getTopicPostCountCallHash === null) {
setGetTopicPostCountCallHash(getTopicPostCountChainData(topicId));
}
}, [getTopicPostCountCallHash, topicId]);
useEffect(() => {
if (topicPostCount !== null && topicPostCount !== 0) {
const startIndex = ITEMS_PER_PAGE * (pageNumber - 1);
const endIndex = Math.min(ITEMS_PER_PAGE * pageNumber - 1, topicPostCount - 1);
setGetTopicPostsCallHash(getTopicPostsChainData(topicId, startIndex, endIndex));
}
}, [pageNumber, topicId, topicPostCount]);
useEffect(() => {
if (getTopicPostCountResult) {
setTopicPostCount(parseInt(getTopicPostCountResult.value, 10));
}
}, [getTopicPostCountResult, topicPostCount]);
useEffect(() => {
if (getTopicPostsResult) {
setPostIds(getTopicPostsResult.value.slice().map(Number));
}
}, [getTopicPostsResult, topicPostCount]);
const handlePageChange = (event, data) => {
setPageNumber(data.activePage);
};
return useMemo(() => {
if (postIds.length && postIds.length !== 0) {
return (
<PostList
postIds={postIds}
numberOfItems={topicPostCount}
onPageChange={handlePageChange}
loading={loading}
focusOnPost={focusOnPost}
/>
);
}
return null;
}, [postIds, topicPostCount, loading, focusOnPost]);
};
export default TopicPostList;

12
packages/concordia-app/src/views/Topic/TopicView/index.jsx

@ -11,7 +11,7 @@ import { TOPICS_DATABASE, USER_DATABASE } from 'concordia-shared/src/constants/o
import { breeze, drizzle } from '../../../redux/store'; import { breeze, drizzle } from '../../../redux/store';
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions'; import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions';
import './styles.css'; import './styles.css';
import PostList from '../../../components/PostList'; import TopicPostList from './TopicPostList';
import determineKVAddress from '../../../utils/orbitUtils'; import determineKVAddress from '../../../utils/orbitUtils';
import { TOPIC_SUBJECT } from '../../../constants/orbit/TopicsDatabaseKeys'; import { TOPIC_SUBJECT } from '../../../constants/orbit/TopicsDatabaseKeys';
import PostCreate from '../../../components/PostCreate'; import PostCreate from '../../../components/PostCreate';
@ -24,8 +24,6 @@ const TopicView = (props) => {
topicId, topicAuthorAddress: initialTopicAuthorAddress, topicAuthor: initialTopicAuthor, topicId, topicAuthorAddress: initialTopicAuthorAddress, topicAuthor: initialTopicAuthor,
timestamp: initialTimestamp, postIds: initialPostIds, focusOnPost, timestamp: initialTimestamp, postIds: initialPostIds, focusOnPost,
} = props; } = props;
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
const userAddress = useSelector((state) => state.user.address); const userAddress = useSelector((state) => state.user.address);
const hasSignedUp = useSelector((state) => state.user.hasSignedUp); const hasSignedUp = useSelector((state) => state.user.hasSignedUp);
const getTopicResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getTopic); const getTopicResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getTopic);
@ -47,12 +45,10 @@ const TopicView = (props) => {
|| timestamp === null || timestamp === null
|| postIds === null; || postIds === null;
if (drizzleInitialized && !drizzleInitializationFailed && shouldGetTopicDataFromChain) { if (shouldGetTopicDataFromChain) {
setGetTopicCallHash(getTopicChainData(topicId)); setGetTopicCallHash(getTopicChainData(topicId));
} }
}, [ }, [postIds, timestamp, topicAuthor, topicAuthorAddress, topicId]);
drizzleInitializationFailed, drizzleInitialized, postIds, timestamp, topicAuthor, topicAuthorAddress, topicId,
]);
useEffect(() => { useEffect(() => {
if (getTopicCallHash && getTopicResults && getTopicResults[getTopicCallHash]) { if (getTopicCallHash && getTopicResults && getTopicResults[getTopicCallHash]) {
@ -149,7 +145,7 @@ const TopicView = (props) => {
</div> </div>
<Divider /> <Divider />
</Dimmer.Dimmable> </Dimmer.Dimmable>
<PostList postIds={postIds || []} loading={postIds === null} focusOnPost={focusOnPost} /> <TopicPostList topicId={topicId} loading={postIds === null} focusOnPost={focusOnPost} />
</Segment> </Segment>
{topicSubject !== null && postIds !== null && hasSignedUp && ( {topicSubject !== null && postIds !== null && hasSignedUp && (

1
packages/concordia-app/src/views/Topic/TopicView/styles.css

@ -1,5 +1,6 @@
#topic-container { #topic-container {
height: auto !important; height: auto !important;
text-align: center;
} }
#topic-header { #topic-header {

52
packages/concordia-contracts/contracts/Forum.sol

@ -7,6 +7,7 @@ contract Forum {
string public constant USERNAME_TAKEN = "Username is already taken."; string public constant USERNAME_TAKEN = "Username is already taken.";
string public constant TOPIC_DOES_NOT_EXIST = "Topic doesn't exist."; string public constant TOPIC_DOES_NOT_EXIST = "Topic doesn't exist.";
string public constant POST_DOES_NOT_EXIST = "Post doesn't exist."; string public constant POST_DOES_NOT_EXIST = "Post doesn't exist.";
string public constant INVALID_RANGE = "Invalid range.";
//----------------------------------------USER---------------------------------------- //----------------------------------------USER----------------------------------------
struct User { struct User {
@ -67,14 +68,40 @@ contract Forum {
return false; return false;
} }
function getUserTopics(address userAddress) public view returns (uint[] memory) { function getUserTopics(address userAddress, uint startIndex, uint endIndex) public view returns (uint[] memory) {
require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP); require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].topicIDs; require(startIndex <= endIndex && users[userAddress].topicIDs.length > endIndex, INVALID_RANGE);
uint length = endIndex - startIndex + 1;
uint[] memory userTopics = new uint[](length);
uint counter = 0;
for (uint i = startIndex; i <= endIndex; i++) {
userTopics[counter] = users[userAddress].topicIDs[i];
counter++;
}
return userTopics;
}
function getUserPosts(address userAddress, uint startIndex, uint endIndex) public view returns (uint[] memory) {
require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
require(startIndex <= endIndex && users[userAddress].postIDs.length > endIndex, INVALID_RANGE);
uint length = endIndex - startIndex + 1;
uint[] memory userPosts = new uint[](length);
uint counter = 0;
for (uint i = startIndex; i <= endIndex; i++) {
userPosts[counter] = users[userAddress].postIDs[i];
counter++;
}
return userPosts;
} }
function getUserPosts(address userAddress) public view returns (uint[] memory) { function getUserTopicCount(address userAddress) public view returns (uint) {
require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP); require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].postIDs; return users[userAddress].topicIDs.length;
}
function getUserPostCount(address userAddress) public view returns (uint) {
require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].postIDs.length;
} }
function getUserDateOfRegister(address userAddress) public view returns (uint) { function getUserDateOfRegister(address userAddress) public view returns (uint) {
@ -161,9 +188,22 @@ contract Forum {
); );
} }
function getTopicPosts(uint topicID) public view returns (uint[] memory) { function getTopicPostCount(uint topicID) public view returns (uint) {
require(topicExists(topicID), TOPIC_DOES_NOT_EXIST);
return topics[topicID].postIDs.length;
}
function getTopicPosts(uint topicID, uint startIndex, uint endIndex) public view returns (uint[] memory) {
require(topicExists(topicID), TOPIC_DOES_NOT_EXIST); require(topicExists(topicID), TOPIC_DOES_NOT_EXIST);
return topics[topicID].postIDs; require(startIndex <= endIndex && topics[topicID].postIDs.length > endIndex, INVALID_RANGE);
uint length = endIndex - startIndex + 1;
uint[] memory topicPosts = new uint[](length);
uint counter = 0;
for (uint i = startIndex; i <= endIndex; i++) {
topicPosts[counter] = topics[topicID].postIDs[i];
counter++;
}
return topicPosts;
} }
function getTopicAuthor(uint topicID) public view returns (address) { function getTopicAuthor(uint topicID) public view returns (address) {

Loading…
Cancel
Save