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-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-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"> | ||||
|     {#each options as 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"> | ||||
|           <line x1="0" y1="0" x2="{option.share}%" y2="0" /> | ||||
|         </svg> | ||||
|       </li> | ||||
|     {/each} | ||||
|   </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> | ||||
| <style> | ||||
|   .poll { | ||||
|     grid-area: poll; | ||||
|     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 { | ||||
|  | @ -28,7 +64,7 @@ | |||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     stroke: var(--svg-fill); | ||||
|     stroke-width: 5px; | ||||
|     stroke-width: 10px; | ||||
|   } | ||||
| 
 | ||||
|   li.option:last-child { | ||||
|  | @ -42,20 +78,168 @@ | |||
|   } | ||||
| 
 | ||||
|   svg { | ||||
|     height: 2px; | ||||
|     height: 10px; | ||||
|     width: 100%; | ||||
|     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> | ||||
| <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 { | ||||
|     oncreate () { | ||||
|       this.onRefreshClick = this.onRefreshClick.bind(this) | ||||
|       let { refreshElementId } = this.get() | ||||
|       registerClickDelegate(this, refreshElementId, this.onRefreshClick) | ||||
|     }, | ||||
|     data: () => ({ | ||||
|       refreshedPoll: null, | ||||
|       refreshing: false | ||||
|     }), | ||||
|     store: () => store, | ||||
|     computed: { | ||||
|       poll: ({ originalStatus }) => originalStatus.poll, | ||||
|       poll: ({ originalStatus, refreshedPoll }) => refreshedPoll || originalStatus.poll, | ||||
|       pollId: ({ poll }) => poll.id, | ||||
|       options: ({ poll }) => poll.options.map(({ title, votes_count: votesCount }) => ({ | ||||
|         title, | ||||
|         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> | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { format } from '../_thirdparty/timeago/timeago' | ||||
| import { mark, stop } from '../_utils/marks' | ||||
| 
 | ||||
| // Format a date in the past
 | ||||
| export function formatTimeagoDate (date, now) { | ||||
|   mark('formatTimeagoDate') | ||||
|   // 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') | ||||
|   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
	
	 Nolan Lawson
						Nolan Lawson