Add community featuring (#169)

This commit is contained in:
Szymon Szlachtowicz 2021-07-29 12:08:33 +02:00 committed by GitHub
parent 7e212a9cdf
commit d9bd35ac20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 248 additions and 29 deletions

View File

@ -12,6 +12,7 @@ import { useEthers } from '@usedapp/core'
import { VoteSubmitButton } from './VoteSubmitButton'
import { VoteSendingBtn, VoteBtn } from '../Button'
import { VotingRoom } from '../../models/smartContract'
import { useWakuFeature } from '../../providers/wakuFeature/provider'
interface CardFeatureProps {
community: CommunityDetail
@ -28,12 +29,11 @@ export const CardFeature = ({ community, heading, icon, sum, timeLeft, currentVo
const [showFeatureModal, setShowFeatureModal] = useState(false)
const [showConfirmModal, setShowConfirmModal] = useState(false)
const [showOngoingVote, setShowOngoingVote] = useState(false)
const { featured } = useWakuFeature()
const setNewModal = (val: boolean) => {
setShowConfirmModal(val)
setShowFeatureModal(false)
}
return (
<CardVoteBlock>
<CardHeadingFeature style={{ fontWeight: timeLeft ? 'normal' : 'bold', fontSize: timeLeft ? '15px' : '17px' }}>
@ -78,7 +78,10 @@ export const CardFeature = ({ community, heading, icon, sum, timeLeft, currentVo
<VoteConfirmModal community={community} selectedVote={{ verb: 'to feature' }} setShowModal={setNewModal} />
</Modal>
)}
<FeatureBtn disabled={Boolean(timeLeft) || !account} onClick={() => setShowFeatureModal(true)}>
<FeatureBtn
disabled={!account || featured.find((el) => el[0] === community.publicKey)}
onClick={() => setShowFeatureModal(true)}
>
Feature this community! <span style={{ fontSize: '20px' }}></span>
</FeatureBtn>
</div>

View File

@ -5,6 +5,7 @@ import { CardCommunity } from './CardCommunity'
import { ButtonPrimary } from '../Button'
import { VotePropose } from '../votes/VotePropose'
import { ColumnFlexDiv } from '../../constants/styles'
import { useSendWakuFeature } from '../../hooks/useSendWakuFeature'
interface FeatureModalProps {
community: CommunityDetail
@ -14,6 +15,7 @@ interface FeatureModalProps {
export function FeatureModal({ community, availableAmount, setShowConfirmModal }: FeatureModalProps) {
const [proposingAmount, setProposingAmount] = useState(0)
const sendWaku = useSendWakuFeature()
const disabled = proposingAmount === 0
return (
@ -26,7 +28,13 @@ export function FeatureModal({ community, availableAmount, setShowConfirmModal }
proposingAmount={proposingAmount}
disabled={disabled}
/>
<VoteConfirmBtn disabled={disabled} onClick={() => setShowConfirmModal(true)}>
<VoteConfirmBtn
disabled={disabled}
onClick={async () => {
await sendWaku(proposingAmount, community.publicKey)
setShowConfirmModal(true)
}}
>
Confirm vote to feature community
</VoteConfirmBtn>
</VoteProposeWrap>

View File

@ -48,9 +48,9 @@ export function DirectoryCard({ community }: DirectoryCardProps) {
<CardFeature
community={community}
heading={timeLeft ? 'This community has to wait until it can be featured again' : 'Weekly Feature vote'}
icon={community?.directoryInfo?.featureVotes ? '⭐' : '⏳'}
sum={community?.directoryInfo?.featureVotes?.toNumber()}
timeLeft={timeLeft}
icon={community?.featureVotes ? '⭐' : '⏳'}
sum={community?.featureVotes?.toNumber()}
timeLeft={''}
currentVoting={currentVoting}
room={votingRoom}
/>

View File

@ -10,7 +10,6 @@ import { DirectoryCardSkeleton } from './DirectoryCardSkeleton'
import { useDirectoryCommunities } from '../../hooks/useDirectoryCommunities'
import { SearchEmpty } from '../SearchEmpty'
import { DirectoryCard } from './DirectoryCard'
export function DirectoryCards() {
const [filterKeyword, setFilterKeyword] = useState('')
const [sortedBy, setSortedBy] = useState(DirectorySortingEnum.IncludedRecently)
@ -27,7 +26,7 @@ export function DirectoryCards() {
/>
<FilterList value={sortedBy} setValue={setSortedBy} options={DirectorySortingOptions} />
</PageBar>
<WeeklyFeature endDate={new Date('07/26/2021')} />
<WeeklyFeature endDate={new Date('07/30/2021')} />
<Voting>
{communities.map((community, idx) => {
if (community) {

View File

@ -64,16 +64,16 @@ export function getCommunitiesInDirectorySync(
break
case DirectorySortingEnum.MostVotes:
sortFunction = (a: CommunityDetail, b: CommunityDetail) => {
if (!a.directoryInfo?.featureVotes) return 1
if (!b.directoryInfo?.featureVotes) return -1
return a?.directoryInfo?.featureVotes < b?.directoryInfo?.featureVotes ? 1 : -1
if (!a?.featureVotes) return 1
if (!b?.featureVotes) return -1
return a?.featureVotes < b?.featureVotes ? 1 : -1
}
break
case DirectorySortingEnum.LeastVotes:
sortFunction = (a: CommunityDetail, b: CommunityDetail) => {
if (!a.directoryInfo?.featureVotes) return 1
if (!b.directoryInfo?.featureVotes) return -1
return a?.directoryInfo?.featureVotes < b?.directoryInfo?.featureVotes ? -1 : 1
if (!a?.featureVotes) return 1
if (!b?.featureVotes) return -1
return a?.featureVotes < b?.featureVotes ? -1 : 1
}
break
}

View File

@ -56,9 +56,9 @@ export const communities: Array<CommunityDetail> = [
voteFor: BigNumber.from(16740235),
voteAgainst: BigNumber.from(6740235),
},
featureVotes: BigNumber.from(62142321),
directoryInfo: {
additionDate: new Date(1622802882000),
featureVotes: BigNumber.from(62142321),
},
},
{
@ -91,9 +91,9 @@ export const communities: Array<CommunityDetail> = [
validForAddition: true,
votingHistory: [],
currentVoting: undefined,
featureVotes: BigNumber.from(5214321),
directoryInfo: {
additionDate: new Date(1622630082000),
featureVotes: BigNumber.from(5214321),
},
},
{
@ -108,9 +108,9 @@ export const communities: Array<CommunityDetail> = [
validForAddition: false,
votingHistory: [],
currentVoting: undefined,
featureVotes: BigNumber.from(314321),
directoryInfo: {
additionDate: new Date(1622543682000),
featureVotes: BigNumber.from(314321),
},
},
{

View File

@ -78,13 +78,13 @@ export function sortDirectoryFunction(sortedBy: DirectorySortingEnum) {
if (!b.directoryInfo) return 1
return a?.directoryInfo?.additionDate < b?.directoryInfo?.additionDate ? 1 : -1
case DirectorySortingEnum.MostVotes:
if (!a.directoryInfo?.featureVotes) return 1
if (!b.directoryInfo?.featureVotes) return -1
return a?.directoryInfo?.featureVotes < b?.directoryInfo?.featureVotes ? 1 : -1
if (!a?.featureVotes) return 1
if (!b?.featureVotes) return -1
return a?.featureVotes < b?.featureVotes ? 1 : -1
case DirectorySortingEnum.LeastVotes:
if (!a.directoryInfo?.featureVotes) return 1
if (!b.directoryInfo?.featureVotes) return -1
return a?.directoryInfo?.featureVotes < b?.directoryInfo?.featureVotes ? -1 : 1
if (!a?.featureVotes) return 1
if (!b?.featureVotes) return -1
return a?.featureVotes < b?.featureVotes ? -1 : 1
}
}
}

View File

@ -0,0 +1,3 @@
export function getWeek(date: Date) {
return Math.floor((date.getTime() - 259200000) / 604800000)
}

View File

@ -0,0 +1,47 @@
import { receiveWakuFeatureMsg } from '../helpers/wakuMessage'
import { getWeek } from '../helpers/getWeek'
import { merge } from 'lodash'
import { BigNumber } from 'ethers'
import { Waku } from 'js-waku'
function sumVotes(map: any) {
for (const [publicKey, community] of Object.entries(map) as any[]) {
map[publicKey]['sum'] = BigNumber.from(0)
for (const votes of Object.entries(community['votes'])) {
map[publicKey]['sum'] = map[publicKey]['sum'].add(votes[1])
}
}
}
function getTop(map: any, top: number) {
sumVotes(map)
return Object.entries(map)
.sort((a: any, b: any) => (a[1].sum > b[1].sum ? -1 : 1))
.slice(0, top)
}
export async function receiveWakuFeature(waku: Waku | undefined, topic: string) {
let messages = await receiveWakuFeatureMsg(waku, topic)
const wakuFeatured: any = {}
let top5: any[] = []
if (messages && messages?.length > 0) {
messages = messages.sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1))
let prevWeek = getWeek(messages[0].timestamp)
messages.forEach((el: any) => {
if (prevWeek === getWeek(el.timestamp)) {
if (!top5.find((featuredComm) => featuredComm[0] === el.publicKey)) {
merge(wakuFeatured, { [el.publicKey]: { votes: { [el.voter]: el.sntAmount } } })
}
} else {
top5 = getTop(wakuFeatured, 5)
top5.forEach((featuredComm) => {
wakuFeatured[featuredComm[0]].votes = {}
wakuFeatured[featuredComm[0]].sum = BigNumber.from(0)
})
prevWeek = getWeek(el.timestamp)
}
})
sumVotes(wakuFeatured)
}
return { wakuFeatured, top5 }
}

View File

@ -45,6 +45,74 @@ export async function receiveWakuMessages(waku: Waku, topic: string, room: numbe
return messages?.map((msg) => JSON.parse(msg.payloadAsUtf8))
}
export async function receiveWakuFeatureMsg(waku: Waku | undefined, topic: string) {
if (waku) {
const messages = await waku.store.queryHistory({ contentTopics: [topic] })
if (messages) {
return messages.filter(verifyWakuFeatureMsg).map((msg) => {
const data = JSON.parse(msg.payloadAsUtf8)
return { ...data, timestamp: new Date(data.timestamp) }
})
}
}
return []
}
export function verifyWakuFeatureMsg(msg: WakuMessage) {
const wakuTimestamp = msg.timestamp
const data = JSON.parse(msg.payloadAsUtf8)
const timestamp = new Date(data.timestamp)
const types = ['address', 'uint256', 'address', 'uint256']
const message = [data.voter, data.sntAmount, data.publicKey, BigNumber.from(timestamp.getTime())]
const verifiedAddress = utils.verifyMessage(packAndArrayify(types, message), data.sign)
if (wakuTimestamp?.getTime() != timestamp.getTime() || verifiedAddress != data.voter) {
return false
}
return true
}
export async function createWakuFeatureMsg(
account: string | null | undefined,
signer: JsonRpcSigner | undefined,
sntAmount: BigNumber,
publicKey: string,
contentTopic: string
) {
if (!account || !signer) {
return undefined
}
const signerAddress = await signer?.getAddress()
if (signerAddress != account) {
return undefined
}
const timestamp = new Date()
const types = ['address', 'uint256', 'address', 'uint256']
const message = [account, sntAmount, publicKey, BigNumber.from(timestamp.getTime())]
const sign = await signer.signMessage(packAndArrayify(types, message))
if (sign) {
const msg = WakuMessage.fromUtf8String(
JSON.stringify({
voter: account,
sntAmount,
publicKey,
timestamp,
sign,
}),
{
contentTopic,
timestamp,
}
)
return msg
}
return undefined
}
export async function createWakuMessage(
account: string | null | undefined,
signer: JsonRpcSigner | undefined,

View File

@ -1,17 +1,28 @@
import { getCommunityDetails } from '../helpers/apiMock'
import { useCommunitiesProvider } from '../providers/communities/provider'
import { useWakuFeature } from '../providers/wakuFeature/provider'
import { BigNumber } from 'ethers'
export function useCommunities(publicKeys: string[]) {
const { communitiesDetails, dispatch } = useCommunitiesProvider()
const { featureVotes } = useWakuFeature()
return publicKeys.map((publicKey) => {
const detail = communitiesDetails[publicKey]
if (detail) {
return { ...detail }
if (featureVotes[publicKey]) {
return { ...detail, featureVotes: featureVotes[publicKey].sum }
} else {
return { ...detail, featureVotes: BigNumber.from(0) }
}
} else {
if (publicKey) {
const setCommunity = async () => {
const communityDetail = await getCommunityDetails(publicKey)
let communityDetail = await getCommunityDetails(publicKey)
if (featureVotes[publicKey]) {
communityDetail = { ...communityDetail, featureVotes: featureVotes[publicKey].sum }
} else {
communityDetail = { ...communityDetail, featureVotes: BigNumber.from(0) }
}
if (communityDetail) {
dispatch(communityDetail)
}

View File

@ -0,0 +1,34 @@
import { useCallback } from 'react'
import { useWaku } from '../providers/waku/provider'
import { useEthers } from '@usedapp/core'
import { useConfig } from '../providers/config'
import { createWakuFeatureMsg } from '../helpers/wakuMessage'
import { BigNumber } from 'ethers'
export function useSendWakuFeature() {
const { waku } = useWaku()
const { account, library } = useEthers()
const { config } = useConfig()
const sendWakuFeature = useCallback(
async (voteAmount: number, publicKey: string) => {
const msg = await createWakuFeatureMsg(
account,
library?.getSigner(),
BigNumber.from(voteAmount),
publicKey,
config.wakuFeatureTopic
)
if (msg) {
if (waku) {
await waku.relay.send(msg)
} else {
alert('error sending vote please try again')
}
}
},
[waku, library, account]
)
return sendWakuFeature
}

View File

@ -6,6 +6,7 @@ import { DEFAULT_CONFIG } from '@usedapp/core/dist/cjs/src/model/config/default'
import { ConfigProvider } from './providers/config/provider'
import { WakuProvider } from './providers/waku/provider'
import { CommunitiesProvider } from './providers/communities/provider'
import { WakuFeatureProvider } from './providers/wakuFeature/provider'
const config = {
readOnlyChainId: ChainId.Ropsten,
@ -29,7 +30,9 @@ render(
<DAppProvider config={config}>
<ConfigProvider>
<CommunitiesProvider>
<App />
<WakuFeatureProvider>
<App />
</WakuFeatureProvider>
</CommunitiesProvider>
</ConfigProvider>
</DAppProvider>

View File

@ -29,10 +29,10 @@ export type CommunityDetail = {
}[]
| []
currentVoting: CurrentVoting | undefined
featureVotes?: BigNumber // number of votes for featuring community undefined if community can't be voted on
directoryInfo?: {
// if community is in directory this object describes additional directory info
additionDate: Date // date of addition to directory
featureVotes?: BigNumber // number of votes for featuring community undefined if community can't be voted on
untilNextFeature?: number // number of seconds until community can be featured again
}
}

View File

@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid'
export interface Config {
numberPerPage: number
wakuTopic: string
wakuFeatureTopic: string
contracts: {
[chainID: number]: {
[name: string]: string
@ -28,16 +29,19 @@ const contracts = {
export const config: EnvConfigs = {
localhost: {
wakuTopic: `/myApp/localhost/${uuidv4()}/0.0.5/votingRoom/`,
wakuFeatureTopic: `/myApp/localhost/${uuidv4()}/0.0.5/feature/`,
numberPerPage: 2,
contracts,
},
development: {
wakuTopic: '/myApp/development/0.0.5/votingRoom/',
wakuFeatureTopic: `/myApp/development/0.0.5/feature/`,
numberPerPage: 3,
contracts,
},
production: {
wakuTopic: '/myApp/production/0.0.5/votingRoom/',
wakuFeatureTopic: `/myApp/production/0.0.5/feature/`,
numberPerPage: 4,
contracts,
},

View File

@ -0,0 +1,39 @@
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react'
import { receiveWakuFeature } from '../../helpers/receiveWakuFeature'
import { useConfig } from '../config'
import { useWaku } from '../waku/provider'
const WakuFeatureContext = createContext<{
featureVotes: any
featured: any[]
}>({
featureVotes: {},
featured: [],
})
export function useWakuFeature() {
return useContext(WakuFeatureContext)
}
interface WakuFeatureProviderProps {
children: ReactNode
}
export function WakuFeatureProvider({ children }: WakuFeatureProviderProps) {
const [featureVotes, setFeatureVotes] = useState<any>({})
const [featured, setFeatured] = useState<any[]>([])
const { waku } = useWaku()
const { config } = useConfig()
useEffect(() => {
const get = async () => {
const { wakuFeatured, top5 } = await receiveWakuFeature(waku, config.wakuFeatureTopic)
setFeatureVotes(wakuFeatured)
setFeatured(top5)
}
get()
const task = setInterval(get, 10000)
return () => clearInterval(task)
}, [waku?.libp2p?.peerId?.toString()])
return <WakuFeatureContext.Provider value={{ featureVotes, featured }} children={children} />
}