[ad_1]
Decentralization / Social media
Last time we added the ability to vote on a debate and to see the status of our own vote but we couldn’t see how many others had voted. In this part we’ll fix that by showing the vote counts next to the thumbs up/down icons in the debate list as shown in the above screenshot. We start from where we left off:
git clone https://github.com/jeremyorme/debate.git
cd debate
git checkout release/0.0.3
Firstly, we need to update bonono-react to at least 0.5.4:
npm update bonono-react
That’s because Bonono 0.5.3 did not provide a means of properly disposing of a DbCollection
— it would remain attached to the Db
instance and continue to consume resources. As we want to load many votes collections (for the debates in our list) this could cause performance issues.
Splitting Debate and Message cards
So far we’ve been using DebateCard
for displaying a debate and also a message. Now we need to make it more debate specific because it’s going to look up the vote numbers for a debate. Therefore, let’s make a copy DebateCard
called MessageCard
and use that for messages instead.
The contents of src/components/MessageCard.css
will be exactly the same as src/components/DebateCard.css
. In src/components/MessageCard.tsx
we need to update the name DebateCard
to MessageCard
in the following places:
// ...
import './MessageCard.css';
// ...const MessageCard: React.FC<ContainerProps> = ({ id, title, description, username, url }) => {
// ...
export default MessageCard;
Then in src/components/MessagesPage.tsx
, we import and use MessageCard
instead of DebateCard
in a couple of places:
// ...
import MessageCard from "../components/MessageCard";
// ...{messages.map(m => <MessageCard key={m._id} username={m._identity.publicKey.slice(-8)} description={m.description} url={findUrl(m.description)} />)}
Multiple vote collections in AppData
We need to open the vote collections for a debate when it scrolls on screen so that we can show its vote counts. There may be more than one debate visible at a time so we need a replace our single votes collection with a map:
// Votesprivate _votes: Map<string, CollectionManager<IVote>> = new Map();
The map is initially empty so we’ll add a helper to return an existing collection or create and insert a new one if none exists for the given debateId
:
private _votesCollection(debateId: string): CollectionManager<IVote> {
let votesCollection = this._votes.get(debateId);
if (!votesCollection) {
votesCollection = new CollectionManager<IVote>();
this._votes.set(debateId, votesCollection);
}
return votesCollection;
}
We’ll modify loadVotes
to only call init
on the collection if it wasn’t already initialized:
async loadVotes(debateId: string) {
if (!this._db)
return;const collectionName = 'debate-' + debateId + '-votes';
const collection = this._votesCollection(debateId);
if (collection.ready())
return;
collection.init(await this._db.collection(collectionName, {
publicAccess: AccessRights.ReadAnyWriteOwn,
conflictResolution: ConflictResolution.LastWriteWins
}));
}
Note this requires that we add a new ready()
method to our CollectionManager<T>
class to determine if it was previously initialized:
ready(): boolean {
return !!this._collection;
}
Now we can call loadVotes each time we encounter a new debate to load its vote data. We can’t just keep opening more and more collections though. If we were to keep open the vote collection for every debate we scroll past then the number of open collections would become a performance issue.
Therefore, we’d like to close a collection when we are no longer displaying its data (in this case the vote counts). For this purpose, we add a closeVotes
method:
closeVotes(debateId: string) {
this._votesCollection(debateId).close();
this._votes.delete(debateId);
}
This closes the collection and deletes it from our map. We need to add the close
method to CollectionManager<T>
to propagate the close to the DbCollection
:
close() {
this._collection?.close();
}
We’re almost done with the changes to AppData
. We just need to call our helper this._votesCollection(debateId)
in place of this._votes
in the other vote methods and pass in debateId
as a parameter to each of them:
votes(debateId: string): IVote[] {
return this._votesCollection(debateId).entries() || [];
}onVotes(debateId: string, callback: () => void) {
return this._votesCollection(debateId).onUpdated(callback);
}
addVote(debateId: string, message: IVote) {
if (this._publicKey)
this._votesCollection(debateId).addEntry({ ...message, _id: this._publicKey });
}
ownVoteDirection(debateId: string): VoteDirection {
if (!this._publicKey)
return VoteDirection.Undecided;
return this._votesCollection(debateId).entry(this._publicKey)?.direction || VoteDirection.Undecided;
}
We need to update the calls to these methods in src/pages/MessagesPage.tsx
to include the debate id:
// ...const [ownVoteDirection, setOwnVoteDirection] = useState(appData.ownVoteDirection(id));
// ...
useEffect(() => {
return appData.onVotes(id, () => {
setOwnVoteDirection(appData.ownVoteDirection(id));
});
}, []);
// ...
const updateOwnVoteDirection = (newDirection: VoteDirection) => {
const direction = newDirection != ownVoteDirection ? newDirection : VoteDirection.Undecided;
const vote: IVote = {
...dbEntryDefaults,
direction
};
appData.addVote(id, vote);
setOwnVoteDirection(direction);
}
// ...
Finally, we need to add a couple of methods to count up the votes for and against for a given debate:
votesFor(debateId: string): number {
return this.votes(debateId).reduce((c, v) => c + v.direction == VoteDirection.For ? 1 : 0, 0);
}votesAgainst(debateId: string): number {
return this.votes(debateId).reduce((c, v) => c + v.direction == VoteDirection.Against ? 1 : 0, 0);
}
Displaying vote counts in DebateCard
Now we have everything in place to open and close vote collections and generate vote counts we can implement the UI changes. First, we need to install a package:
npm install react-intersection-observer
This package provides a hook to detect when an element scrolls in or out of the viewport and we’ll use that to open and close the collection.
In src/components/DebateCard.tsx
, we’re going to pass the AppData
object into the component so update ComponentProps
(note we also make id
and title
mandatory now that we’re no longer sharing this component with MessagesPage
):
interface ContainerProps {
appData: AppData,
id: string;
title: string;
// ...
}
Then add appData
to the DebateCard
component:
const DebateCard: React.FC<ContainerProps> = ({ appData, id, title, description, username, url }) => {
Now we can add state variables for the for and against vote counts:
const [votesFor, setVotesFor] = useState(appData.votesFor(id));
const [votesAgainst, setVotesAgainst] = useState(appData.votesAgainst(id));
We can register for updates from the votes collections and update our state variables:
useEffect(() => {
return appData.onVotes(id, () => {
setVotesFor(appData.votesFor(id));
setVotesAgainst(appData.votesAgainst(id));
});
});
Then we can use these values (votesFor
and votesAgainst
) when rendering the component (note we drop the check for id
being falsy now it is a mandatory property):
<IonItem>
<Link to={'/debate/' + id + '/messages/for'}>
<IonIcon size="small" icon={thumbsUpSharp} />
</Link>
<IonBadge className="count">{votesFor}</IonBadge>
</IonItem>
<IonItem>
<Link to={'/debate/' + id + '/messages/against'}>
<IonIcon size="small" icon={thumbsDownSharp} />
</Link>
<IonBadge className="count">{votesAgainst}</IonBadge>
</IonItem>
While we’re at it, we can assume that title
is set now as well and drop the null check:
<IonCardTitle>{title}</IonCardTitle>
Now we just need to open and close the vote collection based on the visibility of the DebateCard
. Let’s use the in-view hook from react-intersection-observer
:
const { ref, inView } = useInView();
The inView
variable will be set to the in-view status (true
or false
) of the element referred to by ref
. We initialize ref
as follows:
<IonCard ref={ref}>
Now we can use an effect that triggers on changes to inView
to open and close the collection:
useEffect(() => {
if (inView)
appData.loadVotes(id);
else
appData.closeVotes(id);
}, [inView]);
When our DebateCard
scrolls into view, we’ll open the collection and start listening for updates to it and when it scrolls out of view we’ll close it down and stop receiving updates.
All that remains is to pass AppData
into the DebateCard
in src/pages/HomePage.tsx
:
{debates.map(d => <DebateCard key={d._id} appData={appData} id={d._id} username={d._identity.publicKey.slice(-8)} title={d.title} description={d.description} url={findUrl(d.description)} />)}
And there we go! We have efficient vote counts displayed on our debate list page. You can get the full source to the end of this article from the 0.0.4 release branch:
git clone https://github.com/jeremyorme/debate.git
cd debate
git checkout release/0.0.4
Next time we’ll look at adding the presentations page!
[ad_2]
Source link