mirror of
https://github.com/status-im/community-dapp.git
synced 2025-02-23 11:38:40 +00:00
Add community featuring (#169)
This commit is contained in:
parent
7e212a9cdf
commit
d9bd35ac20
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3
packages/DApp/src/helpers/getWeek.ts
Normal file
3
packages/DApp/src/helpers/getWeek.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function getWeek(date: Date) {
|
||||
return Math.floor((date.getTime() - 259200000) / 604800000)
|
||||
}
|
47
packages/DApp/src/helpers/receiveWakuFeature.ts
Normal file
47
packages/DApp/src/helpers/receiveWakuFeature.ts
Normal 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 }
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
34
packages/DApp/src/hooks/useSendWakuFeature.ts
Normal file
34
packages/DApp/src/hooks/useSendWakuFeature.ts
Normal 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
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
|
39
packages/DApp/src/providers/wakuFeature/provider.tsx
Normal file
39
packages/DApp/src/providers/wakuFeature/provider.tsx
Normal 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} />
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user