parent
45441d3a9e
commit
2c1de66592
|
@ -1,4 +1,4 @@
|
||||||
import { getPoll as getPollApi } from '../_api/polls'
|
import { getPoll as getPollApi, voteOnPoll as voteOnPollApi } from '../_api/polls'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
|
|
||||||
|
@ -9,6 +9,17 @@ export async function getPoll (pollId) {
|
||||||
return poll
|
return poll
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast.say(`Unable to refresh poll`)
|
toast.say('Unable to refresh poll: ' + (e.message || ''))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function voteOnPoll (pollId, choices) {
|
||||||
|
let { currentInstance, accessToken } = store.get()
|
||||||
|
try {
|
||||||
|
let poll = await voteOnPollApi(currentInstance, accessToken, pollId, choices.map(_ => _.toString()))
|
||||||
|
return poll
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.say('Unable to vote in poll: ' + (e.message || ''))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { get, DEFAULT_TIMEOUT } from '../_utils/ajax'
|
import { get, post, DEFAULT_TIMEOUT, WRITE_TIMEOUT } from '../_utils/ajax'
|
||||||
import { auth, basename } from './utils'
|
import { auth, basename } from './utils'
|
||||||
|
|
||||||
export async function getPoll (instanceName, accessToken, pollId) {
|
export async function getPoll (instanceName, accessToken, pollId) {
|
||||||
let url = `${basename(instanceName)}/api/v1/polls/${pollId}`
|
let url = `${basename(instanceName)}/api/v1/polls/${pollId}`
|
||||||
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function voteOnPoll (instanceName, accessToken, pollId, choices) {
|
||||||
|
let url = `${basename(instanceName)}/api/v1/polls/${pollId}/votes`
|
||||||
|
return post(url, { choices }, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
||||||
|
}
|
||||||
|
|
|
@ -144,7 +144,7 @@
|
||||||
import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid'
|
import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid'
|
||||||
import { statusHtmlToPlainText } from '../../_utils/statusHtmlToPlainText'
|
import { statusHtmlToPlainText } from '../../_utils/statusHtmlToPlainText'
|
||||||
|
|
||||||
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea'])
|
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea', 'label'])
|
||||||
const isUserInputElement = node => INPUT_TAGS.has(node.localName)
|
const isUserInputElement = node => INPUT_TAGS.has(node.localName)
|
||||||
const isToolbar = node => node.classList.contains('status-toolbar')
|
const isToolbar = node => node.classList.contains('status-toolbar')
|
||||||
const isStatusArticle = node => node.classList.contains('status-article')
|
const isStatusArticle = node => node.classList.contains('status-article')
|
||||||
|
|
|
@ -1,16 +1,35 @@
|
||||||
<div class={computedClass} aria-busy={refreshing} >
|
<div class={computedClass} aria-busy={loading} >
|
||||||
<ul class="options" aria-label="Poll results">
|
{#if voted || expired }
|
||||||
{#each options as option}
|
<ul class="options" aria-label="Poll results">
|
||||||
<li class="option">
|
{#each options as option}
|
||||||
<div class="option-text">
|
<li class="option">
|
||||||
<strong>{option.share}%</strong> {option.title}
|
<div class="option-text">
|
||||||
|
<strong>{option.share}%</strong> {option.title}
|
||||||
|
</div>
|
||||||
|
<svg aria-hidden="true">
|
||||||
|
<line x1="0" y1="0" x2="{option.share}%" y2="0" />
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{:else}
|
||||||
|
<form class="poll-form" aria-label="Vote on poll" on:submit="onSubmit(event)" ref:form>
|
||||||
|
{#each options as option, i}
|
||||||
|
<div class="poll-form-option">
|
||||||
|
<input type="{multiple ? 'checkbox' : 'radio'}"
|
||||||
|
id="poll-choice-{uuid}-{i}"
|
||||||
|
name="poll-choice-{uuid}"
|
||||||
|
value="{i}"
|
||||||
|
on:change="onChange()"
|
||||||
|
>
|
||||||
|
<label for="poll-choice-{uuid}-{i}">
|
||||||
|
{option.title}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<svg aria-hidden="true">
|
{/each}
|
||||||
<line x1="0" y1="0" x2="{option.share}%" y2="0" />
|
<button disabled={formDisabled} type="submit">Vote</button>
|
||||||
</svg>
|
</form>
|
||||||
</li>
|
{/if}
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
<div class="poll-details">
|
<div class="poll-details">
|
||||||
<div class="poll-stat">
|
<div class="poll-stat">
|
||||||
<SvgIcon className="poll-icon" href="#fa-bar-chart" />
|
<SvgIcon className="poll-icon" href="#fa-bar-chart" />
|
||||||
|
@ -37,7 +56,7 @@
|
||||||
.poll {
|
.poll {
|
||||||
grid-area: poll;
|
grid-area: poll;
|
||||||
margin: 10px 10px 10px 5px;
|
margin: 10px 10px 10px 5px;
|
||||||
padding: 10px 20px;
|
padding: 20px;
|
||||||
border: 1px solid var(--main-border);
|
border: 1px solid var(--main-border);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: opacity 0.2s linear;
|
transition: opacity 0.2s linear;
|
||||||
|
@ -47,7 +66,7 @@
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll.poll-refreshing {
|
.poll.poll-loading {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
@ -152,9 +171,24 @@
|
||||||
min-width: 18px;
|
min-width: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.poll-form-option {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-form button {
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-form label {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 479px) {
|
@media (max-width: 479px) {
|
||||||
.poll {
|
.poll {
|
||||||
padding: 5px;
|
padding: 10px 5px;
|
||||||
}
|
}
|
||||||
.poll.status-in-own-thread {
|
.poll.status-in-own-thread {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
@ -173,10 +207,32 @@
|
||||||
import { absoluteDateFormatter } from '../../_utils/formatters'
|
import { absoluteDateFormatter } from '../../_utils/formatters'
|
||||||
import { registerClickDelegate } from '../../_utils/delegate'
|
import { registerClickDelegate } from '../../_utils/delegate'
|
||||||
import { classname } from '../../_utils/classname'
|
import { classname } from '../../_utils/classname'
|
||||||
import { getPoll } from '../../_actions/polls'
|
import { getPoll, voteOnPoll } from '../../_actions/polls'
|
||||||
|
|
||||||
const REFRESH_MIN_DELAY = 1000
|
const REFRESH_MIN_DELAY = 1000
|
||||||
|
|
||||||
|
async function doAsyncActionWithDelay (func) {
|
||||||
|
let start = Date.now()
|
||||||
|
let res = await func()
|
||||||
|
let timeElapsed = Date.now() - start
|
||||||
|
if (timeElapsed < REFRESH_MIN_DELAY) {
|
||||||
|
// If less than five seconds, then continue to show the loading animation
|
||||||
|
// so it's clear that something happened.
|
||||||
|
await new Promise(resolve => setTimeout(resolve, REFRESH_MIN_DELAY - timeElapsed))
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChoices (form, options) {
|
||||||
|
let res = []
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
if (form.elements[i].checked) {
|
||||||
|
res.push(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate () {
|
oncreate () {
|
||||||
this.onRefreshClick = this.onRefreshClick.bind(this)
|
this.onRefreshClick = this.onRefreshClick.bind(this)
|
||||||
|
@ -184,18 +240,20 @@
|
||||||
registerClickDelegate(this, refreshElementId, this.onRefreshClick)
|
registerClickDelegate(this, refreshElementId, this.onRefreshClick)
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
refreshedPoll: null,
|
loading: false,
|
||||||
refreshing: false
|
choices: []
|
||||||
}),
|
}),
|
||||||
store: () => store,
|
store: () => store,
|
||||||
computed: {
|
computed: {
|
||||||
poll: ({ originalStatus, refreshedPoll }) => refreshedPoll || originalStatus.poll,
|
pollId: ({ originalStatus }) => originalStatus.poll.id,
|
||||||
pollId: ({ poll }) => poll.id,
|
poll: ({ originalStatus, $polls, pollId }) => $polls[pollId] || originalStatus.poll,
|
||||||
options: ({ poll }) => poll.options.map(({ title, votes_count: votesCount }) => ({
|
options: ({ poll }) => poll.options.map(({ title, votes_count: votesCount }) => ({
|
||||||
title,
|
title,
|
||||||
share: poll.votes_count ? Math.round(votesCount / poll.votes_count * 100) : 0
|
share: poll.votes_count ? Math.round(votesCount / poll.votes_count * 100) : 0
|
||||||
})),
|
})),
|
||||||
votesCount: ({ poll }) => poll.votes_count,
|
votesCount: ({ poll }) => poll.votes_count,
|
||||||
|
voted: ({ poll }) => poll.voted,
|
||||||
|
multiple: ({ poll }) => poll.multiple,
|
||||||
expired: ({ poll }) => poll.expired,
|
expired: ({ poll }) => poll.expired,
|
||||||
expiresAt: ({ poll }) => poll.expires_at,
|
expiresAt: ({ poll }) => poll.expires_at,
|
||||||
expiresAtTS: ({ expiresAt }) => new Date(expiresAt).getTime(),
|
expiresAtTS: ({ expiresAt }) => new Date(expiresAt).getTime(),
|
||||||
|
@ -206,12 +264,13 @@
|
||||||
expiryText: ({ expired }) => expired ? 'Ended' : 'Ends',
|
expiryText: ({ expired }) => expired ? 'Ended' : 'Ends',
|
||||||
refreshElementId: ({ uuid }) => `poll-refresh-${uuid}`,
|
refreshElementId: ({ uuid }) => `poll-refresh-${uuid}`,
|
||||||
useNarrowSize: ({ $isMobileSize, expired }) => $isMobileSize && !expired,
|
useNarrowSize: ({ $isMobileSize, expired }) => $isMobileSize && !expired,
|
||||||
computedClass: ({ isStatusInNotification, isStatusInOwnThread, refreshing }) => (
|
formDisabled: ({ choices }) => !choices.length,
|
||||||
|
computedClass: ({ isStatusInNotification, isStatusInOwnThread, loading }) => (
|
||||||
classname(
|
classname(
|
||||||
'poll',
|
'poll',
|
||||||
isStatusInNotification && 'status-in-notification',
|
isStatusInNotification && 'status-in-notification',
|
||||||
isStatusInOwnThread && 'status-in-own-thread',
|
isStatusInOwnThread && 'status-in-own-thread',
|
||||||
refreshing && 'poll-refreshing'
|
loading && 'poll-loading'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -220,22 +279,42 @@
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
let { pollId } = this.get()
|
let { pollId } = this.get()
|
||||||
this.set({ refreshing: true })
|
this.set({ loading: true })
|
||||||
try {
|
try {
|
||||||
let start = Date.now()
|
let poll = await doAsyncActionWithDelay(() => getPoll(pollId))
|
||||||
let poll = await getPoll(pollId)
|
|
||||||
let timeElapsed = Date.now() - start
|
|
||||||
if (timeElapsed < REFRESH_MIN_DELAY) {
|
|
||||||
// If less than five seconds, then continue to show the refreshing animation
|
|
||||||
// so it's clear that something happened.
|
|
||||||
await new Promise(resolve => setTimeout(resolve, REFRESH_MIN_DELAY - timeElapsed))
|
|
||||||
}
|
|
||||||
if (poll) {
|
if (poll) {
|
||||||
this.set({ refreshedPoll: poll })
|
let { polls } = this.store.get()
|
||||||
|
polls[pollId] = poll
|
||||||
|
this.store.set({ polls })
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.set({ refreshing: false })
|
this.set({ loading: false })
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async onSubmit (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
let { pollId, options, formDisabled } = this.get()
|
||||||
|
if (formDisabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let choices = getChoices(this.refs.form, options)
|
||||||
|
this.set({ loading: true })
|
||||||
|
try {
|
||||||
|
let poll = await doAsyncActionWithDelay(() => voteOnPoll(pollId, choices))
|
||||||
|
if (poll) {
|
||||||
|
let { polls } = this.store.get()
|
||||||
|
polls[pollId] = poll
|
||||||
|
this.store.set({ polls })
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.set({ loading: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChange () {
|
||||||
|
let { options } = this.get()
|
||||||
|
let choices = getChoices(this.refs.form, options)
|
||||||
|
this.set({ choices: choices })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -39,6 +39,7 @@ const nonPersistedState = {
|
||||||
instanceLists: {},
|
instanceLists: {},
|
||||||
online: !process.browser || navigator.onLine,
|
online: !process.browser || navigator.onLine,
|
||||||
pinnedStatuses: {},
|
pinnedStatuses: {},
|
||||||
|
polls: {},
|
||||||
pushNotificationsSupport:
|
pushNotificationsSupport:
|
||||||
process.browser &&
|
process.browser &&
|
||||||
('serviceWorker' in navigator &&
|
('serviceWorker' in navigator &&
|
||||||
|
|
Loading…
Reference in a new issue