import { Feather } from '@expo/vector-icons' import React, { useMemo, useState } from 'react' import { Pressable, StyleSheet, Text, View } from 'react-native' import { useMutation, useQueryCache } from 'react-query' import client from '@api/client' import { ButtonRow } from '@components/Button' import { toast } from '@components/toast' import relativeTime from '@utils/relativeTime' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import Emojis from './Emojis' const fireMutation = async ({ id, options }: { id: string options: { [key: number]: boolean } }) => { const formData = new FormData() Object.keys(options).forEach(option => { // @ts-ignore if (options[option]) { formData.append('choices[]', option) } }) const res = await client({ method: 'post', instance: 'local', url: `polls/${id}/votes`, body: formData }) if (res.body.id === id) { toast({ type: 'success', content: '投票成功成功' }) return Promise.resolve() } else { toast({ type: 'error', content: '隐藏域名失败,请重试', autoHide: false }) return Promise.reject() } } export interface Props { queryKey: App.QueryKey status: Mastodon.Status } const TimelinePoll: React.FC = ({ queryKey, status: { poll } }) => { const { theme } = useTheme() const queryCache = useQueryCache() const [mutateAction] = useMutation(fireMutation, { onMutate: ({ id, options }) => { queryCache.cancelQueries(queryKey) const oldData = queryCache.getQueryData(queryKey) queryCache.setQueryData(queryKey, old => (old as {}[]).map((paging: any) => ({ toots: paging.toots.map((toot: any) => { if (toot.poll?.id === id) { const poll = toot.poll const myVotes = Object.keys(options).filter( // @ts-ignore option => options[option] ) const myVotesInt = myVotes.map(option => parseInt(option)) toot.poll = { ...toot.poll, votes_count: poll.votes_count ? poll.votes_count + myVotes.length : myVotes.length, voters_count: poll.voters_count ? poll.voters_count + 1 : 1, voted: true, own_votes: myVotesInt, // @ts-ignore options: poll.options.map((o, i) => { if (myVotesInt.includes(i)) { o.votes_count = o.votes_count + 1 } return o }) } } return toot }), pointer: paging.pointer })) ) return oldData }, onError: (err, _, oldData) => { toast({ type: 'error', content: '请重试' }) queryCache.setQueryData(queryKey, oldData) } }) const pollExpiration = useMemo(() => { // how many voted if (poll.expired) { return ( 投票已结束 ) } else { return ( 截止至{relativeTime(poll.expires_at)} ) } }, []) const [singleOptions, setSingleOptions] = useState({ ...[false, false, false, false].slice(0, poll.options.length) }) const [multipleOptions, setMultipleOptions] = useState({ ...[false, false, false, false].slice(0, poll.options.length) }) const isSelected = (index: number) => { if (poll.multiple) { return multipleOptions[index] ? 'check-square' : 'square' } else { return singleOptions[index] ? 'check-circle' : 'circle' } } return ( {poll.options.map((option, index) => poll.voted ? ( {poll.own_votes!.includes(index) && ( )} {Math.round((option.votes_count / poll.votes_count) * 100)}% ) : ( { if (poll.multiple) { setMultipleOptions({ ...multipleOptions, [index]: !multipleOptions[index] }) } else { setSingleOptions({ ...[ index === 0, index === 1, index === 2, index === 3 ].slice(0, poll.options.length) }) } }} > ) )} {!poll.expired && !poll.own_votes?.length && ( { if (poll.multiple) { mutateAction({ id: poll.id, options: multipleOptions }) } else { mutateAction({ id: poll.id, options: singleOptions }) } }} text='投票' /> )} 已投{poll.voters_count}人{' • '} {pollExpiration} ) } const styles = StyleSheet.create({ base: { marginTop: StyleConstants.Spacing.M }, poll: { minHeight: StyleConstants.Font.LineHeight.M * 1.5, marginBottom: StyleConstants.Spacing.XS }, optionSelected: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingLeft: StyleConstants.Spacing.M, paddingRight: StyleConstants.Spacing.M }, optionUnselected: { flex: 1, flexDirection: 'row' }, contentSelected: { flexBasis: '80%', flexDirection: 'row', alignItems: 'center' }, contentUnselected: { flexBasis: '90%' }, voted: { marginLeft: StyleConstants.Spacing.S }, votedNot: { marginRight: StyleConstants.Spacing.S }, percentage: { fontSize: StyleConstants.Font.Size.M }, background: { position: 'absolute', top: 0, left: 0, height: '100%', minWidth: 1, borderTopRightRadius: 6, borderBottomRightRadius: 6 }, meta: { flex: 1, flexDirection: 'row', alignItems: 'center', marginTop: StyleConstants.Spacing.XS }, button: { marginRight: StyleConstants.Spacing.M }, votes: { fontSize: StyleConstants.Font.Size.S }, expiration: { fontSize: StyleConstants.Font.Size.S } }) export default React.memo( TimelinePoll, (prev, next) => prev.status.poll.voted === next.status.poll.voted )