fix: show poll results, time remaining, allow refresh (#1233)
more work towards #1130
This commit is contained in:
parent
dac4b493c8
commit
45441d3a9e
|
@ -47,5 +47,7 @@ module.exports = [
|
||||||
{ id: 'fa-share-square-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/share-square-o.svg' },
|
{ id: 'fa-share-square-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/share-square-o.svg' },
|
||||||
{ id: 'fa-flag', src: 'src/thirdparty/font-awesome-svg-png/white/svg/flag.svg' },
|
{ id: 'fa-flag', src: 'src/thirdparty/font-awesome-svg-png/white/svg/flag.svg' },
|
||||||
{ id: 'fa-suitcase', src: 'src/thirdparty/font-awesome-svg-png/white/svg/suitcase.svg' },
|
{ id: 'fa-suitcase', src: 'src/thirdparty/font-awesome-svg-png/white/svg/suitcase.svg' },
|
||||||
{ id: 'fa-bar-chart', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bar-chart.svg' }
|
{ id: 'fa-bar-chart', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bar-chart.svg' },
|
||||||
|
{ id: 'fa-clock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/clock-o.svg' },
|
||||||
|
{ id: 'fa-refresh', src: 'src/thirdparty/font-awesome-svg-png/white/svg/refresh.svg' }
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { getPoll as getPollApi } from '../_api/polls'
|
||||||
|
import { store } from '../_store/store'
|
||||||
|
import { toast } from '../_components/toast/toast'
|
||||||
|
|
||||||
|
export async function getPoll (pollId) {
|
||||||
|
let { currentInstance, accessToken } = store.get()
|
||||||
|
try {
|
||||||
|
let poll = await getPollApi(currentInstance, accessToken, pollId)
|
||||||
|
return poll
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.say(`Unable to refresh poll`)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { get, DEFAULT_TIMEOUT } from '../_utils/ajax'
|
||||||
|
import { auth, basename } from './utils'
|
||||||
|
|
||||||
|
export async function getPoll (instanceName, accessToken, pollId) {
|
||||||
|
let url = `${basename(instanceName)}/api/v1/polls/${pollId}`
|
||||||
|
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
|
||||||
|
}
|
|
@ -1,19 +1,55 @@
|
||||||
<div class="poll" >
|
<div class={computedClass} aria-busy={refreshing} >
|
||||||
<ul class="options" aria-label="Poll results">
|
<ul class="options" aria-label="Poll results">
|
||||||
{#each options as option}
|
{#each options as option}
|
||||||
<li class="option">
|
<li class="option">
|
||||||
<div class="option-text">{option.title} ({option.share}%)</div>
|
<div class="option-text">
|
||||||
|
<strong>{option.share}%</strong> {option.title}
|
||||||
|
</div>
|
||||||
<svg aria-hidden="true">
|
<svg aria-hidden="true">
|
||||||
<line x1="0" y1="0" x2="{option.share}%" y2="0" />
|
<line x1="0" y1="0" x2="{option.share}%" y2="0" />
|
||||||
</svg>
|
</svg>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
<div class="poll-details">
|
||||||
|
<div class="poll-stat">
|
||||||
|
<SvgIcon className="poll-icon" href="#fa-bar-chart" />
|
||||||
|
<span class="poll-stat-text">{votesCount} {votesCount === 1 ? 'vote' : 'votes'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="poll-stat">
|
||||||
|
<SvgIcon className="poll-icon" href="#fa-clock" />
|
||||||
|
<span class="poll-stat-text poll-stat-expiry">
|
||||||
|
<span class="{useNarrowSize ? 'sr-only' : ''}">{expiryText}</span>
|
||||||
|
<time datetime={expiresAt} title={expiresAtAbsoluteFormatted}>
|
||||||
|
{expiresAtTimeagoFormatted}
|
||||||
|
</time>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button class="poll-stat {expired ? 'poll-expired' : ''}" id={refreshElementId}>
|
||||||
|
<SvgIcon className="poll-icon" href="#fa-refresh" />
|
||||||
|
<span class="poll-stat-text">
|
||||||
|
Refresh
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
.poll {
|
.poll {
|
||||||
grid-area: poll;
|
grid-area: poll;
|
||||||
margin: 10px 10px 10px 5px;
|
margin: 10px 10px 10px 5px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 1px solid var(--main-border);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll.status-in-own-thread {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll.poll-refreshing {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.options {
|
ul.options {
|
||||||
|
@ -28,7 +64,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
stroke: var(--svg-fill);
|
stroke: var(--svg-fill);
|
||||||
stroke-width: 5px;
|
stroke-width: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.option:last-child {
|
li.option:last-child {
|
||||||
|
@ -42,20 +78,168 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
height: 2px;
|
height: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-in-notification .option-text {
|
||||||
|
color: var(--very-deemphasized-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-in-notification svg {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-in-own-thread .option-text {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-details {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content minmax(0, max-content) max-content;
|
||||||
|
grid-gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: left;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.poll-stat {
|
||||||
|
/* reset button styles */
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
border-spacing: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: normal;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: none;
|
||||||
|
text-indent: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.poll-stat:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--deemphasized-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-stat.poll-expired {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-stat-text {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-stat-expiry {
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.poll-icon) {
|
||||||
|
fill: var(--deemphasized-text-color);
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
min-width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.poll {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
.poll.status-in-own-thread {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.poll-details {
|
||||||
|
grid-gap: 5px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
import SvgIcon from '../SvgIcon.html'
|
||||||
|
import { store } from '../../_store/store'
|
||||||
|
import { formatTimeagoFutureDate, formatTimeagoDate } from '../../_intl/formatTimeagoDate'
|
||||||
|
import { absoluteDateFormatter } from '../../_utils/formatters'
|
||||||
|
import { registerClickDelegate } from '../../_utils/delegate'
|
||||||
|
import { classname } from '../../_utils/classname'
|
||||||
|
import { getPoll } from '../../_actions/polls'
|
||||||
|
|
||||||
|
const REFRESH_MIN_DELAY = 1000
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
oncreate () {
|
||||||
|
this.onRefreshClick = this.onRefreshClick.bind(this)
|
||||||
|
let { refreshElementId } = this.get()
|
||||||
|
registerClickDelegate(this, refreshElementId, this.onRefreshClick)
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
refreshedPoll: null,
|
||||||
|
refreshing: false
|
||||||
|
}),
|
||||||
|
store: () => store,
|
||||||
computed: {
|
computed: {
|
||||||
poll: ({ originalStatus }) => originalStatus.poll,
|
poll: ({ originalStatus, refreshedPoll }) => refreshedPoll || originalStatus.poll,
|
||||||
|
pollId: ({ poll }) => poll.id,
|
||||||
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,
|
||||||
|
expired: ({ poll }) => poll.expired,
|
||||||
|
expiresAt: ({ poll }) => poll.expires_at,
|
||||||
|
expiresAtTS: ({ expiresAt }) => new Date(expiresAt).getTime(),
|
||||||
|
expiresAtTimeagoFormatted: ({ expiresAtTS, expired, $now }) => (
|
||||||
|
expired ? formatTimeagoDate(expiresAtTS, $now) : formatTimeagoFutureDate(expiresAtTS, $now)
|
||||||
|
),
|
||||||
|
expiresAtAbsoluteFormatted: ({ expiresAtTS }) => absoluteDateFormatter.format(expiresAtTS),
|
||||||
|
expiryText: ({ expired }) => expired ? 'Ended' : 'Ends',
|
||||||
|
refreshElementId: ({ uuid }) => `poll-refresh-${uuid}`,
|
||||||
|
useNarrowSize: ({ $isMobileSize, expired }) => $isMobileSize && !expired,
|
||||||
|
computedClass: ({ isStatusInNotification, isStatusInOwnThread, refreshing }) => (
|
||||||
|
classname(
|
||||||
|
'poll',
|
||||||
|
isStatusInNotification && 'status-in-notification',
|
||||||
|
isStatusInOwnThread && 'status-in-own-thread',
|
||||||
|
refreshing && 'poll-refreshing'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onRefreshClick (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
let { pollId } = this.get()
|
||||||
|
this.set({ refreshing: true })
|
||||||
|
try {
|
||||||
|
let start = Date.now()
|
||||||
|
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) {
|
||||||
|
this.set({ refreshedPoll: poll })
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.set({ refreshing: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
SvgIcon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { format } from '../_thirdparty/timeago/timeago'
|
import { format } from '../_thirdparty/timeago/timeago'
|
||||||
import { mark, stop } from '../_utils/marks'
|
import { mark, stop } from '../_utils/marks'
|
||||||
|
|
||||||
|
// Format a date in the past
|
||||||
export function formatTimeagoDate (date, now) {
|
export function formatTimeagoDate (date, now) {
|
||||||
mark('formatTimeagoDate')
|
mark('formatTimeagoDate')
|
||||||
// use Math.max() to avoid things like "in 10 seconds" when the timestamps are slightly off
|
// use Math.max() to avoid things like "in 10 seconds" when the timestamps are slightly off
|
||||||
|
@ -8,3 +9,12 @@ export function formatTimeagoDate (date, now) {
|
||||||
stop('formatTimeagoDate')
|
stop('formatTimeagoDate')
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format a date in the future
|
||||||
|
export function formatTimeagoFutureDate (date, now) {
|
||||||
|
mark('formatTimeagoFutureDate')
|
||||||
|
// use Math.min() for same reason as above
|
||||||
|
let res = format(date, Math.min(now, date))
|
||||||
|
stop('formatTimeagoFutureDate')
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue