kopia lustrzana https://github.com/nolanlawson/pinafore
				
				
				
			fix: show poll results, time remaining, allow refresh (#1233)
more work towards #1130pull/1234/head
							rodzic
							
								
									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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Ładowanie…
	
		Reference in New Issue