mirror of
https://github.com/status-im/gasless-democracy.git
synced 2025-02-19 18:28:28 +00:00
Enrich submitted polls
fetch from IPFS add date handling add cards representing polls
This commit is contained in:
parent
deccb1cfa6
commit
2c51a9922a
@ -11,6 +11,7 @@
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/bl": "^2.1.0",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/node": "^12.0.0",
|
||||
|
@ -11,18 +11,11 @@ import { prettySign, verifySignedMessage } from '../utils/signing'
|
||||
import { uploadFilesToIpfs, uploadToIpfs } from '../utils/ipfs'
|
||||
import { sendToPublicChat } from '../utils/status'
|
||||
import { POLLS_CHANNEL } from './constants'
|
||||
|
||||
type FormikValues = {
|
||||
title: string,
|
||||
subtitle: string,
|
||||
pollOptions: string,
|
||||
datePicker: Date | null,
|
||||
description: string
|
||||
}
|
||||
import { IPollInfo } from '../types'
|
||||
|
||||
const TEN_DAYS_FUTURE = new Date(new Date().getTime()+(10*24*60*60*1000))
|
||||
|
||||
const createJSON = (values: FormikValues): string => {
|
||||
const createJSON = (values: IPollInfo): string => {
|
||||
return JSON.stringify(values, null, 2)
|
||||
}
|
||||
function CreatePoll() {
|
||||
@ -54,7 +47,7 @@ function CreatePoll() {
|
||||
handleChange,
|
||||
handleBlur,
|
||||
setFieldValue
|
||||
}: FormikProps<FormikValues>) => {
|
||||
}: FormikProps<IPollInfo>) => {
|
||||
return (
|
||||
<form className={classes.root} onSubmit={handleSubmit}>
|
||||
<Typography className={classes.title} variant="h3">Create a poll</Typography>
|
||||
|
@ -1,22 +1,40 @@
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {
|
||||
gotoPublicChat,
|
||||
getChatMessages,
|
||||
useChatMessages,
|
||||
Topics,
|
||||
Message
|
||||
useChatMessages
|
||||
} from '../utils/status'
|
||||
import { signedMessage, verifySignedMessage } from '../utils/signing'
|
||||
import { verifySignedMessage } from '../utils/signing'
|
||||
import { Topics, Message, ISignedMessage, IEnrichedMessage, IPollInfo, IFormattedDate } from '../types'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import StatusButton from './base/Button'
|
||||
import useStyles from '../styles/listPolls'
|
||||
import { getFromIpfs } from '../utils/ipfs'
|
||||
import { POLLS_CHANNEL } from './constants'
|
||||
import { getFormattedDate } from '../utils/dates'
|
||||
|
||||
async function gotoPolls() {
|
||||
await gotoPublicChat(POLLS_CHANNEL)
|
||||
getChatMessages()
|
||||
}
|
||||
|
||||
async function enrichMessages(messages: Message[], setState: Function) {
|
||||
const updated = messages.map(async (message): Promise<IEnrichedMessage | Message> => {
|
||||
const { sigMsg } = message
|
||||
if (!message || !sigMsg || !sigMsg.msg) return message
|
||||
const res: string = await getFromIpfs(sigMsg.msg)
|
||||
try {
|
||||
const pollInfo: IPollInfo = JSON.parse(res ? res : sigMsg.msg)
|
||||
message.pollInfo = pollInfo
|
||||
message.formattedEndDate = getFormattedDate(pollInfo.datePicker)
|
||||
} catch(e) {}
|
||||
return message
|
||||
})
|
||||
const resolved = await Promise.all(updated)
|
||||
setState(resolved)
|
||||
}
|
||||
|
||||
async function parseMessages(messages: Topics | undefined, setState: Function) {
|
||||
if (!messages) return
|
||||
const fmtMessages: Topics = {}
|
||||
@ -27,7 +45,7 @@ async function parseMessages(messages: Topics | undefined, setState: Function) {
|
||||
const { text } = msg;
|
||||
const newMsg = { ...msg };
|
||||
try {
|
||||
const sigMsg: signedMessage = JSON.parse(text);
|
||||
const sigMsg: ISignedMessage = JSON.parse(text);
|
||||
newMsg['sigMsg'] = sigMsg
|
||||
} catch (e) {
|
||||
console.log({e})
|
||||
@ -38,17 +56,49 @@ async function parseMessages(messages: Topics | undefined, setState: Function) {
|
||||
return newMsg
|
||||
}
|
||||
})
|
||||
//const final = resolved.filter((k: Message) => k.text !== NIL)
|
||||
console.log({parsed})
|
||||
fmtMessages[key] = parsed
|
||||
const verified = parsed.filter(m => m.verified === true)
|
||||
fmtMessages[key] = verified
|
||||
})
|
||||
console.log({fmtMessages})
|
||||
setState(fmtMessages)
|
||||
}
|
||||
|
||||
interface ITableCard {
|
||||
polls: Message[]
|
||||
}
|
||||
|
||||
const isOdd = (num: number): boolean => !!(num % 2)
|
||||
function TableCards({ polls }: ITableCard) {
|
||||
const classes: any = useStyles()
|
||||
const { cardText, cellColor } = classes
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{polls.map((poll, i) => {
|
||||
const { pollInfo, messageId, formattedEndDate } = poll
|
||||
if (!formattedEndDate || !formattedEndDate.plainText) return
|
||||
const { plainText } = formattedEndDate
|
||||
if (!pollInfo) return
|
||||
const { title, description } = pollInfo
|
||||
const cellStyling = isOdd(i) ? classnames(cardText) : classnames(cardText, cellColor)
|
||||
const lightText = classnames(cellStyling, classes.cardLightText)
|
||||
const pollUrl = `/poll/${messageId}`
|
||||
return (
|
||||
<Fragment key={pollUrl}>
|
||||
<Typography className={classnames(cellStyling, classes.cardTitle)}>{title}</Typography>
|
||||
<Typography className={classnames(cellStyling, classes.cardSubTitle)}>{description}</Typography>
|
||||
<Typography className={lightText}>{plainText}</Typography>
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
})}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
function ListPolls() {
|
||||
const [rawMessages] = useChatMessages()
|
||||
const [chatMessages, setChatMessages] = useState()
|
||||
const [enrichedPolls, setEnrichedPolls] = useState()
|
||||
const classes: any = useStyles()
|
||||
|
||||
useEffect(() => {
|
||||
@ -59,16 +109,20 @@ function ListPolls() {
|
||||
parseMessages(rawMessages, setChatMessages)
|
||||
}, [rawMessages])
|
||||
|
||||
//const chatMessages = parseMessages(rawMessages)
|
||||
useEffect(() => {
|
||||
if (chatMessages) enrichMessages(chatMessages['polls'], setEnrichedPolls)
|
||||
}, [chatMessages])
|
||||
|
||||
console.log({chatMessages, rawMessages})
|
||||
return (
|
||||
<Fragment>
|
||||
{!chatMessages && <div className={classes.root}>
|
||||
<StatusButton
|
||||
<div className={classes.root}>
|
||||
{!chatMessages && <StatusButton
|
||||
buttonText="Goto #polls to get started"
|
||||
onClick={gotoPolls}
|
||||
/>
|
||||
</div>}
|
||||
/>}
|
||||
{!!enrichedPolls && <TableCards polls={enrichedPolls} />}
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
@ -9,6 +9,26 @@ const useStyles = makeStyles(theme => ({
|
||||
gridTemplateRows: '4rem 4rem auto auto 6rem',
|
||||
gridColumn: '8 / 42',
|
||||
}
|
||||
},
|
||||
cardText: {
|
||||
gridColumn: '1 / 49',
|
||||
lineHeight: '2rem',
|
||||
padding: '0.25rem 1rem',
|
||||
color: '#000000'
|
||||
},
|
||||
cellColor: {
|
||||
background: '#F5F7F8'
|
||||
},
|
||||
cardLightText: {
|
||||
color: '#545353'
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: '1.2rem',
|
||||
fontWeight: 500,
|
||||
paddingTop: '1rem'
|
||||
},
|
||||
cardSubTitle: {
|
||||
lineHeight: '1.5rem'
|
||||
}
|
||||
}))
|
||||
|
||||
|
@ -1,2 +1,37 @@
|
||||
export type SetAccount = (account: string) => any;
|
||||
export type EnableEthereum = () => Promise<string | undefined>
|
||||
export type IPollInfo = {
|
||||
title: string,
|
||||
subtitle: string,
|
||||
pollOptions: string,
|
||||
datePicker: Date | null,
|
||||
description: string
|
||||
}
|
||||
export type ISignedMessage = {
|
||||
address: string,
|
||||
msg: string,
|
||||
sig: string,
|
||||
version?: number
|
||||
}
|
||||
export type Message = {
|
||||
alias: string,
|
||||
text: string,
|
||||
timestamp: number,
|
||||
from: string,
|
||||
messageId: string,
|
||||
verified?: boolean,
|
||||
sigMsg?: ISignedMessage
|
||||
pollInfo?: IPollInfo
|
||||
formattedEndDate?: IFormattedDate
|
||||
}
|
||||
export type Topics = {
|
||||
[chat: string]: Message[]
|
||||
}
|
||||
export interface IEnrichedMessage extends ISignedMessage {
|
||||
pollInfo: IPollInfo
|
||||
}
|
||||
export type IFormattedDate = {
|
||||
plainText: string,
|
||||
daysRemaining: number,
|
||||
hoursRemaining: number
|
||||
}
|
||||
|
32
dapp/src/utils/dates.ts
Normal file
32
dapp/src/utils/dates.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import DateFnsUtils from '@date-io/date-fns';
|
||||
import differenceInDays from 'date-fns/differenceInDays'
|
||||
import differenceInHours from 'date-fns/differenceInHours'
|
||||
import { IFormattedDate } from '../types'
|
||||
|
||||
const dateFns = new DateFnsUtils();
|
||||
const toDateFn = (date: Date | string | null) => dateFns.date(date)
|
||||
const today = dateFns.date(new Date())
|
||||
const getDateDelta = (date: string) => dateFns.getDiff(toDateFn(date), today)
|
||||
const daysDiff = (date: number | Date): number => differenceInDays(date, today)
|
||||
const hoursDiff = (date: number | Date) => differenceInHours(date, today)
|
||||
|
||||
export const timeRemaining = (date: Date | string | null): string => {
|
||||
const dateStr = toDateFn(date)
|
||||
const totalHours = hoursDiff(dateStr)
|
||||
const remainder = totalHours % 24
|
||||
const days = Math.floor(totalHours / 24)
|
||||
const str = totalHours > 0 ? `${days} days and ${remainder} hours until vote ends` : `Vote ended ${Math.abs(days)} ago`
|
||||
return str
|
||||
}
|
||||
|
||||
export const getFormattedDate = (date: Date | string | null): IFormattedDate => {
|
||||
const dateNum = toDateFn(date)
|
||||
const plainText = timeRemaining(date)
|
||||
const daysRemaining = daysDiff(dateNum)
|
||||
const hoursRemaining = hoursDiff(dateNum)
|
||||
return {
|
||||
plainText,
|
||||
daysRemaining,
|
||||
hoursRemaining
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
// @ts-ignore
|
||||
import ipfsClient from 'ipfs-http-client'
|
||||
// @ts-ignore
|
||||
import BufferList from 'bl/BufferList'
|
||||
|
||||
const ipfsHttpStatus = ipfsClient({ host: 'ipfs.status.im', protocol: 'https', port: '443' })
|
||||
|
||||
@ -33,6 +35,26 @@ export const uploadToIpfs = async (str:string): Promise<string> => {
|
||||
return res.cid.string
|
||||
}
|
||||
|
||||
export const getFromIpfs = async (cid:string) => {
|
||||
try {
|
||||
for await (const file of ipfsHttpStatus.get(cid)) {
|
||||
//console.log(file.path)
|
||||
|
||||
if (!file.content) continue;
|
||||
|
||||
const content = new BufferList()
|
||||
for await (const chunk of file.content) {
|
||||
content.append(chunk)
|
||||
}
|
||||
|
||||
//console.log('content', {content, cid}, content.toString())
|
||||
return content.toString()
|
||||
}
|
||||
} catch(e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const uploadToIpfsGateway = async (files: ipfsFile[]): Promise<string> => {
|
||||
const res = await ipfsHttpStatus.add(files)
|
||||
return `ipfs/${res.slice(-1)[0].hash}`
|
||||
|
@ -4,15 +4,10 @@ import {
|
||||
hashPersonalMessage,
|
||||
pubToAddress
|
||||
} from 'ethereumjs-util';
|
||||
import { ISignedMessage } from '../types'
|
||||
|
||||
declare var web3: any
|
||||
|
||||
export type signedMessage = {
|
||||
address: string,
|
||||
msg: string,
|
||||
sig: string,
|
||||
version?: number
|
||||
}
|
||||
|
||||
export function stripHexPrefix(value: string) {
|
||||
return value.replace('0x', '');
|
||||
@ -26,7 +21,7 @@ export function sign(message: string): Promise<string> {
|
||||
return web3.eth.personal.sign(message, web3.eth.defaultAccount)
|
||||
}
|
||||
|
||||
export function verifySignedMessage({ address, msg, sig, version }: signedMessage) {
|
||||
export function verifySignedMessage({ address, msg, sig, version }: ISignedMessage) {
|
||||
const sigb = new Buffer(stripHexPrefixAndLower(sig), 'hex');
|
||||
if (sigb.length !== 65) {
|
||||
return false;
|
||||
@ -35,12 +30,10 @@ export function verifySignedMessage({ address, msg, sig, version }: signedMessag
|
||||
sigb[64] = sigb[64] === 0 || sigb[64] === 1 ? sigb[64] + 27 : sigb[64];
|
||||
const hash = hashPersonalMessage(new Buffer(msg))
|
||||
const pubKey = ecrecover(hash, sigb[64], sigb.slice(0, 32), sigb.slice(32, 64));
|
||||
console.log({pubKey})
|
||||
|
||||
return stripHexPrefixAndLower(address) === pubToAddress(pubKey).toString('hex');
|
||||
}
|
||||
|
||||
export async function prettySign(message: string): Promise<signedMessage> {
|
||||
export async function prettySign(message: string): Promise<ISignedMessage> {
|
||||
const sig = await sign(message)
|
||||
return {
|
||||
address: web3.eth.defaultAccount,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { signedMessage } from './signing'
|
||||
import { Message, Topics } from '../types'
|
||||
export {}
|
||||
|
||||
declare global {
|
||||
@ -36,18 +36,6 @@ export async function getChatMessages(): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export type Message = {
|
||||
alias: string,
|
||||
text: string,
|
||||
timestamp: number,
|
||||
from: string,
|
||||
messageId: string,
|
||||
verified?: boolean,
|
||||
sigMsg?: signedMessage
|
||||
}
|
||||
export type Topics = {
|
||||
[chat: string]: Message[]
|
||||
}
|
||||
|
||||
type Data = {
|
||||
chat: string,
|
||||
|
@ -1598,6 +1598,13 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.3.0"
|
||||
|
||||
"@types/bl@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/bl/-/bl-2.1.0.tgz#45c881c97feae1223d63bbc5b83166153fcb2a15"
|
||||
integrity sha512-1TdA9IXOy4sdqn8vgieQ6GZAiHiPNrOiO1s2GJjuYPw4QVY7gYoVjkW049avj33Ez7IcIvu43hQsMsoUFbCn2g==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/classnames@^2.2.10":
|
||||
version "2.2.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999"
|
||||
|
@ -35,6 +35,8 @@
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/styles": "^4.10.0",
|
||||
"@types/bl": "^2.1.0",
|
||||
"bl": "^4.0.2",
|
||||
"eth-crypto": "^1.6.0",
|
||||
"ethereumjs-util": "^6.0.0",
|
||||
"ipfs-http-client": "^44.3.0",
|
||||
|
@ -1217,6 +1217,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/async/-/async-3.0.3.tgz#ea3694128c757580e4f9328cd941b81d9c3e9bf6"
|
||||
integrity sha512-FrIcC67Zpko1jO8K4d30C41/KVhAABbMbaSxccvXacxPcKbDBav+8WoFzv72BA2zJvyX4T9PFz0we1hcNymgGA==
|
||||
|
||||
"@types/bl@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/bl/-/bl-2.1.0.tgz#45c881c97feae1223d63bbc5b83166153fcb2a15"
|
||||
integrity sha512-1TdA9IXOy4sdqn8vgieQ6GZAiHiPNrOiO1s2GJjuYPw4QVY7gYoVjkW049avj33Ez7IcIvu43hQsMsoUFbCn2g==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/bn.js@4.11.6", "@types/bn.js@^4.11.3", "@types/bn.js@^4.11.4":
|
||||
version "4.11.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c"
|
||||
@ -2253,7 +2260,7 @@ bl@^1.0.0:
|
||||
readable-stream "^2.3.5"
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
bl@^4.0.0:
|
||||
bl@^4.0.0, bl@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a"
|
||||
integrity sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==
|
||||
|
Loading…
x
Reference in New Issue
Block a user