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. 62
      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. 35
      packages/concordia-app/src/components/TopicList/index.jsx
  11. 9
      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. 18
      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 = {
loading: PropTypes.bool,
loadingMessage: PropTypes.string,
children: PropTypes.element.isRequired,
children: PropTypes.element,
};
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}>
<Feed.Label className="post-profile-picture">
<ProfileImage
topicAuthorAddress={postAuthorAddress}
topicAuthor={postAuthor}
topicAuthorMeta={postAuthorMeta}
profileAddress={postAuthorAddress}
profileUsername={postAuthor}
profileUserMeta={postAuthorMeta}
size="42"
link
/>

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

@ -2,41 +2,34 @@ import React, {
useEffect, useMemo, useState,
} from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import {
Dimmer, Feed, Loader,
Dimmer, Divider, Feed, Loader,
} from 'semantic-ui-react';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import PostListRow from './PostListRow';
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 PostList = (props) => {
const { postIds, loading, focusOnPost } = props;
const {
postIds, numberOfItems, onPageChange, loading, focusOnPost,
} = props;
const [pageNumber, setPageNumber] = useState(1);
const [getPostCallHashes, setGetPostCallHashes] = useState([]);
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
useEffect(() => {
if (drizzleInitialized && !drizzleInitializationFailed && !loading) {
const newPostsFound = postIds
.filter((postId) => !getPostCallHashes
.map((getPostCallHash) => getPostCallHash.id)
.includes(postId));
if (newPostsFound.length > 0) {
setGetPostCallHashes([
...getPostCallHashes,
...newPostsFound
.map((postId) => ({
id: postId,
hash: getPostChainData(postId),
})),
]);
}
if (!loading) {
setGetPostCallHashes(
postIds.map((postId) => ({
id: postId,
hash: getPostChainData(postId),
})),
);
}
}, [drizzleInitializationFailed, drizzleInitialized, getPostCallHashes, loading, postIds]);
}, [loading, postIds]);
const posts = useMemo(() => {
if (loading) {
@ -49,7 +42,7 @@ const PostList = (props) => {
return (
<PostListRow
id={postId}
postIndex={index + 1}
postIndex={ITEMS_PER_PAGE * (pageNumber - 1) + index}
key={postId}
postCallHash={postHash && postHash.hash}
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 (
<Dimmer.Dimmable as={Feed} blurring dimmed={loading} id="post-list" size="large">
<Loader active={loading} />
{posts}
</Dimmer.Dimmable>
<>
<Dimmer.Dimmable as={Feed} blurring dimmed={loading} id="post-list" size="large">
<Loader active={loading} />
{posts}
</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{
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 {
topicAuthorAddress, topicAuthor, topicAuthorMeta, avatarUrl, size, link,
profileAddress, profileUsername, topicAuthorMeta, avatarUrl, size, link,
} = props;
const stopClickPropagation = (event) => {
@ -20,30 +20,30 @@ const ProfileImage = (props) => {
return (
<Avatar
name={topicAuthor}
name={profileUsername}
size={size}
round
src={profileImageUrl}
/>
);
}, [avatarUrl, size, topicAuthor, topicAuthorMeta]);
}, [avatarUrl, size, profileUsername, topicAuthorMeta]);
return useMemo(() => {
if (link && topicAuthorAddress) {
if (link && profileAddress) {
return (
<Link to={`/users/${topicAuthorAddress}`} onClick={stopClickPropagation}>
<Link to={`/users/${profileAddress}`} onClick={stopClickPropagation}>
{authorAvatar}
</Link>
);
}
return authorAvatar;
}, [authorAvatar, link, topicAuthorAddress]);
}, [authorAvatar, link, profileAddress]);
};
ProfileImage.propTypes = {
topicAuthorAddress: PropTypes.string,
topicAuthor: PropTypes.string,
topicAuthorMeta: PropTypes.shape({ id: PropTypes.string, profile_picture: PropTypes.string }),
profileAddress: PropTypes.string,
profileUsername: PropTypes.string,
profileUserMeta: PropTypes.shape({ id: PropTypes.string, profile_picture: PropTypes.string }),
avatarUrl: PropTypes.string,
size: PropTypes.string,
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">
<Item>
<ProfileImage
topicAuthorAddress={topicAuthorAddress}
topicAuthor={topicAuthor}
topicAuthorMeta={topicAuthorMeta}
profileAddress={topicAuthorAddress}
profileUsername={topicAuthor}
profileUserMeta={topicAuthorMeta}
size="65"
link
/>

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

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

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

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

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

@ -1,7 +1,8 @@
.particles {
position: fixed;
right: 0;
top: 0;
bottom: 0;
right: 0;
left: 0;
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 { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { drizzle } from '../../../redux/store';
import TopicList from '../../TopicList';
import PaginationComponent, { ITEMS_PER_PAGE } from '../index';
import './styles.css';
import { ITEMS_PER_PAGE } from '../../../components/PaginationComponent';
import TopicList from '../../../components/TopicList';
const {
contracts: {
@ -19,9 +18,7 @@ const {
},
} = drizzle;
const PaginatedTopicList = () => {
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
const HomeTopicList = () => {
const [pageNumber, setPageNumber] = useState(1);
const [numTopics, setNumTopics] = useState(null);
const [topicIds, setTopicIds] = useState([]);
@ -30,21 +27,21 @@ const PaginatedTopicList = () => {
const numTopicsResult = useSelector((state) => state.contracts[FORUM_CONTRACT].numTopics[numTopicsCallHash]);
useEffect(() => {
if (drizzleInitialized && !drizzleInitializationFailed && numTopicsCallHash === null) {
if (numTopicsCallHash === null) {
setNumTopicsCallHash(numTopicsChainData());
}
}, [drizzleInitializationFailed, drizzleInitialized, numTopicsCallHash]);
}, [numTopicsCallHash]);
useEffect(() => {
if (drizzleInitialized && !drizzleInitializationFailed && numTopics !== null) {
if (numTopics !== null) {
setTopicIds(_.rangeRight(Math.max(numTopics - ITEMS_PER_PAGE * pageNumber, 0),
numTopics - ITEMS_PER_PAGE * (pageNumber - 1)));
}
}, [pageNumber, drizzleInitializationFailed, drizzleInitialized, numTopics]);
}, [numTopics, pageNumber]);
useEffect(() => {
if (numTopicsResult) {
setNumTopics(numTopicsResult.value);
setNumTopics(parseInt(numTopicsResult.value, 10));
}
}, [numTopicsResult]);
@ -52,12 +49,18 @@ const PaginatedTopicList = () => {
setPageNumber(data.activePage);
};
return useMemo(() => (
<div id="paginated-topic-list">
<TopicList topicIds={topicIds} />
<PaginationComponent onPageChange={handlePageChange} numberOfItems={numTopics} />
</div>
), [numTopics, topicIds]);
return useMemo(() => {
if (numTopics !== null) {
return (
<TopicList
topicIds={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, {
memo, useEffect, useMemo, useState,
} 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 { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import Board from './Board';
import HomeScreenTopicList from './HomeTopicList';
import './styles.css';
import { drizzle } from '../../redux/store';
@ -13,6 +15,9 @@ const { contracts: { [FORUM_CONTRACT]: { methods: { numTopics } } } } = drizzle;
const Home = () => {
const [numberOfTopicsCallHash, setNumberOfTopicsCallHash] = useState('');
const numTopicsResults = useSelector((state) => state.contracts[FORUM_CONTRACT].numTopics);
const hasSignedUp = useSelector((state) => state.user.hasSignedUp);
const history = useHistory();
const { t } = useTranslation();
useEffect(() => {
setNumberOfTopicsCallHash(numTopics.cacheCall());
@ -25,9 +30,51 @@ const Home = () => {
return useMemo(() => (
<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>
), [numberOfTopics]);
), [numberOfTopics, hasSignedUp, t, history]);
};
export default memo(Home);

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

@ -1,3 +1,19 @@
#home-container {
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.Cell colSpan="3" className="profile-image">
<ProfileImage
topicAuthorAddress={profileAddress}
topicAuthor={username}
profileAddress={profileAddress}
profileUsername={username}
avatarUrl={userAvatarUrl}
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, {
memo, useEffect, useMemo, useState,
} from 'react';
import { Container, Header, Tab } from 'semantic-ui-react';
import { Container, Tab } from 'semantic-ui-react';
import { useSelector } from 'react-redux';
import { useHistory, useRouteMatch } from 'react-router';
import { useTranslation } from 'react-i18next';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { drizzle } from '../../redux/store';
import CustomLoadingTabPane from '../../components/CustomLoadingTabPane';
import TopicList from '../../components/TopicList';
import PostList from '../../components/PostList';
import ProfileTopicList from './ProfileTopicList';
import ProfilePostList from './ProfilePostList';
import GeneralTab from './GeneralTab';
import { GENERAL_TAB, POSTS_TAB, TOPICS_TAB } from '../../constants/ProfileTabs';
import './styles.css';
@ -76,22 +76,13 @@ const Profile = () => {
loading, profileAddress, self.address, 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 topicsTab = useMemo(() => (
<ProfileTopicList username={username} profileAddress={profileAddress} />
), [profileAddress, 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 postsTab = useMemo(() => (
<ProfilePostList username={username} profileAddress={profileAddress} />
), [profileAddress, username]);
const panes = useMemo(() => {
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 { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions';
import './styles.css';
import PostList from '../../../components/PostList';
import TopicPostList from './TopicPostList';
import determineKVAddress from '../../../utils/orbitUtils';
import { TOPIC_SUBJECT } from '../../../constants/orbit/TopicsDatabaseKeys';
import PostCreate from '../../../components/PostCreate';
@ -24,8 +24,6 @@ const TopicView = (props) => {
topicId, topicAuthorAddress: initialTopicAuthorAddress, topicAuthor: initialTopicAuthor,
timestamp: initialTimestamp, postIds: initialPostIds, focusOnPost,
} = props;
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
const userAddress = useSelector((state) => state.user.address);
const hasSignedUp = useSelector((state) => state.user.hasSignedUp);
const getTopicResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getTopic);
@ -47,12 +45,10 @@ const TopicView = (props) => {
|| timestamp === null
|| postIds === null;
if (drizzleInitialized && !drizzleInitializationFailed && shouldGetTopicDataFromChain) {
if (shouldGetTopicDataFromChain) {
setGetTopicCallHash(getTopicChainData(topicId));
}
}, [
drizzleInitializationFailed, drizzleInitialized, postIds, timestamp, topicAuthor, topicAuthorAddress, topicId,
]);
}, [postIds, timestamp, topicAuthor, topicAuthorAddress, topicId]);
useEffect(() => {
if (getTopicCallHash && getTopicResults && getTopicResults[getTopicCallHash]) {
@ -149,7 +145,7 @@ const TopicView = (props) => {
</div>
<Divider />
</Dimmer.Dimmable>
<PostList postIds={postIds || []} loading={postIds === null} focusOnPost={focusOnPost} />
<TopicPostList topicId={topicId} loading={postIds === null} focusOnPost={focusOnPost} />
</Segment>
{topicSubject !== null && postIds !== null && hasSignedUp && (

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

@ -1,5 +1,6 @@
#topic-container {
height: auto !important;
text-align: center;
}
#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 TOPIC_DOES_NOT_EXIST = "Topic doesn't exist.";
string public constant POST_DOES_NOT_EXIST = "Post doesn't exist.";
string public constant INVALID_RANGE = "Invalid range.";
//----------------------------------------USER----------------------------------------
struct User {
@ -67,14 +68,40 @@ contract Forum {
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);
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 getUserTopicCount(address userAddress) public view returns (uint) {
require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].topicIDs.length;
}
function getUserPosts(address userAddress) public view returns (uint[] memory) {
function getUserPostCount(address userAddress) public view returns (uint) {
require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].postIDs;
return users[userAddress].postIDs.length;
}
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);
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) {

Loading…
Cancel
Save