Enrich submitted polls

fetch from IPFS
add date handling
add cards representing polls
This commit is contained in:
Barry Gitarts 2020-07-21 18:54:10 -04:00
parent deccb1cfa6
commit 2c51a9922a
12 changed files with 202 additions and 48 deletions

View File

@ -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",

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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'
}
}))

View File

@ -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
View 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
}
}

View File

@ -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}`

View File

@ -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,

View File

@ -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,

View File

@ -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"

View File

@ -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",

View File

@ -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==